ejabberd源码分析及开发系列(2) router模块分析

  router模块是xmpp 消息包在每个节点上的主router。它根据每个消息包的目的域对消息包进行路由。该模块有一张route表。首先根据消息包的目的地部分去搜索route表, 如果找到的话,就更加local_hint来判断是否进行相关的处理还是将该消息包路由到相应的进程,如果没有找到,就发送到S2S manager。

  下面来对ejabberd_router.erl源代码进行分析。

  ejabberd_router.erl实现了gen_server的behaviour用户实现异步的route功能。通过直接调用do_route可以实现同步的消息路由功能。

  首先是介绍用来存储路由信息的数据表,该表的结构如下

  

1 -record(route, {domain, pid, local_hint}).
View Code

  首先domain表示目的地的域,pid表示消息包的目的路由进程,local_hint就包含了对该消息包进行处理的相关信息。

  接着查看数据表的创建属性

1     mnesia:create_table(route,
2             [{ram_copies, [node()]},
3              {type, bag},
4              {attributes, record_info(fields, route)}]),
5    

  可以知道该数据表示一个bag类型,由此可得同一个domain可以对应多条route,这样是为了对可以根据不同的策略对消息路由进行均衡。均衡相关内容下面在详细分析

 

  在本模块中功能主要涉及两个函数一个是register_route, 另一个是do_route。

  首先对register_route进行分析。

 

 1 register_route(Domain, LocalHint) ->
 2     case jlib:nameprep(Domain) of 
 3       error -> erlang:error({invalid_domain, Domain}); 
 4       LDomain -> 
 5       Pid = self(), 
 6       case get_component_number(LDomain) of
 7         undefined ->
 8         F = fun () ->
 9                 mnesia:write(#route{domain = LDomain, pid = Pid,
10                         local_hint = LocalHint})
11             end,
12         mnesia:transaction(F);
13         N ->
14         F = fun () ->
15                 case mnesia:wread({route, LDomain}) of
16                   [] ->
17                   mnesia:write(#route{domain = LDomain,
18                               pid = Pid,
19                               local_hint = 1}),
20                   lists:foreach(fun (I) ->
21                             mnesia:write(#route{domain
22                                         =
23                                         LDomain,
24                                         pid
25                                         =
26                                         undefined,
27                                         local_hint
28                                         =
29                                         I})
30                         end,
31                         lists:seq(2, N));
32                   Rs ->
33                   lists:any(fun (#route{pid = undefined,
34                             local_hint = I} =
35                              R) ->
36                             mnesia:write(#route{domain =
37                                         LDomain,
38                                     pid =
39                                         Pid,
40                                     local_hint
41                                         =
42                                         I}),
43                             mnesia:delete_object(R),
44                             true;
45                         (_) -> false
46                         end,
47                         Rs)
48                 end
49             end,
50         mnesia:transaction(F)
51       end
52     end.

 

   首先第二行判断Domain的格式是否正确,错误通知错误, 正确的话就首先获得该请求进程的pid,然后根据Domain查找相应的组件的数据,如果是未定义的,则直接将一条route记录写进数据库用于以后的消息路由,如果是有定义的,则匹配到相应的组件数即N。然后根据Domain从数据库读出相应的route记录。如果记录为空, 则将构造一条route记录写进数据库,然后构造N-1条默认的route写进数据库。如果记录不为空,则则将其中一条默认的记录修改为本请求构造的有效记录。

  

  然后是对do_route进行分析。

  

 1 do_route(OrigFrom, OrigTo, OrigPacket) ->
 2     ?DEBUG("route~n\tfrom ~p~n\tto ~p~n\tpacket "
 3        "~p~n",
 4        [OrigFrom, OrigTo, OrigPacket]),
 5     case ejabberd_hooks:run_fold(filter_packet,
 6                  {OrigFrom, OrigTo, OrigPacket}, [])
 7     of
 8       {From, To, Packet} ->
 9       LDstDomain = To#jid.lserver,
