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分支上百行代码,有时候真的感觉不会再爱了。

感谢大家坚持看完,写得有问题、不清楚的地方还请大家多提宝贵意见。

0 0