英雄遠征Erlang源碼分析(5)-協議解析與玩家登錄處理

現在,客戶端與服務器的連接算是正式建立了,此時用戶需要做的第一件事就是登陸。不過在登錄之前,我們要先研究下前後端通信的協議。

客戶端與服務端建立連接後,通過提前制定好的協議進行交互。具體的協議文檔在doc文件夾下。

典型的協議結構如下(此處爲用戶登錄的10000協議):

*** 協議說明 ***
1.協議號由5位數字組成,前兩位表示協議分類(主要用於區分不同的功能模塊),後三位是該分類下的具體命令號,有效範圍是10,000-64,000;(短整型)
2.同一協議號最多可定義兩種數據結構,分別用於描述上行和下行數據;

協議分爲兩個部分:協議號(Cmd)和協議體(Data),打包爲二進制的協議形式則爲:總長度(Len),協議號(Cmd)和協議體(Data)

我們來看下sd_reader.erl中協議解析相關的部分代碼:

login_parse_packet(Socket, Client) ->
    Ref = async_recv(Socket, ?HEADER_LENGTH, ?HEART_TIMEOUT),
    receive
        {inet_async, Socket, Ref, {ok, <<Len:16, Cmd:16>>}} ->
            BodyLen = Len - ?HEADER_LENGTH,
            case BodyLen > 0 of
                true ->
                    Ref1 = async_recv(Socket, BodyLen, ?TCP_TIMEOUT),
                    receive
                       {inet_async, Socket, Ref1, {ok, Binary}} ->
                            case routing(Cmd, Binary) of
                            ...

async_recv(Sock, Length, Timeout) when is_port(Sock) ->
    case prim_inet:async_recv(Sock, Length, Timeout) of
        {error, Reason} -> throw({Reason});
        {ok, Res}       -> Res;
        Res             -> Res
    end.

協議的解析過程:服務端通過prim_inet:async_recv/3異步獲取指定字長的消息。先獲取4個字節(?HEADER_LENGTH爲4)的數據,其中包括2字節的總協議長度(Len)和2字節的協議號(Cmd),再使用總協議長度Len-4,得到協議體的長度並異步獲取協議體的數據(Data),然後將協議號(Cmd)和獲取的協議體(Data)傳入routing/2函數,傳入具體的協議解析函數進行處理。

舉個栗子:

例如10000協議,客戶端發起登錄請求,需要給服務器發送四個參數:用戶ID,時間戳,用戶賬號,ticket,服務器則返回登錄狀態(成功或失敗)

此時我們定義打包和解包函數pack/2和unpack/1:

pack(Cmd, Data) ->
  Len = byte_size(Data) + 4,
  <<Len:16, Cmd:16, Data/binary>>.

unpack(BinData) ->
  <<Len:16, Cmd:16, Data/binary>> = BinData,
  routing(Cmd, Rest)

假設用戶ID爲123456,時間戳爲1551409604,用戶賬號爲Administrator,ticket爲12riofaed982,則需要發送給服務器的Data爲<<123456:32,1551409604:32,write_string("Administartor")/binary,write_string("12riofaed982")/binary>>,通過pack(10000,Data)獲得打包好的數據,發送給服務器即可。此時協議總長度爲41字節
__________________
|_Len_|_Cmd_|_Data_|     一共41字節長,長度分別爲2,2,37

由於服務端是異步獲取數據的,需要獲取協議的總長度來準確分割每一條協議,先獲取前4個字節的數據,爲<<0,41,39,16>>,通過模式匹配<<Len:16,Cmd:16>>可得長度爲41,協議號爲10000,再根據41-4=37,獲取後37字節的數據,就是<<123456:32,1551409604:32,write_string("Administartor")/binary,write_string("12riofaed982")>>的部分了,服務端傳入routing/2,獲取協議的開頭2位數字爲10,調用pt_10:read(10000,Binary)進行進一步處理。

具體的協議說明:
doc/protocol.txt

協議看完了,我們可以進入sd_reader的登錄過程了。

之前sd_tcp_acceptor建立客戶端進程sd_reader後,會給該進程發送一條消息{go,Sock},此時客戶端進程在init/1裏面進行接收:

init() ->
    process_flag(trap_exit, true),
    Client = #client{...},
    receive
        {go, Socket} ->
            login_parse_packet(Socket, Client)
    end.

收到消息後,進入login_parse_packet/2函數,恰巧就是我們上面拿來分析協議解析過程的函數,這裏不重複貼代碼了。在routing/2裏面解析客戶端發過來的協議,判斷是登錄,創角還是進入遊戲,

如果是登錄:

1.在pp_account:handle(10000,..)裏面做賬號驗證,檢查通行證:將用戶ID,用戶賬號,時間戳拼接並取md5,與ticket做比較

2.進入遊戲:進入mod_login:login/3函數,從數據庫獲取玩家信息,檢查玩家在線/離線狀態,創建/獲取玩家進程,返回玩家進程Pid。

3.登錄成功:加載玩家模塊信息,將玩家進程Pid傳入#client,進入do_parse_packet/2:

do_parse_packet(Socket, Client) ->
    Ref = async_recv(Socket, ?HEADER_LENGTH, ?HEART_TIMEOUT),
    receive
        {inet_async, Socket, Ref, {ok, <<Len:16, Cmd:16>>}} ->
            BodyLen = Len - ?HEADER_LENGTH,
            case BodyLen > 0 of
                true ->
                    Ref1 = async_recv(Socket, BodyLen, ?TCP_TIMEOUT),
                    receive
                       {inet_async, Socket, Ref1, {ok, Binary}} ->
                            case routing(Cmd, Binary) of
                                %%這裏是處理遊戲邏輯
                                {ok, Data} ->
                                    case catch gen:call(Client#client.player, '$gen_call', {'SOCKET_EVENT', Cmd, Data}) of
                                        {ok,_Res} ->
                                            do_parse_packet(Socket, Client);
                                            ...

開始循環等待接收客戶端發過來的協議,並通過routing轉發到相應函數進行處理。

此時玩家就能進行行走,打怪,強化裝備等遊戲內的操作了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章