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:
- BSD Sockets in Swift
- Apple's Network.framework
- SwiftNIO
You can get it from the Guides section:
Table of Contents
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.
TCP
- Checks for package loss, and also cares about the packages order.UDP
- Stateless. It doesn't concern itself with any package loss, and if it receives a package out of order, it'll just drop it.
Based on that, you could select the one that works best for your case. For example:
- If we are sending a document, we want all the data to be received and in order. For this case, use
TCP
. - If we are in a call, we don't mind some loss. The conversation could lose some packages. There is a certain flexibility. In this case, use
UDP
.
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.
- C POSIX (BSD) sockets, this API provides access to the lowest layer (All the way to Link layer through
PF_NDR
check mansocket(2)
) - CFNetwork, Core Foundation Network:
- CFSocket: Provides an abstraction on top of BSD Sockets integrating RunLoop support. You can create a
CFSocket
from a BSD Socket and add it to your current RunLoop(CFRunLoopAddSource
orCFSocketCreateRunLoopSource
). If you are working only on Apple's environment and you need low-level sockets, have a look atCFSocket
instead of directly using BSD sockets. - CFStream: Allows you to stream data in and out of memory, files, and more important for our case, network via Sockets. It abstracts the Socket so you can read/write as if you were using a file descriptor. If you want to read data as is coming through the socket, have a look at CFStreams.
- Other APIs: There are many other APIs provided by Core Foundation, but I won't go into much detail you can read about them in the official documentation. The list includes
CFFTP
,CFHTTP
,CFHTTPAuthentication
,CFHOST
,CFNetServices
andCFNetDiagnostics
.
- CFSocket: Provides an abstraction on top of BSD Sockets integrating RunLoop support. You can create a
- NS: At the Cocoa level, based on the Core Foundation APIs we have:
- NSSTream: This Cocoa API is based on CFStream, so you'll find a lot of common ground. But they are implemented with different methodologies in mind.
NSStream
uses the delegate pattern for asynchronous tasks, whileCFStream
makes use of callbacks to handle the asynchronous tasks. Also, becauseNSStream
belongs to Objective-c, you can subclass it and build new classes that fit your needs. If you are in Objective-c land useNSStream
. - NSURLConnection: It provides a nice abstraction over
NSStream
that give us easier access to making requests to URLs using the most common protocols(HTTP
,HTTPS
andFTP
). This API has been superseded byNSURLSession
, so there is no real need to use it unless you need to keep compatibility for some weird edge case. - NSURLSession: This API provides many new advantages from
NSURLConnection
, it allows you to download code without using a delegate, perform background downloads, and many more features. This API has been superseded byURLSession
on Swift, so if you are using Swift useURLSession
.
- NSSTream: This Cocoa API is based on CFStream, so you'll find a lot of common ground. But they are implemented with different methodologies in mind.
- URLSession: This is the latest abstraction for handling URL requests on Swift. If you are working with requests using common protocols like
HTTP
/HTTPS
useURLSession
. - Network: The
Network
framework provides direct access toTLS
,TCP
, andUDP
. If you need to work at the transport layer level, this would be your go-to.Network
abstracts a lot of the boilerplate and setup required by the alternatives (CFSockets
or raw sockets).
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:
- CRLF - send a Carriage Return Line Feed (\r\n) to the server.
- Return - send a Line Feed (\n) to the server.
- exit - close the connection.
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
- Watch this WWDC (2018) - 715 - Introducing Network.framework, they show an example using
UDP
for streaming video. - Network Framework Documentation, remember to toggle
objective-c
andswift
, to check differences. - If you want to learn more about
URLSession
, check official documentation onURLSession
. Or have a look at this post by John Sundell. - Check the following Apple article on differences between using
NSURLSession
(Recommended) andNSURLSessionConnection
. - Apples Core Foundation Network Concepts
- Apple's Stream Programming (Cocoa)
- Apple's document on Networking should definitely have a look.
- Swift Attributes Documentation, you can check there the
@available
attribute. - Our
rdncat
is useful, but you better have a look atncat
. - If you are curious about the protocol specification, have a look here: HTTP 1.1 - RFC2616, HTTP2 - RFC7540, and FTP - RFC959. Don't spect an engaging read there, they are Technical specs.
- If you are looking for something more digestible: HTTP. I haven't found a good article explaining the FTP protocol. If you know of any good one, send it my way, and I'll add it.
- Apple documentation on using sockets and socket streams.
- Wikipedia article on OSI model and TCP/IP model.
- I wanted to create a
ping
(ICMP protocol) utility. I thought of using any of the newer APIs. ButICMP
is too low level (OSI level 3 - Network), so I will have to use CFSockets. Once it is ready, I'll create a post about it. - I found a good example of
NWFramework
on swift forums. - WWDC20 session - Enable encrypted DNS - Shows the new suport for encrypted DNS. We can define DoH (DNS over HTTPS) and DoT (DNS over TLS), system wide or per app. It also intrgrates with the
Network.framework
.