ejabberd用户失去连接时的在线状态

25

我已经配置了ejabberd作为移动应用之间的xmpp服务器,即自定义的iPhone和Android应用程序。

但是我似乎遇到了ejabberd处理在线状态的限制。

情景:

  • 用户A正在通过他们的手机与用户B聊天。
  • 用户B失去所有连接,因此客户端无法与服务器断开连接。
  • ejabberd仍将用户B列为在线。
  • 由于ejabberd假定用户B仍然在线,因此来自用户A的任何消息都会传递到失效的连接。
  • 因此,用户B将无法收到消息,并且该消息也不会保存为离线消息,因为ejabberd认为该用户在线。
  • 消息丢失。
  • 直到ejabberd意识到该连接已过期,才将其视为在线用户。

如果考虑数据连接更改(wifi到3G到4G等),则会发现这种情况发生得很频繁。

mod_ping:

我尝试在10秒间隔上实现mod_ping。
https://www.process-one.net/docs/ejabberd/guide_en.html#modping
但正如文档所述,ping将等待32秒以获取响应,然后才会断开用户的连接。
这意味着会有一个42秒的时间窗口,用户可能会失去他们的消息。

理想解决方案:

即使可以减少ping等待时间,它仍然不是完美的解决方案。
ejabberd是否可以在放弃消息之前等待客户端的200响应?如果没有响应,则将其保存为脱机消息。
是否可以编写钩子来解决此问题?
或者我错过了某个简单的设置吗?

FYI:我没有使用BOSH。


不完全是答案,但可能对某些人有用:我通过使用具有(社区贡献的)XEP-198插件(称为smacks)的prosody jabber服务器解决了一个问题。维基百科列出了支持198的几个其他服务器,但prosody是默认Debian存储库中唯一的一个。在(Android)客户端方面,我使用了Yaxim。 - Slartibartfast
6个回答

14

这是我编写的修复问题的模块。

要使其正常工作,需要在客户端激活收据并且客户端应该能够处理重复消息。

首先我创建了一个名为confirm_delivery的表。我将每个“聊天”消息保存到该表中。我设置了10秒的定时器,如果我收到确认回复,则删除表条目。

如果我没有收到确认回复,则手动将消息保存到offline_msg表中,并尝试重新发送它(这可能有点过头,但这取决于您),然后从我们的confirm_delivery表中将其删除。

我剪掉了所有我认为不必要的代码,所以我希望这仍然可以编译。

希望这对其他ejabberd开发人员有所帮助!

https://github.com/johanvorster/ejabberd_confirm_delivery.git


%% name of module must match file name
-module(mod_confirm_delivery).

-author("Johan Vorster").

%% Every ejabberd module implements the gen_mod behavior
%% The gen_mod behavior requires two functions: start/2 and stop/1
-behaviour(gen_mod).

%% public methods for this module
-export([start/2, stop/1, send_packet/3, receive_packet/4, get_session/5, set_offline_message/5]).

%% included for writing to ejabberd log file
-include("ejabberd.hrl").

-record(session, {sid, usr, us, priority, info}).
-record(offline_msg, {us, timestamp, expire, from, to, packet}).

-record(confirm_delivery, {messageid, timerref}).

start(_Host, _Opt) -> 

        ?INFO_MSG("mod_confirm_delivery loading", []),
        mnesia:create_table(confirm_delivery, 
            [{attributes, record_info(fields, confirm_delivery)}]),
        mnesia:clear_table(confirm_delivery),
        ?INFO_MSG("created timer ref table", []),

        ?INFO_MSG("start user_send_packet hook", []),
        ejabberd_hooks:add(user_send_packet, _Host, ?MODULE, send_packet, 50),   
        ?INFO_MSG("start user_receive_packet hook", []),
        ejabberd_hooks:add(user_receive_packet, _Host, ?MODULE, receive_packet, 50).   

stop(_Host) -> 
        ?INFO_MSG("stopping mod_confirm_delivery", []),
        ejabberd_hooks:delete(user_send_packet, _Host, ?MODULE, send_packet, 50),
        ejabberd_hooks:delete(user_receive_packet, _Host, ?MODULE, receive_packet, 50). 

