In this tutorial we’ll learn how to build a Swift gRPC server and client from scratch, configuring an interactive development environment using Xcode and a simple deployment mechanism using Docker. We will build a basic Echo service, where the server will reply with the same content as sent by the client.

We’ll also go over some shortcomings of the Swift ecosystem encountered while developing this tutorial, and “hackish” solutions to these issues.

Technologies we’ll use:

Before we start, we’ll need the following dependencies already installed in our machines:

Xcode 9 or above

Docker

Project Scaffolding

To start off, we’ll create an empty Swift project called Echo, and we’ll add some files and folders where we will add code during the tutorial:

$ mkdir Echo

$ cd Echo

$ swift package init --type empty

$ mkdir -p Sources/EchoServer

$ mkdir -p Sources/EchoClient

$ mkdir -p Sources/Protos

$ touch Sources/EchoServer/main.swift

$ touch Sources/EchoClient/main.swift

$ find .

./.gitignore

./Package.swift

./README.md

./Sources

./Sources/EchoClient

./Sources/EchoServer

./Sources/Protos

./Tests

This will create a basic and empty project structure where we’ll set up our code. I chose to use the Swift Package Manager as it provides a simple interface for building command line applications without having to rely on Xcode, while still allowing an interactive development experience by generating an Xcode project.

Protocol Buffers and gRPC

As Google states:

Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.

In practice, Protocol Buffers offer a shareable interface for exchanging structured data between different services. It is generally composed of two parts: a compiler that generates language bindings from .proto files, and a language support library to support Protocol Buffers entities at runtime.

gRPC, on the other hand, is an RPC framework used to connect services across the Internet. We’ll use gRPC as the transport mechanism for our Protocol Buffer messages between the server and the client. As Protocol Buffers, it also has a compiler part which generates gRPC endpoint stubs for different languages from .proto service definitions, and a language support library.

We’ll then add a service definition for our Echo service using the Protocol Buffers language under Echo/Sources/Protos/echo.proto :

// Echo/Sources/Protos/echo.proto

syntax = "proto3"; service EchoService {

rpc echo(EchoRequest) returns (EchoResponse);

} message EchoRequest {

string contents = 1;

} message EchoResponse {

string contents = 1;

}

In this file, we’re defining an EchoService RPC service, which has 1 endpoint named echo . This endpoint expects a message of type EchoRequest and it replies with a message of type EchoResponse . The definitions of EchoRequest and EchoResponse are below, and each define a string field where the request and response values will be populated, respectively.

To generate Swift Protocol Buffer and gRPC sources, we’ll use the Swift gRPC Docker image. The benefit of using Docker to generate the sources is that we don’t have to install any of the dependencies on our machines. We just take advantage of an image that is already configured for us, and it’s also easier to clean once we’re done with it. To generate the Swift code generators we’ll run¹:

$ cd Sources/Protos

$ docker run -i -t -v `pwd`:/working_dir -w /working_dir \

sergiocampama/swift_grpc \

protoc --swift_out=. --swiftgrpc_out=. \

echo.proto

That’s one scary command! Let’s explain it. docker run tells Docker that we want to run something using an image, in this case, one called sergiocampama/swift_grpc . One of the advantages of Docker is that it will download the image from Docker Cloud upon the first reference to it. The -i flag tells Docker to run in interactive mode by displaying the output of the command back to your terminal.

The -v `pwd`:/working_dir flag tells Docker that when running that image, we want to mount the current directory where we’re running the command (as returned by pwd ) into a directory named /working_dir inside the Docker image. The -w /working_dir flag tells Docker that we want it to run inside the /working_dir directory. These two flags have the effect of making the files inside the current folder available to the Docker image under the /working_dir directory.

Finally we get to the actual Protocol Buffers generation command, which will be run inside the Docker image. protoc is the code generator engine. The --swift_out=. flag tells protoc that we want to generate Swift bindings in the current directory for the Protocol Buffers message s given, while --swiftgrpc_out=. requests Swift gRPC code to be generated in the current directory.

After running this command, two files will appear in Echo/Sources/Protos , echo.pb.swift and echo.grpc.swift . These will be the Protocol Buffers and gRPC bindings for the Echo service, respectively. We’ll compile both of these files into the server and client, so we’ll copy them into the Echo/Sources/EchoServer and Echo/Sources/EchoClient folders².

$ cp *.swift ../EchoServer

$ cp *.swift ../EchoClient

