Building a server-client application using Apple's Network Framework Sep 10 2019 Latest Update: Aug 20 2020

Apple's network APIs are many, the older APIs are well documented, and you'll find lots of examples. Not so much about the new Network framework. One factor might be that the name is not that search-friendly. In this post, I'll explain how to use the NWFramework by creating a basic TCP server-client application.

The server will work as an echo, any message received will be sent back to the client. The client will allow us to send messages to the server and display the server response.

Let's start by reviewing some networking concepts.

(You can find the complete code on the Github repository https://github.com/rderik/rdncat)

* NOTE: You can also get the "macOS network programming in Swift" guide. It includes more topics on building network applications on macOS, including:

You can get it from the Guides section:

rderik.com - Guides

Networking

We are not going into too much detail, only a general overview of networking, so we start from the same base.

If you've worked on networks, you probably remember the OSI model.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|-----+--------------+--------------------------------------------------------------|
| No. | Layer        | Description ( in simple terms and not academic in nature )   |
|-----+--------------+--------------------------------------------------------------|
| 7   | Application  | High level API(e.g HTTP, Websocket)                          |
|-----+--------------+--------------------------------------------------------------|
| 6   | Presentation | This is where the data form the network                      |
|     |              | is translated to the application (encoding,                  |
|     |              | compression, encryption). This is where TLS lives.           |
|-----+--------------+--------------------------------------------------------------|
| 5   | Session      | Where the sessions are established, think Sockets.           |
|-----+--------------+--------------------------------------------------------------|
| 4   | Transport    | Provides the means to send variable length data sequences.   |
|     |              | Think TCP, UDP.                                              |
|-----+--------------+--------------------------------------------------------------|
| 3   | Network      | Provides the capability to send data sequences between       |
|     |              | different networks. Think of routing of datagrams.           |
|-----+--------------+--------------------------------------------------------------|
| 2   | Data link    | This layer is in charge of the node to node data transfer.   |
|     |              | Directly connected nodes.                                    |
|-----+--------------+--------------------------------------------------------------|
| 1   | Physical     | In this layer data is transmitted/received to/from a         |
|     |              | physical device.                                             |
|-----+--------------+--------------------------------------------------------------|

(Read more on OSI Model, or if you find the TCP/IP model easier to understand. Whichever works for you.)

We are going to be working on layers five to seven(Transport-Application). We use all the lower layers, but we don't directly interface with them.

We'll use Sockets to make connections between nodes (Client/Server in our example). And we are going to be using TCP as our Transport Protocol.

Transport protocols

The most common transport protocols are TCP and UDP. Both of them provide the capability of sending datagrams, one of the key differences is in how they handle package loss and order.

Based on that, you could select the one that works best for your case. For example:

There are more transport protocols, but unless you are in a particular case, you are more likely to work with TCP or UDP.

Right, that should be enough of networking to get us started. But before we continue, I want to encourage you to learn more about networking. Networking is a fascinating topic to explore and it is the base of Internet as we know it, it is worth your time to learn. Ok, let's continue.

Apple's networking API families

Apple provides various APIs to cover your networking needs. Here is a list of them with a small suggestion on when would you decide to use them.

In our client/server implementation, We are going to be using the Network framework (from now on NWFramework). I'm not sure why they decided on such a generic name. Searching for "Network Framework" will drive you crazy trying to filter out the results. My suggestion is to search directly for NWConnection or any other method/object in the framework.

Ok, let's get started.

Client/Server

There is lots of ground to cover so we are going to go fast. First, let's see what are we going to be building.

We are going to create a server that echoes everything it receives, and a client that can send commands and read the server response. We are going to use NWFramework because we don't want to only support common application protocols(e.g. HTTP or FPT). We want our server to handle any custom protocol, and that our client can send any message to any server.

We are going to build a command-line tool. The tool can be run as a Server (we'll specify the port for listening) or as a client (we specify the server and port we want to connect to). It will be our own simple version of ncat.

How will it work?

Our tool is going to be called rdncat (as in RDerik ncat, clever I know). To run the command as a server, we'll use the following pattern:

1
$ rdncat -l <PORT>

This will start our tool in server-mode(listening). It'll listen for connections on port 8888 and will echo everything it receives on that port.

If we want to run our tool as a client, we'll use the following pattern:

1
$ rdncat <SERVER> <PORT>

That will establish a TCP connection, and we'll be able to send messages to the server and receive it's response.

It'll be a simple but useful tool. I'm going to be using the Swift Package Manager in the examples to create the command-line tool. But you can use Xcode, I'll explain any stumbling blocks you might find using Xcode.

Building our command-line tool rdncat

Let's first create the skeleton of the tool so we can then focus on the networking part.

If you are using Xcode, create a new project "File > New > Project" and Select macOS > Command Line Tool and name it rdncat.

If you are using SPM.

1
2
3
$ mkdir rdncat
$ cd rdncat
$ swift package init --type executable

Now let's work on main.swift. First, we are going to parse the arguments. If we find the -l flag as the first argument, we assume its a server, else we assume its a client.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Foundation

var isServer = false

let firstArgument = CommandLine.arguments[1]
switch (firstArgument) {
case "-l":
    isServer = true
default:
    break
}

if isServer {
    if let port = UInt16(CommandLine.arguments[2]) {
        print("Starting as server on port: \(port)")
    } else {
        print("Error invalid port")
    }
} else {
    let server = CommandLine.arguments[1]
    if let port = UInt16(CommandLine.arguments[2]) {
      print("Starting as client, connecting to server: \(server) port: \(port)")
    } else {
        print("Error invalid port")
    }
}
RunLoop.current.run()

We can build it and run it.

Using Xcode: we could set the parameters by editing the scheme but is a little bit annoying. We are going to build it, and then search for the executable on the File Navigator under Products. You'll find rdncat there. Drag and drop the rdncat executable on the Terminal, you'll see the path is already added to the command line. Now, you can set the parameters before running it.

If you are using the swift package manager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Running as server
$ swift run rdncat -l 8888
# Starting as a server on port: 8888
# Press Ctrl+c to stop

# Running as client
$ swift run rdncat localhost 8888
# Starting as client, connecting to server: localhost port: 8888
# Press Ctrl+c to stop

# If you dragged the executable from Xcode, you'd only need to add the arguments.
# Your path will be different.
# As server
$  /Users/derik/Library/Developer/Xcode/DerivedData/rdncat-ggdyulmvlnwvhxhbyimmjzmlcauu/Build/Products/Debug/rdncat -l 8888
# Starting as server on port: 8888
# PressCtrl+c to stop

# As client
$  /Users/derik/Library/Developer/Xcode/DerivedData/rdncat-ggdyulmvlnwvhxhbyimmjzmlcauu/Build/Products/Debug/rdncat localhost 8888
# Starting as client, connecting to server: localhost port: 8888
# Press Ctrl+c to stop

Alright, that is the last time I explain the distinction between SPM and Xcode. I think you get the idea.

We now have our main structure ready. Let's focus on making the networking aspect work.

The server

We'll begin with the server. Our server will listen for incoming connections, once the connection is established, the server should keep a list of all the active connections. To handle the listening, we are going to use an NWListener object. The NWListener waits for connections and will call the newConnectionHandler function every time it gets a new connection. You'll see that the NWFramework objects rely on handler functions to manage events and changes in state.

Let's create our server, create the file Server.swift.

I'll show you the code in parts. Some parts require additional explanation to make them clear. At the end of this section, you'll see the complete file.

1
2
3
4
5
6
7
8
9
import Foundation
import Network

@available(macOS 10.14, *)
class Server {
    let port: NWEndpoint.Port
    let listener: NWListener

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

We import the Network framework, to have access to NWListener, NWEndpoint and NWConnection. Also, we'll add the @available(macOS 10.14, *) attribute because NWFramework is only available from 10.14 onwards. If you were wondering the NWEndoint represents an endpoint on a network connection.

Our initializer will look like this:

1
2
3
4
    init(port: UInt16) {
        self.port = NWEndpoint.Port(rawValue: port)!
        listener = try! NWListener(using: .tcp, on: self.port)
    }

Now we are going to create a start function that will start the listening and handling of connections:

1
2
3
4
5
6
    func start() throws {
        print("Server starting...")
        listener.stateUpdateHandler = self.stateDidChange(to:)
        listener.newConnectionHandler = self.didAccept(nwConnection:)
        listener.start(queue: .main)
    }

We use the main queue to run our code on the server. In our client code, that will be different. We will create a specific queue to handle the asynchronous connections.

As you can see, we define the handler functions for the listener.stateUpdateHandler and listener.newConnectionHandler. Let's look at stateDidChange:

1
2
3
4
5
6
7
8
9
10
11
    func stateDidChange(to newState: NWListener.State) {
        switch newState {
        case .ready:
          print("Server ready.")
        case .failed(let error):
            print("Server failure, error: \(error.localizedDescription)")
            exit(EXIT_FAILURE)
        default:
            break
        }
    }

Simple enough, there are other cases(e.g. cancelled, waiting, etcetera) but I'm not taking care of them directly to keep things simple. Now let's see our didAccept function:

1
2
3
4
5
6
7
8
9
10
    private func didAccept(nwConnection: NWConnection) {
        let connection = ServerConnection(nwConnection: nwConnection)
        self.connectionsByID[connection.id] = connection
        connection.didStopCallback = { _ in
            self.connectionDidStop(connection)
        }
        connection.start()
        connection.send(data: "Welcome you are connection: \(connection.id)".data(using: .utf8)!)
        print("server did open connection \(connection.id)")
    }

We create a ServerConnection, this is a class that we will create to wrap NWConnection so it'll be easy to keep the Connection code separated and easier to understand. Then we set up a callback function that will be called when the connection is stopped (We define it next). Also, we send a welcome message to the client when the connection is established. Let's see connectionDidStop:

1
2
3
4
    private func connectionDidStop(_ connection: ServerConnection) {
        self.connectionsByID.removeValue(forKey: connection.id)
        print("server did close connection \(connection.id)")
    }

We just remove the connection from our list and display a message. Here is the complete code for our Server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import Foundation
import Network

@available(macOS 10.14, *)
class Server {
    let port: NWEndpoint.Port
    let listener: NWListener

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

    init(port: UInt16) {
        self.port = NWEndpoint.Port(rawValue: port)!
        listener = try! NWListener(using: .tcp, on: self.port)
    }

    func start() throws {
        print("Server starting...")
        listener.stateUpdateHandler = self.stateDidChange(to:)
        listener.newConnectionHandler = self.didAccept(nwConnection:)
        listener.start(queue: .main)
    }

    func stateDidChange(to newState: NWListener.State) {
        switch newState {
        case .ready:
          print("Server ready.")
        case .failed(let error):
            print("Server failure, error: \(error.localizedDescription)")
            exit(EXIT_FAILURE)
        default:
            break
        }
    }

    private func didAccept(nwConnection: NWConnection) {
        let connection = ServerConnection(nwConnection: nwConnection)
        self.connectionsByID[connection.id] = connection
        connection.didStopCallback = { _ in
            self.connectionDidStop(connection)
        }
        connection.start()
        connection.send(data: "Welcome you are connection: \(connection.id)".data(using: .utf8)!)
        print("server did open connection \(connection.id)")
    }

    private func connectionDidStop(_ connection: ServerConnection) {
        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()
    }
}

Notice the last function stop, we use it to stop our server cleanly.

Let's implement our connection wrapper class.

Create a new file: ServerConnection.swift. As before I'll show parts of the code and then post the whole file at the end of the section:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import Foundation
import Network

@available(macOS 10.14, *)
class ServerConnection {
    //The TCP maximum package size is 64K 65536
    let MTU = 65536

    private static var nextID: Int = 0
    let  connection: NWConnection
    let id: Int

    init(nwConnection: NWConnection) {
        connection = nwConnection
        id = ServerConnection.nextID
        ServerConnection.nextID += 1
    }

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

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

We are keeping track of nextID, it represents an incremental counter for our connection ids. Every time we create a new connection, we assign the id and increment the counter. As you can see the NWConnection also defines a stateUpdateHandler to manage the state changes. The function setupReceive prepares the connection to receive data, we'll see it shortly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    private func stateDidChange(to state: NWConnection.State) {
        switch state {
        case .waiting(let error):
            connectionDidFail(error: error)
        case .ready:
            print("connection \(id) ready")
        case .failed(let error):
            connectionDidFail(error: error)
        default:
            break
        }
    }

    private func setupReceive() {
        connection.receive(minimumIncompleteLength: 1, maximumLength: MTU) { (data, _, isComplete, error) in
            if let data = data, !data.isEmpty {
                let message = String(data: data, encoding: .utf8)
                print("connection \(self.id) did receive, data: \(data as NSData) string: \(message ?? "-")")
                self.send(data: data)
            }
            if isComplete {
                self.connectionDidEnd()
            } else if let error = error {
                self.connectionDidFail(error: error)
            } else {
                self.setupReceive()
            }
        }
    }

Our function to handle the state change(stateDidChange) is simple, it just displays what event is occurring. Now, setupReceive calls receive on NWConnection and passes a closure that will be called when data is received. In our case, we are reading the data and sending it back to the client as an echo.

1
2
3
4
5
6
7
8
9
    func send(data: Data) {
        self.connection.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)")
        }))
    }

