Socket API

Dear Sirs, with the upcoming release of swift 5, it will be possible to have an API for the socket programming?

Thanks a lot in advance!

And have a good day!

Gian Luca

1 Like

You can already interact with the standard POSIX socket APIs as you would normally, but if you wanted a higher level abstraction you can use SwiftNIO.

If you're on Apple platforms, there's also Network.framework.

You can start with IBMs BlueSocket as it's relatively simple and straightforward. Though at some point you may notice that there's something wrong with blocking networking API, and then it would be the perfect moment to meet SwiftNIO :) It will take a while to get used to Future/Promise philosophy, but I promise (sic) that it will pay off. Closest analogy here to SwiftNIO would be asyncio module from Python.

2 Likes

if I would use the Network framework, someone can provide a quick and
easy example of server and client?

Pasted in below is a bunch of snippets from a simple TCP client and server.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple


Client:

var connection: NWConnection?

func start() {
    let connection = NWConnection(host: "example.com", port: 80, using: .tcp)
    connection.stateUpdateHandler = self.stateDidChange(to:)
    self.setupReceive(on: connection)
    connection.start(queue: .main)
    self.connection = connection
}

func stateDidChange(to state: NWConnection.State) {
    switch state {
    case .setup:
        break
    case .waiting(let error):
        self.connectionDidFail(error: error)
    case .preparing:
        break
    case .ready:
        self.status = "Connected"
    case .failed(let error):
        self.connectionDidFail(error: error)
    case .cancelled:
        break
    }
}

Server:

var listener: NWListener?

func start() throws {
    let listener = try NWListener(using: .tcp, on: 12345)
    listener.stateUpdateHandler = self.stateDidChange(to:)
    listener.newConnectionHandler = self.didAccept(connection:)
    listener.start(queue: .main)
    self.listener = listener
}

func stateDidChange(to newState: NWListener.State) {
    switch newState {
    case .setup:
        break
    case .waiting:
        break
    case .ready:
        break
    case .failed(let error):
        self.listenerDidFail(error: error)
    case .cancelled:
        break
    }
}

var nextID: Int = 0

var connectionsHandlers: [Int: ConnectionHandler] = [:]

func didAccept(connection: NWConnection) {
    let handler = ConnectionHandler(connection: connection, uniqueID: self.nextID)
    self.nextID += 1
    self.connectionsHandlers[handler.uniqueID] = handler
    handler.didStopCallback = self.connectionDidStop(_:)
    handler.start()
}

func stop() {
    if let listener = self.listener {
        self.listener = nil
        listener.cancel()
    }
    for handler in self.connectionsHandlers.values {
        handler.cancel()
    }
    self.connectionsHandlers.removeAll()
}

Data transfer:

func setupReceive(on connection: NWConnection) {
    connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { (data, contentContext, isComplete, error) in
        if let data = data, !data.isEmpty {
            // … process the data …
            self.status = "did receive \(data.count) bytes"
        }
        if isComplete {
            // … handle end of stream …
            self.stop(status: "EOF")
        } else if let error = error {
            // … handle error …
            self.connectionDidFail(error: error)
        } else {
            self.setupReceive(on: connection)
        }
    }
}

func sendStreamOriented(connection: NWConnection, data: Data) {
    connection.send(content: data, completion: .contentProcessed({ error in
        if let error = error {
            self.connectionDidFail(error: error)
        }
    }))
}

func sendEndOfStream(connection: NWConnection) {
    connection.send(content: nil, contentContext: .defaultStream, isComplete: true, completion: .contentProcessed({ error in
        if let error = error {
            self.connectionDidFail(error: error)
        }
    }))
}
8 Likes

I wonder if you could provide a link for this code or a sample project. these snippets are incomplete, they contain undeclared types and variables.

I wonder if you could provide a link for this code or a sample
project.

The project I took that from is not publicly available.

These snippets are incomplete …

Indeed. Both snippets are meant to be embedded inside a class. For the client side the missing declarations are:

class Client {

    var status: String = ""

    func connectionDidFail(error: Error) {
        …
    }

    func stop(status: String) {
        …
    }
}

The server side is a little more complex, in that you’ll need a ConnectionHandler class that wraps the connection and its ID. Each instance of that class works much like the client class.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

Hi @eskimo, I too am a tad confused. I'm sure I'm missing something very obvious, but I just cannot get the message received to trigger. I setup two basic functions to make sure I wasn't overcomplicating things, but it's still not working.

Test Server
func server() {
    listener = try? NWListener(using: .tcp, on: 91)

    listener?.newConnectionHandler = rec(_:)
    
    listener?.stateUpdateHandler = { state in
        switch(state) {
        case .ready:
            print("Listening")
        default:
            break;
        }
    }
    
    listener?.start(queue: .main)
}