10       case mnesia:dirty_read(route, LDstDomain) of
11         [] -> ejabberd_s2s:route(From, To, Packet);
12         [R] ->
13         Pid = R#route.pid,
14         if node(Pid) == node() ->
15                case R#route.local_hint of
16              {apply, Module, Function} ->
17                  Module:Function(From, To, Packet);
18              _ -> Pid ! {route, From, To, Packet}
19                end;
20            is_pid(Pid) -> Pid ! {route, From, To, Packet};
21            true -> drop
22         end;
23         Rs ->
24         Value = case
25               ejabberd_config:get_local_option({domain_balancing,
26                                 LDstDomain}, fun(D) when is_atom(D) -> D end)
27                 of
28               undefined -> now();
29               random -> now();
30               source -> jlib:jid_tolower(From);
31               destination -> jlib:jid_tolower(To);
32               bare_source ->
33                   jlib:jid_remove_resource(jlib:jid_tolower(From));
34               bare_destination ->
35                   jlib:jid_remove_resource(jlib:jid_tolower(To))
36             end,
37         case get_component_number(LDstDomain) of
38           undefined ->
39               case [R || R <- Rs, node(R#route.pid) == node()] of
40             [] ->
41                 R = lists:nth(erlang:phash(Value, length(Rs)), Rs),
42                 Pid = R#route.pid,
43                 if is_pid(Pid) -> Pid ! {route, From, To, Packet};
44                    true -> drop
45                 end;
46             LRs ->
47                 R = lists:nth(erlang:phash(Value, length(LRs)),
48                       LRs),
49                 Pid = R#route.pid,
50                 case R#route.local_hint of
51                   {apply, Module, Function} ->
52                   Module:Function(From, To, Packet);
53                   _ -> Pid ! {route, From, To, Packet}
54                 end
55               end;
56           _ ->
57               SRs = lists:ukeysort(#route.local_hint, Rs),
58               R = lists:nth(erlang:phash(Value, length(SRs)), SRs),
59               Pid = R#route.pid,
60               if is_pid(Pid) -> Pid ! {route, From, To, Packet};
61              true -> drop
62               end
63         end
64       end;
65       drop -> ok
66     end.

  首先对消息包进行进行hook的filter_packet处理(hook相关机制会在后面的文章进行分析),如果返回drop,则本函数返回ok, 如果返回新的{From, To, Packet} tuple,则首先取得路由的目的地LDstDomain, 然后根据LDstDomain从数据库里读出route记录,如果读出来的记录为空, 则将调用ejabberd_s2s:route(From, To, Packet), 交给S2S模块进行处理。如果记录不为空,则分为两种情况进行处理,一种是只有一条记录的情况, 一种是有多条记录的情况。对于只有一条记录的情况,则首先获得该记录的pid,然后判断该pid是否标识本节点的进程,如果是的话就根据local_hint判断,如果local是标识一个apply的话就直接调用改apply的方法, 如果不是的话就将该消息包路由到相应的进程进行处理。如果不是本地进程的话就判断该pid是否是一个真正的pid,如果是的话就将该消息路由到相应的进程,最后如果两者都不是的话,则丢弃该消息。对于有多条记录的情况, 首先获得用于hash的值Value,然后判断本目的地路由是否是具有组件number的路由,如果不是则后获得本地的route记录列表,如果本地的route记录列表为空,则从Rs列表中取一个进程,并向该进程路由消息包,如果本地route记录列表不为空, 则从本地LRs列表中取一个记录,并根据该记录的local_hint,进行如前面所示的类似处理。 如果是具有组件number的路由, 则根据local_hint进行排序获得新的SRs, 然后根据Value取得其中的一个route记录,然后将消息包路由到改进程。具体的过程就是这样。

posted @ 2016-02-15 17:05  wuguowoniu  阅读(778)  评论(0编辑  收藏  举报