Running a SwiftNIO Server in an iOS app
Have you ever wondered if it is possible to run a server in an iOS app? A while ago I had this question! It seems that SwiftNIO and SwiftNIO Transport Services extension, a.k.a NIOTS are here to help us do so.
SwiftNIO is a non-blocking event-driven network framework and as per the documentation, it is like Netty, but written for Swift, meaning that it is following Netty’s architecture and concepts though in a Swifty way.
To get a better grasp of the architecture, you can refer to SwiftNIO’s documentation, where you can find a detailed description of the building blocks and how they interact with each other.
Back to our topic, a potential use case for such an app could be to act as a mock server for other apps. For example, such a mock server app could be used for usability testing, especially if it provides a UI to manipulate the responses and thus enabling the person performing the usability testing to create different scenarios and flows in the app.
So, for this post, I will try to create a simple app that will start an HTTP server, and this server will handle a GET request and return a JSON response.
Implementation
Dependencies
First thing first, we have to add SwiftNIO
, SwiftNIOTransportServices
and SwiftNIOHTTP1
as dependencies to our project. Let’s open the Podfile
and add the following dependencies:
pod 'SwiftNIO', '~> 2.0.0'
pod 'SwiftNIOTransportServices', '~> 1.0.0'
pod 'SwiftNIOHTTP1', '~> 2.0.0'
After that, run bundle exec pod install
and wait until the Pods are installed.
Having installed the dependencies we are ready to move to the actual implementation.
ChannelHandler
To do the data manipulation, SwiftNIO
is using the terms ChannelPipeline
, ChannelHandler
and ChannelContext
.
For a thorough description of what each of these building blocks is, you can refer to the corresponding documentation.
Long story short, ChannelHandler
is a base protocol for handlers that handle I/O events or intercept an I/O operation.
ChannelHandler
should not be used directly but rather through its sub-protocols. A ChannelHandler can be Inbound, Outbound or both. The first one refers to handlers which process inbound events like reading data, while the second refers to handlers which process outbound events like writes.
Those handlers are added to a sequence, the ChannelPipeline
. Then, with each new event, each handler processes the event in order. For read events that order is from the front to the back of the sequence whereas for write events is the reverse order.
Finally, each handler is using ChannelHandlerContext
to communicate with other handlers by emitting events.
In our case, we are going to create a class that conforms to ChannelInboundHandler
, and its main responsibility will be to read the data coming from any other previous ChannelInboundHandler
and prepare the response and the corresponding header which will be forwarded to the next ChannelHandler
using the ChannelHandlerContext
.
An example of a dummy ChannelInboundHandler
is the following:
import Foundation
import NIOHTTP1
import NIO
final class DummyHandler: ChannelInboundHandler {
typealias InboundIn = HTTPServerRequestPart
typealias OutboundOut = HTTPServerResponsePart
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let part = self.unwrapInboundIn(data)
guard case .head = part else {
return
}
// Prepare the response body
let message = ["message": "Hello World"]
let response = try! JSONEncoder().encode(message)
// set the headers
var headers = HTTPHeaders()
headers.add(name: "Content-Type", value: "application/json")
headers.add(name: "Content-Length", value: "\(response.count)")
let responseHead = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok, headers: headers)
context.write(self.wrapOutboundOut(.head(responseHead)), promise: nil)
// Set the data
var buffer = context.channel.allocator.buffer(capacity: response.count)
buffer.writeBytes(response)
let body = HTTPServerResponsePart.body(.byteBuffer(buffer))
context.writeAndFlush(self.wrapOutboundOut(body), promise: nil)
}
}
First we declare the InboundIn
and the OutboundOut
types. InboundIn
defines the kind of data that this handler is expecting to receive from any previous ChannelHandler
whereas OutboundOut
is the kind of data that will be forwarded to the next.
In our case they are HTTPServerRequestPart
and HTTPServerResponsePart
respectively, both of which are typeliases for the generic enum HTTPPart<HeadT: Equatable, BodyT: Equatable>
that contains three cases; .head
, .body
and .end
.
After that, we provide an implementation for the channelRead
function which is called when there are some data to be read. The data is passed as a NIOAny
object, so we have to unwrap it to the InboundIn
type. Next, we prepare the data that we want as a response. In our case it is a simple map and we use the JSONEncoder
to encode it into data.
Then, we set the response headers, which consist of the Content-Type
and the Content-Length
headers and using the context.write
we are sending an event to the next ChannelHandler
in the ChannelPipeline
.
Carrying on, we create a buffer with the response data and we set it as the body of the response. We are also calling the writeAndFlush
function which is sending a write
event to the next ChannelHandler
followed by a flush
event. With the write
event the body data will be enqueued to be written to the socket when the flush
event arrives.
Now, let’s move to the server that will use this ChannelHandler
!
Server
An example of a server that is using the ChannelHandler
can be found on the following snippet.
import NIOTransportServices
import NIO
class Server {
// MARK: - Initializers
init(host: String, port: Int) {
self.host = host
self.port = port
}
// MARK: - Public functions
func start() {
do {
let bootstrap = NIOTSListenerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.configureHTTPServerPipeline()
.flatMap {
channel.pipeline.addHandler(DummyHandler())
}
}
let channel = try bootstrap
.bind(host: host, port: port)
.wait()
try channel.closeFuture.wait()
} catch {
print("An error happed \(error.localizedDescription)")
exit(0)
}
}
func stop() {
do {
try group.syncShutdownGracefully()
} catch {
print("An error happed \(error.localizedDescription)")
exit(0)
}
}
// MARK: - Private properties
private let group = NIOTSEventLoopGroup()
private var host: String
private var port: Int
}
In this class, we define an initializer with a host
and port
variable and two functions; start
and stop
which are, as you can imagine, responsible to start and stop the server.
Let’s focus on the start
function first.
Initially, we have to bootstrap our channel. We are using the NIOTSListenerBootstrap
to do so and its childChannelInitializer
function to add ChannelHandlers to the ChannelPipeline. To add HTTP-server capabilities to our server, we call the configureHTTPServerPipeline
which adds some HTTP-related ChannelHandlers to the ChannelPipeline. After that, we are using flatMap to add our own DummyHandler
to the ChannelPipeline
. We are, then, using this bootstrap to bind our channel to the port and the host set through the initializer and finally, we are using try channel.closeFuture.wait()
to make our server run until we decide to close it.
As for the stop
function, we just call try group.syncShutdownGracefully()
to shut down the EventLoopGroup
gracefully.
Results
The last missing piece to run our server is to initialize a server instance and call the start
function, like in the following lines:
let app = Server(host: "localhost", port: 8888)
app.start()
And we are ready to run the app!
Now, if you head to the Terminal and execute the command curl -X GET http://localhost:8888
(requires to have curl
installed) or use a browser either on the device or the emulator that you use to run the app, and you visit http://localhost:8888
, you will receive the {"message":"Hello World"}
response.
Conclusion
To sum up, in this post, we have seen how to use SwiftNIO
in an iOS app to run an HTTP server that will serve a static JSON response.
But given that SwiftNIO is a low-level framework, in order to reach my goal of building a mock server app, there are a lot of things to be implemented, like a routing implementation that will make it easy to change the response on the fly.
For this reason, as alternatives to SwiftNIO, there are also some other high-level framework that you can also use to run a server on an iOS app, such as GCDWebServer, swifter, Ambassador and Kitura.
SwiftNIO or not, knowing that it is possible to run a server in an iOS app is something that could be useful in many ways!!
Thanks for reading and should you have any questions, suggestions or comments, just let me know on Twitter or email me!!