func rec(_ connection: NWConnection) {
    print("New Connection")
    
    sharedConnection = connection
    
    sharedConnection?.stateUpdateHandler = { state in
        switch(state) {
        case .ready:
            print("Setting Receive")
            sharedConnection?.receiveMessage { (data, context, done, error) in
                print("Received Something")
            }
        default:
            break
        }
    }
    
    sharedConnection?.start(queue: .main)
}
Test Client
func client() {
    connector = NWConnection(host: "192.168.0.5", port: 91, using: .tcp)
    
    connector?.stateUpdateHandler = { state in
        switch(state) {
        case .ready:
            print("Sending Message")
            connector?.send(content: "TEST".data(using: .utf8), contentContext: .defaultMessage, isComplete: false, completion: .idempotent)
            break
        default:
            break
        }
    }
    
    connector?.start(queue: .main)
}

I just call server and then client, all the print statements fire except for the receive one.

It’s hard to say what’s going wrong here, so I just sat down and reworked my code into something that you can run:

Summary
import Foundation
import Network

class Connection {

    init(nwConnection: NWConnection) {
        self.nwConnection = nwConnection
        self.id = Connection.nextID
        Connection.nextID += 1
    }

    private static var nextID: Int = 0

    let nwConnection: NWConnection
    let id: Int

    var didStopCallback: ((Error?) -> Void)? = nil

    func start() {
        print("connection \(self.id) will start")
        self.nwConnection.stateUpdateHandler = self.stateDidChange(to:)
        self.setupReceive()
        self.nwConnection.start(queue: .main)
    }

    func send(data: Data) {
        self.nwConnection.send(content: data, completion: .contentProcessed( { error in
            if let error = error {
                self.connectionDidFail(error: error)
                return
            }
            print("connection \(self.id) did send, data: \(data as NSData)")
        }))
    }

    func stop() {
        print("connection \(self.id) will stop")
    }

    private func stateDidChange(to state: NWConnection.State) {
        switch state {
        case .setup:
            break
        case .waiting(let error):
            self.connectionDidFail(error: error)
        case .preparing:
            break
        case .ready:
            print("connection \(self.id) ready")
        case .failed(let error):
            self.connectionDidFail(error: error)
        case .cancelled:
            break
        default:
            break
        }
    }

    private func connectionDidFail(error: Error) {
        print("connection \(self.id) did fail, error: \(error)")
        self.stop(error: error)
    }

    private func connectionDidEnd() {
        print("connection \(self.id) did end")
        self.stop(error: nil)
    }

    private func stop(error: Error?) {
        self.nwConnection.stateUpdateHandler = nil
        self.nwConnection.cancel()
        if let didStopCallback = self.didStopCallback {
            self.didStopCallback = nil
            didStopCallback(error)
        }
    }

    private func setupReceive() {
        self.nwConnection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { (data, _, isComplete, error) in
            if let data = data, !data.isEmpty {
                print("connection \(self.id) did receive, data: \(data as NSData)")
            }
            if isComplete {
                self.connectionDidEnd()
            } else if let error = error {
                self.connectionDidFail(error: error)
            } else {
                self.setupReceive()
            }
        }
    }
}

class Client {

    init() {
        let nwConnection = NWConnection(host: "127.0.0.1", port: 12345, using: .tcp)
        self.connection = Connection(nwConnection: nwConnection)
    }

    let connection: Connection

    func start() {
        self.connection.didStopCallback = self.didStopCallback(error:)
        self.connection.start()
    }

    func didStopCallback(error: Error?) {
        if error == nil {
            exit(EXIT_SUCCESS)
        } else {
            exit(EXIT_FAILURE)
        }
    }

    static func run() {
        let client = Client()
        client.start()
        dispatchMain()
    }
}

class Server {

    init() {
        self.listener = try! NWListener(using: .tcp, on: 12345)
        self.timer = DispatchSource.makeTimerSource(queue: .main)
    }

    let listener: NWListener
    let timer: DispatchSourceTimer

    func start() throws {
        print("server will start")
        self.listener.stateUpdateHandler = self.stateDidChange(to:)
        self.listener.newConnectionHandler = self.didAccept(nwConnection:)
        self.listener.start(queue: .main)
    
        self.timer.setEventHandler(handler: self.heartbeat)
        self.timer.schedule(deadline: .now() + 5.0, repeating: 5.0)
        self.timer.activate()
    }

    func stateDidChange(to newState: NWListener.State) {
        switch newState {
        case .setup:
            break
        case .waiting:
            break
        case .ready:
            break
        case .failed(let error):
            print("server did fail, error: \(error)")
            self.stop()
        case .cancelled:
            break
        default:
            break
        }
    }

