この記事はiOS #2 Advent Calendar 2020の10日目の記事です。
こんにちは。株式会社ビデオマーケットの仙台オフィスでMIRAILのiOS/Androidアプリ開発を担当しているasmzです。
ちょっと業務でiOSアプリでのWebSocketクライアントの実装方法を調査していて、その中でApple公式のAPIとして用意されているURLSessionWebSocketTask
の使い方を初めて知ったので、今回は簡単なWebSocketクライアントを実装しながらその実装方法をまとめてみたいと思います。
WebSocketとは
詳細は省きますが、Webアプリケーションにおいて双方向通信を実現するための技術仕様です。
WebSocketではサーバとクライアントがコネクションを確立後、そのコネクションを用いてメッセージの送受信を行うことで、低コストなリアルタイム通信を可能としています。
URLSessionWebSocketTaskとは
WWDC 2019で発表され iOS13より追加されたWebSocketクライアント処理を行うためのクラスで、従来のHTTP通信を行うURLSessionTask
のサブクラスとして実装されています。
これまでiOSアプリでWebSocket通信を実装するものとしてはNetwork.frameworkなどがありましたが、URLSessionWebSocketTask
はそれをより手軽に扱えるようにした感じのクラスです。
WebSocketクライアント実装
それでは早速、具体的な実装方法の説明に入っていきましょう。
タスク生成
まず最初にURLSessionWebSocketTaskのインスタンスを生成する必要があります。
let urlSession = URLSession(configuration: .default) let webSocketTask = urlSession.webSocketTask(with: "ws://localhost:5001/")
ここでWebSocketサーバのURLを指定します。
接続
接続は通常のURLSessionと同様にresume()
を実行するだけです。
webSocketTask.resume()
メッセージ送信
コネクションが確立したら、send()
でメッセージの送信が可能となります。
// Defined: public func send(_ message: URLSessionWebSocketTask.Message, completionHandler: @escaping (Error?) -> Void)
ここで出てくる URLSessionWebSocketTask.Message
は以下のように文字列とバイナリの列挙型となっており、テキストメッセージとバイナリデータの2種類の形式で送信可能となっています。
// Defined: public enum Message { case data(Data) case string(String) }
let msg = URLSessionWebSocketTask.Message.string("テキストメッセージ") webSocketTask.send(msg) { error in if let error = error { print(error) // some error handling } }
2種類の形式と言っても、JSONなどのテキストで送受信すれば他にもいろんな情報を詰め込むことは可能です。
メッセージ受信
サーバからのメッセージはreceive()
のcompletionHandler
で受けとることができます。
// Defined: public func receive(completionHandler: @escaping (Result<URLSessionWebSocketTask.Message, Error>) -> Void)
ただしここで注意点として、receive()
メソッドはメッセージ受信時の1回しか呼ばれません。
そのため、継続してメッセージを受信したい場合は、以下のようにreceive()
メソッドをWrapし、メッセージ受信後にこのWrapperメソッドを再帰的に呼び出す必要があります。
func receiveMessage() { webSocketTask.receive { [weak self] result in switch result { case .success(let message): switch message { case .string(let text): print("Received! text: \(text)") case .data(let data): print("Received! binary: \(data)") @unknown default: fatalError() } receiveMessage() // <- 継続して受信するために再帰的に呼び出す case .failure(let error): print("Failed! error: \(error)") } } }
この辺、Delegateなど設定すれば自動的に入ってくると良いなと思うのですが、現状そういった方法は用意されていないようです。
切断
コネクション切断にはcancel()
を使用します。
// Defined: open func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?)
切断時には切断コード値と理由をサーバへ通知することができます。URLSessionWebSocketTask.CloseCodeは以下のように定義されています。
public enum CloseCode : Int { case invalid = 0 case normalClosure = 1000 case goingAway = 1001 case protocolError = 1002 case unsupportedData = 1003 case noStatusReceived = 1005 case abnormalClosure = 1006 case invalidFramePayloadData = 1007 case policyViolation = 1008 case messageTooBig = 1009 case mandatoryExtensionMissing = 1010 case internalServerError = 1011 case tlsHandshakeFailure = 1015 }
webSocketTask.cancel(with: .goingAway, reason: nil)
接続ステータス監視
URLSessionWebSocketDelegateを用いて、WebSocketのコネクションが確立・切断されたタイミングを監視し、アプリ側でハンドリングすることが可能です。
[コネクション確立]
// Defined: func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?)
[コネクション切断]
// Defined: func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?)
なお、URLSessionWebSocketDelegate
はURLSessionTaskDelegate
のサブクラスなので、通信エラーなどの場合はURLSessionTask利用時と同じようにurlSession(_:task:didCompleteWithError:)
で補足できます。
実装サンプル
クライアント
というわけで、上記の内容を踏まえて簡単なチャット風WebSocketクライアントアプリを用意してみました。
WebSocketClient
クラスがWebSocketクライアント本体となります。
なお普段の業務ではSwiftUI使っていないのですが、今回は興味本位でSwiftUIで実装してみました。(SwiftUIでの開発には全く慣れていないので、UI周りの実装については正直自信ありません。。)
サーバ
今回のエントリの範疇ではないですが、とりあえず動作確認したかったので雑に以下のNode.jsのサーバをlocalhostに立ち上げました。
var server = require('ws').Server; var s = new server( { port: 5001 } ); s.on('connection',function(ws){ ws.on('message',function(message){ console.log("Received: " + message); s.clients.forEach(function(client){ client.send(message); }); }); ws.on('close',function(){ console.log('closed.'); }); });
やってることは、単純にクライアントから受信したメッセージを全クライアントに返すだけです。
これでWebSocketサーバを立ち上げると、URLは ws://localhost:5001
となります。
ちなみに ws
はHTTPでの通信となるので、iOSでの実行の際はATS設定が必要です。
実行結果
シミュレータを2台立ち上げて、同一WebSocketサーバと接続した状態での挙動です。
一方のアプリでメッセージを送信すると、もう一方のアプリもリアルタイムでメッセージを受信していることがわかるかと思います。
おわりに
以上、URLSessionWebSocketTaskを用いたWebSocket通信処理の基本的な実装手順を紹介しました。
個人的な感想としてはちょっとreceive()
の扱いにクセがあるように思いましたが、公式APIのみでだいぶ手軽にWebSocketが扱えるようになったという印象です。
なお、URLSessionWebSocketTaskが使えるのはiOS13以上となりますのでご注意ください。(ここまで調べといて何ですが、私はそれでURLSessionWebSocketTaskを使うのは諦めて、サードパーティ製のライブラリを使用することにしました。。)
参考
- https://developer.apple.com/videos/play/wwdc2019/712/
- https://developer.apple.com/documentation/foundation/urlsessionwebsockettask
- https://appspector.com/blog/websockets-in-ios-using-urlsessionwebsockettask
- https://medium.com/better-programming/websockets-in-ios-13-using-swift-and-xcode-11-18fa3000d802