Porting old socket code to Network, need equivalent to socketpair()

I have some old network code that I am porting to Network.framework. The code is ported over easily, however I am struggling to get the tests ported. My old code accepted posix sockets from accept() and passed them off to handler objects. In the tests, I would instead create a pre-connected pipe with socketpair() and pass one of sockets off to a handler object and drive the other socket from my test.

In my new code, the handler objects are normally passed an NWConnection, but for testing I don't see a way to create both ends of the pipe.

How can I create two NWConnection objects that are connected to eachother?

Accepted Reply

One way to setup two NWConnections that are connected to each is by using an NWListener in between the connections.

> How can I create two NWConnection objects that are connected to eachother?


As an example, you'll first want to setup an NWListener to listen on an address and port. Once the listener is established, make a new opening connection to the listening address and port. Once the listener detects the new connection, take this new connection and use it to open a new receiving connection. At that point both NWConnection objects are connected to each other and can send data back and forth.


Here is an example of the code behind this:


The ViewController that delegates the initial interaction and setup.

class ViewController: UIViewController {
   
    @IBOutlet weak var connect: UIButton!
    @IBOutlet weak var sendOpeningConnectionData: UIButton!
    @IBOutlet weak var sendReceivingConnectionData: UIButton!
    var nwListener: NWListenerTest?


    @IBAction func makeInitialConnection() {
        nwListener = NWListenerTest(delegate: self)
        nwListener?.startListener()
    }
   
    @IBAction func sendOpeningData() {
        openingConnection?.sendDataOnConnection(strData: "sendOpeningData")
    }
   
    @IBAction func sendReceivingData() {
        receivingConnection?.sendDataOnConnection(strData: "sendReceivingData")
    }
}

extension ViewController: NetworkDelegate {
    func portReady(port: Int) {
        openingConnection = Connection(host: "127.0.0.1", port: "8080", context: "OpeningConnection")
    }
}


The NWListener that is listening for connections on 127.0.0.1:8080

protocol NetworkDelegate: class {
    func portReady(port: Int)
}

class NWListenerTest {
   
    private weak var delegate: NetworkDelegate?
    private var listener: NWListener?
   
    init (delegate: NetworkDelegate) {
        self.delegate = delegate
    }
   
    func startListener() {
        do {
            let tcpOption = NWProtocolTCP.Options()
            let params = NWParameters(tls: nil, tcp: tcpOption)
            params.requiredLocalEndpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: 8080)
           
            listener = try NWListener(using: params)
            // Used for state update handling on the readiness of the listener.
            listener?.stateUpdateHandler = { newState in
                switch newState {
                case .ready:
                    if let port = self.listener?.port {
                        self.delegate?.portReady(port: Int(port.rawValue))
                    }
                case .failed(let error):
                    // If the listener goes into the failed state, cancel and restart.
                    self.listener?.cancel()
                default:
                    break
                }
            }
           
            // Used for receiving a new connection from the listener
            listener?.newConnectionHandler = { newConnection in
                // Act upon new connection
                receivingConnection = Connection(nwConnection: newConnection, context: "ReceivingConnection")
            }

            // Start listening, and request updates on the main queue.
            listener?.start(queue: .main)
        } catch {
            os_log("Failed to create listener")
        }
    }
}


The NWConnection object that is used for the opening and receiving connections.

var receivingConnection: Connection?
var openingConnection: Connection?

class Connection {


    private var connection: NWConnection?
    private var connectionContext: String = ""
   
    // Setup receivingConnection
    init(nwConnection: NWConnection, context: String) {
        connection = nwConnection
        connectionContext = context
        startConnection()
    }
   
    // Setup openingConnection
    init(host: String, port: String, context: String) {
       
        let tcpOption = NWProtocolTCP.Options()
        let params = NWParameters(tls: nil, tcp: tcpOption)
        connectionContext = context
        guard let port = NWEndpoint.Port(port) else {
            return
        }
       
        let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(host),
                                           port: port)
                                          
