1
/
5

【iOS】URLSessionWebSocketTaskを利用したWebSocket通信【株式会社ライトコード】

はじめに

こんにちは!株式会社ライトコードの福岡本社でモバイルエンジニアやってる こー です!

今回は業務上で行った iOS アプリで WebSocket 通信を行う 方法について、備忘録的に書いていこうと思います。

iOSアプリにWebSocket通信を実装

それでは、iOSアプリでWebSocket通信を実現するための実装例を見ていきましょう。

今回は、 ローカルサーバーとWebSocketで通信を行う クライアントクラスを作ります。

URLSessionWebSocketTask というクラスがiOSの標準SDKに用意されており、これを使ってWebSocket通信を簡単に実装することができます。

まずはコード全体に一度目を通した後、要点を絞って解説していきます。

protocol WebSocketClientProtocol {

var delegate: WebSocketClientDelegate? { get set }
func connect() async throws
func send(text: String) async throws
func send(data: Data) async throws
func disconnect()
}

protocol WebSocketClientDelegate: AnyObject {
func webSocketClient(didConnect client: WebSocketClient)
func webSocketClient(_ client: WebSocketClient, didReceiveText text: String)
func webSocketClient(_ client: WebSocketClient, didReceiveData data: Data)
func webSocketClient(didDisconnect client: WebSocketClient)
}

/// MARK: - WebSocket通信を行うクライアントクラス
class WebSocketClient: NSObject, WebSocketClientProtocol {

enum Host {
case local

var urlString: String {
switch self {
case .local:
return "wss://localhost:8080"
}
}
}

weak var delegate: WebSocketClientDelegate?

private var webSocketTask: URLSessionWebSocketTask?
private let session: URLSession
private let url: URL

// 1. セッションインスタンスの生成
init(host: WebSocketClient.Host) {
self.url = URL(string: host.urlString)!
self.session = URLSession(
configuration: .default,
delegate: AllowLocalhostSignedCertificateDelegate(),
delegateQueue: nil
)
}

// 2. サーバーとの接続を確立する
func connect() async throws {
self.webSocketTask = session.webSocketTask(with: url)
webSocketTask?.resume()
delegate?.webSocketClient(didConnect: self)
try await receive()
}

// 3. サーバーからのメッセージを待機する
private func receive() async throws {
while webSocketTask != nil {
do {
let message = try await webSocketTask?.receive()

switch message {
case .string(let text):
print("Received text message: \(text)")
delegate?.webSocket(self, didReceiveText: text)
case .data(let data):
print("Received data message: \(data)")
delegate?.webSocket(self, didReceiveData: data)
}
} catch {
// 既にWebSocketTaskが開放されている場合、エラーはスルーする
guard let _ = webSocketTask else {
return
}
print("WebSocket receive error: \(error)")
disconnect()
throw error
}
}
}

// 4.1 サーバーにテキストメッセージを送信する
func send(text: String) async throws {
let message = URLSessionWebSocketTask.Message.string(text)
do {
try await webSocketTask?.send(message)
} catch {
print("WebSocket send text error: \(error)")
throw error
}
}

// 4.2 サーバーにバイナリメッセージを送信する
func send(data: Data) async throws {
let message = URLSessionWebSocketTask.Message.data(data)
do {
try await webSocketTask?.send(message)
} catch {
print("WebSocket send data error: \(error)")
throw error
}
}

// 5. サーバーとの接続を切断する
func disconnect() {
webSocketTask?.cancel(with: .goingAway, reason: nil)
webSocketTask = nil
delegate?.webSocketClient(didDisconnect: self)
}
}

// MARK: - 認証チャレンジのハンドリングデリゲートクラス
class AllowLocalhostSignedCertificateDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {

let protectionSpace = challenge.protectionSpace
// 以下条件に合致する場合、信頼されていない証明書であっても受け入れ接続を許可する
// - "localhost" での接続
// - サーバー認証である
guard protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
protectionSpace.host == "localhost",
let serverTrust = protectionSpace.serverTrust else {
return (.performDefaultHandling, nil)
}

return (.useCredential, URLCredential(trust: serverTrust))
}
}

1. セッションインスタンスの生成

    // 1. セッションインスタンスの生成

init(host: WebSocketClient.Host) {
self.url = URL(string: host.urlString)!
self.session = URLSession(
configuration: .default,
delegate: AllowLocalhostSignedCertificateDelegate(),
delegateQueue: nil
)
}

// ~~ 中略 ~~ //

// MARK: - 認証チャレンジのハンドリングデリゲートクラス
class AllowLocalhostSignedCertificateDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {

let protectionSpace = challenge.protectionSpace
// 以下条件に合致する場合、信頼されていない証明書であっても受け入れ接続を許可する
// - "localhost" での接続
// - サーバー認証である
guard protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
protectionSpace.host == "localhost",
let serverTrust = protectionSpace.serverTrust else {
return (.performDefaultHandling, nil)
}

return (.useCredential, URLCredential(trust: serverTrust))
}
}

まず URLSessionWebSocketTask のインスタンス生成を行いますが、ここで 認証チャレンジのハンドリングデリゲートクラス を作って渡す必要があります。

通常WebSocketのURLはwsスキームですが、HTML通信同様にセキュア通信のスキームwssがあり、通信対象のサーバーがセキュア通信である場合は、サーバー接続を行う際に認証チャレンジが行われます。

この時、サーバー側に証明書が存在していなかったり、独自証明書を利用している場合は接続が拒否されてしまいます。

そのため、今回は「localhostの通信」&「サーバー認証」である場合に通信を許可する認証チャレンジのハンドリングデリゲートクラスを作り初期化の際に渡すようにします。

記事の続きは下のURLをクリック!

https://rightcode.co.jp/blogs/48403



エンジニア積極採用中です!

現在、WEBエンジニア、モバイルエンジニア、デザイナー、営業などを積極採用中です!

採用ページはこちら:https://rightcode.co.jp/recruit

社員の声や社風などを知りたい方はこちら:https://rightcode.co.jp/blogs?category=life

社長と一杯飲みながらお話しませんか?(転職者向け)

特設ページはこちら: https://rightcode.co.jp/gohan-sake-president-talk

もっとワクワクしたいあなたへ

現在、ライトコードでは「WEBエンジニア」「モバイルエンジニア」「ゲームエンジニア」、「デザイナー」「WEBディレクター」「営業」などを積極採用中です!

ライトコードは技術力に定評のある受託開発をメインにしているIT企業です。

有名WEBサービスやアプリの受託開発などの企画、開発案件が目白押しの状況です。

  • もっと大きなことに挑戦したい!
  • エンジニアとしてもっと成長したい!
  • モダンな技術に触れたい!

現状に満足していない方は、まずは、エンジニアとしても第一線を走り続ける弊社代表と気軽にお話してみませんか?

ネット上では、ちょっとユルそうな会社に感じると思いますが(笑)、
実は技術力に定評があり、沢山の実績を残している会社ということをお伝えしたいと思っております。

  • ライトコードの魅力を知っていただきたい!
  • 社風や文化なども知っていただきたい!
  • 技術に対して熱意のある方に入社していただきたい!

一度、【Wantedly内の弊社ページ】や【コーポレートサイト】をのぞいてみてください。

Invitation from 株式会社ライトコード
If this story triggered your interest, have a chat with the team?
株式会社ライトコード's job postings

Weekly ranking

Show other rankings
Like Hiroyuki Choshi's Story
Let Hiroyuki Choshi's company know you're interested in their content