上一篇講了創建服務器項目以及添加ranch網絡庫,本篇利用網絡庫創建client socket消息處理的用戶層代碼以及服務器開發調試運維的日誌集成。
二、編寫用戶層代碼
前面知道網絡層處理客戶端連接,以及可以將客戶端socket文件描述符授權給其它worker進程來一對一爲客戶端服務(得益於erlang actor模型的輕進程,可以做到一個連接一個進程),而網絡庫我們使用的ranch,因此可以參照ranch的example程序編寫一個處理socket消息的進程,也就是爲用戶一對一服務器的用戶層代碼。
仿照ranch例子創建一個erlserver_user.erl文件
添加如下代碼:
-module(erlserver_user).
-behaviour(gen_server).
-behaviour(ranch_protocol).
%% API.
-export([start_link/4]).
%% gen_server.
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([terminate/2]).
-export([code_change/3]).
-define(TIMEOUT, 5000).
-record(state, {socket, transport}).
%% API.
start_link(Ref, Socket, Transport, Opts) ->
{ok, proc_lib:spawn_link(?MODULE, init, [{Ref, Socket, Transport, Opts}])}.
init({Ref, Socket, Transport, _Opts = []}) ->
ok = ranch:accept_ack(Ref),
ok = Transport:setopts(Socket, [{active, once}]),
gen_server:enter_loop(?MODULE, [],
#state{socket=Socket, transport=Transport},
?TIMEOUT).
handle_info({tcp, Socket, Data}, State=#state{
socket=Socket, transport=Transport})
when byte_size(Data) > 1 ->
Transport:setopts(Socket, [{active, once}]),
{ok, PeerName} = inet:peername(Socket),
io:format("receive from client[~w], msg:~p~n", [PeerName, Data]),
{noreply, State, ?TIMEOUT};
handle_info({tcp_closed, _Socket}, State) ->
{stop, normal, State};
handle_info({tcp_error, _, Reason}, State) ->
{stop, Reason, State};
handle_info(timeout, State) ->
{stop, normal, State};
handle_info(_Info, State) ->
{stop, normal, State}.
handle_call(_Request, _From, State) ->
{reply, ok, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
閱讀過ranch源碼,就知道ranch_acceptor在監聽一個客戶端連接後,就會調用我們指定的模塊的start_link/4方法,當然,我們可以直接在start_link中啓動一個gen_server,不過ranch的這個例子因爲要有一個ranch:accept_ack操作,所以會阻塞在init,因此沒有用gen_server,用了prob_lib:spawn_link,立即返回一個可用的pid給ranch庫,然後在慢慢初始化,初始化完成後用gen_server:enter_loop將這個進程轉變爲一個gen_server進程。三、啓動ranch監聽功能
上一篇只在app.src啓動了ranch,這只是表示應用啓動起來,要使ranch開始監聽某個端口,還需要顯示調用ranch:start_listener/5,這裏我們要監聽本機8888端口,並且使用我們在第二步寫好的客戶端socket套接字消息處理進程,其它tcp參數都使用默認,則我們在erlserver_app.erl啓動中添加以下代碼:
{ok, _} = ranch:start_listener(erlserver,
ranch_tcp, [{port, 8888}], erlserver_user, []),
rebar3 shell啓動我們的應用測試一下是否可用,另開一臺機器進入erlang shell,輸入{ok, Fd} = gen_tcp:connect({xxx,xxx,xxx,xxx}, 8888, [binary, {active, false}, {packet, raw}]), gen_tcp:send(Fd, "for test"). 看到輸出,就說明現在可以創建一個併發服務器了,並且每個客戶端連接都對應一個erlserver_user進程單獨處理。四、添加lager日誌庫
在第二步的代碼中,我們用了io:format來打印收到的客戶端信息,這樣做其實是不好的,終端打印的消息無法輸出到文件記錄下來,並且io打印的消息都是原始消息,不方便調試,要同時輸出模塊、行號等信息,每一次的io打印還都要加上這些信息,總之,對於服務器來說,沒有一個專門的日誌記錄都是不好的。
1、rebar.config添加依賴:
{lager, {git, "https://github.com/erlang-lager/lager", {branch, "master"}}}
2、erlserver.app.src啓動lager(lager的順序儘量放在除erlang runtime庫之前,例如放在ranch之前,這樣我們就可以在其它應用啓動中也能使用lager輸出應用啓動的信息了)。
3、添加lager配置
這裏前期爲了調試方便,我們直接利用rebar3 shell加載一個本地調試配置文件
在項目根目錄創建一個config文件夾,並在裏面創建一個erlserver.config配置文件,輸入以下內容
[
{lager, [
{log_root, "./log"},
{handlers, [
{lager_console_backend,
[{level, info}, {formatter, lager_default_formatter},
{formatter_config, [date, "/", time, "[", severity, "][", module, ":", function, ":", line,"]", "|", message, "\n"]}]},
{lager_file_backend,
[{file, "error.log"}, {level, error}, {formatter, lager_default_formatter},
{formatter_config, [date, "/", time, "[", module, ":", function, ":", line,"]", "|", message, "\n"]}]},
{lager_file_backend,
[{file, "console.log"}, {level, info}, {formatter, lager_default_formatter},
{formatter_config, [date, "/", time, "[", module, ":", function, ":", line,"]", "|", message, "\n"]}]}
]}
]}
].
這就是一個lager的簡單配置,日誌都輸出到log目錄,並且info對應console.log文件(目前都是本地調試,並不是發佈的配置)。4、配置rebar3 shell讀取config文件以及添加lager的parse_transform
修改rebar.config的shell配置
{shell, [
{apps, [erlserver, sync]},
{config, "config/erlserver.config"}
]}.
修改rebar.config的erl_opts配置(這裏使用到的知識點可以參考我的另一篇erlang抽象語法樹博客){erl_opts, [
debug_info,
{parse_transform, lager_transform}
]}.
五、使用日誌庫
修改第二步的erlserver_user的io:format打印爲lager:info("xxxxx"),再來嘗試客戶端連接並且傳輸信息。
六、後續
客戶端消息處理層(用戶層)以及日誌都寫好了,後續開始制定協議層了。
(project-https://github.com/xlkness/erlserver.git)