$ cd ../..

Implementing the business logic

Now that we have the Protocol Buffers and gRPC bindings generated, we can start implementing the server and the client. We’ll start by defining the EchoServer and EchoClient products in the package by modifying the Package.swift file to the following:

// Echo/Package.swift

// swift-tools-version:4.1

import PackageDescription let package = Package(

name: "Echo",

products: [

.executable(

name: "EchoServer",

targets: ["EchoServer"]

),

.executable(

name: "EchoClient",

targets: ["EchoClient"]

),

],

dependencies: [

.package(

url: "https://github.com/grpc/grpc-swift.git",

from: "0.4.1"

),

],

targets: [

.target(

name: "EchoServer",

dependencies: [

"SwiftGRPC",

]

),

.target(

name: "EchoClient",

dependencies: [

"SwiftGRPC",

]

),

]

)

At this point, we can already start compiling our code. For each target, Swift Package Manager will look for sources under the Sources/<TargetName> directory, which for both EchoServer and EchoClient should already have the echo.pb.swift and echo.grpc.swift files:

$ swift build # First time takes a while as it compiles dependencies

Implementing EchoServer

But we haven’t done anything useful for now. Let’s implement the EchoServer logic by creating Echo/Sources/EchoServer/main.swift file with the following contents:

import Dispatch

import SwiftGRPC class EchoProvider: EchoServiceProvider {

func echo(request: EchoRequest,

session: EchoServiceechoSession)

throws -> EchoResponse {

var response = EchoResponse()

response.contents = "You sent: \(request.contents)"

return response

}

} let address = "0.0.0.0:9000"

print("Starting server in \(address)") let server = ServiceServer(

address: address, serviceProviders: [EchoProvider()]

) server.start() dispatchMain()

Let’s step through what the code is doing. Skipping over the imports, we first see an EchoProvider class, which implements the EchoServiceProvider protocol. This EchoServiceProvider protocol is defined in Echo/Sources/EchoServer/echo.grpc.swift file. The purpose of the EchoProvider class is to implement the logic behind the service EchoService definition in Echo/Sources/Protos/echo.proto . You can see that it only has one method, named echo (just like the rpc definition), and that it takes an EchoRequest message as input and returns an EchoResponse message, both defined in Echo/Sources/EchoServer/echo.pb.swift . Because this is an echo service, the response is equal to the request prepended with You sent: .

Then we define the address, which will be hardcoded to the local machine under port 9000, and create an instance of the ServiceServer class provided by SwiftGRPC with the address and an instance of the EchoProvider class. With this we’re configuring the server to respond to the EchoService.echo commands using the EchoProvider implementation.

Finally, we use a DispatchSemaphore to keep the server running. This has the effect of keeping the server alive while yielding the execution to the ServiceServer class whenever it gets a new connection.

We can run this server by running swift run EchoServer and you’ll see the Starting server in 0.0.0.0:9000 message. You can close the server with Ctrl + C .

Implementing EchoClient

Let’s implement a simple client that makes use of the EchoServer we just implemented. Add the following contents into Echo/Sources/EchoClient/main.swift :

let client = EchoServiceServiceClient(

address: "0.0.0.0:9000", secure: false

) var request = EchoRequest()

request.contents = "Hello, world!"

let response = try client.echo(request)

print(response.contents)

First, we make an instance of the EchoServiceServiceClient class defined in Echo/Sources/EchoClient/echo.grpc.swift , then we create an EchoRequest , defined in Echo/Sources/EchoClient/echo.pb.swift and set Hello, world! as its contents. We finally invoke the echo endpoint in EchoService with the request, which returns an EchoResponse , and we print its contents.

Now, make sure the EchoServer is running on a terminal as described above, and on a different terminal navigate to the Echo project and run swift run EchoClient . After compiling EchoClient , it will run and if everything went ok, it should print You sent: Hello, world! . We’ve successfully ran a gRPC server and client connection!

Interactive Development

With what we’ve accomplished at this point we could start building more complex servers and clients, but with more logic comes more potential issues that we’d have to debug. Wouldn’t it be nice to use a rich development IDE with debugging support for implementing the client and the server?

Luckily for us, the Swift Package Manager has support for generating an Xcode project based on the Package.swift project definition³. We can generate and open this project in Xcode with:

$ swift package generate-xcodeproj

$ open Echo.xcodeproj

We’re now presented with something that should look like this: