Skip to content

Advanced

第 7 章 進階應用

7-1 訊息安全(Security)

訊息在網路上傳遞時,最好能夠加密,目前加解密演算法分為對稱式與非對稱式兩種,由於非對稱式演算法須耗去大量的 CPU 運算時間,為了符合遊戲快速訊息處理的需要,建議採用對稱式的加密演算法。

SGC 將訊息封包的加密解密交由開發者自行處理,這是因為對稱式加密與解密使用的金鑰是同一種,為了避免所有 SGC 遊戲都在同一個加解密演算法與金鑰之下運作,讓開發者保有自己的加解密方法,自然可以降低被破解的危險性。

以下範例程式,我們定義了一個全域物件 MessageKey 用來存放金鑰,配合 Security 物件內的加密解密函式,就可以在傳送與接受訊息時進行加密與解密:

//TripleDes.cs 
using System;
using System.Security.Cryptography;
using System.IO;
using System.Text;
using GameCloud;

sealed internal class MessageKey
{
    static MessageKey _context;
    // 以下KEY值僅供參考,請自行以任意16個byte數字組成的陣列做為KEY 
    internal byte[] bKey = {0x13,0xcd,0x8a,0xc5,0x7a,0x9,0xab,0x4f,0xd6, 0x33, 0x59, 0x2b, 0x65, 0xe3, 0xf1, 0x4d};

    // 以下IV值僅供參考,請自行以任意8個byte數字組成的陣列做為IV
    internal byte[] bIV = {0x9a,0xdd, 0xef,0x1c,0x88,0x48,0xa0,0xf6};

    static public MessageKey instance
    {
        get{
            lock(typeof(MessageKey))
            { 
                if (_context == null)
                {
                    _context = new MessageKey();
                }
            }
            return _context;
        }
    }
}

public class Security
{
    public Security()
    {
        mCSP.Key = MessageKey.instance.bKey;
        mCSP.IV = MessageKey.instance.bIV;
    }

    public string EncryptString(string Value)
    {
        ICryptoTransform ct;
        MemoryStream ms;
        CryptoStream cs;
        byte[] byt;
        mCSP.Mode = System.Security.Cryptography.CipherMode.ECB;
        mCSP.Padding = System.Security.Cryptography.PaddingMode.PKCS7;

        ct = mCSP.CreateEncryptor(mCSP.Key, mCSP.IV);

        byt = Encoding.UTF8.GetBytes(Value);
        ms = new MemoryStream();
        cs = new CryptoStream(ms, ct, CryptoStreamMode.Write);

        cs.Write(byt, 0, byt.Length);
        cs.FlushFinalBlock();
        cs.Close();
        return Convert.ToBase64String(ms.ToArray());
    }

    public string DecryptString(string Value)
    {
        ICryptoTransform ct;
        MemoryStream ms;
        CryptoStream cs;
        byte[] byt;

        mCSP.Mode = System.Security.Cryptography.CipherMode.ECB;
        mCSP.Padding = System.Security.Cryptography.PaddingMode.PKCS7;

        ct = mCSP.CreateDecryptor(mCSP.Key, mCSP.IV);

        byt = Convert.FromBase64String(Value);
        ms = new MemoryStream();
        cs = new CryptoStream(ms, ct, CryptoStreamMode.Write);

        cs.Write(byt, 0, byt.Length);
        cs.FlushFinalBlock();
        cs.Close();
        return Encoding.UTF8.GetString(ms.ToArray());
    }
}

建立Security物件

程式一開始就可以建立 Security 物件,由於 Security 物件內的加密解密函式是用在訊息接收與傳送,所以此物件必須是訊息接收與傳送函式可以存取的物件。

以下範例是在DP 的 FormLoad 時建立 Security 物件:

//(DP) dp001.cs

Security so;

private void dp001_Load(object sender, EventArgs e)
{
    so = new Security();
    labelSystemStatus.Text = "尚未連線";
    ag = new CloudGame(userid, passwd, gguid, gcert);
    ag.onCompletion += OnCompletion;
    ag.onStateChanged += OnStateChanged;
    ag.onPrivateMessageIn += OnPrivateMessageIn;  
    labelSystemStatus.Text = "連線中   ";
    ag.Launch();
}

有了 Sercurity 物件後,往後就可以使用此物件內的加密與解密函式進行加解密了。

傳輸與接收訊息

接收訊息是在OnPrivateMessageIn 事件函式中,只要在事件函式中,把收到的字串訊息進行解密即可,不過,為了確保訊息可以解密,如果傳進來的訊息有問題,導至解密發生Exception,我們只要將之視為無效訊息,不須理會。

以下範例是 OnPrivateMessageIn(),我們只在最上面加了一行呼叫解密函式:

//(DP) dp001.cs

void OnPrivateMessageIn(string msg, int d, CloudGame game)
{
    try
    {
        string m = so.DecryptString(msg);
        string[] cmds = m.Split(':');
        uint ClientPoid;
        (以下省略)
    }
    catch (Exception)
    {}
}

至於傳送訊息,我們可以自己定義 SecureSend()函式,用來取代 Send()函式,日後只要記得不要直接使用Send()即可:

//(DP) dp001.cs

// 私人訊息
void SecurePrivacySend(string msg, uint poid)
{
    ag.PrivacySend(so.EncryptString(msg), poid);
}

// 場景訊息
void SecureSeceneSend(CloudScene scene, string msg)
{
    scene.Send(so.EncryptString(msg));
}

請注意,由於 DP 在傳送與接收訊息已經增加了加解密的功能,用戶端的程式同樣也要加上加解密的功能,而且所使用的 KEY 與 IV 值也必須和DP 所使用的完全一樣。

7-2 系統記錄檔(Log)

開發者可以依照需要,規劃各種不同的系統記錄檔,將系統運作過程中的重要資料記錄起來。基本上,系統記錄檔大約分為除錯追蹤要用的程式記錄檔,以及與遊戲運作有關的遊戲記錄檔

  • PC 執行檔

PC 執行檔程式可以直接將記錄直接寫入用戶端的磁碟檔,由於 PC 磁碟檔容量很大,比較沒有磁碟已滿的問題,所以實作系統記錄檔的限制較小。

  • Web Based

Web Based 程式受限於瀏覽器的安全性設定,一般來說是無法直接寫入磁碟檔的,所以無法實作系統記錄檔。

  • App

App 程式雖然可以將系統記錄寫入到手機(或平板)的記憶卡裡,不過由於記憶卡的容量有限,如果我們開發的 App 程式導致用戶記憶體不足,為了避免這種狀況發生,除非是程式研發階段, 否則應該避免將系統記錄寫入手機記憶卡。

  • DP

DP 程式運行於獨立主機或是 VM,和 PC 執行檔一樣沒有磁碟容量的問題,因此可以盡量記錄各種資訊,以因應各種需要。

系統紀錄檔程式範例

以下範例DPLog.cs 可用於 DP 程式中:

using System;
using System.IO;

sealed internal class DPLogPath
{
    static DPLogPath _context;
    internal string LogBasePath = null;
    static public DPLogPath instance
    {
        get
        {
            lock(typeof(DPLogPath))
            {
                if (_context == null)
                {
                    _context = new DPLogPath();
                }
            }
            return _context;
        }
    }
}
public class DBLog
{
    public static void WriteLog(string subdir, int tag, string log)
    {
        string LogFileName;
        try
        {
            if (DPLogPath.instance.LogBasePath == null) return;
            if (subdir == null || subdir == "")
            {
                LogFileName = DPLogPath.instance.LogBasePath + "\\";
            }
            else
            {
                LogFileName = DPLogPath.instance.LogBasePath + "\\" + subdir + "\\";
            }
            LogFileName += DateTime.Now.Year.ToString("D4") +
                DateTime.Now.Month.ToString("D2") +
                DateTime.Now.Day.ToString("D2") + ".log";
            string LogData = DateTime.Now.Year.ToString("D4") + "-" +
                DateTime.Now.Month.ToString("D2") + "-" + 
                DateTime.Now.Day.ToString("D2") + " " + 
                DateTime.Now.Hour.ToString("D2") + ":" +
                DateTime.Now.Minute.ToString("D2") + ":" + 
                DateTime.Now.Second.ToString("D2") + "," + "[" + tag.ToString("D8") + "]" + log;
            lock(DPLogPath.instance)
            {
                using(StreamWriter sw = File.AppendText(LogFileName))
                {
                    sw.WriteLine(LogData);
                    sw.Flush();
                    sw.Close();
                }
            }
        }
        catch (Exception e)
        {}
    }
}