        connection = NWConnection(to: endpoint, using: params)
        startConnection()
    }
   

    func startConnection() {
       
        guard let connection = connection else {
            return
        }
       
        // Receive state updates for the connection and perform actions upon the ready and failed state.
        connection.stateUpdateHandler = { newState in
           
            switch newState {
            case .ready:
                os_log("%{PUBLIC}@ - Connection established", self.connectionContext)
                // Setup the receive method to ensure data is caputured on the incoming connection.
                self.receiveIncomingDataOnConnection()
            case .preparing:
                os_log("%{PUBLIC}@ - Connection preparing", self.connectionContext)
            case .setup:
                os_log("%{PUBLIC}@ - Connection setup", self.connectionContext)
            case .waiting(let error):
                os_log("%{PUBLIC}@ - Connection waiting: %{PUBLIC}@", self.connectionContext, error.localizedDescription)
            case .failed(let error):
                os_log("%{PUBLIC}@ - Connection failed: %{PUBLIC}@", self.connectionContext, error.localizedDescription)
                // Cancel the connection upon a failure.
                self.connection?.cancel()
               
            default:
                break
            }
        }
        // Start the connection and send receive responses on the main queue
        connection.start(queue: .main)
    }
   
    func sendDataOnConnection(strData: String) {
       
        guard let connection = connection else {
            return
        }
       
        let data = Data(strData.utf8)
        os_log("%{PUBLIC}@ - Sending: %{PUBLIC}d bytes", connectionContext, data.count)
       
        connection.send(content: data, completion: NWConnection.SendCompletion.contentProcessed { error in
            if let error = error {
                os_log("%{PUBLIC}@ - Error sending data:", error.localizedDescription)
            }
        })
    }


    func receiveIncomingDataOnConnection() {
       
        guard let connection = connection else {
            return
        }
       
        connection.receive(minimumIncompleteLength: 8, maximumLength: 1024, completion: { (content, context, isComplete, error) in
            if let messageData = content,
                let strData = String(bytes: messageData, encoding: .utf8) {
                os_log("%{PUBLIC}@ - Message Data: %{PUBLIC}@",self.connectionContext, strData)
            }
            if error == nil {
                // Called to ensure future data can be read off the connection.
                self.receiveIncomingDataOnConnection()
            }
        })
    }
}

Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Replies

One way to setup two NWConnections that are connected to each is by using an NWListener in between the connections.

> How can I create two NWConnection objects that are connected to eachother?


As an example, you'll first want to setup an NWListener to listen on an address and port. Once the listener is established, make a new opening connection to the listening address and port. Once the listener detects the new connection, take this new connection and use it to open a new receiving connection. At that point both NWConnection objects are connected to each other and can send data back and forth.


Here is an example of the code behind this:


The ViewController that delegates the initial interaction and setup.

class ViewController: UIViewController {
   
    @IBOutlet weak var connect: UIButton!
    @IBOutlet weak var sendOpeningConnectionData: UIButton!
    @IBOutlet weak var sendReceivingConnectionData: UIButton!
    var nwListener: NWListenerTest?


    @IBAction func makeInitialConnection() {
        nwListener = NWListenerTest(delegate: self)
        nwListener?.startListener()
    }
   
    @IBAction func sendOpeningData() {
        openingConnection?.sendDataOnConnection(strData: "sendOpeningData")
    }
   
    @IBAction func sendReceivingData() {
        receivingConnection?.sendDataOnConnection(strData: "sendReceivingData")
    }
}

extension ViewController: NetworkDelegate {
    func portReady(port: Int) {
        openingConnection = Connection(host: "127.0.0.1", port: "8080", context: "OpeningConnection")
    }
}


The NWListener that is listening for connections on 127.0.0.1:8080

protocol NetworkDelegate: class {
    func portReady(port: Int)
}

class NWListenerTest {
   
    private weak var delegate: NetworkDelegate?
    private var listener: NWListener?
   
    init (delegate: NetworkDelegate) {
        self.delegate = delegate
    }
   
    func startListener() {
        do {
            let tcpOption = NWProtocolTCP.Options()
            let params = NWParameters(tls: nil, tcp: tcpOption)
            params.requiredLocalEndpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: 8080)
           
            listener = try NWListener(using: params)
            // Used for state update handling on the readiness of the listener.
            listener?.stateUpdateHandler = { newState in
                switch newState {
                case .ready:
                    if let port = self.listener?.port {
                        self.delegate?.portReady(port: Int(port.rawValue))
                    }
                case .failed(let error):
                    // If the listener goes into the failed state, cancel and restart.
                    self.listener?.cancel()
                default:
                    break
                }
            }
           
            // Used for receiving a new connection from the listener
            listener?.newConnectionHandler = { newConnection in
                // Act upon new connection
                receivingConnection = Connection(nwConnection: newConnection, context: "ReceivingConnection")
            }

            // Start listening, and request updates on the main queue.
            listener?.start(queue: .main)
        } catch {
            os_log("Failed to create listener")
        }
    }
}


The NWConnection object that is used for the opening and receiving connections.

var receivingConnection: Connection?
var openingConnection: Connection?

class Connection {


    private var connection: NWConnection?
    private var connectionContext: String = ""
   
    // Setup receivingConnection
    init(nwConnection: NWConnection, context: String) {
        connection = nwConnection
        connectionContext = context
        startConnection()
    }
   
    // Setup openingConnection
    init(host: String, port: String, context: String) {
       
        let tcpOption = NWProtocolTCP.Options()
        let params = NWParameters(tls: nil, tcp: tcpOption)
        connectionContext = context
        guard let port = NWEndpoint.Port(port) else {
            return
        }
       
        let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(host),
                                           port: port)
                                          
        connection = NWConnection(to: endpoint, using: params)
        startConnection()
    }
   

    func startConnection() {
       
        guard let connection = connection else {
            return
        }
       
        // Receive state updates for the connection and perform actions upon the ready and failed state.
        connection.stateUpdateHandler = { newState in
           
            switch newState {
            case .ready:
                os_log("%{PUBLIC}@ - Connection established", self.connectionContext)
                // Setup the receive method to ensure data is caputured on the incoming connection.
                self.receiveIncomingDataOnConnection()
            case .preparing:
                os_log("%{PUBLIC}@ - Connection preparing", self.connectionContext)
            case .setup:
                os_log("%{PUBLIC}@ - Connection setup", self.connectionContext)
            case .waiting(let error):
                os_log("%{PUBLIC}@ - Connection waiting: %{PUBLIC}@", self.connectionContext, error.localizedDescription)
            case .failed(let error):
                os_log("%{PUBLIC}@ - Connection failed: %{PUBLIC}@", self.connectionContext, error.localizedDescription)
                // Cancel the connection upon a failure.
                self.connection?.cancel()
               
            default:
                break
            }
        }
        // Start the connection and send receive responses on the main queue
        connection.start(queue: .main)
    }
   
    func sendDataOnConnection(strData: String) {
       
        guard let connection = connection else {
            return
        }
       
        let data = Data(strData.utf8)
        os_log("%{PUBLIC}@ - Sending: %{PUBLIC}d bytes", connectionContext, data.count)
       
        connection.send(content: data, completion: NWConnection.SendCompletion.contentProcessed { error in
            if let error = error {
                os_log("%{PUBLIC}@ - Error sending data:", error.localizedDescription)
            }
        })
    }


    func receiveIncomingDataOnConnection() {
       
        guard let connection = connection else {
            return
        }
       
        connection.receive(minimumIncompleteLength: 8, maximumLength: 1024, completion: { (content, context, isComplete, error) in
            if let messageData = content,
                let strData = String(bytes: messageData, encoding: .utf8) {
                os_log("%{PUBLIC}@ - Message Data: %{PUBLIC}@",self.connectionContext, strData)
            }
            if error == nil {
                // Called to ensure future data can be read off the connection.
                self.receiveIncomingDataOnConnection()
            }
        })
    }
}

Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

To answer your direct question:

How can I create two

NWConnection
objects that are connected to each other?

You can’t, at least not in the way that you’re looking for.

socketpair
creates UNIX domain sockets (
AF_LOCAL
), and Network framework has no equivalent to that.

The workaround is to go via the network, as Matt explained.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"