开发基于TCP的RPC服务

一:RPC服务

该RPC服务器会监听TCP套接字并接受来自外来TCP客户端的连接。建立连接之后,客户端将可以通过TCP之上的简易ASCII文本协议执行函数调用。下图展示了该RPC服务器的设计和功能:

 注:RPC服务器进程通过套接字与外界相连。它通过TCP接收和执行请求,并将结果返回给客户端。

该图展示了两个进程。一个是监督进程;由它派生出实际的RPC服务器进程。第二个进程会创建一个监听套接字等待其他人的连接。当连接到来时,它会从连接上读取描述普通Erlang函数调用的ASCI文本,并在执行调用后将结果通过TCP流返回。这类功能在很多场合都能派上用场,包括在紧要关头下的远程管理和诊断。此外,该RPC服务器遵循建立在TCP流之上的一个看似标准Erlang函数调用的基本文本协议。该协议的一般形式为:

Module:Function(Arg1, ..., ArgN).

例如:

lists:append("Hello","Dolly").

为了解释这些请求,RPC服务器会解析ASCII文本,提取出模块名函数名参数,并将它们转换成合法的Erlang项式。接着它会执行请求中的函数调用,并将调用结果所对应的Erlang项式,格式化成ASCIl文本,最终通过TCP流回传给客户端。

 1.行为模式基础

行为模式是面向进程编程中各种常见模式的一种形式化表述。比如,服务器这个概念就非常通用,你所编写的进程之中很大一部分都符合这个概念。这些进程有很多共通之处——尤其是在遵循OTP监督规范等方面。每出现一种新的服务器进程就重新编写一遍这类代码是毫无意义的,这样做还会到处引人各种琐碎的bug和细微的差异。相反,OTP行为模式将这类反复出现的模式分成了两个部分:通用部分具体应用相关的实现部分。二者通过一套简单明确的接口进行通信。这次所编写的模块其实就是一个实现部分,它对应的正是OTP中最常见也最实用的行为模式:通用服务器,即gen_server

行为模式的组成部分
在日常交流中,行为模式这个词大有被滥用的嫌疑,它可指代下列多个概念:
(1)行为模式接口;
(2)行为模式实现;
(3)行为模式容器。

行为模式的接口是一组特定的函数和相关的调用规范。gen_server行为模式的接口包含六
个函数:init/1handle_call/3handle_cast/2handle_info/2terminate/2
code_change/3

所谓实现,指的是由程序员提供的具体应用相关的代码。行为模式的实现是一个导出了接口所需的全部函数的回调模块。实现模块中还应包含一项属性-behaviour(...),用以说明该模块所实现的行为模式的名称,这样编译器便可以协助检查模块是否完整地导出了接口所需的所有函数。下面代码就列出了模块的首部以及实现gen_server所必需的接口函数:

%% genserver行为模式最精简的实现模块
-module(...).

-behaviour(gen_server).

-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-record(state, {}).

init([]) ->
    {ok, #state()}.

handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok,State}.

如果遗漏了其中的某些函数,该行为模式实现就无法完全满足gen_server接口的要求,这种情况下编译器会给出警告。行为模式的第三个也是最后一个部分就是容器。容器是一个进程,它执行的是某个库模块中的代码,并且会调用与行为模式实现相对应的回调模块来处理应用相关的逻辑。(从技术角度说,容器也可以由多个密切相关的进程构成,但通常只有一个进程。)该库模块的名称与对应的行为模式的名称一致。库模块中包含行为模式中的通用代码,其中也包括新容器的启动函数。例如,对于gen_server行为模式而言,这部分代码就位于Erlang/OTP库中stdlib部分的gen server模块中。当你调用gen_server:start(..., foo, ...)时,就会创建一个新的以foo为回调模块
的gen_server容器。

2.行为模式的实例化

行为模式的目的在于为特定类型的进程提供一套模板。每个行为模式库中的模块都有一个或多个用于启动新的容器进程的API函数(通常名为start和/或start_1ink)。我们将新容器进程的启动称作行为模式的实例化。

二:实现RPC服务器