程式一開始必須先指定記錄檔的路徑,而且只要指定一次即可。假設記錄檔位於「C:\DP-Log」資料匣,程式碼如下:

DPLogPath.instance.LogBasePath = "c:\\DP-Log";

路徑指定之後,要寫入一筆記錄,程式碼如下:

DBLog.WriteLog(null,theTag,"System Startup!!");

其中字串

"System Startup!!"

就是我們要寫入的記錄資料;另外,由於參數 subdir 為 null,所以記錄檔是在「C:\DP-Log」中, 檔案則以年月日(YYYYMMDD.Log)為檔名,YYYY 為目前的西元年,MM 為目前月份,DD 則是目前日期。

7-3 DP代管(Co-location)

由於DP 是一個獨立運作於遠端的程式,目前 SGC 尚未提供 VM 的服務,建議開發者可以向雲端 VM 業者租用,千萬不要以訂戶線(Subscriber Line)做為 DP 與 SGC 連線的線路,這是因為一般的訂戶線如ADSL、FTTB 等,都是共用頻寬,很容易受到其它使用者的影響,使得 DP 的品質與效能降低。未來如果 SGC 的 VM 服務上線,建議將 DP 在 SGC 的 VM 上運行,直接與 SGC 主機群連線,可增加效能與穩定度。 目前全世界提供 VM 服務的業者非常多,請選擇頻寬穩定,機房品質良好,又有口碑的業者,例如Microsoft Azure、Amazon 的 AWS 服務等。

7-4 申請玩家遊戲

由於 iOS 的應用程式上架規定中有個潛規定,如果應用程式中需要用到第三方的登入帳號,申請帳號的過程若由應用程式跳到 Web 頁面,這個 Web 網站只能是單純的會員系統,如果網站涉及金流付款服務,應用程式將無法上架。

為了避免這個問題,應用程式必須內建「申請玩家帳號」功能,開發者可以在應用程式中規劃一個申請帳號的表單,包含以下欄位:

  • 登入帳號
  • 遊戲密碼
  • 確認密碼
  • 電子郵件

然後呼叫CloudSystem.ApplyNewUser()函式,定義如下:

static void ApplyNewUser(string gguid, byte[] gamecode, string userid, string passwd, string email, OnCallCompletion cb, object token)

(1) gguid是遊戲的GGUID。
(2) gamecode是遊戲驗證碼。
(3) userid 是所申請的玩家帳號,請注意帳號長度必須介於 3 到 10 個字元,而且只能是英數字, 符號只能是「-」和「.」。

(4) passwd 是帳號的密碼,長度限制 6 到 18 字元。
(5) email 是帳號的 e-mail 信箱,用來驗證身份。

cb 為完成API 後的callback 函式,其函式定義如下:

void cb(int code, object token)

token 是和 callback 函式傳遞相依資料的物件。

此外, 如果遊戲想要降低玩家登入遊戲的門檻, 甚至連申請帳號都不必時, 可透過CloudSystem.ApplyNewUser()函式的另外一個 Overloading,此函式不必傳入帳號、密碼、e-mail 等申請者資料,而是由系統配發帳號,並傳回帳號的名稱與密碼,此帳號又可稱為玩家體驗帳號。

static void ApplyNewUser(string gguid, byte[] gamecode, OnCallCompletion cb, object token)

體驗帳號的帳號密碼傳回後,遊戲程式必須儲存在本機儲存體中,玩家在遊戲中的資料,如CloudItem、CloudScore 所儲存的資料也可下次登入遊戲時取出使用。由於體驗帳號無法在 SGC 官網取得登入網站的帳號密碼,因此體驗帳號必須升級成正式帳號,升級正式帳號可以呼叫 CloudSystem.UpgradeUser(),函式定義如下:

static void UpgradeUser(CloudGame game, string userid,string passwd,string email, OnCallCompletion cb, object token)