-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit dfa906f
Showing
89 changed files
with
5,159 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.DS_Store | ||
.build/ | ||
Packages/ | ||
/Server/*.xcodeproj | ||
*.xcworkspace | ||
Pods/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]) | ||
] | ||
) |
51 changes: 51 additions & 0 deletions
51
Server-completed/Sources/ChatCommon/Model/ClientCommand+Codable.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
8
Server-completed/Sources/ChatCommon/Model/ClientCommand.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
71 changes: 71 additions & 0 deletions
71
Server-completed/Sources/ChatCommon/Model/ServerMessage+Codable.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
10
Server-completed/Sources/ChatCommon/Model/ServerMessage.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.") | ||
|
||
|
Oops, something went wrong.