send_packet(From, To, Packet) ->    
    ?INFO_MSG("send_packet FromJID ~p ToJID ~p Packet ~p~n",[From, To, Packet]),

    Type = xml:get_tag_attr_s("type", Packet),
    ?INFO_MSG("Message Type ~p~n",[Type]),

    Body = xml:get_path_s(Packet, [{elem, "body"}, cdata]), 
    ?INFO_MSG("Message Body ~p~n",[Body]),

    MessageId = xml:get_tag_attr_s("id", Packet),
    ?INFO_MSG("send_packet MessageId ~p~n",[MessageId]), 

    LUser = element(2, To),
    ?INFO_MSG("send_packet LUser ~p~n",[LUser]), 

    LServer = element(3, To), 
    ?INFO_MSG("send_packet LServer ~p~n",[LServer]), 

    Sessions = mnesia:dirty_index_read(session, {LUser, LServer}, #session.us),
    ?INFO_MSG("Session: ~p~n",[Sessions]),

    case Type =:= "chat" andalso Body =/= [] andalso Sessions =/= [] of
        true ->                

        {ok, Ref} = timer:apply_after(10000, mod_confirm_delivery, get_session, [LUser, LServer, From, To, Packet]),

        ?INFO_MSG("Saving To ~p Ref ~p~n",[MessageId, Ref]),

        F = fun() ->
            mnesia:write(#confirm_delivery{messageid=MessageId, timerref=Ref})
        end,

        mnesia:transaction(F);

    _ ->
        ok
    end.   

receive_packet(_JID, From, To, Packet) ->
    ?INFO_MSG("receive_packet JID: ~p From: ~p To: ~p Packet: ~p~n",[_JID, From, To, Packet]), 

    Received = xml:get_subtag(Packet, "received"), 
    ?INFO_MSG("receive_packet Received Tag ~p~n",[Received]),    

    if Received =/= false andalso Received =/= [] ->
        MessageId = xml:get_tag_attr_s("id", Received),
        ?INFO_MSG("receive_packet MessageId ~p~n",[MessageId]);       
    true ->
        MessageId = []
    end, 

    if MessageId =/= [] ->
        Record = mnesia:dirty_read(confirm_delivery, MessageId),
        ?INFO_MSG("receive_packet Record: ~p~n",[Record]);       
    true ->
        Record = []
    end, 

    if Record =/= [] ->
        [R] = Record,
        ?INFO_MSG("receive_packet Record Elements ~p~n",[R]), 

        Ref = element(3, R),

        ?INFO_MSG("receive_packet Cancel Timer ~p~n",[Ref]), 
        timer:cancel(Ref),

        mnesia:dirty_delete(confirm_delivery, MessageId),
        ?INFO_MSG("confirm_delivery clean up",[]);     
    true ->
        ok
    end.


get_session(User, Server, From, To, Packet) ->   
    ?INFO_MSG("get_session User: ~p Server: ~p From: ~p To ~p Packet ~p~n",[User, Server, From, To, Packet]),   

    ejabberd_router:route(From, To, Packet),
    ?INFO_MSG("Resend message",[]),

    set_offline_message(User, Server, From, To, Packet),
    ?INFO_MSG("Set offline message",[]),

    MessageId = xml:get_tag_attr_s("id", Packet), 
    ?INFO_MSG("get_session MessageId ~p~n",[MessageId]),    

    case MessageId =/= [] of
        true ->        

        mnesia:dirty_delete(confirm_delivery, MessageId),
        ?INFO_MSG("confirm_delivery clean up",[]);

     _ ->
        ok
    end.

set_offline_message(User, Server, From, To, Packet) ->
    ?INFO_MSG("set_offline_message User: ~p Server: ~p From: ~p To ~p Packet ~p~n",[User, Server, From, To, Packet]),    

    F = fun() ->
        mnesia:write(#offline_msg{us = {User, Server}, timestamp = now(), expire = "never", from = From, to = To, packet = Packet})
    end,

    mnesia:transaction(F).    

谢谢您提供的代码。请问这个模块能否与ejabberd 2.1.11一起使用?我该如何编译这个模块呢?比如说,如何创建ejabberd_confirm_delivery.beam文件?希望您能尽快回复! - Dev
1
是的,在ejabberd 2.1.11上可以运行。运行erlang shell,指向文件保存的目录并使用c(mod_confirm_delivery)命令。这应该会为您生成一个beam文件。http://www.erlang.org/documentation/doc-5.3/doc/getting_started/getting_started.html - Johan Vorster
我是ejabbered的新手。我正在使用mod_offline_odbc。如何将离线消息存储到ODBC而不是Mnesia? - Vishnu Pradeep
@JohanVorster 我不知道你是怎么理解我的问题的,我想知道如何修改上述ejabberd模块以使用odbc而不是Mnesia数据库。 - Vishnu Pradeep
@Heisenberg 你好 Heisenberg,我现在没有任何可用的ejabberd环境,因此我无法对14.07上发生的情况进行评论。你可以考虑发布有关你问题的问题,或者与该帖子中的其他人联系,因为他们可能会遇到同样的问题。 - Johan Vorster
显示剩余15条评论

5
这是TCP连接的一个众所周知的限制。您需要引入一些确认功能。
其中一个选项是xep-0184。消息可以携带收据请求,当它被传递时,收据会回到发送者。
另一个选项是xep-0198。这是流管理,可以确认数据包。
您还可以完全在应用程序层实现并从接收者发送消息给发送者。在未收到确认时要相应地采取行动。
请注意,发送者 -> 服务器连接也可能以这种方式中断。
我不知道ejabberd中是否有这些xep和功能的实现。根据项目要求,我自己实现了它们。

2
不幸的是,ejabberd不支持xep-0198。我们已经实现了xep-0184,但ejabberd服务器实际上并不验证收据,它只是将其传递回发送方。因此没有服务器验证来查看消息是否已被接收。我可能需要在发送消息之前每次ping客户端以查看他们是否仍然连接。这可能比每10秒钟ping所有连接的客户端更少的开销。 - Johan Vorster
我同意Johan Vorster的观点。 - Chathura Wijesinghe

2

ejabberd最新版本默认支持流管理。在大多数移动库(如Android的Smack和iOS的XMPPFramework)中都已实现。

这是目前XMPP规范的最新技术水平。


1
干得好!!感谢你的团队!!我相信很多人都在期待这个功能! - Johan Vorster
流管理究竟如何解决这个问题? - John Yepthomi

1
我认为更好的方式是,如果消息未被接收,则将用户设为离线,并将消息存储在离线消息表中,然后使用推送服务并配置其用于离线消息。
然后会发送一个推送通知,如果有更多的消息,它们将被存储在离线消息中。为了在服务器上理解消息未被接收,您可以使用此https://github.com/Mingism/ejabberd-stanza-ack
我认为Facebook在消息无法投递时也采用了类似的方式,使用户离线直到再次上线。

Johan Vorster的模块非常好。我们在ejabberd 14.12上安装了它,但是没有起作用。我们应该做哪些更改才能使其在ejabberd 14.12上正常工作? - user3503159
看起来ejabberd在13.10版本中更改了INFO_MSG宏和send_packet参数。 - Johan Vorster

1
在ejabberd上实现XEP-198相当复杂。Erlang Solutions(我为他们工作)为ejabberd提供了一个名为XEP-184的模块,具有增强功能,可以解决此问题。它在服务器端执行缓冲和验证。只要客户端发送携带回执请求的消息并且消息被传递,回执就会返回给发送者。该模块验证收据以查看是否已接收消息。如果在超时时间内未接收到消息,则将其保存为离线消息。

我们使用收据 (XEP-184) 来验证接收方是否已收到消息。但通常情况下,如果一条消息丢失或离线保存,发送方是不知道的。 - Johan Vorster
1
我创建了一个模块并连接到了send_packet和receive_packet事件。将消息ID保存到表中。启动一个10秒的等待线程。如果receive_packet钩子在10秒内收到消息ID,则杀死线程,否则手动将消息存储在离线表中。现在最坏的情况是,我可能会在离线表中有两次相同的消息。但它们将具有相同的ID,我们的客户端知道不要重复消息。 - Johan Vorster
@JohanVorster 你好,我在使用ejabberd时遇到了类似的问题。你能否考虑分享一下你的mod?我会非常感激的,这将为我节省很多时间。谢谢! - Chris McCabe
@ChrisMcCabe 我已将代码添加为答案,请查看是否适用于您。 - Johan Vorster
@JohanVorster 谢谢Johan,我很感激! - Chris McCabe

0
Ejabberd在最新版本中默认支持流管理。在ejabberd_c2s中设置流管理器配置后,您应该在客户端中设置一些配置。请参阅此帖子以获取有关客户端中此配置的详细信息。https://community.igniterealtime.org/thread/55715

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接