Here our send function calls send on our NWConnection object and passes a closure that will be called when the content is processed. The rest of the methods are straight forward so I'll just show you the whole Server.swift file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import Foundation
import Network

@available(macOS 10.14, *)
class ServerConnection {
    //The TCP maximum package size is 64K 65536
    let MTU = 65536

    private static var nextID: Int = 0
    let  connection: NWConnection
    let id: Int

    init(nwConnection: NWConnection) {
        connection = nwConnection
        id = ServerConnection.nextID
        ServerConnection.nextID += 1
    }

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

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

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

    private func setupReceive() {
        connection.receive(minimumIncompleteLength: 1, maximumLength: MTU) { (data, _, isComplete, error) in
            if let data = data, !data.isEmpty {
                let message = String(data: data, encoding: .utf8)
                print("connection \(self.id) did receive, data: \(data as NSData) string: \(message ?? "-")")
                self.send(data: data)
            }
            if isComplete {
                self.connectionDidEnd()
            } else if let error = error {
                self.connectionDidFail(error: error)
            } else {
                self.setupReceive()
            }
        }
    }


    func send(data: Data) {
        self.connection.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 \(id) will stop")
    }



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

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

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

With that, we can implement our server functionality. Let's go back to our main:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import Foundation


if #available(macOS 10.14, *) {
var isServer = false

func initServer(port: UInt16) {
    let server = Server(port: port)
    try! server.start()
}

let firstArgument = CommandLine.arguments[1]
switch (firstArgument) {
case "-l":
    isServer = true
default:
    break
}

if isServer {
    if let port = UInt16(CommandLine.arguments[2]) {
      initServer(port: port)
    } else {
        print("Error invalid port")
    }
} else {
    let server = CommandLine.arguments[1]
    if let port = UInt16(CommandLine.arguments[2]) {
      print("Starting as client, connecting to server: \(server) port: \(port)")
    } else {
        print("Error invalid port")
    }
}
RunLoop.current.run()

} else {
  let stderr = FileHandle.standardError
  let message = "Requires macOS 10.14 or newer"
  stderr.write(message.data(using: .utf8)!)
  exit(EXIT_FAILURE)
}

