Beliebte Suchanfragen
//

Full-stack Swift – Part 1

16.10.2016 | 13 minutes of reading time

Combining server-side Swift, and Google’s Protocol Buffers by creating a simple server-client system.

In the first part of this blog post, we will build the server-side application.
Second part of this blog post will be about building the client iOS application.
Both applications will be built with Swift, and communication between them will be done using protocol buffers.

Purpose of the system will be to give the user an app to keep track of books that he has read, and user’s data will be stored on the server.
Every Book will have:

  • A title – textual representation of book’s name
  • An author – textual representation of book’s author
  • Number of pages – numeric representation of number of pages in a book

Swift programming language, introduced by Apple, is not iOS-only programming language, at least not anymore.
Swift is open source, and you can actually use it on Linux, which in turn means that building a web application with Swift and hosting it on Linux is actually possible, because Macs aren’t exactly best known for their use as web servers, mainly because of their price, and hardware limitations.

You might ask, of what importance is this for anyone, what does it change?
It depends on your perspective:

  • For iOS/macOS developers it could mean that maybe they wouldn’t have to wait for the backend developer to perform a change on the backend, instead they could do it themselves with a programming language that is already familiar to them.
  • For an IT Consultancy, it could mean better utilisation of iOS/macOS developers. iOS/macOS developer could work both on the client and the server-side code, all the while not being compelled to learn a totally different programming language from the one they are used to working with.

The notion of being able to work on both client and server-side code, while not being forced to change programming languages and IDE sounds very good to me.

Still, there remains a question, why should we use Swift instead of one of the already mature and proven server side programming languages such as Java, Ruby or Python?

Advantages of using Swift are:

  1. Executes as native code, that means that it’s quite fast, as the name suggests
  2. Strongly typed programming language
  3. More expressive than other strongly typed programming languages, which in turn means that you can work faster and not need the complier’s help as often as in other safe programming languages
  4. Open source, multi-platform and supported by major players in the IT industry such as IBM

After Swift was open sourced, many web frameworks appeared, here are the few most popular ones, ordered by number of stars they have on GitHub:

Vapor is inspired by Laravel, on the other side Perfect and Kitura are inspired by Rails and Express, respectively (Kitura is even being developed by the same people who developed Express).
There are also many other Swift web frameworks out there, but this article won’t be about comparing Swift web frameworks, and we will be using only one of the three beforehand mentioned frameworks.

I tried all three of them, but in the end I made Vapor my prime choice for server side Swift.
As you may already know, the team behind Perfect raised a 1.5 million dollars to continue building the framework, Kitura is being made by IBM, and Vapor has only two dedicated developers.
When you have a choice between a framework built by the one of the most powerful IT companies of today, a well-funded framework and a framework that is neither well funded, neither backed by a big company, why would you chose the underdog over the two other prime candidates?

Vapor has the largest and the most vibrant community of them all, the best documentation and tutorials, and it feels Swifty.
I have to dissapoint those of you who thought that being Swifty has something to do with Taylor Swift. Unfortunately it doesn’t.
Swifty means that the framework leverages all the goodies that Swift has to offer, and the framework’s API is in compliance with Swift API Design Guidelines .

In addition to everything previously mentioned, Vapor is a highly modular framework. You could swap some of it’s components with some of Kitura’s components, or even some other framework’s.
It gives you a freedom to pick modules from different frameworks that best suit your needs, which is a large plus in my book.

PROTOCOL BUFFERS

Protocol buffers (from now on protobuf) are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data, like XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

Working with protobuf consists of two parts:

  1. Describing data structures with a dedicated interface description language
  2. Using a program that generates source code for generating or parsing a stream of bytes that represents the structured data

Advantages of using protobuf:

  1. Performance – 3-10 times smaller and 20-100 times faster than XML
  2. Simplicity – easier to use than JSON
  3. Portability – data structures can be shared between different platforms, you just need a dedicated source code generator for each platform

Books will be contained inside an instance of the Shelf object:

  • A name – textual representation of shelf’s name
  • Collection of books – collection of “Book” objects

Create a file named Bookshelf.proto and describe the data model (this is an adjusted copy of Apple’s official protobuf example ):


syntax = "proto3";
 
message Book {
   int64 id = 1;
   string title = 2;
   string author = 3;
   int32 numberOfPages = 4;
}

message Shelf {
   int64 id = 1;
   string name = 2;
   repeated Book books = 3;
   map keys = 4;
}

