ottijp blog

VaporでWebSocketチャットサーバを作る

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でハンドラを登録しますが,切断時はonCloseFutureで非同期処理する必要があり,書き方が異なります. エラー時はonTextと同様にonErrorでハンドラを登録します.

参考


ottijp
都内でアプリケーションエンジニアをしています
© 2026, ottijp