Because we are using functionality only available on macOS 10.14 and higher we'll add that validation. We only added the initServer function to initialize our Server.

Now, we can run the Server:

1
2
# running our server on port 8888
$ swift run rdncat -l 8888

Now in another shell, let's send an HTTP request using curl.

1
$ curl localhost:8888

We should see the request on our server.

Great! We send the echo back to curl. But curl doesn't know what to do with that so it won't show anything, that is ok.

Perfect!

Now let's move to the client.

The client

Our client will allow us to send messages to any server, it will work with our server or with any other server that accepts a TCP connection.

As we did with ServerConnection, we are going to wrap the client NWConnection on its own object. But first, let's create our Client object(create the file Client.swift).

The client class is small, so I'll show the complete file next:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import Foundation
import Network

@available(macOS 10.14, *)
class Client {
    let connection: ClientConnection
    let host: NWEndpoint.Host
    let port: NWEndpoint.Port

    init(host: String, port: UInt16) {
        self.host = NWEndpoint.Host(host)
        self.port = NWEndpoint.Port(rawValue: port)!
        let nwConnection = NWConnection(host: self.host, port: self.port, using: .tcp)
        connection = ClientConnection(nwConnection: nwConnection)
    }

    func start() {
        print("Client started \(host) \(port)")
        connection.didStopCallback = didStopCallback(error:)
        connection.start()
    }

