Using BSD Sockets in Swift Sep 24 2019 Latest Update: Jun 27 2020
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 socket
s.
** 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:
Table of Contents
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
# lets cast our sockaddr_in6 to sockaddr
var address = sockaddr_in6(sin6_len: UInt8(MemoryLayout<sockaddr_in6>.size),
sin6_family: sa_family_t(PF_INET6),
sin6_port: port,
sin6_flowinfo: 0,
sin6_addr: in6addr_any,
sin6_scope_id: 0)
let result = withUnsafePointer(to: &address) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
bind(bsdSocket, $0, socklen_t(MemoryLayout<sockaddr_in6>.stride))
}
}
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:
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
var addressInfo: UnsafeMutablePointer<addrinfo>? = nil
/*
addrinfo has the following interface:
public struct addrinfo {
public var ai_flags: Int32 /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */
public var ai_family: Int32 /* PF_xxx */
public var ai_socktype: Int32 /* SOCK_xxx */
public var ai_protocol: Int32 /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
public var ai_addrlen: socklen_t /* length of ai_addr */
public var ai_canonname: UnsafeMutablePointer<Int8>! /* canonical name for hostname */
public var ai_addr: UnsafeMutablePointer<sockaddr>! /* binary address */
public var ai_next: UnsafeMutablePointer<addrinfo>! /* next structure in linked list */
public init()
public init(ai_flags: Int32, ai_family: Int32, ai_socktype: Int32, ai_protocol: Int32, ai_addrlen: socklen_t, ai_canonname: UnsafeMutablePointer<Int8>!, ai_addr: UnsafeMutablePointer<sockaddr>!, ai_next: UnsafeMutablePointer<addrinfo>!)
}
*/
function_that_initializes_our_pointer(&addressInfo)
print(addressInfo!.pointee.ai_family)
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 socket
s 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 socket
s
We are going to build a command-line tool using the Swift Package Manager. So let's create it.
1
2
3
$ mkdir echoSocketServer
$ cd echoSocketServer
$ swift package init --type executable
Now our main will only be the entry point for our server. Edit it and add the following content:
1
2
3
4
5
6
7
8
9
import Foundation
print("Welcome to our simple echo server!")
var server = Server()
server.start()
RunLoop.main.run()
exit(EXIT_SUCCESS)
Let's now create the Server
class. Create a new file Server.swift
inside the sources directory, and add the following content.
1
2
3
4
5
6
7
8
9
10
11
import Foundation
class Server {
let servicePort = "1234"
func start() {
print("Server starting...")
}
}
Now we can build and run it. We should see our message "Server starting...". You can stop the program pressing [Ctrl+C].
1
2
3
4
# run it on your application's root directory
$ swift run
#Welcome to our simple echo server!
#Server starting...
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 begining 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
:
1
2
3
4
5
6
7
8
let socketFD = socket(AF_INET6, //Domain e.g. [AF_INET,AF_INET6, AF_UNIX]
SOCK_STREAM, //Type e.g. [SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET, SOCK_RAW]
IPPROTO_TCP //Protocol e.g. [IPPROTO_TCP, IPPROTO_SCTP, IPPROTO_UDP, IPPROTO_DCCP]
)//Return a FileDescriptor -1 = error
if socketFD == -1 {
print("Error creating BSD Socket")
return
}
The socket domain
specifies the protocol family that the socket will use. Some examples are:
AF_INET
for IPV4.AF_INET6
for IPV6.AF_UNIX
for local communication using file descriptros.
The typical type
s you'll see are:
SOCK_STREAM
for TCPSOCK_DGRAM
for UDPSOCK_RAW
if 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_UDP
for UDP.IPPROTO_TCP
for TCP.IPPROTO_ICMP
for 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:
1
2
3
4
5
struct sockaddr {
unsigned char sa_len; /* total length */
sa_family_t sa_family; /* address family */
char sa_data[14]; /* actually longer; address value */
};
sockaddr
is the structure that most of the kernel functions expect, but there are two other structs that represent IPV4
and IPV6
specific cases.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* Socket address, internet style.
* Expected in IPV4
*/
struct sockaddr_in {
uint8_t sin_len; /* length of this struct */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* Transport layer port # */
struct in_addr sin_addr; /* IPV4 address */
char sin_zero[8]; /* Structure padding */
};
/*
* Expected in IPV6
*/
struct sockaddr_in6 {
uint8_t sin6_len; /* length of this struct */
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IP6 flow information */
struct in6_addr sin6_addr; /* IP6 address */
uint32_t sin6_scope_id; /* scope zone index */
};
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:
1
2
3
int getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res);
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 newaddrinfo
socket returned by the function.- The function returns an
int
, error is represented by-1
.
Let's use getaddrinfo
to obtain our sockaddr
structure.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var hints = addrinfo(
ai_flags: AI_PASSIVE, // AI_PASSIVE in conjuntion with a nil node in getaddrinfo function, makes sure that the returned socket is suitable for binding a socket that accept connections.
ai_family: AF_UNSPEC, // Either IPv4 or IPv6
ai_socktype: SOCK_STREAM, // TCP
ai_protocol: 0,
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
var servinfo: UnsafeMutablePointer<addrinfo>? = nil
let addrInfoResult = getaddrinfo(
nil, // Any interface
socketPort, // The port on which will be listenend
&hints, // Protocol configuration as per above
&servinfo)
if addrInfoResult != 0 {
print("Error getting address info: \(errno)")
return
}
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
:
1
2
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
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 lenght of our sockaddr
.
1
2
3
4
5
6
let bindResult = bind(socketFD, servinfo!.pointee.ai_addr, socklen_t(servinfo!.pointee.ai_addrlen))
if bindResult == -1 {
print("Error binding socket to Address: \(errno)")
return
}
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
:
1
int listen(int s, int backlog);
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:
1
2
3
4
5
6
7
8
let listenResult = listen(socketFD, //Socket File descriptor
8 // The backlog argument defines the maximum length the queue of pending connections may grow to
)
if listenResult == -1 {
print("Error setting our socket to listen")
return
}
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.
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
while (true) {
let MTU = 65536
var addr = sockaddr()
var addr_len :socklen_t = 0
print("About to accept")
let clientFD = accept(socketFD, &addr, &addr_len)
print("Accepted new client with file descriptor: \(clientFD)")
if clientFD == -1 {
print("Error accepting connection")
}
var buffer = UnsafeMutableRawPointer.allocate(byteCount: MTU,alignment: MemoryLayout<CChar>.size)
while(true) {
let readResult = read(clientFD, &buffer, MTU)
if (readResult == 0) {
break; // end of file
} else if (readResult == -1) {
print("Error reading form client\(clientFD) - \(errno)")
break; // error
} else {
let strResult = withUnsafePointer(to: &buffer) {
$0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: readResult)) {
String(cString: $0)
}
}
print("Received form client(\(clientFD)): \(strResult)")
write(clientFD, &buffer, readResult)
}
}
}
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 syncronously, 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.
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
class Server {
let servicePort = "1234"
func start() {
print("Server starting...")
let socketFD = socket(AF_INET6, //Domain [AF_INET,AF_INET6, AF_UNIX]
SOCK_STREAM, //Type [SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET, SOCK_RAW]
IPPROTO_TCP //Protocol [IPPROTO_TCP, IPPROTO_SCTP, IPPROTO_UDP, IPPROTO_DCCP]
)//Return a FileDescriptor -1 = error
if socketFD == -1 {
print("Error creating BSD Socket")
return
}
var hints = addrinfo(
ai_flags: AI_PASSIVE, // Assign the address of the local host to the socket structures
ai_family: AF_UNSPEC, // Either IPv4 or IPv6
ai_socktype: SOCK_STREAM, // TCP
ai_protocol: 0,
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
var servinfo: UnsafeMutablePointer<addrinfo>? = nil
let addrInfoResult = getaddrinfo(
nil, // Any interface
servicePort, // The port on which will be listenend
&hints, // Protocol configuration as per above
&servinfo)
if addrInfoResult != 0 {
print("Error getting address info: \(errno)")
return
}
let bindResult = bind(socketFD, servinfo!.pointee.ai_addr, socklen_t(servinfo!.pointee.ai_addrlen))
if bindResult == -1 {
print("Error binding socket to Address: \(errno)")
return
}
let listenResult = listen(socketFD, //Socket File descriptor
8 // The backlog argument defines the maximum length the queue of pending connections may grow to
)
if listenResult == -1 {
print("Error setting our socket to listen")
return
}
while (true) {
let MTU = 65536
var addr = sockaddr()
var addr_len :socklen_t = 0
print("About to accept")
let clientFD = accept(socketFD, &addr, &addr_len)
print("Accepted new client with file descriptor: \(clientFD)")
if clientFD == -1 {
print("Error accepting connection")
}
var buffer = UnsafeMutableRawPointer.allocate(byteCount: MTU,alignment: MemoryLayout<CChar>.size)
while(true) {
let readResult = read(clientFD, &buffer, MTU)
if (readResult == 0) {
break; // end of file
} else if (readResult == -1) {
print("Error reading form client\(clientFD) - \(errno)")
break; // error
} else {
let strResult = withUnsafePointer(to: &buffer) {
$0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: readResult)) {
String(cString: $0)
}
}
print("Received form client(\(clientFD)): \(strResult)")
write(clientFD, &buffer, readResult)
}
}
}
}
}
Right, now we can build and run it:
1
2
3
4
$ swift run
#Welcome to our simple echo server!
#Server starting...
#About to accept
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
.
1
$ ncat localhost 1234
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:
1
2
3
4
5
6
7
8
9
$ swift run
Welcome to our simple echo server!
Server starting...
About to accept
Accepted new client with file descriptor: 7
# the client sends the text "hello"
Received form client(7): hello
# the client sends the text "hi"
Received form client(7): hillo
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.
1
2
3
4
5
6
7
// This is an ugly way to add the null-terminator at the end of the buffer we just read
// if you know a better way let me know and I'll update the code.
withUnsafeMutablePointer(to: &buffer) {
$0.withMemoryRebound(to: UInt8.self, capacity: readResult + 1) {
$0.advanced(by: readResult).assign(repeating: 0, count: 1)
}
}
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.
1
print(MemoryLayout<sockaddr_t>.size)
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
C
interoperability. - 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
PID
and use the following command:
1
$ lsof -Pan -p PID -i
- Another post on using sockets in Swift.
- FreeBSD essential socket functions.
- Apple's WWDC20 session on Unsafe Swift - Explains when and how to use
Unsafe
types. - Apple's WWDC20 session on Safely manage pointers in Swift