現在,客戶端與服務器的連接算是正式建立了,此時用戶需要做的第一件事就是登陸。不過在登錄之前,我們要先研究下前後端通信的協議。
客戶端與服務端建立連接後,通過提前制定好的協議進行交互。具體的協議文檔在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轉發到相應函數進行處理。
此時玩家就能進行行走,打怪,強化裝備等遊戲內的操作了。