根据XEP-0199, XMPP客户端和服务器都可以在XML流上发送应用层PING请求。因为XMPP依赖底层的TCP连接,有可能TCP连接意外中断,而上层的XMPP并不知晓,从而影响消息传递。通过发送应用层PING请求可以来确认对端的连接可用性。
以服务器发给客户端为例,协议如下:
发送的PING请求:
1 | <iq from='capulet.lit' to='juliet@capulet.lit/balcony' id='s2c1' type='get'> |
如果对端支持PING请求,则返回对应的”PONG”回应。
1 | <iq from='juliet@capulet.lit/balcony' to='capulet.lit' id='s2c1' type='result'/> |
如果对端不支持则返回
1 | <iq from='juliet@capulet.lit/balcony' to='capulet.lit' id='s2c1' type='error'> |
ejabberd中PING功能实现位于mod_ping.erl。它主要支持3个配置:
- send_pings: true|false
如果这个选项设置为true, 当客户端在给定时间间隔内没有活动,则向客户端发送一个ping请求。
- ping_interval: Seconds
设置上述send_pings选项中客户端没有活动的时间间隔。
- timeout_action: none|kill
表示当PING请求发出32秒后,ejabberd依然没有收到PING响应,服务端如何处理。none表示什么也不做,kill表示关闭客户端连接。
当ejabberd启动时会调用mod_ping:start/2。
1 | start(Host, Opts) -> |
start函数调用supervisor:start_child/2为每个支持的host创建一个负责该host的worker进程。
进程树模型如下:
1 | +------------+ |
每个worker是一个gen_server进程,进程调用init函数进行初始化。
1 | init([Host, Opts]) -> |
首先获取相关配置
接着调用mod_disco:register_feature注册PING功能的XMLNS。这样当客户端请求”Service Discovery”信息时,ejabberd返回的特征中会包括”urn:xmpp:ping”。
ServiceDiscovery请求:
1 | <iq type='get' |
ServiceDiscovery响应:
1 | <iq type='result' |
ServiceDiscovery相关信息参考XEP-0030。
接下来,注册IQ处理器,令XMLNS为”urn:xmpp:ping”的IQ请求由函数iq_ping处理。iq_ping简单地返回相应响应或者错误。
1
2
3
4
5
6
7iq_ping(_From, _To, #iq{type = Type, sub_el = SubEl} = IQ) ->
case {Type, SubEl} of
{get, {xmlelement, "ping", _, _}} ->
IQ#iq{type = result, sub_el = []};
_ ->
IQ#iq{type = error, sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]}
end.如果send_pings配置为true, mod_ping在ejabberd中注册n以下3个hook函数:
sm_register_connection_hook
: 它在客户端完成登录验证,建立session信息时调用。
1 | open_session(SID, User, Server, Resource, Info) -> |
sm_remove_connection_hook
: 在用户退出,关闭session时调用。1
2
3
4
5
6
7
8
9
10
11
12
13
14close_session(SID, User, Server, Resource) ->
Info = case mnesia:dirty_read({session, SID}) of
[] -> [];
[#session{info=I}] -> I
end,
F = fun() ->
mnesia:delete({session, SID}),
mnesia:dirty_update_counter(session_counter,
jlib:nameprep(Server), -1)
end,
mnesia:sync_dirty(F),
JID = jlib:make_jid(User, Server, Resource),
ejabberd_hooks:run(sm_remove_connection_hook, JID#jid.lserver,
[SID, JID, Info]).user_send_packet
: 在C2S进程收到客户端发送的消息时被调用。
sm_register_connection_hook
的hook函数user_online
和user_send_packet
的hook函数user_send
都会调用start_ping函数。
1 | start_ping(Host, JID) -> |
start_ping向该HOST的worker进程发送一个{start_ping, JID}消息。worker进程调用handle_cast进行处理:
1 | handle_cast({start_ping, JID}, State) -> |
handle_cast调用add_timer为该客户端创建一个timer。
1 | add_timer(JID, Interval, Timers) -> |
由于用户每次发送消息时都会调用add_timer函数,因而add_timer中需要检查之前是否已经存在timer。如果存在,则先取消旧的timer, 再创建新的Timer。
当timer超时后,即客户若干时间内没有活动,进程收到{ping, JID}消息,此时ejabberd应向客户端发送PING消息。进程调用handle_info处理。
1 | handle_info({timeout, _TRef, {ping, JID}}, State) -> |
handle_info创建IQ消息后,设置回调函数F,调用ejabberd_local:route_iq/4消息IQ消息发送给客户端。当收到该IQ消息的响应或者超过32秒依然没有收到客户端的响应,回调函数F将会被调用。如果响应超时,Response为timeout,F将向进程发送{iq_pong, JID, timeout}消息。进程调用handle_cast处理。
1 | handle_cast({iq_pong, JID, timeout}, State) -> |
如果timeout_action设置为kill, 则调用ejabberd_c2s:stop关闭相应的客户端连接。
因为在sm_remove_connection_hook
注册了hook函数user_offline
, 当用户退出时会调用stop_ping函数,向worker进程发送{stop_ping, JID}消息。
1 | stop_ping(Host, JID) -> |
worker进程调用del_timer函数将该客户端的timer删除。
1 | handle_cast({stop_ping, JID}, State) -> |
1 | del_timer(JID, Timers) -> |
模块及进程停止的逻辑与模块和进程初始化的逻辑相反,本文略过。