How to Reply to a Message Received by Multicast Receiver and Extract Connection for Communication

Hello everyone,

I’m currently working on a Swift project using the Network framework to create a multicast-based communication system. Specifically, I’m implementing both a multicast receiver and a sender that join the same multicast group for communication. However, I’ve run into some challenges with the connection management, replying to multicast messages, and handling state updates for both connections and connection groups.

Below is a breakdown of my setup and the specific issues I’ve encountered.

I have two main parts in the implementation: the multicast receiver and the multicast sender. The goal is for the receiver to join the multicast group, receive messages from the sender, and send a reply back to the sender using a direct connection.

Multicast Receiver Code:

import Network
import Foundation

func setupMulticastGroup() -> NWConnectionGroup? {
    let multicastEndpoint1 = NWEndpoint.hostPort(host: NWEndpoint.Host("224.0.0.1"), port: NWEndpoint.Port(rawValue: 45000)!)
    let multicastParameters = NWParameters.udp
    multicastParameters.multipathServiceType = .aggregate

    do {
        let multicastGroup = try NWMulticastGroup(for: [multicastEndpoint1], from: nil, disableUnicast: false)
        let multicastConnections = NWConnectionGroup(with: multicastGroup, using: multicastParameters)
        multicastConnections.stateUpdateHandler = InternalConnectionStateUpdateHandler
        multicastConnections.setReceiveHandler(maximumMessageSize: 16384, rejectOversizedMessages: false, handler: receiveHandler)
        multicastConnections.newConnectionHandler = newConnectionHandler
        multicastConnections.start(queue: .global())
        
        return multicastConnections
    } catch {
        return nil
    }
}

func receiveHandler(message: NWConnectionGroup.Message, content: Data?, isComplete: Bool) {
    print("Received message from \(String(describing: message.remoteEndpoint))")

    if let content = content, let messageString = String(data: content, encoding: .utf8) {
        print("Received Message: \(messageString)")
    }

    let remoteEndpoint = message.remoteEndpoint
    message.reply(content: "Multicast group on 144 machine ACK from recv handler".data(using: .utf8))

    if let connection = multicastConnections?.extract(connectionTo: remoteEndpoint) {
        connection.stateUpdateHandler = InternalConnectionRecvStateUpdateHandler
        connection.start(queue: .global())
        connection.send(content: "Multicast group on 144 machine ACK from recv handler".data(using: .utf8), completion: NWConnection.SendCompletion.contentProcessed({ error in
            print("Error code: \(error?.errorCode ?? 0)")
            print("Ack sent to \(connection.endpoint)")
        }))
    }
}

func newConnectionHandler(connection: NWConnection) {
    connection.start(queue: .global())
    connection.send(content: "Multicast group on 144 machine ACK".data(using: .utf8), completion: NWConnection.SendCompletion.contentProcessed({ error in
        print("Error code: \(error?.errorCode ?? 0)")
        print("Ack sent to \(connection.endpoint)")
    }))
}

func InternalConnectionRecvStateUpdateHandler(_ pState: NWConnection.State) {
    switch pState {
    case .setup:
        NSLog("The connection has been initialized but not started")
    case .preparing:
        NSLog("The connection is preparing")
    case .waiting(let error):
        NSLog("The connection is waiting for a network path change. Error: \(error)")
    case .ready:
        NSLog("The connection is established and ready to send and receive data.")
    case .failed(let error):
        NSLog("The connection has disconnected or encountered an error. Error: \(error)")
    case .cancelled:
        NSLog("The connection has been canceled.")
    default:
        NSLog("Unknown NWConnection.State.")
    }
}

func InternalConnectionStateUpdateHandler(_ pState: NWConnectionGroup.State) {
    switch pState {
    case .setup:
        NSLog("The connection has been initialized but not started")
    case .waiting(let error):
        NSLog("The connection is waiting for a network path change. Error: \(error)")
    case .ready:
        NSLog("The connection is established and ready to send and receive data.")
    case .failed(let error):
        NSLog("The connection has disconnected or encountered an error. Error: \(error)")
    case .cancelled:
        NSLog("The connection has been canceled.")
    default:
        NSLog("Unknown NWConnection.State.")
    }
}
let multicastConnections = setupMulticastGroup()
RunLoop.main.run()

Multicast Sender Code:

import Foundation
import Network
func setupConnection() -> NWConnection {
    let params = NWParameters.udp
    params.allowLocalEndpointReuse = true
    return NWConnection(to: NWEndpoint.hostPort(host: NWEndpoint.Host("224.0.0.1"), port: NWEndpoint.Port(rawValue: 45000)!), using: params)
}

func sendData(using connection: NWConnection, data: Data) {
    connection.send(content: data, completion: .contentProcessed { nwError in
        if let error = nwError {
            print("Failed to send message with error: \(error)")
        } else {
            print("Message sent successfully")
        }
    })
}
func setupReceiveHandler(for connection: NWConnection) {
    connection.receive(minimumIncompleteLength: 1, maximumLength: 65000) { content, contentContext, isComplete, error in
        print("Received data:")
        print(content as Any)
        print(contentContext as Any)
        print(error as Any)
     
        setupReceiveHandler(for: connection)
    }
}