1.行为模式实现模块的典型布局

行为模式的一大优点就在于它们的高度一致性。查看行为模式实现模块时,你一眼就能识别出这类模块中诸如行为模式接口函数这样的公共部分,以及startstart_link函数等自定义的部分。采用下述的行为模式实现模块的典型布局还可以进一步增加这些文件的可辨识性。这份标准布局可分为4段。按照在源码文件中出现的顺序,下表分别列出了这些段落的详情:

标准行为模式实现模块中的源码段落


段落
 
描述有无导出函数EDoc标注
首部模块属性和样板内容N/A有,文件级别
API编程接口,描述外界如何与模块交互有,函数级别
行为模式接口行为模式接口所需的回调函数可选
内部函数API和行为模式接口函数的辅助函数可选

 我们将从模块首部开始,依次考察这些段落的实现细节。

2.模块首部

在创建模块的首部之前,你得先建立一个用于容纳它的文件。既然是在开发基于TCP的RPC服务器,不妨将源文件命名为r_server.erl。下面代码将展示r_server.erl的完整首部:

%% trserver..erl的完整首部
%% --------------------------------------------------------------------------------
%% @author Martin & Eric <erlware-dev@googlegroups.com>
%% [http://www.erlware.org]
%% @copyright 2008 Erlware
%% @doc RPC over TCP server. This module defines a server process that
%%      listens for incoming TCP connections and allows the user to
%%      execute RPC commands via that TCP stream.
%% @end
%% --------------------------------------------------------------------------------
-module(tr_server).

-behaviour(gen_server).

%% API
-export([
         start_link/1,
         start_link/0,
         get_count/0,
         stop/0
        ]).

%% gen_server callbacks
-export ([init/1, handle_call/3, handle_cast/2, handle_info/2, 
          terminate/2, code_change/3]).

%% 将SERVER设置为模块名
-define(SERVER, ?MODULE).

%% 定义默认端口
-define(DEFAULT_PORT,1055).

%% 用于保存进程状态
-record(state, {port, lsock, request_count = 0 }).

宏常被用于定义常量,这样一来要修改常量时只需修改一处代码即可。此处用宏定义了默认端口,并将SERVER定义成了模块名(服务器名后续还可能会修改,因此不要认为服务器名总是与模块名保持一致)。定义完宏,你还定义了一个记录,指定了该记录的名字和格式,这个记录将被用于保存服务器进程的运行时状态3。首部已经介绍完了,我们来看一下行为模式实现模块的下一个段落:API。

3. API段

模块的所有功能都是通过应用编程接口(API)提供给用户的((用户才不会关心你的实现细节)。对于通用服务器而言,用户主要完成以下两件事:

(1) 启动服务器进程;
(2) 向进程发消息(并获取应答)。

gen_server提供了3个主要的库函数来实现这些基本功能。如下表所示: 

gen_server实现API的库函数

库函数对应的回调函数描述
gen_server:start_link/4Module:init/1启动并链接一个gen_server容器进程
gen_server:call/2Module:handle_call/3向gen_server进程发送同步消息并等待应答
gen_server:cast/2Module:handle_cast/2向gen_serveri进程发送异步消息

基本上,API函数只对这些库函数做了一些简单包装,以便在用户面前屏蔽实现细节。展现这些函数的工作机理的最佳手段,就是用它们来实现rserver模块的API,代码如下所示:

%% server.erl的API段
%%=====================================================================
%% API
%%=====================================================================

%% --------------------------------------------------------------------
%% (段落起始处的标题)
%% @doc Starts the server.
%%
%% @spec start_link(Port:integer()) -> {ok, Pid}
%% where
%% Pid = pid()
%% @end
%% --------------------------------------------------------------------

%% 派生服务器进程
start_link(Port) ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []).

%% @spec start._link() -> {ok, Pid}.
%% @doc Calls 'start_link(Port)' using the default port.
start_link() ->
    start_link(?DEFAULT_PORT).

%% --------------------------------------------------------------------
%% @doc Fetches the number of requests made to this server.
%% @spec get_count() -> {ok, Count}
%% where
%% Count = integer()
%% @end
%% --------------------------------------------------------------------

%% 调用方会等待应答
get_count() ->
    gen_server:call(?SERVER, get_count).

%% --------------------------------------------------------------------
%% @doc Stops the server.
%% @spec stop() -> ok
%% @end
%% --------------------------------------------------------------------

%% 无须坐等应答
stop() ->
    gen_server:cast(?SERVER, stop).

“调用方会等待”使用的是gen_server:cal1/2,发送请求后调用方必须坐等应答。而“无须坐等应答”这类简单命令通常会选用异步的gen_server:cast/2来实现。

4.回调函数段

你在API中用到的每个gen_server库函数都有一个gen_server行为模式接口指定的回调函数与之对应。现在该实现这些回调了。下表就是一些gen_server的库函数及回调:

gen_server的库函数及回调

库函数对应的回调函数描述
gen_server:start_link/4Module:init/1启动并链接一个gen_server容器进程
gen_server:call/2Module:handle_call/3向gen_server进程发送同步消息并等待应答
gen_server:cast/2Module:handle_cast/2向gen_serveri进程发送异步消息
N/AModule:handle_info/2处理通过call或cast函数以外的手段发送给gen_server容器的消息。这些都是带外
(out-of-band)消息

但请注意handle_info/2没有对应的gen_server库函数。这个回调是一个重要的特例,所有未经callcast库函数发送至gen_server信箱的消息都由它处理(通常是直接用!运算符发送的裸消息)。出于多种原因,gen_server容器的信箱中会出现这类消息一例如,回调代码有可能向第三方请求数据。就你的RPC服务器而言,它接收的TCP数据离开套接字之后便会被转换为普通消息发送给服务器进程。下面代码就是要实现的回调函数:

%% tr_server的gen_server回调
%%=================================================================
%% gen_server callbacks
%%=================================================================
%% 初始化服务器
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}.

