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

时间:2013-07-02 11:21:51

标签: erlang xmpp ejabberd

我将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响应?如果没有响应,则将其保存为脱机。
是否可以编写一个钩子来解决这个问题?
或者是否有一个我错过的简单设置?

仅供参考:我没有使用BOSH。

6 个答案:

答案 0 :(得分:11)

这是我写的mod修复了我的问题。

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

首先,我创建了一个名为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).    

答案 1 :(得分:5)

这是众所周知的TCP连接限制。您需要引入一些确认功能。

xep-0184中的一个选项。消息可以携带收据请求,并且当它被递送时,收据将返回给发件人。

另一种选择是xep-0198。这是流管理,承认节。

您还可以在应用程序层中完全实现它,并将邮件从收件人发送到发件人。 当确认未送达时,相应地采取行动。 介意发件人 - >服务器连接也可能以这种方式切断。

我不知道ejabberd中那些xeps和功能的实现。我根据项目要求自己实现它们。

答案 2 :(得分:2)

ejabberd支持在最新版本中默认使用流管理。它在大多数移动库中实现,如Smack for Android和XMPPFramework for iOS。

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

答案 3 :(得分:1)

在ejabberd上实施XEP-198非常复杂。

Erlang Solutions(我为他们工作)有一个用于ejabberd的XEP-184模块,具有增强的功能,可以解决这个问题。它在服务器端进行缓冲和验证。只要客户端发送带有收据请求的邮件,并且邮件一旦发送,收据就会返回给发件人。

模块验证收据以查看是否已收到邮件。如果它没有超时,它将被保存为离线消息。

答案 4 :(得分:1)

我认为更好的方法是,如果没有收到消息,请让用户离线,然后将消息存储在离线消息表中,并使用推送服务并将其配置为脱机消息。

然后将发送推送,如果有更多消息,它们将存储在离线消息中,并且为了在服务器上了解未收到消息,您可以使用此https://github.com/Mingism/ejabberd-stanza-ack

我认为Facebook在消息无法提供时会有同样的方式让用户离线直到他再次上线

答案 5 :(得分:0)

Ejabberd支持在最新版本中默认使用流管理。 在ejabberd_c2s中设置流管理器配置后,您应该在客户端中设置一些配置。 请在客户端查看此帖子中的此配置。 https://community.igniterealtime.org/thread/55715