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}).
首先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记录,然后将消息包路由到改进程。具体的过程就是这样。