%% 关闭gen_server
handle_cast(stop, State) ->
    {stop, normal, State}.

初始化服务器时,init函数会创建一个TCP监听套接字,设置初始的状态记录,还会立即触发一次超时。然后,是用于告知客户端进程当前已处理请求数的代码。在最后一个函数中,返回值stop比较特殊,用于让gen_server进程退出。

三:运行RPC服务器

执行前的第一步就是编译代码。执行命令erlc tr_server.erl。没有出现错误的话,当前目录下就会多出一个名为tr_server.beam的文件。在同一目录下启动Erlang shell,然后启动服务器:

Eshell V5.6.2 (abort with G)
1> tr_server:start_link(1055).
[ok, <0.33.0>]

此处我们选了一个很好记的端口号1055(10 = 5 + 5)。start_link调用返回了一个元组, 包含原子。ok和新服务器进程的进程标识符(不过当前你还用不上它)。接下来,向1055端口发起一个Telnet会话。在大部分系统上,直接在系统shell提示符下(不是Erlang shell)输人telnet localhost1055即可(不过,某些版本的Windows上没有Telnet——需要的话可以去下载一个免费的Telnet2客户端,比如PuTTY)。例如:

$ telnet localhost 1055
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
init:stop().
ok
Connection closed by foreign host.

首次会话成功!为什么这么说?让我们来仔细分析一下整个会话过程,看看到底发生了些什么。
首先,你用Telneti通过TCP的1055端口连接至运行中的tr_server。连接成功后,你输人了
文本init:stop(),服务器随即读取并解析这段文本。可以预料到服务器将会调用apply(init,stop,
[]
)。同时你也知道init:stop/0会返回原子ok,这与你看到的打印结果相符。不过接下来你看到的却是“Connection closed by foreign host.”。这是Telnet打印的,因为突然间远程端连接的套接字被关闭了。这是因为init:stop()关闭了运行着RPC服务器的整个Erlang节点。这个例子不仅演示了RP℃服务器的工作原理,还演示了让人不受限制地在你的节点上随意运行代码是多么危险!你可以在后续的改进版本中对用户做出限制,令用户只能调用特定的函数,你甚至还可以配置这些限制。

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明明如皓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值