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.

Thanks a lot for the replies!

Please, I need help. I don't understand how to install the library SwiftNIO, and if I would use the Network framework, someone can provide a quick and easy example of server and client?

Thanks again, and excuse me, I am a noob in swift I know only something of VB6 and Python...

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.

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)
        }
    }))
}
3 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

1 Like