    func stop() {
        connection.stop()
    }

    func send(data: Data) {
        connection.send(data: data)
    }

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

We use the NWEndpoint to define the host and the port, then create our NWConnection and use it to build a ClientConnection. Remember ClientConnection is our wrapper around NWConnection.

Let's create ClientConnection. The new connection wrapper might seem more familiar to you now, after working on the ServerConnection.

Create the ClientConnection.swift file. I'll show a fragment first to make some details clearer, then I'll post the complete content.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Foundation
import Network

@available(macOS 10.14, *)
class ClientConnection {

    let  nwConnection: NWConnection
    let queue = DispatchQueue(label: "Client connection Q")

    init(nwConnection: NWConnection) {
        self.nwConnection = nwConnection
    }

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

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

As you can see, we are creating our own DispatchQueue to manage the asynchronous code. We do this, so it doesn't block the main queue. We will want this so we can do other things in the main queue (like prompting the user for a message to send, etcetera). Also, we declare the didStopCallback property, it will hold the closure that will be executed when the connection stops. Those are the two main differences between the ServerConnection and ClientConnection, the rest is almost identical. Here is the full file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import Foundation
import Network

@available(macOS 10.14, *)
class ClientConnection {

    let  nwConnection: NWConnection
    let queue = DispatchQueue(label: "Client connection Q")