    private var connectionsByID: [Int: Connection] = [:]

    private func didAccept(nwConnection: NWConnection) {
        let connection = Connection(nwConnection: nwConnection)
        self.connectionsByID[connection.id] = connection
        connection.didStopCallback = { _ in
            self.connectionDidStop(connection)
        }
        connection.start()
        print("server did open connection \(connection.id)")
    }

    private func connectionDidStop(_ connection: Connection) {
        self.connectionsByID.removeValue(forKey: connection.id)
        print("server did close connection \(connection.id)")
    }

    private func stop() {
        self.listener.stateUpdateHandler = nil
        self.listener.newConnectionHandler = nil
        self.listener.cancel()
        for connection in self.connectionsByID.values {
            connection.didStopCallback = nil
            connection.stop()
        }
        self.connectionsByID.removeAll()
        self.timer.cancel()
    }

    private func heartbeat() {
        let timestamp = Date()
        print("server heartbeat, timestamp: \(timestamp)")
        for connection in self.connectionsByID.values {
            let data = "heartbeat, connection: \(connection.id), timestamp: \(timestamp)\r\n"
            connection.send(data: Data(data.utf8))
        }
    }

    static func run() {
        let listener = Server()
        try! listener.start()
        dispatchMain()
    }
}

func main() {
    switch CommandLine.arguments.dropFirst() {
    case ["client"]: Client.run()
    case ["server"]: Server.run()
    default:
        print("usage: NWTest server | client")
        exit(EXIT_FAILURE)
    }
}

main()

I tested this on macOS 10.14.4.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

7 Likes

Hi Quinn,

I have code that uses NWConnection, as above, but I am stuck understanding how to wrap that in a unit test such that I can wait for the state to change to .ready, send a message, and receive a response. As it is the XCTest ends before the message is even sent.

Typically one uses XCTestExpectations when testing asynchronous code. The expectation is fulfilled when the response is received. See

https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations/testing_asynchronous_operations_with_expectations

2 Likes

Can someone (hopefully @eskimo Quinn “The Eskimo!” @ DTS @ Apple) help me understand how can "data" mutate in this example? The crash is intermittent and took me a day of running the app to catch. See screenshot:

Can someone … help me understand how can data mutate in this
example?

To summarise, it seems your issue is that:

  • On line 2578 you check that data.count is of a specific value.

  • The code then runs down to line 2586.

  • Which calls a delegate method.

  • Which calls a whole bunch of other stuff.

  • Which eventually crashes with a memory access exception.

  • You then look up the backtrace to this code (and that’s a way up the backtrace, 35 frames in all).

  • And the debugger shows you that data.count is now 0.

Right?

If so, it’s hard to say exactly what might be causing that. Even if the debugger is working perfectly — which is a pretty big ‘if’ — you’ve crashed with a memory access exception, which means that by definition your program is in an undefined state.

The crash is intermittent and took me a day of running the app to
catch.

That makes things tricky. I recommend that you start with the standard memory debugging tools to see if that makes the problem easier to reproduce.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

Thanks for a speedy reply.

"-Which calls a delegate method.
-Which calls a whole bunch of other stuff.
-Which eventually crashes with a memory access exception."

The delegate method tries to decode "data" into a struct in the first line, like so:
if let packet = try? JSONDecoder().decode(MyStruct.self, from: data){
That line crashes due to "data" being 0 (although "data" may not be 0, if I understand you correctly, due to the program being in an undefined state after crash, which causes debugger to misreport).

I guess what I am trying to ask is--can I count on "data" not to mutate? It's declared as "let Data?", so do I need to clone it before using it? My guess is no, but I have to ask... @eskimo

Thank you.

I may be mis-reading your screenshot, but sizePrefix on line 2578 has the white background that makes me think it was autofilled by the editor, and not written in. I'd expected Xcode to throw an error in that situation, but check to make sure that's not a code-block waiting to be filled in.

I highlighted it to copy and paste in po

1 Like

I think data shown as empty is a red herring, and when in doubt I wouldn't trust debugger, as I've seen many instances when it lied to me. Logging is your friend – put it in place, reproduce the issue and analyse the logs, it would probably require several iterations until you catch it, but it's doable.

1 Like

You are reading the sizePrefix as radix 16 (hex) number. How does that play together with receive and all? Sorry if this is a stupid question, haven't used NWConnection myself.

I make two calls to receive one TCP packet. First is for a min/max of fixed size and that packet (sizePrefix) is (once decoded) a hexadecimal string, which represents the byte size of actual data. The second call is to receive actual data and is of min/max = sizePrefix

You nailed it (red herring)!

I got help from @itaiferber -- with his advice I nailed the defect(s) :D
Much respect!