2014年8月10日 星期日

Unity:使用 PHP 的 Session 進行 Server 端驗證以及傳遞資料

相信曾經使用 PHP 製作過具備會員機制的互動式網頁系統的人對於 Session 都不陌生,它可以很方便的確認網頁客戶端的身份、驗證登入狀態以及傳遞資料給同一使用者於下一次請求時使用;而現今已有許多行動裝置遊戲也都具備營運方自己的會員機制以及 Server 端的遊戲邏輯應用及資料庫,以行動裝置的使用族群環境來看,似乎比較少使用所謂長連接的 socket 連線,而比較常見的是類似互動式網頁的非同步連線機制;所以,以下將解說如何使用 Unity 實現 PHP 的 Session。

通常在製作互動式網頁系統時,由於 Client 是網頁瀏覽器,所以當 Server 端需要使用 Session 的話,只要在 PHP 的程式最前面寫一行 session_start() 就可以從 $_SESSION 這個陣列中存取本次連線所使用 session id 所記錄的資料,因此我們可以很輕鬆的標記及判斷此次連線請求者的身份,而本次連線所使用的 session id 是什麼呢?我們可以很簡單地從瀏覽器的 cookie 中找到,由於這整個機制流程大部份的工作都是 Server 和瀏覽器已經完成的,所以在互動式網頁系統的開發上,我們不需要懂得太多,也不需要太多手續,就能使用 Session,但是 Unity 遊戲引擎雖然有 WWW 等相關的 class 可以對 Server 發出請求以及接收回應資料,但本身並沒有 Cookie,所以無法像網頁一樣直接就能使用 Session。

由於 Unity 本身沒有類似 Cookie 的機制導致無法像網頁一樣直接使用 Session,所以很多人會自行設計 token 的編碼及驗證機制,有些是登入時產生驗證碼作為 token 儲存在 Server 端(檔案或資料庫),在每次送出請求時都包含這個 token,然後再撈取資料出來比對驗證身份,也有些是每次送出請求時使用傳送的參數以及 token 依照預先定義好的規則進行編碼成為所謂的 sign 作為請求中的其中一個參數傳送給 Server,然後 Server 再以接收到的參數使用相同規則編碼來進行比對驗證,這些做法看起來相當安全,就是感覺有點太繁雜了,個人覺得類似這種做法通常是用於與第三方之間進行資料傳輸的身份驗證,否則還是應該想辦法實現 Session;另外,如果在前一次連線請求處理完之後有某些資料想要給以後連線請求時存取,前面所提到的做法就必須要自行把資料儲存在檔案或是資料庫中,等待下次連線需要時再讀取出來使用,那麼在使用上還要確認該資料是否已過期,不可給本次連線請求使用,但 Session 只需要直接對 $_SESSION 這個陣列進行存取,只要有使用 session_start(),我們的程式碼甚至不需要知道 session id 是什麼,也不需要進行任何比對的動作,就能直接提取到正確的資料;選擇使用 Session,不需要多餘的編碼規則、不需要多餘的判斷、不需要多餘的存取,Client 與 Server 兩端的程式也就不需要多餘的程式碼,效能自然就會比較好,同時 Client 與 Server 之間傳送的資料也會變得比較少一些,但安全性卻未必會比較差,另外,幾乎所有會寫 PHP 的人都會用 Session,所以在團隊合作中,也可以節省為了制定編碼以及驗證規則所花費的資源及時間。

在實作之前,先大概了解一下 Server 的 PHP 程式與 Client 的瀏覽器之間的 Session 關係。如果我們在 Server 的 PHP 程式中使用了 session_start(),當 Client 送出請求之後會從 Server 回應的封包 header 中夾帶 SET-COOKIE、EXPIRES、CACHE-CONTROL、PRAGMA 等資料,其中的 SET-COOKIE 則包含了一串 PHPSESSID 編碼,這就是此次連線所產生的 session id,瀏覽器則會將它記錄在 cookie 中,直到它過期失效,之後在每次對相同網域發送請求的封包 header 中會同時夾帶他的 Cookie 給 Server,所以當有使用到 session_start() 的 PHP 程式在收到有效的 session id 時,就會延續使用該 Session 的資料,否則就建立新的 session id 傳回給 Client,至於,Server 是如何存取 Session 的呢?通常 Server 會以檔案的形式自行以 session id 來命名檔名並將資料儲存在該檔案中,當然我們也可以在 PHP 中自己寫 Class 讓 Session 儲存在資料庫或是記憶體中,但這部分不在本文討論範圍,就不在此詳述。



接下來,我們知道 Unity 是使用 WWW class 對 Web Server 送出請求以及取得回應,所以,我們可以從取得 Server 回應的 WWW 物件中的 responseHeaders 獲得回傳封包的 header,這個 responseHeaders 的形態是 Dictionary<string , string>,將它拆解開來就能找到 PHPSESSID,接下來只要在下次對 Server 送出請求時,於封包的 header 裡面加入個包含 session id 的 Cookie 即可;通常,一般對 Server 送出請求,我們可能都是使用 new WWW(url),由於在獲得 session id 之後的每次請求中,我們必須讓 Server 知道我們是使用哪個 session id 的身份,所以要改成使用 new WWW(url , postData , headers) 來送出請求,而這個 headers 的型態可以是 Hashtable,也可以是 Dictionary<string , string>,不過在目前最新版的 Unity (Version 4.5.2p3) 已不建議在這裡使用 Hashtable,所以我們在這裡製作一個含有 key 為 “COOKIE”、value 為 “PHPSESSID={session id}” 的 Dictionary<string , string> 作為 headers 參數內容,如此,Server 在收到使用有效的 session id 時,將會使用相同身份的 Session 資料;那麼,Unity 的 WWW 與 Server 端的 PHP 之間的關係就如同網頁瀏覽器與 Server 端的 PHP 之間的關係一樣,將來,如果需要使用 Unity 製作關於會員驗證機制、與 Server 端的非同步互動等,就跟製作互動式網頁系統一樣囉!以下提供簡單的 C# 程式碼作為參考、測試用:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class TestSession : MonoBehaviour {

    public string url;
    private string sessionId;
    private Dictionary<string,string> headers = new Dictionary<string, string>();

    void OnGUI(){

        if(GUILayout.Button("Submit")){
            StartCoroutine(this.WebRequest());
        }
    }

    private IEnumerator WebRequest(){

        WWW www = !this.headers.ContainsKey("COOKIE") ? new WWW(this.url) : new WWW(this.url , null , this.headers);

        yield return www;

        Debug.Log(www.text);

        this.GetSessionId(www.responseHeaders);

    }

    private void GetSessionId(Dictionary<string , string> responseHeaders){

        foreach(KeyValuePair<string , string> header in responseHeaders){

            Debug.Log(string.Format("{0} : {1}" , header.Key , header.Value));

            if(header.Key == "SET-COOKIE"){

                string[] cookies = header.Value.Split(';');
                for(int i = 0 ; i < cookies.Length ; i++){

                    if(cookies[i].Split('=')[0] == "PHPSESSID" && !this.headers.ContainsKey("COOKIE")){
                        this.sessionId = cookies[i];
                        this.headers.Add("COOKIE" , this.sessionId);
                        break;
                    }
                }
            }
        }
    }
}

以上程式碼,只用於測試 Session 是否正確,在專案實作中,還需要依照實用性進行修改,至於 Server 端的 PHP 程式碼,由於只使用到 session_start() 以及 $_SESSION 而已,就不在此舉例了。