XMPP eJabberd 的消息路由

DavidAlphaFox · 发布于 2017年11月27日 · 599 次阅读
84794b

什么是路由

在网络工程上,定义路由(routing)是非常简单的,路由就是指分组从源到目的地时,决定端到端路径的网络范围的进程 。在eJabberd中,路由有这相似的定义,同样也是江西消息分组,从源送到目的地址。

ejabberd_c2s进程

ejabberd_c2s进程,是客户端在eJabberd服务进程中的代理(agent),全面负责一个客户端在服务进程中的所有的动作,包括认证,特性使用以及我们要说的路由。

因为ejabberd_c2s进程作为客户端在服务进程中的代理,就要注意以下几个事项

  • 所有incoming,代表由服务端发给客户端的消息,所有outcoming,代表客户端发给服务端的消息
  • 只代表一个客户端,并且和客户端的链接状况紧密相连
  • 不负责网络消息的解码操作,只负责协议的动作操作,包括认证,特性协商和路由

ejabberd_router模块

ejabberd_router模块在启动的时候,会创建一个进程,但是该进程并不负责消息的路由,只负责路由的部分原信息管理和路由的高级抽象。

eJabberd中路由种类

普通路由

这类路由,一般都是非常简单的消息,只需要经过简单的目的查找,即可将消息直接交付给目标进行处理。这种路由就像网络中路由一样可以存在多个层级,在eJabberd中是被做成分层处理的。

外部服务路由

这类路由,是代表那些不在eJabberd服务进程内的服务,需要通过特定方式将消息转发给特定服务,这种外部服务是通过某种方式在当前的eJabberd进程中注册的。

全局外部服务路由

这类路由,同样代表那些不在eJabberd服务进程内的服务,但是唯一区别的是,当前的eJabberd进程是无法直接访问的,需要通过eJabberd集群中特定eJabberd进程来完成访问。

s2s路由

这类路由,是eJabberd进行不同域和域之间互通的路由。只要双发都符合XMPP规范,并建立互相信任的链接后,就可以让A域下的用户和B域下的用户消息互通。

路由抽象

ejabberd_router将路由过程抽象为一个按序列进行的流水线,按照预先设定好的数据,将一个消息按序列流过各个路由模块,并且在路由模块中将消息处理分成两个阶段,一个阶段是filter,一个阶段是route。简单的说就是先进行初步过滤,在路由前尽可能过滤掉不需要的数据包,减少路由压力。

route(From, To, Packet, []) ->
    ?ERROR_MSG("error routing from=~ts to=~ts, packet=~ts, reason: no more routing modules",
               [jid:to_binary(From), jid:to_binary(To),
                exml:to_binary(Packet)]),
    mongoose_metrics:update(global, routingErrors, 1),
    ok;
route(OrigFrom, OrigTo, OrigPacket, [M|Tail]) ->
    ?DEBUG("Using module ~p", [M]),
    %% 先过滤数据
    case (catch M:filter(OrigFrom, OrigTo, OrigPacket)) of
        {'EXIT', Reason} ->
            %% 过滤阶段出现异常,记录下来
            %% 之后就不做过多的处理
            ?DEBUG("Filtering error", []),
            ?ERROR_MSG("error when filtering from=~ts to=~ts in module=~p, reason=~p, packet=~ts, stack_trace=~p",
                       [jid:to_binary(OrigFrom), jid:to_binary(OrigTo),
                        M, Reason, exml:to_binary(OrigPacket),
                        erlang:get_stacktrace()]),
            ok;
        drop ->
            %% 过滤后发现需要丢弃,就直接结束路由过程
            ?DEBUG("filter dropped packet", []),
            ok;
        {OrigFrom, OrigTo, OrigPacketFiltered} ->
            ?DEBUG("filter passed", []),
            %% 任何一个匹配且路由成功的模块,都可以直接结束
            case catch(M:route(OrigFrom, OrigTo, OrigPacketFiltered)) of
                {'EXIT', Reason} ->
                    ?ERROR_MSG("error when routing from=~ts to=~ts in module=~p, reason=~p, packet=~ts, stack_trace=~p",
                               [jid:to_binary(OrigFrom), jid:to_binary(OrigTo),
                                M, Reason, exml:to_binary(OrigPacketFiltered),
                                erlang:get_stacktrace()]),
                    ?DEBUG("routing error", []),
                    ok;
                done ->
                    %% 在该模块已经成功路由,就不再向下个模块进行传递了
                    ?DEBUG("routing done", []),
                    ok;
                {From, To, Packet} ->
                    ?DEBUG("routing skipped", []),
                    route(From, To, Packet, Tail)
            end
    end.

eJabberd优先进行全局过滤,接着进行常规消息路由,再接着进行外部服务的消息路由,最后进行s2s路由。

