Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
fpillet committed Nov 26, 2018
0 parents commit dfa906f
Show file tree
Hide file tree
Showing 89 changed files with 5,159 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
.build/
Packages/
/Server/*.xcodeproj
*.xcworkspace
Pods/
56 changes: 56 additions & 0 deletions ChatClient.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Mission: create a chat client

The client for our chat solution is an iOS application (albeit a simple one, designed to run on iPad). The client connects to the server and talks a simple protocol: client sends `ClientCommand`s, sever replies with `ServerMessage`s.

The server will accept commands from clients applications that connect to it. It holds the chat rooms, dispatches the messages sent by clients, and supports direct messages between clients.

To talk to the server, you'll use Network.framework's new API, available in iOS 12, tvOS 12 and macOS Mojave.

For this project you'll work in the `iOS` directory. If you're stuck or want to check out a hint, the completed project is in the `iOS-Complete` folder.

## Introduction: the simplicity and versatility of Network.framework

Let's discuss Network.framework! It' sa powerful networking API which provides a small but powerful API surface.

Its main components are:

* `NWEndpoint`, an endpoint in a network connection.
* `NWConnection`, a bidirectional data connection between a local endpoint and a remote endpoint.
* `NWListener`, an object you use to listen for incoming network connections.
* `NWParameter`, an object that stores the protocols to use for connections, options for sending data, and network path constraints.

Network.framework also replaces goold old Reachability with `NWPath` and `NWPathMonitor`. It add a ton of information and control over the type of connections, network transitions (i.e. wifi to cellular, etc), supports multipath TCP, proxies, TLS, etc etc. We're only going to scratch the surface with this iOS client application.

## Prelude: familiarize yourself with the application

It's a bare-bones chat application, nothing complicated about it. All the communication with the server, and carrying the state of each chat room is not in the `ChatClientService` class. This is where you'll be doing all your network-related work.

To get started, make sure you get the one Pod we need for this application: MessengerKit, a framework that makes it easy to display a chat user interface.

Make sure you have CocoaPods install:
`$ sudo gem install cocoapods`

Then simply update pods from within the iOS folder:
`$ pod update`

## Task 1: setup the necessary bits for NWConnection

You'll need to create an endpoint (`NWEndpoint`) that describes the server location, and prepare a queue for your connection to run on. This is done in `init` and above.

## Task 2: fill in the `connect()` method

Create the `NWConnection` object you need in the `connect()` method. Setup a connection state handler by filling the `setupConnectionStateHandler(_:)` method. You'll learn about the various states a connection can be in.

## Task 3: write the code that sends packets

An easy task (as is the rest of this project), you'll fill in the `sendUnframed(command:)` function which sends raw JSON data to the server. But you may want to directly fill in the `sendFramed(command:)` method which sends a JSON packet prefixed with an `UInt32` (big endian) the gives the size of the JSON data. This is to deal with TCP packet fragmentation on the receiving side.

Filling both method will give you a sense of a very useful distinction in Network.framework: the ability to indicate when the content you are sending is complete, even when you write multiple chunks.

## Task 4: write the code that receives messages from the server

Reading messages from the server is slightly more involved than writig, but not much. Again you'll appreciate the simplicity of the Network.framework API which really is a joy to work with. Make sure you feel in both the `readNextUnframedMessage(_:)` and `readNextFrameMessage(_:)` methods to first understand the basic of asynchronous reads in Network.framework, and learn how you split multiple reads and chain them together.

## Uber-challenge: write the iOS side with Swift-NIO and NIOTransportServices!

Although no solution to this challenge is presented here, if you worked on the Swift-NIO side of the project with the server you may understand it well enough to write the client side with Swift-NIO and maybe reuse some of the channel handlers you prepared for the server!
104 changes: 104 additions & 0 deletions ChatServer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Mission: create a chat server

We're going to build a simple chat server that can run on macOS and Linux, using Swift-NIO.

The server will accept commands from clients applications that connect to it. It holds the chat rooms, dispatches the messages sent by clients, and supports direct messages between clients.

To simplify the development, most of the infrastructure you need (model, utilities, general project structure) is ready for you to start with.

For this project you'll work in the `Server` directory. If you're stuck or want to check out a hint, the completed project is in the `Server-Complete` folder.

## Prelude: environment setup

The chat server relies on Swift-NIO, which can be obtained using the Swift Package Manager. Firer up a terminal, `cd` to the `Server` folder and run these commands:

`$ swift package update`

then

`$ swift package generate-xcodeproj`

A new `ChatServer.xcodeproj` project will appear in the Server folder.

## Introduction: understanding Swift-NIO's general model

Let's discuss Swift-NIO architecture! The introduction on the repository states that:

> SwiftNIO is a cross-platform asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
> It's like Netty, but written for Swift.
I'll go with you over the main concepts and building blocks in Swift-NIO:

* `EventLoop` and `EventLoopGroup`:
* `Channel`, a protocol
* `ChannelHandler` and `ChannelPipeline`: single-purpose data handlers and pipelines to assemble them together
* `ServerBootstrap`, `ClientBootstrap` and `DatagramBootstrap`: helpers to quickly get setup for a server or client
* `EventLoopFuture` and `EventLoopPromise`, asynchronous production of results
* `ByteBuffer`, high performance contiguous storage

In this introduction and simple server development, we'll focus on the 5 first items, and will make light use of `Future` to setup the server.

Let me go over Swift-NIO's model, then we'll kick in the first task.

## Task 1: write a logging channel handler

Write a simple channel handler named that you'll insert in the processing pipeline and which logs incoming commands from clients. It will take a `ClientCommand` as its in / out type, log what it sees then pass the data on to the next handler.

Open the `ClientCommandLogHandler.swift` file to get going. Remember that what you process needs to be carried on to the next handler in the pipeline!


## Task 1: write the basic packet format encoder and decoder

You'll need at least one channel handler that decodes the JSON to `ClientCommand` enums, and one that encodes outgoing messages from `ServerCommand` enums.

Remember that data goes in but also needs to get carried out to the next handler in the pipeline.

Open the `ClientCommandDecoderChannelHandler.swift` file to get going with the incoming data decoder.

Next, open `ServerMessageEncoderChannelHandler.swift` to code the outgoing handler. Notice that this time, it will adopt the `MessageToByteEncoder` protocol, worth to know about!


## Task 3: create an EventLoopGroup

An easy one to get started with the actual server. Open `ServerMain.swift` and create your new group.


## Task 4: boostrap the server

This one is more involved as you'll have to understand what `ServerBoostrap` does and how to use it. This all happens in `ServerMain.swift`.

Hints at what you want to do:

* Create a `ServerBootstrap` for your EventLoopGroup
* Set options for the main server channel (the one that listens to client connections). Look into the various `ChannelOptions` and pick the ones you need
* Setup a child channel initializer which will configure the processing pipeline for client connections. At a minimum, you'll want to decode JSON to actual `ClientCommand` instances,
* Add the second channel handler which will log the decoded client commands

At this stage you should be able to start your server, although it won't do much besides logging what comes in. You should be able to test it by running the iOS client and see one incoming message upon connection.


## Task 5: create the actual Chat handler channel

You are now at a point where you're ready to inject the actual functionality of your server:

- It needs to be a ChannelHandler that will come late in the pipeline
- It must receive `ClientCommand` objects
- It must send `ServerMessage` objects to clients

Start by opening the file `ServerMain.swift` then fill the gaps in the `startServer(rooms:)`function.

## Task #6: run the tests

The tests have already been written for you. If you run tests, either from Xcode or from the commandline, they should mostly pass. "Mostly" because you'll quickly realize that there is one issue left that needs to be taken care of ...

See, TCP doesn't guarantee that everything that's being sent from one side will arrive in a single piece on the other side. There may be packet fragmentation, which means (and this happens during testing, which establishes real connections internally) that you may have JSON packets that arrive in several pieces.

The solution to tackle this issue is to frame your packets in a way that make it easy from the receiving end to reassemble, regardless of the number of chunks they have been split into.

So you'll want to implement a simple framing protocol: send 4 bytes with the length of the data, followed by the data (the JSON representation) itself.

Open the `FrameMessageDecoder.swift` file and get going if you feel you can do it! Otherwise, you'll find a reference implementation in the complete product source code.

Once you've coded this part, make sure you uncomment the lines about `FrameMessageCodec` in `TestingChatClient.swift` and `TestingChatServer.swift` !

That's all for the server. Congrats for making it this far!
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Swift Alps Networking Workshop

### Prepared by Florent Pillet - @fpillet

Welcome to the Swift Alps Networking Workshop! You'll learn a lot about Swift-NIO, a new high performance network application framework from Apple, as well as about Network.framework, the new networking API in iOS 12, tvOS 12 and macOS Mojave which deprecates most of the past networking APIs.

In this project, you will work on a chat server using Swift-NIO, and a chat client on iOS using Network.framework. You can choose to work on one or both, depending on your interest and your learning speed. Once you get back from Swift Alps, you can check out this material to learn about networking techniques using both frameworks.

I hope you'll enjoy them as much as I do!

Please check out `ChatClient.md` and `ChatServer.md` for the challenges with each project.
25 changes: 25 additions & 0 deletions Server-completed/Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"object": {
"pins": [
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",
"state": {
"branch": null,
"revision": "035962e6b6e03c8721a91a9b96dc084289795cb4",
"version": "1.11.0"
}
},
{
"package": "swift-nio-zlib-support",
"repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",
"state": {
"branch": null,
"revision": "37760e9a52030bb9011972c5213c3350fa9d41fd",
"version": "1.0.0"
}
}
]
},
"version": 1
}
22 changes: 22 additions & 0 deletions Server-completed/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// swift-tools-version:4.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "ChatServer",
products: [
.library(name: "ChatCommon", targets: ["ChatCommon"]),
.library(name: "ChatServerLib", targets: ["ChatServerLib"]),
.executable(name: "ChatServer", targets: ["ChatServer"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "1.11.0"),
],
targets: [
.target(name: "ChatCommon", dependencies: ["NIO"]),
.target(name: "ChatServerLib", dependencies: ["NIO","ChatCommon"]),
.target(name: "ChatServer", dependencies: ["ChatServerLib"]),
.testTarget(name: "ChatServerTests", dependencies: ["ChatServerLib"])
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation

extension ClientCommand: Codable {
private enum CodingKeys: String, CodingKey {
case command
case data
}

private enum Cmd: String, Codable {
case connect, disconnect, message, privateMessage
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(Cmd.self, forKey: .command) {
case .connect:
let username = try container.decode(String.self, forKey: .data)
self = .connect(username: username)
case .disconnect:
self = .disconnect
case .message:
let msg = try container.decode(MessageData.self, forKey: .data)
self = .message(room: msg.to, text: msg.text)
case .privateMessage:
let msg = try container.decode(MessageData.self, forKey: .data)
self = .privateMessage(username: msg.to, text: msg.text)
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .connect(let username):
try container.encode(Cmd.connect, forKey: .command)
try container.encode(username, forKey: .data)
case .disconnect:
try container.encode(Cmd.disconnect, forKey: .command)
case .message(let room, let text):
try container.encode(Cmd.message, forKey: .command)
try container.encode(MessageData(to: room, text: text), forKey: .data)
case .privateMessage(let username, let text):
try container.encode(Cmd.privateMessage, forKey: .command)
try container.encode(MessageData(to: username, text: text), forKey: .data)
}
}
}

private struct MessageData: Codable {
let to: String
let text: String
}
8 changes: 8 additions & 0 deletions Server-completed/Sources/ChatCommon/Model/ClientCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

public enum ClientCommand: Equatable {
case connect(username: String)
case disconnect
case message(room: String, text: String)
case privateMessage(username: String, text: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation

extension ServerMessage: Codable {
private enum CodingKeys: String, CodingKey {
case command
case data
}

private enum Cmd: String, Codable {
case connected, disconnected, rooms, users, message, privateMessage
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(Cmd.self, forKey: .command) {
case .connected:
let server = try container.decode(String.self, forKey: .data)
self = .connected(to: server)
case .disconnected:
self = .disconnected
case .rooms:
let rooms = try container.decode([String].self, forKey: .data)
self = .rooms(rooms)
case .users:
let users = try container.decode([String].self, forKey: .data)
self = .users(users)
case .message:
let data = try container.decode(MessageData.self, forKey: .data)
self = .message(room: data.to, username: data.from, text: data.text)
case .privateMessage:
let data = try container.decode(MessageData.self, forKey: .data)
self = .privateMessage(from: data.from, to: data.to, text: data.text)
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .connected(let to):
try container.encode(Cmd.connected, forKey: .command)
try container.encode(to, forKey: .data)
case .disconnected:
try container.encode(Cmd.disconnected, forKey: .command)
case .rooms(let rooms):
try container.encode(Cmd.rooms, forKey: .command)
try container.encode(rooms, forKey: .data)
case .users(let users):
try container.encode(Cmd.users, forKey: .command)
try container.encode(users, forKey: .data)
case .message(let room, let username, let text):
try container.encode(Cmd.message, forKey: .command)
try container.encode(MessageData(from: username, to: room, text: text), forKey: .data)
case .privateMessage(let from, let to, let text):
try container.encode(Cmd.privateMessage, forKey: .command)
try container.encode(MessageData(from: from, to: to, text: text), forKey: .data)
}
}
}

// Pieces of data we need to encode / decode JSON

private struct RoomAndUsername: Codable {
let room: String
let username: String
}

private struct MessageData: Codable {
let from: String
let to: String
let text: String
}
10 changes: 10 additions & 0 deletions Server-completed/Sources/ChatCommon/Model/ServerMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

public enum ServerMessage: Equatable {
case connected(to: String)
case disconnected
case rooms([String])
case users([String])
case message(room: String, username: String, text: String)
case privateMessage(from: String, to: String, text: String)
}
12 changes: 12 additions & 0 deletions Server-completed/Sources/ChatServer/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ChatServerLib

guard let server = startServer(rooms: ["Red Team","Blue Team","General","Random"]) else {
fatalError("Failed starting server")
}

// this will never exit
try! server.1.closeFuture.wait()

print("Done.")


Loading

0 comments on commit dfa906f

Please sign in to comment.