这几天一直在看Erlang,用的当然是Joe大爷亲自写的那本书了。
前面语法看着没什么问题了,但是当看到OTP的时候,一下回不过神来。而Joe大爷又说这是整本书最重要的一章,一遍看不懂就两遍,两遍不行就百遍,反正你小子只要想学erlang就得把OTP的思想塞进脑子里。没办法,想要和erlang成为好朋友,他爹的话还是要听的。
OTP是Open Telecom Platform的缩写,我第一次也被这个名字搞懵了,心说这应该是一个开发电信应用的平台吧,实际上我低估了爱立信那帮人的实力,这是一个很强大并且很通用的框架,它能帮我们编写大型的、容错的,分布式的系统。
我们每学一样东西都要把握它的中心思想,OTP的中心思想就是把程序的通用部分和业务部分切开,我帮你把通用部分做好,你做你的业务逻辑就行了。如果你看过一些分布式系统,最火的应该是分布式存储系统了,像Amazon的Dynamo、Google的Bigtable、LiveJournal的memcache等,就知道保证分布式系统的容错能力、数据一致性、请求高并发性是非常困难的。但是他们所用的算法都非常固定,当你去实现另一个分布式系统时,很可能就是参考他们的算法。然后一些技术功底不够的程序员就想,要是能把这些基础的设施都做好,并且提供插件扩展,那多好啊,这样当我想写个分布式数据库的时候就能跟写普通数据库一样简单了。没错,OTP就是帮你干那些又脏又累的活。
下面我们就来具体分析OTP是怎样把所有系统的通用部分提取出来的。不过在这之前我们有必要讨论下面向对象的观点,因为我个人觉得用面向对象的观点更容易理解OTP的思想,并且erlang语言把面向对象运用的更出神入化,这与它的两大特性有关系 —— 动态类型和函数式编程。
什么是面向对象?封装、继承、多态是面向对象的必有属性吗?这些问题太高深了,我回答不了,我只能做出如下理解:
面向对象是着这样一种看待系统的思想:系统就是若干对象相互交流信息时组成的有机整体。
比如,中国大陆的人之间互相交流信息就形成了中国大陆这个人类社会系统。一个公司内部员工交流信息就形成了公司系统。
所以一个系统由两部分组成,对象和对象之间的交流。对象间的交流就是通过发送消息呗,比如说我跟食堂师傅说“来碗牛肉面”,向某MM说“I Love You“。下面我们深入分析对象这个概念,以人类社会为例。
人类社会是由人组成的,人与人之间交流信息,社会就运转起来了。那么人在交流信息时是怎样一种思想感情呢,我跟食堂师傅要牛肉面他会给羊肉的吗?我向MM求爱她会送入我怀中还是一脚把我踢开呢?人们到底是怎么做决定的呢?拿MM说事儿吧,因为我最近被一个女人搞得心烦。MM首先对你有些认识,不管是长期交往还是第一印象,然后你向她求爱,她就会根据那些已有的认识用自己的方法去判断拒绝还是接受,并且向她求爱这种行为也为影响她对你的看法(比如一个平常内向沉默寡言的人在大街上大喊‘我爱你’)。我们把MM对你的印象称为状态,MM的判断方法称为逻辑,向她求爱就是向她发送消息,然后MM就会根据状态运用自己的逻辑作出答复,并且修改自己对你的认识,也就是自己的状态。这样我们就抽象出了所有物体交流的方式。
发送的消息称为Request,发送消息的对象是client,接受消息的对象server,server接受Request后根据State用Logic计算出Response返回给client,并得出新的State。我们用erlang代码表达这一模型,ServerLogic在erlang中表现为module,里面包含各种函数
1 loop(ServerName, ServerLogic, ServerState) ->
2 receive
3 {ClientName, Request} ->
4 {Response, NewServerState} = ServerLogic:handle(Response, ServerState),
5 ClientName ! {ServerName, Response},
6 loop(ServerName, ServerLogic, NewServerState)
7 end.
8
代码中注意两点,ServerName和ClientName是用来标识Erlang进程的,也就是我们这里说的对象; end之前又重新调用了loop函数,因为对象一直都在接受消息,如果你只想接受一次,那么就不要再loop了,不过加loop合理一点,我们人就每时每刻都在接受消息。
对象(进程)不是无缘无故产生的吧,美猴王还要爆炸一下从石头里出来呢,所以我们还要提供一个创建该对象的函数,相当于C++的new:
start(ServerName,ServerLogic, ServerState) ->
register(ServerName,spawn(Fun()->loop(ServerName, ServerLogic, ServerState) end)).
这样当我们调用start时,就会创建一个运行loop函数的进程,相当于一个叫ServerName的人刚从娘胎里出来,然后就马上准备接受消息。你有没有发现这跟书上的有点不一样,书上的start函数接受2个参数,而这里接受3个。别急,进程刚刚被创建时是什么状态呢,谁知道呢?所以我们把设定初始状态的方法放进ServerLogic模块,对应于C++,就是把构造函数和成员函数放在一起。构造函数叫init,所以start函数就变成了如下形式
start(ServerName,ServerLogic) ->
register(ServerName,spawn(Fun()->loop(ServerName, ServerLogic, ServerLogic::init() end)).
OK,现在我们可以创建进程了,进程也可以处理消息了,C++中是直接调用成员函数发消息,erlang是怎样的呢,客户端代码如下:
1 rpc(ServerName, Request) ->
2 ServerName ! {self(), Request},
3 receive
4 {ServerName, Response} = Response
5 end.
综上所述,我们写的通用框架总共包含3部分,一个创建服务器的函数start/2,一个处理消息的函数loop/3,一个接受消息的函数rpc/2, 把这三个函数都放在gen_server中(这个是我们自己写的gen_server),OPT自己有个gen_server,功能当然比这个强多了。这个gen_server只是展示了如何得到一个generic server。我们写的gen_server代码如下,没有代码高亮,罪过。
- module(gen_server). % 因为这是所有服务器的通用模块, 所以叫 generic server
- export([start / 2 , rpc / 2 ]).
% 创建服务器的函数
% 特定服务器模块ServerLogic中必须要由 init / 0 函数,用来初始化服务器的状态
start(ServerName, ServerLogic) ->
register(ServerName,spawn(Fun() -> loop(ServerName,ServerLogic, ServerLogic:init()) end)).
% 服务器是如何处理请求的呢
loop(ServerName, ServerLogic, ServerState) ->
receive
{ClientName, Request} ->
{Response, NewServerState} = ServerLogic:handle(Request, ServerState),
ClientName ! {ServerName, Response},
loop(ServerName, ServerLogic, NewServerState}
end.
% 供客户端调用的代码
rpc(ServerName, Request) ->
ServerName ! {self(), Request},
receive
{ServerName, Response} -> Response
end.
上文展示了如何用面向对象的思想理解OTP,可以看出OTP的实现与erlang语言的动态性和高阶函数密不可分。