本文旨在分享笔者分析RPC服务器的本质以及实践过程中的一些现象。
先上源码
%% 基于TCP的RPC服务器
%% TR server
-module(tr_server).
-behaviour(gen_server).
%% gen_server callback API
-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,code_change/3]).
%% 自定义API
-export([start_link/1,start_link/0,get_count/0,stop/0]).
-define(SERVER,?MODULE).
-define(DEFAULT_PORT,8081).
-record(state,{port,lsock,request_count = 0}).
start_link(Port) ->
gen_server:start_link({local,?SERVER},?MODULE,[Port],[]).
start_link() ->
start_link(?DEFAULT_PORT).
get_count() ->
gen_server:call(?SERVER,get_count).
stop() ->
gen_server:cast(?SERVER,stop).
%% gen_server
init([Port])->
{ok, LSock} = gen_tcp:listen(Port,[{active,true}]),
{ok, #state{port = Port, lsock = LSock}, 0}.
handle_call(get_count, _From, State) ->
{reply, {ok ,State#state.request_count},State}.
handle_cast(stop, State) ->
{stop, normal ,State}.
handle_info({tcp,Socket,RawData},State) ->
do_rpc(Socket,RawData),
RequestCount = State#state.request_count,
{noreply,State#state{request_count = RequestCount + 1}};
handle_info(timeout,#state{lsock = LSock} = State) ->
{ok, _Sock} = gen_tcp:accept(LSock),
{noreply ,State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
do_rpc(Socket, RawData) ->
try
{M, F, A} = split_out_mfa(RawData),
Result = apply(M, F, A),
gen_tcp:send(Socket, io_lib:fwrite("~p~n", [Result]))
catch
_Class:Err ->
gen_tcp:send(Socket, io_lib:fwrite("~p~n", [Err]))
end.
split_out_mfa(RawData) ->
MFA = re:replace(RawData, "\r\n$", "", [{return, list}]),
{match, [M, F, A]} =
re:run(MFA,
"(.*):(.*)\s*\\((.*)\s*\\)\s*.\s*$",
[{capture, [1,2,3], list}, ungreedy]),
{list_to_atom(M), list_to_atom(F), args_to_terms(A)}.
args_to_terms(RawArgs) ->
{ok, Toks, _Line} = erl_scan:string("[" ++ RawArgs ++ "]. ", 1),
{ok, Args} = erl_parse:parse_term(Toks),
Args.
整个RPC服务器分为两部分组成,API以及回调函数。
我们直接先从Api开始:
用户实际上并不关心服务器如何运行,用户只关心服务器如何启用,并达到他想要的响应的效果
所以这里提供了四个方法(实际上是三个)给用户使用
- start_link 用户启动服务
- get_count 发送消息 并获取回复
- stop 停止服务
这些是提供给用户的 用户可以通过TCP请求建立连接后 使用命令获得相应的服务
通过我们的调用也可以分析出我们的RPC服务器其实本质上是一种gen_server通用服务器的派生
深入分析其作用机制如下:
start_link(Port) -> 启动服务器并链接服务器进程,当执行这个调用时,会派生出一个新的gen_server容器进程,新进程以SERVER宏展开并在本地节点完成注册,然后行为模式调用init完成初始化,服务器正式完成启动工作。值得注意的是,这些对于用户来说,全都是屏蔽的,用户只需要关心监听的接口即可!
get_count() -> 向服务器发送原子get_count,并获取应答结果。很简单,只需要注意不要随心所欲的设置交流的原子即可,会破坏服务器的可读性。
stop() -> 内部是一个cast,调用cast不会产生reply,对于即将关闭的服务,我们并不关心其返回值,所以调用cast而不是call。
第二部分就是回调函数段:
这几个是和开发给用户的三个接口是对应的,负责处理其中的逻辑问题。
init -> 初始化回调,启动gen_server容器进程时函数调用,首先其接受一个参数,参数代表用户想要监听的端口(值得注意的是,哪怕只有一个参数,也需要使用列表传递,这是一种规范),然后使用标准库中的tcp模块建立一个TCP监听,然后返回一个三元组,包含原子ok,初始化状态,和一个超时值0。(为什么是0,是希望init结束后立刻触发一次超时,立刻调用一次handle_info)。
handle_call -> 主要的处理函数,处理用户想要的业务。
handle_cast -> 异步回调,处理用户不太关心的操作,比如这里的stop
handle_info -> 带外消息,采用call和cast以外的手段发送的信息都由该方法处理,当我们的服务器需要与第三方模块通信,但是这种通信依赖直接消息通信而不是OTP库调用时,就需要使用这种方式。
内部函数区,这里的三个函数都是内部函数,因为不是本次分析的重点,所以这里简单介绍下功能,主要是四个作用:切分输入、解析函数参数、执行请求调用以及回传结果
接下来就是编译启动服务器:
很简单 一次启动成功,接下来我们用telnet测试tcp服务器是否运作
不过使用telnet需要该项服务处于启用状态
如图所示我们需要去系统中打开telnet才可以使用
打开后我们在cmd或者其他shell中使用
telnet localhost 8081
即可链接到tcp服务器,我们关闭shell
可以看到erl的shell中成功打印了因为主动断开产生的tcp_closed信号,虽然我们没有处理tcp_closd的信号,但是也成功证明我们的服务器架设成功并且通过tcp成功链接