As you can see, the format is pretty simple – each data structure is represented by a message.
Each message has one or more uniquely numbered fields, and each field has a name and a value type.
Those numbers remove the need for version checks which is one of the explicitly stated motivations for the desing and implementation of Protocol Buffers.
New fields can be easily introduced, and intermediate servers don’t need to inspect the data, instead they can simply parse it and pass through the data without needing to know about all the fields.

Value types can be numbers (integer or floating-point), booleans, strings, raw bytes, or even other protocol buffer message types (as the Shelf message in the example above) , allowing you to structure your data hierarchically. You can specify optional fields, required fields, and repeated fields.
Once you’ve defined your messages, you run the protocol buffer compiler for your application’s language on your .proto file to generate data access classes. These provide simple accessors for each field as well as methods to serialize/parse the whole structure to/from raw bytes.

In order to generate Swift code for this data structure two prerequisites are needed:

  1. Google’s protobuf compiler – download the right version of binary for your system and add it to your PATH
  2. Apple protobuf plugin in order to generate Swift code – clone the repository and build the binary and add it to your PATH

Generate Swift code from previously declared protobuf data structure description:

1$ protoc --swift_out=. Bookshelf.proto

Swift file generated by this command will be used by both the server and the client:

1public struct Book: ProtobufGeneratedMessage { ... }
2 
3public struct Shelf: ProtobufGeneratedMessage { ... }

File contains two swift structs that represent Book and Shelf, respectively.
We can deserialize protobuf or even json into these structs, and also we can serialize instances of these structs into protobuf or json.
Serialization of protobuf objects into raw bytes is achieved by calling the method

1func serializeProtobuf()

on instance of Book or Shelf.
Deserialization of raw bytes into protobuf objects is achieved by using the protobuf convenience initializer

1convenience init(protobufBytes: [UInt8])

.

Example of deserializing raw bytes into protobuf objects:

1guard let book = try? Book(protobufBytes: bytes) else { ... }

Example of serializing protobuf objects into raw bytes:

1guard let protobuf = try? self.shelf.serializeProtobuf() else { ... }

Adequate guard statements as stated in examples should be in place in order to minimize the possibility of run-time errors.

SERVER

There are several approaches to starting a new Vapor project. For the sake of simplicity we will be taking the Swift Package Manager route.
If you want a more streamlined experience, give Vapor’s toolbox a try.

Generate a new empty Swift project:

1$ swift package init --type executable

Package.swift file is where you define your dependencies. After issuing the build command to the Swift Package Manager, it will prowl the repositories and build the libraries and all of their dependencies, recursively.
Add Vapor and Apple’s protobuf runtime library to the list of dependencies:

1import PackageDescription
2 
3let package = Package(
4    name: "BookshelfServer",
5    dependencies: [
6        .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1, minor: 0),
7        .Package(url: "https://github.com/apple/swift-protobuf.git", Version(0,9,24)),
8        ]
9)

Swift Package Manager can generate an Xcode project from Package.swift file.
Xcode builds the project using an internal build process instead of swift build and is usually a bit faster.

1$ swift package generate-xcodeproj

Open the newly generated Xcode project.
Xcode build scheme defaults to the module of the same name, which isn’t runnable.
Change the build scheme to “BookshelfServer” executable:

Run the project in order to make sure that everything is working fine.
It prints out a “Hello, world!” message in Xcode’s console.

Keep in mind that this is a purely a tech-demo, and there are better ways to build a proper Vapor web server, be sure to check out Vapor’s official documentation .

The entry point in a plain Swift module is the file in the module called main.swift.
The project that we are building is sufficiently small that we can write all our code in this file.

Import Vapor module:

1import Vapor

Create a Droplet:

1let drop = Droplet()

Droplet is a wrapper around many of core Vapor functionalities.
It can be used to register routes, start the server, and many other things.

Add a simple route that will return a “Hello, World” message:

1drop.get("hello") { request in
2    return "Hello, world!"
3}

In orded to start the server, add this code:

1drop.run()

Run the project, and you should see “Starting server at 0.0.0.0:8080” in the Xcode’s console.
Open localhost:8080/hello in your browser and you will be greeted with “Hello, world!” message.

Congratulations, you have successfully setup a Vapor web server.

It is time to add a data source.

Before that, we need to add the previously generated Bookshelf.pb.swift into our project’s Sources directory in order for us to be able to work with protobuf objects.
Be sure to select the BookshelfServer target when adding the file.

Because I’m a big fan of Warhammer40K, and I am currently reading through the Horus Heresy book series (currently at book number 29, “Vengeful Spirit”), I will create a shelf with couple of books from that series.
We won’t be using any persistent storage, we will store everything in memory.
If you want you can later expand on this project and add a persistence layer. Vapor has an excellent ORM called Fluent .

