SwiftのWebフレームワークであるVaporを使って,WebSocketチャットサーバを作ってみました.
環境
- Vapor 3.1
- swift 4.2.1
- Xcode 10.1
- macOS 10.14 (Mojave)
WebSocketライブラリの追加
Vaporの新規プロジェクトを作成し,WebSocketライブラリをPackage.swift
に入れます.
(apiテンプレートを元にしていますが,テンプレートで提供しているAPIサーバ機能は今回使っていません).
$ vapor new --api vapor-websocket
Pakage.swift
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "vapor-websocket",
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
// 🔵 Swift ORM (queries, models, relations, etc) built on SQLite 3.
.package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/websocket.git", from: "1.0.0"),
],
targets: [
.target(name: "App", dependencies: ["FluentSQLite", "Vapor", "WebSocket"]),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"])
]
)
WebSocketチャットサーバ機能の追加
Sources/App/chat.swift
というファイルを新規作成し,以下のようなチャットルームクラスを作りました.
chat.swift
import Vapor
public class ChatRoom {
// 接続中のWebSocketクライアント
var clients = [WebSocket]()
// WebSocketクライアントの接続ハンドラ
func handler() -> ((WebSocket, Request) throws -> ()) {
return { ws, req in
// 接続中クライアントリストに追加
self.clients.append(ws)
// メッセージ受信時のハンドラを登録
ws.onText(self.onText)
// 切断時にクライアントリストから除去
ws.onClose.always {
self.clients = self.clients.filter { $0 === ws }
}
}
}
// WebSocketクライアントからのメッセージハンドラ
private func onText(sender: WebSocket, text: String) -> () {
// 送られたメッセージをそのまま全クライアントに送信する
self.clients.forEach{ ws in
if ws !== sender {
ws.send("> \(text)")
}
}
}
}
- 接続中のWebSocketクライアントのリストを
clients
で保持し,接続・切断で追加・除去しています - メッセージの受信時に,送信元以外のクライアントにメッセージを転送しています
これを,Sources/App/configure.swift
でサービスに追加します.
configure.swift
// WebSocketサーバの作成
let wss = NIOWebSocketServer.default()
// WebSocketアップグレードサポートを/echoに追加
let chatRoom = ChatRoom()
wss.get("echo", use: chatRoom.handler())
// WebSocketサーバの登録
services.register(wss, as: WebSocketServer.self)
実行
サーバを起動し,wstaで動作確認します.
$ vapor build && vapor run
Running vapor-websocket ...
[ INFO ] Migrating 'sqlite' database (/Users/hoge/sandbox/vapor-websocket/.build/checkouts/fluent.git--929032494386059648/Sources/Fluent/Migration/MigrationConfig.swift:69)
[ INFO ] Preparing migration 'Todo' (/Users/hoge/sandbox/vapor-websocket/.build/checkouts/fluent.git--929032494386059648/Sources/Fluent/Migration/Migrations.swift:111)
[ INFO ] Migrations complete (/Users/hoge/sandbox/vapor-websocket/.build/checkouts/fluent.git--929032494386059648/Sources/Fluent/Migration/MigrationConfig.swift:73)
Running default command: .build/debug/Run serve
Server starting on http://localhost:8080
クライアント1
$ wsta ws://localhost:8080/echo
Connected to ws://localhost:8080/echo
Hi, my name is client1.
> Hi! I'm client2!
クライアント2
$ wsta ws://localhost:8080/echo
Connected to ws://localhost:8080/echo
> Hi, my name is client1.
Hi! I'm client2!
両方のクライアントで接続後,クライアント1からHi, my name is client1.
と送信し,その後クライアント2からHi! I'm client2!
と送信しました.
注意点
WebSocketクライアントからのメッセージ受信時はonText
でハンドラを登録しますが,切断時はonClose
Futureで非同期処理する必要があり,書き方が異なります.
エラー時はonText
と同様にonError
でハンドラを登録します.