%% 默认的路由模块
default_routing_modules() ->
    [mongoose_router_global,%% 只使用filter_packet进行过滤,不进行任何路由操作
     mongoose_router_localdomain, %% 只路由本host和subhosts的消息
     mongoose_router_external_localnode, %% 本节点内外挂功能路由
     mongoose_router_external, %% 所有节点外挂功能路由
     ejabberd_s2s].

节点内路由

节点内路由,会将消息路由给用户和节点内的IQ handler。节点内路由是由ejabberd_local进行负责的,ejabberd_local在eJabberd启动的时候,会建立一个进程,用来管理本地注册的IQ handler。ejabberd_local在启动的时候,不单单建立了进程而且还将模块注册成host节点内默认路由。

当消息进行路由的时候,ejabberd_local会先查看JID的user部分是否是空的,非空的情况下交给ejabberd_sm处理,当是空的时候那么一定是服务,就交给IQ处理,剩下的消息一概都忽略。

do_route(From, To, Packet) ->
    ?DEBUG("local route~n\tfrom ~p~n\tto ~p~n\tpacket ~P~n",
           [From, To, Packet, 8]),
    if
        %% user部分不为空
        %% 说明是给用户的,或者特定服务的,所以需要让ejabberd_sm来处理这个路由信息
        To#jid.luser /= <<>> ->
            ejabberd_sm:route(From, To, Packet);
        %% resource部分为空,user部分为空了
        %% 如果是message或者presence的消息不进行任何处理
        To#jid.lresource == <<>> ->
            #xmlel{name = Name} = Packet,
            case Name of
                <<"iq">> ->
                    %% 如果是IQ信息,就交给IQ handler来进行处理
                    process_iq(From, To, Packet);
                <<"message">> ->
                    ok;
                <<"presence">> ->
                    ok;
                _ ->
                    ok
            end;
        true ->
            #xmlel{attrs = Attrs} = 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.

用户间消息路由

真正在做用户间消息路由的模块ejabberd_sm模块,该模块同样会创建一个进程,但是该进程同样不进行任何路由操作,只保存路由元信息。其中保存着每个客户端JID和ejabberd_c2s进程对应的关系,以及IQ和处理模块和进程对应的关系。

其中最核心的部分是,通过对session表的查找,将消息路由给特定的ejabberd_c2s进程,让该进程将消息发送给客户端。当无法找到相应的进程,就会进行离线存储处理。当然,当JID的resource部分为空的时候,会尝试匹配群组消息和IQ消息,如果是IQ消息的时候,会去查找sm_iqtable来确定IQ处理模块或进程,从而完成处理。

%% 进行普通路由
do_route(From, To, Packet) ->
    ?DEBUG("session manager~n\tfrom ~p~n\tto ~p~n\tpacket ~P~n",
           [From, To, Packet, 8]),
    #jid{ luser = LUser, lserver = LServer, lresource = LResource} = To,
    #xmlel{name = Name, attrs = Attrs} = Packet,
    %% 没有资源信息的路由里面包含IQ处理
    case LResource of
        <<>> ->
            %% 特殊处理
            do_route_no_resource(Name, xml:get_attr_s(<<"type">>, Attrs),
                                 From, To, Packet);
        _ ->
            case ?SM_BACKEND:get_sessions(LUser, LServer, LResource) of
                [] ->
                    %% 此处进行离线处理
                    do_route_offline(Name, xml:get_attr_s(<<"type">>, Attrs),
                                     From, To, Packet);
                Ss ->
                    Session = lists:max(Ss),
                    Pid = element(2, Session#session.sid),
                    ?DEBUG("sending to process ~p~n", [Pid]),
                    %% 向目标进程route消息
                    Pid ! {route, From, To, Packet}
            end
    end.

谁来完成路由

我们可以产出,eJabberd的路由是非常复杂的,如果只让一个进程来处理相关操作,会形成非常大的瓶颈。因此,eJabberd选择让发送者的ejabberd_c2s进程来进行路由操作,利用mnesia的高并发性和Erlang的变量不变特性,达到高效路由。

后续

eJabberd的路由虽然是个非常简单的部分,但是在eJabberd中确实重中之重。它的设计不单单影响整个eJabberd的消息转发性能,同时影响着eJabberd的扩展性。为什么会影响扩展性,将在后续介绍s2s和外部服务XEP-0114中详细说明。

共收到 0 条回复
84794b DavidAlphaFox eJabberd 的 IQ 处理 中提及了此贴 11月28日 21:05
84794b DavidAlphaFox eJabberd 的服务发现 中提及了此贴 12月01日 00:01
需要 登录/注册 后方可回复, 如果你还没有账号请点击这里 注册