Using BSD Sockets in Swift
Apple provides many useful network frameworks. Network.framework (you can check my previous article on Network.framework if you want to see an example) is the latest. But sometimes we need to go deeper, and the abstractions might get in the way. In this post, we are going to see how to use BSD sockets directly in Swift.
Using BSD sockets means interfacing with C from Swift. Let’s first have a look at some concepts and tips that will help us with using sockets.
** You can check the full code of the server in the GitHub repository.
* 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:
C interoperability
In macOS we have access to BSD sockets, via the C socket(2) function. Using the socket syscall will require us to interoperate with C code, which will inevitably bring us to pointers.
In C we are in charge of memory management, which makes it very flexible but error-prone. One of Swift’s strengths is that memory management is taken care for us by the language (ARC, etc.). Calling C functions in Swift is as easy as any other function call. Things start to get trickier when we have to deal with pointers. The compiler will try to keep us safe, so we will have to make some accommodations.
When we use pointers in Swift, we use one of its eight types of pointers:
- UnsafePointer
- UnsafeMutablePointer
- UnsafeBufferPointer
- UnsafeMutableBufferPointer
- UnsafeRawPointer
- UnsafeMutableRawPointer
- UnsafeRawBufferPointer
- UnsafeMutableRawBufferPointer
Pointers are marked as Unsafe to make clear we are treading in territory that the compiler can not keep us entirely safe. When we see Mutable, it means that we can change the pointer’s pointee content. When we see Raw, it means that we are pointing directly to memory address without any particular representation. And Buffer means that the pointer “points” to a collection of elements(e.g. an array of chars). That should give us an idea of which type to use, but in reality, the Compiler will let you know when you are using the incorrect type (Which makes things easy).
Another common scenario we might find ourselves in when working with C functions is pointer casting. We get a pointer to one structure, and that structure can be interchanged by another structure (That sounds complicated, but think about casting one struct type to another). The problem here is that the compiler will not let us use our structure because it is defined as a specific type. We will need to cast it to the other equivalent type somehow. If we are completely sure of what we are doing, we use the function withUnsafe[PointerType]. The function withUnsafe[Mutable][Raw][Buffer]Pointer allows us to get a reference to our pointer and use it inside a closure. Now we can call withMemoryRebound on our new pointer. The function withMemoryRebound allows us to get a temporary pointer of a specific type and execute a closure that has access to that temporary pointer. Let’s see an example:
| |
Here bind expects a pointer to a sockaddr but we have a sockaddr_in6. Our address variable has more info than a regular sockaddr, but if we were to treat it as a sockaddr, it would have the required info a sockaddr structure has.
Also, note that the $0 in each closure references a different object, we are trying to be succinct and not declare a temporary variable (e.g. { newPointer in ... }) so we are only using the default $0. The variable $0 is an autogenerated variable that points to the first parameter of our closure.
We don’t need to use this technique often. We only use it if we are sure that our structure can be used as a different structure type. We have to be careful. If our structures are not interchangeable, we might cause some odd behaviour (i.e. not necessarily a crash, but a weird result).
Another useful concept to know when dealing with pointers in Swift is pointee. ̇When we have a pointer we can access the object it points to by using the pointee function. For example:
| |
Ok, that should be enough for us to work with native sockets from Swift.
If you find yourself working a lot with C code from Swift, you should read Apple’s documentation on C Interoperability with Swift.
Ok, let’s start working with sockets.
Using sockets in Swift
A socket is a structure that allows us to communicate two processes. It can be in the same machine or remote. Nowadays, we usually think that sockets are only used for Internet connections. But sockets can also be used as Inter-Process-Communication (IPC) structures. We are going to focus on sockets for Internet connections. I encourage you to check how sockets could also be used to create IPC behaviour, for example, pipes(using AF_UNIX).
In general, when we are working with sockets we’ll have in one end a Server and in the other a Client. Depending on the use we’ll give a socket, the creation and setup changes.
If we are going to create a socket as a client that connects with a server, we would have the following workflow:
- Create a socket - specifying the domain, type and protocol.
- Define the socket address structure that will represent the address we want to connect to.
- Connect the client socket to the server.
- After connecting, we obtain a file descriptor that we can read/write from/to.
If we need a socket for a server, the general build and setup will look like the following:
- Create a socket - specifying the domain, type and protocol.
- Define the socket address structure that will represent our server address, port and other properties.
- We bind the socket to the address.
- After binding the socket to the address, prepare the file descriptor to listen for incoming connections.
- And finally we accept connections - Every new connection will give us a new file descriptor to communicate with each client.
Alright, let’s build a TCP server using BSD sockets.
Echo Server using BSD sockets
We are going to build a command-line tool using the Swift Package Manager. So let’s create it.
| |
Now our main will only be the entry point for our server. Edit it and add the following content:
| |
Let’s now create the Server class. Create a new file Server.swift inside the sources directory, and add the following content.
| |
Now we can build and run it. We should see our message “Server starting…”. You can stop the program pressing [Ctrl+C].
| |
Good, we have our basic program working, now we can focus on learning about sockets.
Steps to build our server socket
Remember the steps we defined for a server socket at the beginning of the post:
- Create a socket - specifying the domain, type and protocol.
- Define the socket address structure that will represent our server address, port and other properties.
- We bind the socket to the address.
- After binding the socket to the address, prepare the file descriptor to listen for incomming connections.
- And finally we accept connections - Every new connection will give us a new file descriptor to communicate with each client.
We are going to be working on the start function in our Server. I’ll show you blocks of the code that we are working on so we can focus on the details for each step of the process. At the end of this section, you’ll find the complete code for the Server.
Let’s begin.
1. Creating the socket
Creating the socket is the easy part. Let’s declare the socket and initialize it. We are going to declare a socket that uses the IPV6 family of protocols and uses TCP:
| |
The socket domain specifies the protocol family that the socket will use. Some examples are:
AF_INETfor IPV4.AF_INET6for IPV6.AF_UNIXfor local communication using file descriptros.
The typical types you’ll see are:
SOCK_STREAMfor TCPSOCK_DGRAMfor UDPSOCK_RAWif you want to be incharge of the protocol.
Finally, we define the protocol. Again, I’ll list some common protocols that you might recognize:
IPPROTO_UDPfor UDP.IPPROTO_TCPfor TCP.IPPROTO_ICMPfor ICMP (ping uses this protocol).
2. Define the socket address structure
The second step would be to create the socket address structure. Here we see a difference between using IPV4 and IPV6. Each has a different socket address structure. An abstraction exists here that is used to “emulate” something similar to inheritance. We have the following structures:
| |
sockaddr is the structure that most of the kernel functions expect, but there are two other structs that represent IPV4 and IPV6 specific cases.
| |
The required fields are the ones represented by sockaddr. If we get a sockaddr and we want to know if it is an in or in6, we can check the sa_family_t and also the length.
We rarely define the sockaddr_t directly, only in specific cases that we are sure we require one or the other. The general “best practice” is to use the getaddrinfo function. Here is the signature of the getaddrinfo function:
| |
node- represents the host.service- this parameter can be the name of a common service (You can see the services declared in the system by readingman 5 services) or a port number.hits- the hints structure provides additional information that the function uses to select the return address structures better.res- is a pointer that will contain the newaddrinfosocket returned by the function.- The function returns an
int, error is represented by-1.
Let’s use getaddrinfo to obtain our sockaddr structure.
| |
Now we have in serverinfo the ai_addr pointing to a sockaddr structure that we can use in the bind function. Let’s bind our socket to the sockaddr.
3. Binding the socket to the address.
Binding is an easy process. We only need to call bind with our socket and sockaddr. Here is the signature of bind:
| |
The first parameter is the file descriptor we obtained when we created the socket. The second parameter is a pointer to our sockaddr structure. And last, we need to pass the length of our sockaddr.
| |
4. Prepare to listen for connections
Before we are ready to accept connections on our socket, we need to prepare it. We use the listen function to set up the socket that is going to be accepting connections and also define the queue limit for the incoming connections. Here is the signature of listen:
| |
The first parameter is the file descriptor we obtained when we created our socket. The second parameter, backlog, defines how big the queue of incomming connections can be before connections start being dropped. The function returns -1 for error. In this and the previous functions, we can check errno for error code.
Let’s prepare our socket for listening to connections:
| |
Now we can finally accept a connection.
5. Accept a connection
We are going to create a never ending loop that will accept connections. The server will be sending back the message it received from the client as an echo.
| |
Alright, there is a lot to take here, so let’s break it down. First, we declared MTU that will be the Maximum Transmition Unit that we are expecting. Our Server can only process clients synchronously, that means, we will only serve one client at a time. I didn’t want to add more complexity to our example using threads or GCD.
We now wait for a client to try to connect to us using accept. Once a client has connected, we’ll obtain a file descriptor from where we can read and write. We are going to create a variable named buffer to put the data we read from our client. And we are going to enter a cycle to read and echo whatever the client sends.
Here is whole Server.swift file.
| |
Right, now we can build and run it:
| |
Perfect, our server is now ready to accept connections.
To test it you can use ncat, or rdncat if you have the version we created in the previous post about Network.framework. In a different shell run your favourite version of ncat.
| |
You can send text, and you’ll get it back. That’s it. Our server is working correctly. Congratulations!
Final Thoughts
I tried to create the barebones of the server, so we don’t get distracted with anything else, and we focus on using sockets in Swift. We are not going to build the client. I think you’ll be able to piece it together with what we did for the server. The client is more straightforward, but if you want me to write about it, let me know, and I’ll add it.
We also avoided concurrency, no threads and/or GrandCentralDispatch. Again, I did it to keep the server as simple as possible. But I encourage you to explore those topics. They are essential to building a useful server and not only a skeleton as we’ve done today.
Working directly with memory in Swift is not super fun (ok, maybe it can be fun), but it also brings many dangers. If you were observant when you were running your server you might have noticed that the buffer was never cleared. Let me show you what I mean with the following example:
| |
You see the problem? hi is smaller than hello and the buffer was never cleared, or the string was not null-terminated. That is a simple mistake. Let’s fix it. We can add a null (Character 0) after the bytes we just read to mark the end of the string.
| |
You can see how things can start getting messy if we don’t pay attention to what our pointers are doing.
I would also encourage you to have a look at MemoryLayout enumeration, it’s handy to check the size, stride, and alignment of system types. For example, we can get the system size for sockaddr_t structure.
| |
Here is the link to Apple’s documentation to MemoryLayout.
Using sockets directly is not encouraged. It is better to use CFSockets and CFStreams they provide a friendlier interface. But if you need to use sockets, now you know how to use native sockets from Swift. Congratulations!
Ok, that’s it for today. I hope you find the post useful. Let me know what you think. And if you have tips on how to work with sockets send them my way. I’m always looking for better ways to do things.
You can check the full code of the server in the GitHub repository.
#Related topics/notes of interest
- Apple’s documentation on
Cinteroperability. - I suggest you have a look at the man pages for socket(2), bind(2), listen(2), accept(2), connect(2), getaddrinfo(3), services(5).
- If you want to check which ports are being used by your process, get the
PIDand use the following command:
| |
- Another post on using sockets in Swift.
- FreeBSD essential socket functions.
- Apple’s WWDC20 session on Unsafe Swift - Explains when and how to use
Unsafetypes. - Apple’s WWDC20 session on Safely manage pointers in Swift