Darrell Root on Networking and Swift

Opening a socket with Network Framework

Apple introduced the new Network framework in 2018. This framework is usable by Swift, but is an Apple-specific framework (so not available on Linux or Windows). It provides a new API for opening sockets which is easier to use than the old BSD sockets API.

My telnetswift CLI application (currently under development) uses the Network framework to open a TCP socket to a destination. We will use that as an example.

A socket is identified by 5 things: 1) Source IP 2) Source Port 3) Destination IP 4) Destination Port 5) Protocol

While it is possible to specify a source IP or source Port when opening a socket, you normally allow the OS to pick the optimal source IP address and a "random high" source port when initiating an outbound socket.

For destination IP address, it is best to let the Network framework perform the DNS lookup on your behalf. That allows the framework to decide whether to communicate over IPv4 or IPv6. Apple's modern frameworks, including the Network framework, prefer IPv6 over IPv4 if performance is similar.

Destination ports are integers between 0 and 65535. They identify the service or process to connect to. Well-known services include ssh (port 22), telnet (port 23), www/http (port 80) and www/https (port 443). If you allow a user to specify a port, you may want to validate that the integer they provide is between 0 and 65535.

import Foundation
import Network

class TcpReader {
    let connection: NWConnection

init(hostname: String, port: Int) throws {
    guard port > 0 && port < 65536 else {
        throw TelnetError.invalidPort
    }
    guard let portEndpoint = NWEndpoint.Port(port.description) else {
        throw TelnetError.invalidPort
        
    }
    
    let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(hostname), port: portEndpoint)

    self.connection = NWConnection(to: endpoint, using: .tcp)
    

While we have created our NWEndpoing.hostPort and NWConnection objects, we have not yet activated our connection (and DNS resolution of our hostname has not yet been attempted). Before we start our connection, we need to set up our "state update handler" with closures to handle each state. This is the bulk of our code.

connection.stateUpdateHandler = { [weak self] (newState) in
    debugPrint("\(#file) \(#function) \(newState.description)")
    switch newState {
    
    case .setup:
        break
    case .waiting(_):
        break
    case .preparing:
        break
    case .ready:
        self?.receive()
        break
    case .failed(_):
        break
    case .cancelled:
        break
    }
}

In the code above, we print out the state each time we change state, but only take action when we receive data (we will call a function on our NWConnection to send data separately). Here is what it looks like when we try to connect to a server without

Tagged with: