├── .gitignore ├── Package.resolved ├── Package.swift ├── README.md └── Sources ├── App ├── configure.swift └── websocket │ ├── WebsocketClient.swift │ ├── WebsocketClients.swift │ ├── WebsocketManager.swift │ ├── WebsocketMessage.swift │ └── messages │ ├── Connect.swift │ └── Person.swift └── Run └── main.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /.vscode 5 | /Packages 6 | /*.xcodeproj 7 | xcuserdata/ 8 | DerivedData/ 9 | .swiftpm/config/registries.json 10 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 11 | .netrc 12 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "async-http-client", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/swift-server/async-http-client.git", 7 | "state" : { 8 | "revision" : "7a4dfe026f6ee0f8ad741b58df74c60af296365d", 9 | "version" : "1.9.0" 10 | } 11 | }, 12 | { 13 | "identity" : "async-kit", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/vapor/async-kit.git", 16 | "state" : { 17 | "revision" : "e2f741640364c1d271405da637029ea6a33f754e", 18 | "version" : "1.11.1" 19 | } 20 | }, 21 | { 22 | "identity" : "console-kit", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/vapor/console-kit.git", 25 | "state" : { 26 | "revision" : "75ea3b627d88221440b878e5dfccc73fd06842ed", 27 | "version" : "4.2.7" 28 | } 29 | }, 30 | { 31 | "identity" : "multipart-kit", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/vapor/multipart-kit.git", 34 | "state" : { 35 | "revision" : "2dd9368a3c9580792b77c7ef364f3735909d9996", 36 | "version" : "4.5.1" 37 | } 38 | }, 39 | { 40 | "identity" : "routing-kit", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/vapor/routing-kit.git", 43 | "state" : { 44 | "revision" : "5603b81ceb744b8318feab1e60943704977a866b", 45 | "version" : "4.3.1" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-backtrace", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/swift-server/swift-backtrace.git", 52 | "state" : { 53 | "revision" : "d3e04a9d4b3833363fb6192065b763310b156d54", 54 | "version" : "1.3.1" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-crypto", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-crypto.git", 61 | "state" : { 62 | "revision" : "a8911e0fadc25aef1071d582355bd1037a176060", 63 | "version" : "2.0.4" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-log", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-log.git", 70 | "state" : { 71 | "revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", 72 | "version" : "1.4.2" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-metrics", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/apple/swift-metrics.git", 79 | "state" : { 80 | "revision" : "3edd2f57afc4e68e23c3e4956bc8b65ca6b5b2ff", 81 | "version" : "2.2.0" 82 | } 83 | }, 84 | { 85 | "identity" : "swift-nio", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/apple/swift-nio.git", 88 | "state" : { 89 | "revision" : "154f1d32366449dcccf6375a173adf4ed2a74429", 90 | "version" : "2.38.0" 91 | } 92 | }, 93 | { 94 | "identity" : "swift-nio-extras", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/apple/swift-nio-extras.git", 97 | "state" : { 98 | "revision" : "f73ca5ee9c6806800243f1ac415fcf82de9a4c91", 99 | "version" : "1.10.2" 100 | } 101 | }, 102 | { 103 | "identity" : "swift-nio-http2", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/apple/swift-nio-http2.git", 106 | "state" : { 107 | "revision" : "000ca94f9de92c95b9ac85d44600b7b0fe25a3e5", 108 | "version" : "1.19.2" 109 | } 110 | }, 111 | { 112 | "identity" : "swift-nio-ssl", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/apple/swift-nio-ssl.git", 115 | "state" : { 116 | "revision" : "52a486ff6de9bc3e26bf634c5413c41c5fa89ca5", 117 | "version" : "2.17.2" 118 | } 119 | }, 120 | { 121 | "identity" : "swift-nio-transport-services", 122 | "kind" : "remoteSourceControl", 123 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 124 | "state" : { 125 | "revision" : "8ab824b140d0ebcd87e9149266ddc353e3705a3e", 126 | "version" : "1.11.4" 127 | } 128 | }, 129 | { 130 | "identity" : "vapor", 131 | "kind" : "remoteSourceControl", 132 | "location" : "https://github.com/vapor/vapor.git", 133 | "state" : { 134 | "revision" : "18e9419cae5049e43ca1e8002ca3cf0449f2c8ed", 135 | "version" : "4.55.0" 136 | } 137 | }, 138 | { 139 | "identity" : "websocket-kit", 140 | "kind" : "remoteSourceControl", 141 | "location" : "https://github.com/vapor/websocket-kit.git", 142 | "state" : { 143 | "revision" : "ff8fbce837ef01a93d49c6fb49a72be0f150dac7", 144 | "version" : "2.3.0" 145 | } 146 | } 147 | ], 148 | "version" : 2 149 | } 150 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "test-websocket", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | ], 11 | dependencies: [ 12 | // Dependencies declare other packages that this package depends on. 13 | // .package(url: /* package url */, from: "1.0.0"), 14 | .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), 15 | ], 16 | targets: [ 17 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 18 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 19 | .target( 20 | name: "App", 21 | dependencies: [ 22 | .product(name: "Vapor", package: "vapor"), 23 | ], 24 | swiftSettings: [ 25 | // Enable better optimizations when building in Release configuration. Despite the use of 26 | // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release 27 | // builds. See for details. 28 | .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)), 29 | ] 30 | ), 31 | .executableTarget(name: "Run", dependencies: [.target(name: "App")]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Websocket Server Example using Vapor 4.0 2 | 3 | This project includes a minimum working example for a websocket server written in Swift. To interact with it I recommend using `websocat` ([Github-Link](https://github.com/vi/websocat)). 4 | 5 | Part of the challenge was to binary-encode and decode JSON payload into predefined Swift structs which are stored in the `Sources/App/websocket/messages` folder. 6 | 7 | ## How to use 8 | 1. Build and run using `swift build` and `swift run` 9 | 2. In this example the websocket is served under the `/channel` path. In the terminal connect to the websocket server using `websocat`: 10 | ```bash 11 | # '-b' transfers payload binary encoded 12 | websocat -b ws://127.0.0.1:8080/channel 13 | ``` 14 | 3. Paste into the console the JSON payload in the valid format (it is of type `WebsocketMessage`): 15 | ```JSON 16 | {"client":"C13C2DA8-13FA-4BA6-A361-61488AC5B66A","data":{"connect":true}} 17 | ``` 18 | 4. The websocket server should return the following message which should be displayed in the terminal decoded as string: 19 | ```JSON 20 | {"client":"C13C2DA8-13FA-4BA6-A361-61488AC5B66A","data":{"name":"Adrian","male":true,"age":33}} 21 | ``` 22 | -------------------------------------------------------------------------------- /Sources/App/configure.swift: -------------------------------------------------------------------------------- 1 | 2 | import Vapor 3 | 4 | public func configure(_ app: Application) throws { 5 | app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) 6 | 7 | let websocketManager = WebsocketManager(eventLoop: app.eventLoopGroup.next()) 8 | 9 | app.webSocket("channel") { _, ws in 10 | websocketManager.connect(ws) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/App/websocket/WebsocketClient.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | open class WebsocketClient { 4 | open var id: UUID 5 | open var socket: WebSocket 6 | 7 | public init(id: UUID, socket: WebSocket) { 8 | self.id = id 9 | self.socket = socket 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/App/websocket/WebsocketClients.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | open class WebsocketClients { 4 | var eventLoop: EventLoop 5 | var storage: [UUID: WebsocketClient] 6 | 7 | var active: [WebsocketClient] { 8 | storage.values.filter { !$0.socket.isClosed } 9 | } 10 | 11 | init(eventLoop: EventLoop, clients: [UUID: WebsocketClient] = [:]) { 12 | self.eventLoop = eventLoop 13 | storage = clients 14 | } 15 | 16 | func add(_ client: WebsocketClient) { 17 | storage[client.id] = client 18 | } 19 | 20 | func remove(_ client: WebsocketClient) { 21 | storage[client.id] = nil 22 | } 23 | 24 | func find(_ uuid: UUID) -> WebsocketClient? { 25 | storage[uuid] 26 | } 27 | 28 | deinit { 29 | let futures = self.storage.values.map { $0.socket.close() } 30 | try! self.eventLoop.flatten(futures).wait() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/App/websocket/WebsocketManager.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | class WebsocketManager { 4 | var clients: WebsocketClients 5 | 6 | init(eventLoop: EventLoop) { 7 | clients = WebsocketClients(eventLoop: eventLoop) 8 | } 9 | 10 | func connect(_ ws: WebSocket) { 11 | ws.onBinary { [unowned self] ws, buffer in 12 | if let msg = buffer.decodeWebsocketMessage(Connect.self) { 13 | let app = WebsocketClient(id: msg.client, socket: ws) 14 | self.clients.add(app) 15 | print("Added client app: \(msg.client)") 16 | 17 | // for demonstration purposes immediately respond to the client 18 | notify() 19 | } 20 | } 21 | 22 | ws.onText { ws, _ in 23 | ws.send("pong") 24 | } 25 | } 26 | 27 | func notify() { 28 | let connectedClients = clients.active.compactMap { $0 as WebsocketClient } 29 | guard !connectedClients.isEmpty else { 30 | return 31 | } 32 | 33 | connectedClients.forEach { client in 34 | 35 | let person = Person(name: "Adrian", male: true, age: 33) 36 | let msg = WebsocketMessage(client: client.id, data: person) 37 | let data = try! JSONEncoder().encode(msg) 38 | 39 | client.socket.send([UInt8](data)) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/App/websocket/WebsocketMessage.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | struct WebsocketMessage: Codable { 4 | let client: UUID 5 | let data: T 6 | } 7 | 8 | extension ByteBuffer { 9 | func decodeWebsocketMessage(_: T.Type) -> WebsocketMessage? { 10 | return try? JSONDecoder().decode(WebsocketMessage.self, from: self) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/App/websocket/messages/Connect.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Connect: Codable { 4 | let connect: Bool 5 | } 6 | -------------------------------------------------------------------------------- /Sources/App/websocket/messages/Person.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Person: Codable { 4 | let name: String 5 | let male: Bool 6 | let age: Int 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Run/main.swift: -------------------------------------------------------------------------------- 1 | import App 2 | import Cocoa 3 | import Vapor 4 | 5 | // Handle CTRL+C event to terminate the app 6 | signal(SIGINT, SIG_IGN) 7 | let sigint = DispatchSource.makeSignalSource(signal: SIGINT, queue: DispatchQueue.main) 8 | sigint.setEventHandler { 9 | NSApp.terminate(nil) 10 | } 11 | 12 | sigint.resume() 13 | 14 | var env = try Environment.detect() 15 | try LoggingSystem.bootstrap(from: &env) 16 | let app = Application(env) 17 | defer { app.shutdown() } 18 | try configure(app) 19 | try app.run() 20 | --------------------------------------------------------------------------------