SwiftyZeroMQ Publisher does not recognise subscriber after first message is published

Viewed 136

Problem: I fail subscribing to my publisher implemented in swift as soon as the publisher has published its first message.

Goal: Publish a data stream over ZeroMQ from my swift app. Then connect and disconnect a few subscribers and get messages through.

Background: I use swift5 and SwiftyZeroMQ5 (I have tested SwiftyZeroMQ too), I deploy on iPhone. I try to subscribe from both swift and python3. It only works if I connect my subscriber prior to publishing the first message. It also works if I first connect my subscriber then start the publisher app, then publishes. Corresponding publish and subscribe code on python3 does not require launching in any specific order and represents the behaviour I want. Since I get the sub/pub to work if I start i specific order, I know that IP-numbers, ports and topic, formatting etc are correct. Note that the behaviour is the same when I subscribe from both python3 and swift - it is not compatibility issues between swift and python.

Error messages: No error messages, the poller simply does not trigger if the processes are not started in the described order. The poller do trigger and messages and received if the processes are started in the described order.

What I tried: I've tried different combinations of publishers and subscribers regarding code base and devices.

  • Swift pub to swift sub on same device [only works in described order]
  • Swift pub to swift sub on different devices [only works in described order]
  • Swift pub to python3 sub [only works in described order]
  • Python3 pub to swift sub [works independent of start order]
  • Python3 pub to python3 sub [works independent of start order]

My conclusion: There is a problem in the swift publisher socket: it fails to recognise new subscribers after it has published its first message.

Swift code for publisher, initPublisher is called in viewDidLoad(). ZeroMQ library version is 4.2.2:

import SwiftyZeroMQ5
var context: SwiftyZeroMQ.Context = try! SwiftyZeroMQ.Context()
var gpsPublisher: SwiftyZeroMQ.Socket?
let gpsPublishEndPoint = "tcp://*:5560"
// Init the publisher socket
func initPublisher()->Bool{
    do{
        self.gpsPublisher = try context.socket(.publish)
        try self.gpsPublisher?.setSendBufferSize(4096)
        try self.gpsPublisher?.setLinger(0)                  // Dont buffer messages
        try self.gpsPublisher?.bind(self.gpsPublishEndPoint)
        return true
        }
    catch{
        print("Publish setup failed!")
        return false
    }
}
// ZMQ publish. Publishes string and serialized json-object
func publish(socket: SwiftyZeroMQ.Socket?, topic: String, json: JSON)->Bool{
    // Create string with topic and json representation
    let publishStr = getJsonStringAndTopic(topic: topic, json: json)
    do{
        try socket?.send(string: publishStr)
        print("publish: Published: " + publishStr)
        return true
    }
    catch{
        print("publish: Error, tried to publish, but failed: " + publishStr)
        return false
    }
}
//The function is repeatedly called in a thread. Only function call shown here below.
_ = self.publish(socket: self.gpsPublisher, topic: "myTopic", json: json)

The python3 subscriber code, zmq.zmq_version() -> '4.3.2':

context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://192.168.1.2:5560")
socket.setsockopt_string(zmq.SUBSCRIBE, 'myTopic')
socket.RCVTIMEO = 1000  # in milliseconds
while socket:
  try:
    msg = str(socket.recv(), 'utf-8')
    (topic, msg) = auxiliaries.zmq.demogrify(msg)
    _print((topic, msg))
  except zmq.error.Again as error:
    _print(str(error))
  except KeyboardInterrupt:
    auxiliaries.zmq.close_socket_gracefully(socket)
    socket = None

Any help, interesting test setups etc is very much appreciated.

1 Answers

I did some more testing and found out a few things and a workaround.

  1. The code works as intended when running on an iPhone simulator (simulator is x86_64 architecture, iPhone8 is armv7)
  2. I don't think it is related, but I did find it interesting. Certain multicast and broadcast protocols require an approval from Apple. You can run without the approval on simulators, but not in devices. Apple networking multicast entitlement. Since it partially works I did rule this out.
  3. The workaround is to bind the socket again prior to each publish. This throws the error "Address already in use", butt seem to not do much harm.
  4. Without unreasonable delays in between publish messages, the pub-sub fails after 500-1000 messages if the publish socket is not binded again when running from the iPhone.

I made a minimal working example app that you can play around with if you want to, or need to dig deeper. It has buttons for "init", "bind", "publish" and "bind and publish". You can send a batch of messages and check timings etc. I run the app towards a python3 script. I included SwiftyZeroMQ5 via cocoapods.

Podfile:

platform :ios, '13.0'
target 'ZMQtest' do
  use_frameworks!
  pod 'SwiftyZeroMQ5'
end

SwiftCode (set up the buttons your self..)

import UIKit
import SwiftyZeroMQ5
class ViewController: UIViewController {
    let context = try! SwiftyZeroMQ.Context()
    var publisher: SwiftyZeroMQ.Socket?
    let publishEndPoint = "tcp://*:5560"
    var cnt = 0
    let quota = 1200
    @IBAction func publishButtonPressed(_ sender: Any) {
        while cnt < quota {
            publish()
            //usleep(10000)
        }
        cnt = 1
    }
    @IBAction func bindAndPublishButtonPressed(_ sender: Any) {
        while cnt < quota {
            bindPublisher()
            publish()
        }
        cnt = 1
    }
    @IBAction func initPubButtonPressed(_ sender: Any) {
        initPublisher()
    }
    @IBAction func bindPublisherButtonPressed(_ sender: Any) {
        bindPublisher()
    }
    @IBOutlet weak var statusLabel: UILabel!

    // **************
    // Init publisher
    func initPublisher(){
        do {
            self.publisher = try context.socket(.publish)
            print("Publisher socket created")
        }
        catch {
            print("initPublisher error: ", error)
        }
     }

    // **************
    // Bind publisher
    func bindPublisher(){
        do {
            try self.publisher?.bind(publishEndPoint)
            print("Publisher socket binded to :", publishEndPoint)
        }
        catch {
            print("bindPublisher error: ", error)
        }
    }

    // *****************
    // Publish a message
    func publish(){
        // Publish dummy string
        do{
            cnt += 1
            let str = "topic {\"key\": \"" + String(cnt) + "\"}"
            try self.publisher?.send(string: str)
            statusLabel.text = str
            print("Publish message no: ", String(cnt))
        }
        catch{
            print("publisher error: ", error)
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

And the python3 code:

#!/usr/bin/env python3
'''Minimal running example of a ZMQ SUB socket.'''
import json
import zmq

def demogrify(msg: str):
  '''inverse of mogrify()'''
  try:
    (topic, message) = msg.split(maxsplit=1)
  except ValueError:
    (topic, message) = (msg, '{}')

  return topic, json.loads(message)

def close_socket_gracefully(socket):
  '''graceful termination'''
  socket.setsockopt(zmq.LINGER, 0) # to avoid hanging infinitely
  socket.close()

if __name__ == "__main__":
  context = zmq.Context()

  socket = context.socket(zmq.SUB)  #pylint: disable=no-member
  socket.connect("tcp://192.168.1.2:5560")
  socket.setsockopt_string(zmq.SUBSCRIBE, '')  #pylint: disable=no-member
  socket.RCVTIMEO = 1000  # in milliseconds

  while socket:
    try:
      msg = str(socket.recv(), 'utf-8')
      (topic, msg) = demogrify(msg)
      print((topic, msg))
    except zmq.error.Again as error:
      print(str(error))
    except KeyboardInterrupt:
      close_socket_gracefully(socket)
      socket = None

Should I mark this issue as solved or not?

Related