Ejabberd源码学习——端口监听及报文转发流程
来源:互联网 发布:网络销售月收入是多少 编辑:程序博客网 时间:2024/06/18 03:40
这篇文章是我之前在RYTong内部分享的一篇文章。上一篇文章说到Ejabberd在启动的时候会监听配置的端口,但没有详细解释监听的流程。这篇我们就来看看Ejabberd监听端口的实现逻辑,了解下一个XMPP实体如何连接到Ejabberd,Ejabberd又是如何将该实体发送的报文转发给目标实体的。
端口监听与连接建立
在介绍启动流程时已经提到Ejabberd会使用ejabberd_listener:start/3方法创建一个Worker进程,这个进程负责接收TCP连接。start/3方法最终会使用gen_tcp:listen/2函数监听指定端口,并调用本地accept/3方法来接收TCP连接。
accept(ListenSocket, Module, Opts) -> case gen_tcp:accept(ListenSocket) of {ok, Socket} -> case {inet:sockname(Socket), inet:peername(Socket)} of {{ok, Addr}, {ok, PAddr}} -> ?INFO_MSG("(~w) Accepted connection ~w -> ~w", [Socket, PAddr, Addr]); _ -> ok end, CallMod = case is_frontend(Module) of true -> ejabberd_frontend_socket; false -> ejabberd_socket end, CallMod:start(strip_frontend(Module), gen_tcp, Socket, Opts), accept(ListenSocket, Module, Opts); {error, Reason} -> ?ERROR_MSG("(~w) Failed TCP accept: ~w", [ListenSocket, Reason]), accept(ListenSocket, Module, Opts) end.
如上,accept/3方法会调用gen_tcp:accept/1方法来接收连接,并使用ejabberd_socket:start/4函数来处理每个连接。ejabberd_socket:start/4函数会为每个连接启动两个进程:一为ejabberd_receiver,另一个为使用listen配置项中该端口配置的Module(ejabberd_c2s或ejabberd_s2s)的start/2方法启动的进程。
{ReceiverMod, Receiver, RecRef} = case catch SockMod:custom_receiver(Socket) of {receiver, RecMod, RecPid} -> {RecMod, RecPid, RecMod}; _ -> RecPid = ejabberd_receiver:start( Socket, SockMod, none, MaxStanzaSize), {ejabberd_receiver, RecPid, RecPid} end, SocketData = #socket_state{sockmod = SockMod, socket = Socket, receiver = RecRef}, case Module:start({?MODULE, SocketData}, Opts) of {ok, Pid} -> case SockMod:controlling_process(Socket, Receiver) of ok -> ok; {error, _Reason} -> SockMod:close(Socket) end, ReceiverMod:become_controller(Receiver, Pid);
从SockMod:controlling_process(Socket, Receiver)这句话我们大概可以推测出ejabberd_receiver进程应该是用来接收TCP包的,通过其源码可以验证这个推测。ejabberd_receiver进程在接收到TCP包后会进行XML解析,并将解析后的结果发给controller进程。这个controller进程就是ejabberd_c2s或ejabberd_s2s启动的进程。在controller进程启动成功之后会执行ReceiverMod:become_controller(Receiver, Pid)方法,将controller进程的pid告诉ejabberd_receiver进程。
我们以ejabberd_c2s为例,看一下controller进程是做什么的。通过ejabberd_c2s的代码,可以知道其是一个gen_fsm实现的状态机进程,有wait_for_stream、wait_for_auth、session_established等等状态。
初始状态为wait_for_stream,然后通过与Client端的一些列XML报文的交互来建立会话,成功建立后会进入session_established状态。这时该Client端就可以与其他实体交互了。
报文转发
当XMPP Client与Ejabberd成功建立会话之后,便可以向其他XMPP实体发送报文。当这个报文通过TCP连接发到Ejabberd后,该连接对应的ejabberd_receiver进程会对报文进行解析,并交给对应的ejabberd_c2s进程处理,最终ejabberd_c2s进程会根据报文根节点的from和to属性的JID调用ejabberd_router:route(FromRoute, To, Packet)方法将这个报文转发给对应的目标实体。代码如下:
check_privacy_route(From, StateData, FromRoute, To, Packet) -> case privacy_check_packet(StateData, From, To, Packet, out) of deny -> Lang = StateData#state.lang, ErrText = "Your active privacy list has denied the routing of this stanza.", Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), ejabberd_router:route(To, From, Err), ok; allow -> ejabberd_router:route(FromRoute, To, Packet) end.
在继续研究ejabberd_router的代码前,我们先来了解一下JID的概念。JID在XMPP协议里相当于是用户ID,格式为user@server/resource。user相当于用户名,server是该用户所注册的主机,resource是用户所占用的资源。XMPP是允许同一用户在一个主机上建立多个会话的,这些会话将与resource绑定。
了解了JID的概念后,我们继续看看ejabberd_router的转发逻辑。route/3方法会调用do_route/3内部函数:
LDstDomain = To#jid.lserver, case mnesia:dirty_read(route, LDstDomain) of [] -> ejabberd_s2s:route(From, To, Packet); [R] -> Pid = R#route.pid, if node(Pid) == node() -> case R#route.local_hint of {apply, Module, Function} -> Module:Function(From, To, Packet); _ -> Pid ! {route, From, To, Packet} end; is_pid(Pid) -> Pid ! {route, From, To, Packet}; true -> drop end;
通过do_route/3代码我们可以知道,Ejabberd是通过mnesia的内存表route来存储转发的路由映射的,并以JID的server为键。根据to属性表示的目标server查找route表内的记录并转发。转发通过两种方式,一种为调用指定函数,此时route记录的local_hint域须为{apply, Module, Function}。另一种方式为转发给对应进程,此进程为route记录的pid域。
route表的记录是通过ejabberd_router:register_route/1,/2来添加的。因此为了继续查看ejabberd_router转发之后的具体逻辑,我们就需要知道哪些模块添加了route记录。可以通过在代码中搜索register_route关键字来查找模块,也可以在启动Ejabberd之后查看mnesia route表直接查找记录。
为了继续研究转发逻辑,我们以ejabberd_local模块为例,看看它是如何工作的。
init([]) -> lists:foreach( fun(Host) -> ejabberd_router:register_route(Host, {apply, ?MODULE, route}), ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE, bounce_resource_packet, 100) end, ?MYHOSTS), catch ets:new(?IQTABLE, [named_table, public]), update_table(), mnesia:create_table(iq_response, [{ram_copies, [node()]}, {attributes, record_info(fields, iq_response)}]), mnesia:add_table_copy(iq_response, node(), ram_copies), {ok, #state{}}.
在ejabberd_sup启动后,会启动ejabberd_local Worker进程。ejabberd_local进程在初始化时会为每个虚拟主机添加一个route记录,并指定ejabberd_local:route/3方法来处理转发的报文。这样当XMPP Client向一个在Ejabberd虚拟主机上已注册的实体发送报文时(此时,to属性的JID server是Ejabberd已配置的一个虚拟主机),这个报文最终会调用ejabberd_local:route/3来完成转发。
if To#jid.luser /= "" -> ejabberd_sm:route(From, To, Packet); To#jid.lresource == "" -> {xmlelement, Name, _Attrs, _Els} = Packet, case Name of "iq" -> process_iq(From, To, Packet); "message" -> ok; "presence" -> ok; _ -> ok end; true -> {xmlelement, _Name, Attrs, _Els} = Packet, case xml:get_attr_s("type", Attrs) of "error" -> ok; "result" -> ok; _ -> ejabberd_hooks:run(local_send_to_resource_hook, To#jid.lserver, [From, To, Packet]) end end.
当目标JID为另一个XMPP用户时,判断条件To#jid.luser /= “”将会成立,此时又会调用ejabberd_sm:route(From, To, Packet)方法来处理。看到这里,本人已经有些崩溃了,这转发逻辑已经经过N多模块的处理了,什么时候才能熬成阿香婆啊。童鞋们再坚持下,让我们来终结它!
ejabberd_sm这个模块名很多人咋一看容易误解为Ejabberd的某种特殊癖好(-_-*),在此奉劝大家少看点两三个人演的电影!ejabberd_sm是Ejabberd Session Manager的缩写,用来管理与XMPP Clients建立的会话。ejabberd_sm进程在初始化时会创建mnesia内存表session,当XMPP Client与Ejabberd成功建立会话之后,ejabberd_c2s进程会调用ejabberd_sm:open_session/5方法(可在wait_for_session和wait_for_auth状态下找到调用代码),此时session会增加一条记录并将该ejabberd_c2s进程的pid保存。
USR = {LUser, LServer, LResource}, case mnesia:dirty_index_read(session, USR, #session.usr) of [] -> case Name of "message" -> route_message(From, To, Packet); "iq" -> case xml:get_attr_s("type", Attrs) of "error" -> ok; "result" -> ok; _ -> Err = jlib:make_error_reply( Packet, ?ERR_SERVICE_UNAVAILABLE), ejabberd_router:route(To, From, Err) end; _ -> ?DEBUG("packet droped~n", []) end; Ss -> Session = lists:max(Ss), Pid = element(2, Session#session.sid), ?DEBUG("sending to process ~p~n", [Pid]), Pid ! {route, From, To, Packet} end
在执行ejabberd_sm:route/3时,会根据目标JID来查找session记录并获取ejabberd_c2s进程的pid,向其发送{route, From, To, Packet}消息。我们可以在ejabberd_c2s的handle_info方法中找到处理{route, From, To, Packet}的逻辑。这段逻辑很复杂,就不给大家贴代码了。经过一系列验证之后,最终会调用gen_tcp或tls的send方法将报文发给目标实体。
至此,Ejabberd报文转发的一个分支流程就介绍完了。简单总结一下:
XMPP Client发送一个包 -> ejabberd_receiver进程接收并解析,将解析的数据发给ejabberd_c2s进程 -> ejabberd_c2s进程调用ejabberd_router:route(FromRoute, To, Packet)将包转发 -> ejabberd_router:route/3会调用ejabberd_local:route/3 -> ejabberd_local:route/3会调用ejabberd_sm:route/3 -> ejabberd_sm:route/3根据目标JID查找对应的ejabberd_c2s进程,向其发送{route, From, To, Packet}消息 -> ejabberd_c2s调用gen_tcp或tls的send方法将包发送给目标。
以上就是跟大家分享的全部内容了。虽然感觉写得很多很复杂,但也只是截取了Ejabberd的一小部分逻辑。看了Ejabberd代码之后,给人的感觉是Ejabberd程序猿们的代码规范真心不怎么样。各种三四层case语句嵌套,一个case分支上百行代码,有时候真的感觉不会再爱了。
感谢大家坚持看完,写得有问题、不清楚的地方还请大家多提宝贵意见。
- Ejabberd源码学习——端口监听及报文转发流程
- Ejabberd源码学习——启动流程
- 如何把Android手机变成一个WIFI下载热点? — 报文转发及DNS报文拦截
- iptables学习笔记--Netfilter的报文转发流程
- 三层交换机报文转发流程
- ejabberd源码分析《二》客户端网络监听
- LCX端口转发源码分析
- LCX端口转发源码分析
- SSH连接及端口转发
- PE框架——发送报文流程
- PostgreSQL学习第五篇--监听地址及端口修改
- iptables学习 03 端口转发
- Nginx80端口转发+域名——实现IP+端口隐藏
- PE框架学习之道:PE框架——发送报文流程
- ejabberd 源码编译
- 在linux下监听转发后的端口
- nginx监听一个端口多域名转发配置
- 初学Nginx(一)监听两个端口,实现转发
- TortoiseSVN 加锁,服务器地址更换,切换登录用户
- 90后IT团队的一个月:从濒临解散到起死回生
- 在MyEclipse、Eclipse中安装Properties插件
- 跑马灯效果
- 堆和栈的区别
- Ejabberd源码学习——端口监听及报文转发流程
- 4.JSON:JavaScript对象表示法
- JavaScript undefined与null的区别
- CodeForces 660D Number of Parallelograms
- Luajit笔记---关于如何FFI与C++代码交互
- iOS 9.3.1真机报错"could not find developer disk image"
- HTTP POST请求报文格式分析与Java实现文件上传
- 欢迎使用CSDN-markdown编辑器
- spring依赖注入使用小记