Using Swift Protocols to Separate Socket Functionality

Back in the POSIX Socket API Wrapper in Swift blog post, we created an object oriented wrapper for sockets by creating a struct Socket with methods that wrapped the low-level POSIX functions. While the resulting API was more comfortable to use than the plain C functions, it did not address a problem that the original API has: Given an instance of Socket, how do we know which methods we can or should actually call on it? A client needs connect(), send(), and receive(), whereas a server needs bind(), listen(), and accept(). But it never makes sense to call accept() on a socket that was previously connect()ed, which simply yields an error.

To make things more clear for users of our API, our SocketWrapper library uses a protocol for each “kind” of socket. This allows related functionality to be grouped together and introduces separate types that make code clearer and also helps Xcode’s code completion.

This may not be one of those sexy use cases of Swift protocols that use an associatedtype or clever constraints, but we can at least take advantage of protocol extensions to provide default implementations to minimize the code that adopters of a protocol need to write.

Quick Recap: Methods in Protocols

Before we get into the meat of this post, here’s a quick recap of what it means when a method is defined in a protocol versus when it is defined in an extension of a protocol. A code snippet is worth a thousand words:

protocol MyProtocol {

    // MUST be implemented by protocol adopter.
    func requiredMethod()

    // MAY be implemented by protocol adopter (see below).
    func overridableMethod()

}

extension MyProtocol {

    // MAY be overridden by protocol adopter because
    // it is defined in the protocol (see above).
    func overridableMethod() {
        ... // Implementation
    }

    // CANNOT be overridden by protocol adopter because
    // it is NOT defined in the protocol.
    func nonOverridableMethod() {
        ... // Implementation
    }

}

The takeaway for us is that we can add methods to our socket protocols that an adopter automatically gets without any further code. We can also prevent the adopter from overriding these methods, if we want.

Kinds of Sockets

There are two obvious kinds of sockets we want to model, as mentioned before:

  • Client: A socket that can connect() to a peer and then send() and receive() data.
  • Server: A socket that can bind() to an address, listen() for client connections, and then accept() them.

Additionally, both kinds need a close() method. In terms of properties, both must provide storage for a socket and a socket address, for which we use the types from my previous blog posts: Socket and SocketAddress.

There’s a third kind of socket we need to model. When a client actually does connect to a server socket and that server calls accept(), a new socket is returned. This socket is a representation of the client in the server. It’s similar in functionality to the “Client” above, but it doesn’t need to connect() (the connection is already established). In our SocketWrapper library, this type is called ConnectedClientSocket. (Although AcceptedClientSocket might be a better name, now that I think of it. Oh well, naming things is hard…)

Now, if we’re really pedantic and want to go overboard with protocols, we could argue that a SendSocketType only needs access to the var socket, but not to var socketAddress and we could therefore factor the var socketAddress out of SocketType into a separate AddressSocketType. And since our SocketWrapper library is more a playground for trying out concepts in Swift than anything else, I’d say we do that!

This leads us to the following hierarchy of protocols:

That’s a lot of protocols but it cleanly separates the responsibilities of each type. The three protocols at the bottom represent the types that correspond to the actual kinds of sockets described above.

Protocol Implementations

Don’t worry, I’m not going to list the code for all protocols here 😅, instead let’s just go through some of the things that are relevant for a client: SocketType, AddressSocketType, SendSocketType, and ClientSocketType.

SocketType

protocol SocketType {
    var socket: Socket { get }
}

extension SocketType {
    func close() throws {
        try socket.close()
    }
}

This simply requires that any adopter provides storage for a Socket. Also, it introduces a method to close() it.

AddressSocketType

protocol AddressSocketType: SocketType {
    var address: SocketAddress { get }
    init(socket: Socket, address: SocketAddress) throws
}

Again, this protocol requires adopters to provide storage for a SocketAddress. It also requires an initializer that takes both the SocketAddress for the property it introduces, plus the Socket for the SocketType it inherits from. If the initializer took only the SocketAddress, there would be no way to set the Socket on the SocketType, since that protocol only has a getter for the socket.

Callers probably won’t have a Socket and SocketAddress handy, so we’ll add another convenience initializer that takes an addrinfo because that’s what our code actually has available:

extension AddressSocketType {
    init(addrInfo: addrinfo) throws {
        let socket = try Socket(addrInfo: addrInfo)
        let address = SocketAddress(addrInfo: addrInfo)
        try self.init(socket: socket, address: address)
    }
}

For details on the addrinfo, see my blog post Low-Level Network Address Resolving in Swift.

A quick aside about protocols with initializer requirements: Take care when defining those! By requiring any adopter of AddressSocketType to implement init(socket:, address:) they cannot freely add any other properties unless they provide default values. For example, if a struct were to adopt the protocol and it had an additional property like var x: Int, that property must be initialized even if init(socket:, address:) is called. There are only two sensible ways to do this: (1) Use a hardcoded default value that is assigned in the initializer or in the property declaration (e.g. var x: Int = 42), or (2) derive it from one of the initializer’s parameters. In any case, a value for x cannot be passed as a parameter. This is very restrictive if you try to use composition to build your types.

SendSocketType

protocol SendSocketType: SocketType { }

extension SendSocketType {
    func send(buffer: UnsafeBufferPointer<Socket.Byte>) throws {
        let bytesToSend = buffer.count
        var bytesSent = 0

        while bytesSent < bytesToSend {
            bytesSent += try socket.send(buffer.baseAddress + bytesSent,
                                         count: bytesToSend - bytesSent)
        }
    }
}

