A long time ago in a galaxy far, far away, software developers wrote client-server applications with TCP/IP sockets. That was before the dark times, before HTTP.
I am of course, joking. HTTP can be leveraged to provide a wide variety of client-server applications and is of course at the base of REST applications. What HTTP brings to the table though is not the plumbing to get packets on the wire, but an agreed upon protocol structure (and to some degree a standard as to what port is being used) for those packets. Action verbs such as GET, POST, PUT, etc. and the HTTP header itself are what makes HTTP ideal for developing client-server applications.
In the end though, at the bottom of the stack, bits and bytes are marshaled through your operating system’s socket interface. The API for interacting with network sockets is quite rich and many a tutorial and books have been written on the topic. IP networking routines in C can be considerably verbose, and were one of the first “real-world” APIs encapsulated in object-oriented routines with C++. That tradition continued with Foundation’s CFStream
class and now our Swift swiftysockets
API.
Swiftychat
To illustrate how to use TCP/IP network sockets with Swift, we’ll be developing Swiftychat, a basic “chat system” application. It’s quite a naive application, limited in functionality, and would stand no chance in being used in the real world, but, even still, it’s a working example of how to send and receive strings on TCP/IP sockets in Swift.
swiftysockets
Swiftychat will make use of swiftysockets, a Swift Package Manager-ready TCP/IP socket implementation that was originally developed by the Zewo team. Unfortunately due to packaging constraints we have to do a little bit of a dance first to get an underlying C library, Tide, installed on our system. So let’s do that now.
$ git clone https://github.com/iachievedit/Tide Cloning into 'Tide'... ... $ cd Tide $ sudo make install clang -c Tide/tcp.c Tide/ip.c Tide/utils.c ar -rcs libtide.a *.o rm *.o mkdir -p tide/usr/local/lib mkdir -p tide/usr/local/include/tide cp Tide/tcp.h Tide/ip.h Tide/utils.h Tide/tide_swift.h tide/usr/local/include/tide # copy .a cp libtide.a tide/usr/local/lib/ mkdir -p /usr/local cp -r tide/usr/local/* /usr/local/
At some point we believe the Swift Package Manager will be able to compile C libraries that can be linked against in the rest of your package build. Until then, this is the best we can do.
Once Tide is installed we can leverage swiftysockets in our Swiftychat apps.
Start Coding!
Our main.swift
file is as simple as it gets. Create a ChatterServer
and start
it.
main.swift
1 2 3 |
if let server = ChatterServer() { server.start() } |
Of course, a brief main.swift
can only mean one thing. Invasion. Oh wait, I’m done with the Star Wars references.
A brief main.swift
means our implementation is tucked away in the ChatterServer
class, which looks like this:
ChatterServer.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 60 61 62 63 64 65 66 67 68 69 70 |
import swiftysockets import Foundation class ChatterServer { private let ip:IP? private let server:TCPServerSocket? init?() { do { self.ip = try IP(port:5555) self.server = try TCPServerSocket(ip:self.ip!) } catch let error { print(error) return nil } } func start() { while true { do { let client = try server!.accept() self.addClient(client) } catch let error { print(error) } } } private var connectedClients:[TCPClientSocket] = [] private var connectionCount = 0 private func addClient(client:TCPClientSocket) { self.connectionCount += 1 let handlerThread = NSThread(){ let clientId = self.connectionCount print("Client \(clientId) connected") while true { do { if let s = try client.receiveString(untilDelimiter: "\n") { print("Received from client \(clientId): \(s)", terminator:"") self.broadcastMessage(s, except:client) } } catch let error { print ("Client \(clientId) disconnected: \(error)") self.removeClient(client) return } } } handlerThread.start() connectedClients.append(client) } private func removeClient(client:TCPClientSocket) { connectedClients = connectedClients.filter(){$0 !== client} } private func broadcastMessage(message:String, except:TCPClientSocket) { for client in connectedClients where client !== except { do { try client.sendString(message) try client.flush() } catch { // } } } } |
Breaking down our server we have:
1. Initialization
We make use of the init?
initializer to signal that nil
is a possible return value since both the IP
and TCPServerSocket
classes (from swiftysockets) can throw an error. IP
encapsulates our IP address and port information nicely and we provide an instance of it to the TCPServerSocket
initializer. If init
succeeds we now have TCP socket on the given port ready to accept incoming connections.
2. The Main Loop
Name the function startListening
, start
, main
, we don’t care. It is the main event loop that accepts new client connections (server!.accept()
) and adds them to the list of connected clients. server!.accept()
is a blocking function that hangs out and waits for new connections. Pretty standard stuff.
3. Client Management
The rest of the ChatterServer
contains all of the “client management” functions. There are a few variables and three routines that manage clients.
Our variables are straightforward:
- an array of connected clients (
[TCPClientSocket]
) - a connection counter that is used to hand out a “client identifier”
The routines are straightforward as well:
addClient
takes aTCPClientSocket
, increments our connection count, and then sets up anNSThread
whose sole purpose is “manage” that given client’s connection. As additional connections come in newNSThread
s are created for them as well. We’ll talk about theNSThread
routine itself in a moment. Once the thread is startedaddClient
will then append theTCPClientSocket
onto the end of the array of connected clients.removeClient
removes a client from the connected clients list using thefilter
function to “filter out” the given client. Note here we use the!==
identity operator.broadcastMessage
is what makes theChatterServer
a chat server. It uses thewhere
keyword to create a filtered array that broadcasts a message created by a client to all of the other connected clients. Again, we use the!==
operator.
Recall that a thread is a separate execution path that runs inside the main process. Our server creates a separate thread for each client that is connected. Now, you can argue whether or not that is a good idea, and if we’re designing a server that will eventually handle tens of thousands of clients, I’d argue that it isn’t. For our purposes though we’ll be fine.
Looking once more at our thread:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
let handlerThread = NSThread(){ let clientId = self.connectionCount print("Client \(clientId) connected") while true { do { if let s = try client.receiveString(untilDelimiter: "\n") { print("Received from client \(clientId): \(s)", terminator:"") self.broadcastMessage(s, except:client) } } catch let error { print ("Client \(clientId) disconnected: \(error)") self.removeClient(client) return } } } handlerThread.start() |
Our client handling thread also sits in a loop waiting for input through the receiveString
method of the TCPClientSocket
class. When a string is received the server logs it to the console and then broadcasts a response. If the try
results in an error (a disconnect) the server removes the client.
Putting it All Together
Our goal is to, as much as possible, use the Swift Package Manager for building our applications. For an introduction to swiftpm
check out our tutorial.
Add the following to Package.swift
:
1 2 3 4 5 6 7 8 |
import PackageDescription let package = Package( name: "chatterserver", dependencies: [ .Package(url: "https://github.com/iachievedit/swiftysockets", majorVersion: 0), ] ) |
and in a directory named Sources
add your main.swift
and ChatterServer.swift
code.
Running swift build
should download and build our two dependencies (Tide
and swiftysockets
) and compile our application code. If all goes well you’ll have a binary named chatterserver
in a directory named .build/debug/
.
Testing it Out
Our next tutorial will be writing up a nifty little chat client, but for now we can test our server with the nc
(netcat) command. Start your server and then in another terminal window type nc localhost 5555
. You should see in your server’s window Client 1 connected
. If you hit CTRL-C in your netcat “client” window the server will print a disconnect message along with the reason (like, Connection reset by peer
).
For the real test we’ll start up the server and three connected clients.
In the left-hand terminal our chat server is running. On the right we have 3 clients, each started by typing the command nc localhost 5555
. As each client connects the server prints out a connection message.
Recall that our broadcastMessage
function excludes the originator of the message from the broadcast. This prevents a client from receiving his own message back (take out the where
clause and you will see what we mean).
What’s Next
Using nc
as our client is a bit boring. We can’t set a nickname, there’s no “structure” to our messages, no timestamps, etc. In the above example someone receiving a message has no idea who was writing! swiftysockets
already has a TCPClientSocket
class, why not create a more robust chat client?
Getting the Code
Of course we’ve put together the code for our little chat server here on Github. It also contains a chatterclient
project which at the moment is not implemented. If you start with the download, you can type make
in the top-level directory and it will build both the client and server. Remember: You must have installed libtide.a
and its associated headers before using swiftysockets
!
You could simply use libevent or something similar based on epoll.
Indeed, libevent is also a possibility. If you have a working example of using it with Swift I’d love to explore it!
I do have one but since it’s about using a C library from within Swift the code is ugly.
error: The package at `/Packages/Tide’ has no Package.swift for the specific version: 0.1.0
swiftysockets create a empty Package.swift in Tide , how to make a Package.swift non empty?