Create a shelf with couple of books:

1var shelf = Shelf(name: "Horus Heresy",
2                  books: [
3                    Book(title: "Horus Rising", author: "Dan Abnett", numberOfPages: 412),
4                    Book(title: "False Gods", author: "Graham McNeill", numberOfPages: 406),
5                    Book(title: "Galaxy in Flames", author: "Ben Counter", numberOfPages: 407),
6                    ]
7)

We need to make these books available to the client, and also to provide a client with a way of adding new books to the shelf.
This will be done over one get and one post http request.

In order to use Vapor’s http response class, import the HTTP module:

1import HTTP

Inside the Main.swift, just after the declaration of shelf variable, create a computed variable for generating a protobuf response:

1var protobufResponse: Response {
2    guard let protobuf = try? shelf.serializeProtobuf() else {
3        return Response(status: .internalServerError)
4    }
5    return Response(status: .ok, headers: ["Content-Type": "application/octet-stream"], body: protobuf)
6}

Inside the Main.swift, just after the computed protobufResponse variable, create a new route for sending the shelf to the client.

1drop.get("shelf") { request in
2    guard let accept = request.headers["Accept"], accept == "application/octet-stream" else {
3        return Response(status: .badRequest)
4    }
5    return protobufResponse
6}

First, we check the http request headers in order to confirm that client wants the response body to be protobuf serialized into raw bytes. If he doesn’t, bad luck, we don’t support anything other than protobuf, send him a response with bad request status.
If the accept header is the right value, send a response with protobuf serialized into raw bytes.

First route is finished, run the project.

Try out the route by sending a http request using a http testing tool of your choice:

  • Set the method: get
  • Set the url: localhost:8080/shelf
  • Set the Accept header: application/octet-stream
  • Send the request

I will be using httpie :

1http get localhost:8080/shelf accept:application/octet-stream

You should get a response that looks like this:

1HTTP/1.1 200 OK
2Content-Length: 114
3Content-Type: application/octet-stream
4Date: Fri, 14 Oct 2016 09:04:05 GMT
5Server: Vapor 1.0.2
6 
7 
8Horus Heresy
9            Horus Rising
10Dan Abnett �
11False GodsGraham McNeill �"Galaxy in Flames
12                                           Ben Counter �

It looks strange, but this is because it’s protobuf serialized into raw bytes, and the httpie program is smart enough to interpret this as some poorly structured text. When you build the iOS app, in part 2 of this blog post, to deserialize raw bytes into protobuf objects properly, the response will look just fine.

For adding a book, everything is very similar with a few small changes.

We need a function that will handle deserialization and adding of books:

1func handle(protobuf bytes: Bytes?) -> Response {
2    guard let bytes = bytes else {
3        return Response(status: .badRequest)
4    }
5    guard let book = try? Book(protobufBytes: bytes) else {
6        return Response(status: .badRequest)
7    }
8    if !shelf.books.contains(book) {
9        shelf.books.append(book)
10        return Response(status: .ok)
11    } else {
12        return Response(status: .badRequest)
13    }
14}

Bytes == UInt8 Swift type, logically, and it is what we need to pass to protobuf init.
We check if the data in the http request body is protobuf, and if deserialization was successful and the book wasn’t already in the shelf we send a response with status ok, otherwise we respond with bad request status.
You could take this further and add custom responses that will inform the client that the book is a duplicate, etc. but this is outside of the scope of this blog post.

Create the route:

1drop.post("book") { request in
2    guard let contentType = request.headers["Content-Type"], contentType == "application/octet-stream" else {
3        return Response(status: .badRequest)
4    }
5    return handle(protobuf: request.body.bytes)
6}

Check the http request headers in order to confirm that content type of the request is protobuf. If it’s not, bad luck, we don’t support anything other than protobuf, send the client a response with bad request status.
If the content type header is the right value, return the result of previously declared handle(protobuf: Bytes) function.

Unfortunately, it would be a bit hard to test this endpoint without a proper client app, so for now, you will have to take my word that this actually works.

What have we done here?

  • We created a protobuf data model that represents our collection of books
  • We created a simple web server powered by Swift and using the Vapor web framework
  • On our web server, we made the data model available over two endpoints, one for reading the existing books from the model, and one for adding new books to it.

WRAPPING UP

We have successfully setup a Vapor web server.
Also, we added support for using Protocol Buffers for serializing and deserializing data in our HTTP requests.

Full source code is available at the project’s github repository.

I hope you have enjoyed this blog post, feel free to comment and ask any questions you want!

For the iOS app that will communicate with this web server, continue on to the second part of this blog post .

share post

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.