let connectionSender = setupConnection()
connectionSender.stateUpdateHandler = internalConnectionStateUpdateHandler
connectionSender.start(queue: .global())

let sendingData = "Hello, this is a multicast message from the process on mac machine 144".data(using: .utf8)!

sendData(using: connectionSender, data: sendingData)
setupReceiveHandler(for: connectionSender)
RunLoop.main.run()

Issues Encountered:

  1. Error Code 0 Even When Connection Refused:

On the receiver side, I encountered this log:

nw_socket_get_input_frames [C1.1.1:1] recvmsg(fd 8, 9216 bytes) [61: Connection refused]
Error code: 0
Ack sent to 10.20.16.144:62707

Questions:

  1. how do I reply to the message if above usage pattern is wrong?
  2. how do I get a NWConnection from the received message to create a separate connection for communication with the sender.

Any insights or suggestions on resolving these issues or improving my multicast communication setup would be greatly appreciated.

Thanks :)

Also posted the same on swift forums in hopes of getting swifter response :( here's the link: https://forums.swift.org/t/how-to-reply-to-a-message-received-by-multicast-receiver-and-extract-connection-for-communication/75454

Lemme take a step back and ask about the big picture. Why are you doing this?

Why do I ask? Because my experience is that folks get into multicast for two reasons:

  • They’re implementing local network service discovery, in which case it’s much easier to use Bonjour.

  • They think it’ll be a performance boost, which is often not the case. My Wi-Fi Fundamentals post explains why.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Why are you doing this?

@DTS Engineer To clarify our situation:

  1. While we are exploring mDNS-based discovery using Bonjour, our primary goal is to create cross-platform applications that can run on the same network and discover each other using IP-based multicast. We're facing challenges in this area and would like to better understand why these issues occur and how we can resolve them when using NWConnectionGroup.

  2. Unlike traditional service discovery, our use case isn’t about discovering specific services. Instead, we are focused on process discovery—identifying processes running on the same network, regardless of whether they expose a service. This changes the approach we need to take compared to service-based discovery.

  3. We are avoiding the use of multicast DNS (mDNS) because it adds overhead and costs. For a simple use case like ours, where IP multicast works seamlessly on other platforms, we want to avoid the complexity and resource consumption associated with mDNS queries.

  4. Additionally, when we use NWConnection to send a multicast request and the responder is a non-Apple process (e.g., a Linux process using BSD sockets), we notice that the responses from that process are not received by the NWConnection, unlike what happens in similar scenarios on other platforms. We are curious about why this occurs and how we can resolve it?

Any guidance on how to handle these multicast challenges more effectively would be greatly appreciated.

Apologies for the delayed response :)

Sorry about the delay responding; I’m juggling too many cases right now.

I’m going to start with the big picture:

While we are exploring mDNS-based discovery using Bonjour, our primary goal is to create cross-platform applications …

Support for the Bonjour protocols [1] are either built-in or readily installable on all major platforms.

we are focused on process discovery—identifying processes running on the same network, regardless of whether they expose a service.

OK. But that doesn’t preclude the use of Bonjour, you just need to think of each process as a service.

We are avoiding the use of multicast DNS (mDNS) because it adds overhead and costs.

What sort of overhead and cost? On the wire cost?

One of the key design goals of Bonjour was to minimise on-the-wire traffic. My experience is that folks often create their own ad hoc service discovery protocol only to find that they have to either:

  • Live with a lot more on-the-wire traffic than Bonjour

  • Or substantially complicated their protocol, to the point where they should’ve just used Bonjour in the first place


Coming back to your implementation, you wrote:

Additionally, when we use NWConnection to send a multicast request and the responder is a non-Apple process … we notice that the responses from that process are not received by the NWConnection

I think you’re mixed up as to the intended use case for NWConnection and UDP. When you use UDP, NWConnection models a UDP flow, that is, a sequences of packets that share the same local IP / local port / remote IP / remote port tuple. You generally don’t use NWConnection for multicast work. Rather, you use NWConnectionGroup for the multicasts stuff and then, if you want to set up a UDP flow based on what you find over multicast, use NWConnection for that.

This means that it’s not possible to model all UDP use cases supported by BSD Sockets in NWConnection, or even with the combination of NWConnection and NWConnectionGroup. Network framework trades ease of use for flexibility, something we call out in TN3151.

Honestly, if you have existing BSD Sockets code that works on other platforms then it’d be easier for you to use that on macOS as well. Network framework isn’t really buying you much in this scenario.

But if you want to continue with Network framework then I’ll need to get a better understanding of the on-the-wire protocol you’re trying to implement. Specifically, I’d like to know the tuples for all the traffic generated by the various peers involved in your protocol.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Bonjour is an Apple term for three industry-standard protocols:

How to Reply to a Message Received by Multicast Receiver and Extract Connection for Communication
 
 
Q