    init(nwConnection: NWConnection) {
        self.nwConnection = nwConnection
    }

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

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

    private func stateDidChange(to state: NWConnection.State) {
        switch state {
        case .waiting(let error):
            connectionDidFail(error: error)
        case .ready:
            print("Client connection ready")
        case .failed(let error):
            connectionDidFail(error: error)
        default:
            break
        }
    }

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

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

    func stop() {
        print("connection will stop")
        stop(error: nil)
    }

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

    private func connectionDidEnd() {
        print("connection 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)
        }
    }
}

With this, we have all the pieces needed to work on our main file and add the client code. We want to allow the user to enter a loop of sending messages and reading the response from the server, so we will add that to our main. Let's see the initClient(server:port).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func initClient(server: String, port: UInt16) {
    let client = Client(host: server, port: port)
    client.start()
    while(true) {
      var command = readLine(strippingNewline: true)
      switch (command){
      case "CRLF":
          command = "\r\n"
      case "RETURN":
          command = "\n"
      case "exit":
          client.stop()
      default:
          break
      }
      client.connection.send(data: (command?.data(using: .utf8))!)
    }
}

We are going to provide some minimum set of commands:

The rest is quite simple. Let's see the full code of main.swift:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import Foundation

if #available(macOS 10.14, *) {
var isServer = false

func initServer(port: UInt16) {
    let server = Server(port: port)
    try! server.start()
}

func initClient(server: String, port: UInt16) {
    let client = Client(host: server, port: port)
    client.start()
    while(true) {
      var command = readLine(strippingNewline: true)
      switch (command){
      case "CRLF":
          command = "\r\n"
      case "RETURN":
          command = "\n"
      case "exit":
          client.stop()
      default:
          break
      }
      client.connection.send(data: (command?.data(using: .utf8))!)
    }
}

let firstArgument = CommandLine.arguments[1]
switch (firstArgument) {
case "-l":
    isServer = true
default:
    break
}

if isServer {
    if let port = UInt16(CommandLine.arguments[2]) {
      initServer(port: port)
    } else {
        print("Error invalid port")
    }
} else {
    let server = CommandLine.arguments[1]
    if let port = UInt16(CommandLine.arguments[2]) {
      initClient(server: server, port: port)
    } else {
        print("Error invalid port")
    }
}
RunLoop.current.run()

} else {
  let stderr = FileHandle.standardError
  let message = "Requires macOS 10.14 or newer"
  stderr.write(message.data(using: .utf8)!)
  exit(EXIT_FAILURE)
}

Great, now we can run both the server and the client. In one shell run the server:

1
$ swift run rdncat -l 8888

In a new shell run the client:

1
$ swift run rdncat localhost 8888

On the client, you can send messages and see the echo from the server.

Good job! You've created a TCP server/client using NWFramework.

Final Thoughts

I hope you've seen how easy it is to work with network connections using the new NWFramework. We used TCP when creating our NWConnection, but you can also explore using the UDP.

Also, you can try to connect to a webserver using our client. Try this:

1
$ swift run rdncat google.com 80

Send the following commands:

1
2
GET / HTTP/1.1
CRLF

You could play with more back and forth of the HTTP protocol. Or maybe try our client with an FTP server:

1
$ swift run rdncat yourserver.test 21

And send the following commands:

1
2
USER youruser
CRLF

You'll see that you started the handshake for establishing an FTP session. You can see how our client can be handy to test your own custom protocols.

The server and client are very basic, so they are not to be used in production and least of all send private data like passwords through them.

I hope you find our implementation useful and that you enjoyed the post. As always, feedback is welcome.

(You can find the complete code on the Github repository https://github.com/rderik/rdncat)

Related topics/notes of interest


** If you want to check what else I'm currently doing, be sure to follow me on twitter @rderik or subscribe to the newsletter. If you want to send me a direct message, you can send it to derik@rderik.com.