This protocol doesn’t require anything from an adopter (except that it’s already a SocketType), which means an adopter only has to declare its conformance to get the send() method.

Two things to note about the send() implementation: First, Socket.Byte was introduced in my blog post Modeling Simple Data in Swift: struct versus typealias (TL;DR: it’s just typealias Byte = UInt8).

Second, The implementation of send() takes an UnsafeBufferPointer<Socket.Byte>, which may not always be the most practical thing to work with because the caller would need to convert the data it wants to send into an UnsafeBufferPointer first. It would be convenient if we could just add an overload that took a String so callers wouldn’t need to do extra work in the not-so-uncommon-case of sending Strings:

extension SendSocketType {
    func send(message: String, includeNulTerminator: Bool = false) throws {
        try message.withUTF8UnsafeBufferPointer(includeNulTerminator: includeNulTerminator) {
            try send($0)
        }
    }
}

(The withUTF8UnsafeBufferPointer() method is implemented in the SocketWrapper library.)

ClientSocketType

Finally, we’re at the bottom of the hierarchy, at the protocol that will actually be adopted:

protocol ClientSocketType: AddressSocketType, SendSocketType, ReceiveSocketType { }

Again, this is a protocol that doesn’t require anything from its adopters, but it adds two things in an extension:

extension ClientSocketType {
    init(host: String, port: String) throws {
        let addressInfoSequence = try AddressInfoSequence(forConnectingToHost: host,
                                                          port: port)

        self = try addressInfoSequence.withFirstAddrInfo { addrInfo in
            try Self.init(addrInfo: addrInfo)
        }
    }

    func connect() throws {
        try address.withSockAddrPointer { sockAddr, length in
            try socket.connect(address: sockAddr, length: length)
        }
    }
}

… and these two things are very useful: The initializer is not required by the protocol, but implemented by the extension, which means that adopters of ClientSocketType can simply be initialized with a host and a port and then be connect()ed without needing any more code.

A Minimal Adopter

Let’s quickly take stock of all the requirements that a minimal adopter needs to fulfill to implement ClientSocketType. We’ll do this by listing all the protocols involved, without any extensions. I’ll also include ReceiveSocketType, which I omitted above:

protocol SocketType {
    var socket: Socket { get }
}

protocol AddressSocketType: SocketType {
    var address: SocketAddress { get }
    init(socket: Socket, address: SocketAddress) throws
}

protocol SendSocketType: SocketType { }

protocol ReceiveSocketType: SocketType { }

protocol ClientSocketType: AddressSocketType, SendSocketType, ReceiveSocketType { }

As we can see, most protocols don’t require us to do anything. It’s actually just two properties and an initializer that takes values for those two properties. Therefore, a minimal implementation looks like this:

struct MinimalClientSocket: ClientSocketType {
    let socket: Socket
    let address: SocketAddress
}

That’s it. Because a struct that doesn’t implement any initializers will automatically get one that takes the struct’s properties in the order they were declared, we get the init(socket:, address:) initializer required by AddressSocketType for free.

And through the power of protocol extensions, we can simply create a MinimalClientSocket using the initializer added by ClientSocketType above:

let socket = try MinimalClientSocket(host: "example.com", port: "80")

Nice! Now to do something useful, let’s create a MinimalClientSocket, connect it to httpbin.org on port 80 (so we can talk straight HTTP to it), send a request, and then receive a response.

We’ll do a GET request to http://httpbin.org/user-agent, which simply returns the user agent in a small JSON structure. Because we’re hand-writing the HTTP request, we can of course put in whatever we want as the user agent.

struct MinimalClientSocket: ClientSocketType {
    let socket: Socket
    let address: SocketAddress
}

let socket = try MinimalClientSocket(host: "httpbin.org", port: "80")
try socket.connect()

let lineBreak = "\r\n"
let message = [
    "GET /user-agent HTTP/1.1",
    "Host: httpbin.org",
    "User-Agent: MinimalClientSocket"
    ].joinWithSeparator(lineBreak) + lineBreak + lineBreak

try socket.send(message)
if let serverResponse = try socket.receiveUTF8String(maxBytes: 2048, blocking: true) {
    print(serverResponse)
} else {
    print("Response data wasn't UTF-8")
}

The socket.send(message) call uses the send(message:, includeNulTerminator:) that we added in the extension of SendSocketType above. Similarly, the receiveUTF8String(maxBytes:, blocking:) method comes from an extension of ReceiveSocketType, which wasn’t shown here.

Running the above code should print something like this:

HTTP/1.1 200 OK
Server: nginx
Date: Mon, 23 May 2016 10:31:37 GMT
Content-Type: application/json
Content-Length: 42
Connection: keep-alive
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "user-agent": "MinimalClientSocket"
}

As you can see, this is an HTTP response header, followed by the expected JSON with exactly the user agent string we sent. It worked! 🎉

Conclusion

By separating all the different kinds of functionality that sockets have into distinct protocols, we can group related methods together, making it clear which calls a user of our API may want to call. And by implementing that functionality in protocol extensions, we can provide adopters of the protocols with actual functionality, just like we could with a class hierarchy, but without restricting them to an inflexible single-inheritance hierarchy.

One thing that can be restrictive, though, are protocols with initializer requirements. Only introduce those sparingly.