xmppmessageejabberdxmppframework

xmpp messages are lost when client connection lost suddently


I am using ejabberd server and ios xmppframework. there are two clients, A and B.

  1. When A and B are online, A can send message to B successfully.
  2. If B is offline, B can receive the message when B is online again.
  3. But when B is suddenly/unexpectedly lost connection, such as manually close wi-fi, the message sent by A is lost. B will never receive this message.

I guess the reason is that B lost connection suddenly and the server still think B is online. Thus the offline message does work under this condition.

How to ensure the message that sent by A will be received by B, i.e that there is no messages lost?


Solution

  • I've spent the last week trying to track down missing messages in my XMPPFramework and eJabberd messaging app. Here are the full steps I went through to guarantee message delivery and what the effects of each step are.

    Mod_offline

    In the ejabberd.yml config file ensure that you have this in the access rules:

    max_user_offline_messages:
      admin: 5000
      all: 100
    

    and this in the modules section:

    mod_offline:
      access_max_user_messages: max_user_offline_messages
    

    When the server knows the recipient of a message is offline they will store it and deliver it when they re-connect.

    Ping (XEP-199)

    xmppPing = XMPPPing()
    xmppPing.respondsToQueries = true
    xmppPing.activate(xmppStream)
    
    xmppAutoPing = XMPPAutoPing()
    xmppAutoPing.pingInterval = 2 * 60
    xmppAutoPing.pingTimeout = 10.0
    xmppAutoPing.activate(xmppStream)
    

    Ping acts like a heartbeat so the server knows when the user is offline but didn't disconnect normally. It's a good idea to not rely on this by disconnecting on applicationDidEnterBackground but when the client looses connectivity or the stream disconnects for unknown reasons there is a window of time where a client is offline but the server doesn't know it yet because the ping wasn't expected until sometime in the future. In this scenario the message isn't delivered and isn't stored for offline delivery.

    Stream Management (XEP-198)

    xmppStreamManagement = XMPPStreamManagement(storage: XMPPStreamManagementMemoryStorage(), dispatchQueue: dispatch_get_main_queue())
    xmppStreamManagement.autoResume = true
    xmppStreamManagement.addDelegate(self, delegateQueue: dispatch_get_main_queue())
    xmppStreamManagement.activate(xmppStream)
    

    and then in xmppStreamDidAuthenticate

    xmppStreamManagement.enableStreamManagementWithResumption(true, maxTimeout: 100)
    

    Nearly there. The final step is to go back to the ejabberd.yml and add this line to the listening ports section underneath access: c2s:

    resend_on_timeout: true
    

    Stream Management adds req/akn handshakes after each message delivery. On it's own it won't have any effect on the server side unless that resend_on_timeout is set (which it isn't by default on eJabberd).

    There is a final edge case which needs to be considered when the acknowledgement of a received message doesn't get to the server and it decides to hold it for offline delivery. The next time the client logs in they are likely to get a duplicate message. To handle this we set that delegate for the XMPPStreamManager. Implement the xmppStreamManagement getIsHandled: and if the message has a chat body set the isHandledPtr to false. When you construct an outbound message add an xmppElement with a unique id:

    let xmppMessage = XMPPMessage(type: "chat", to: partnerJID)
    let xmppElement = DDXMLElement(name: "message")
    xmppElement.addAttributeWithName("id", stringValue: xmppStream.generateUUID())
    xmppElement.addAttributeWithName("type", stringValue: "chat")
    xmppElement.addAttributeWithName("to", stringValue: partnerJID.bare())
    xmppMessage.addBody(message)
    xmppMessage.addChild(xmppElement)
    xmppMessage.addReceiptRequest()
    xmppStream.sendElement(xmppMessage)
    

    Then when you receive a message, inform the stream manager that the message has been handled with xmppStreamManager.markHandledStanzaId(message.from().resource)

    The purpose of this final step is to establish a unique identifier that you can add to the XMPPMessageArchivingCoreDataStorage and check for duplicates before displaying.