.NETのWebSocketと、切断の扱い

リアルタイムな通信を行う際に何かと活躍するプロトコル WebSocket。 .NETには System.Net.WebSockets として組み込まれており、簡単に使い始めることができる。

しかし、WebSocketにおいて「切断」という概念は取り扱いに注意が必要だ。 本記事ではSystem.Net.WebSocketsが切断をどのように扱っているか見ていく。

WebSocketにおける正しい切断

WebSocket は Socket という名前からも想像できるように、基となるのはTCP Socketだ。 よってTCP Socketを閉じれば切断できるが、これは正しい切断ではない。 RFC 6455 - The WebSocket Protocol の "Section 1.4 Closing Handshake" によると切断時にもHandshake処理があり、流れは次のようになる。

  • 切断したい側から Close Frame を送信する
  • 受け取った側は Close Frame を応答として返す
  • Close Frame の送信・受信がどちらも終わったらTCP socketを閉じる

System.Net.WebSocketsにおける切断処理

では、.NETでのClosing Handshakeの実装はどうなっているか。 関わるメソッドがいくつかあるので表にまとめた。

メソッド 処理内容
WebSocket.CloseAsync こちら側からClose Frameを送り、相手からClose Frameの応答が返ってくるのを待つ。その後TCPを切断する。
WebSocket.CloseOutputAsync 相手からClose Frameの応答が返ってくるのを待つ。その後TCPを切断する。
WebSocket.Dispose TCPを切断する。

一見似た名前のメソッドが3つもあるが、それぞれの処理内容を把握すれば使い所がわかる。 Active-close(こちら側からの能動的な切断)であればCloseAsyncが正常に終われば正しい切断となる。

// Close Frameの送信を行い、Close Frame応答の受信を待機する。
await webSocket.CloseAsync();

そして、Passive-close(Close Frameを受け取った側)はメッセージ受信処理に次のような対応をすればよい。

var result = webSocket.ReceiveAsync(buffer, ct);
switch (result.MessageType)
{
    // ...
    case WebSocketMessageType.Close:
        // Close Frameの応答を返す
        await client.CloseOutputAsync(result.CloseStatus!.Value, result.CloseStatusDescription, ct);
        break;
}

最後に残ったのはDisposeだが、これはCloseAsyncまたはCloseOutputAsyncの中で呼ばれるため基本は呼ぶ必要がない。 Closing Handshakeをすっ飛ばして強制的にTCP Closeをしたい場合は使えるが…そんな機会はあまりないだろう。

ReceiveとCloseの並列性

WebSocketはリアルタイム性のある通信処理に利用されるため、Receive (ReceiveAsync)を継続的に呼び出し相手からの受信メッセージを処理することが多い。 その場合、Receiveを待機しつつ他のスレッドからCloseを呼ぶ、または相手からCloseが飛んでくるケースが考えられる。

CloseAsyncまたはCloseOutputAsync内では「相手からのClose Frameの受信を待機する」処理があるため、Receive と並列で呼んで大丈夫なのか?と気になるところだ。

しかし、結論から言うとこれは問題ない。ManagedWebSocketのxmldocに次のように書かれている。

  • It's acceptable to have a pending ReceiveAsync while CloseOutputAsync or CloseAsync is called.

実際にReceiveしながらCloseAsyncを呼んだところ、CloseAsyncは正常に終わり、ReceiveAsyncでは Close Frame を受け取ることができた。

Close完了後の例外

Closing Handshakeが完了したあとにSend/Receiveをはじめとする通信メソッドを呼ぶとWebSocketException例外が投げられる。 エラーコードは WebSocketError.InvalidState となる。

不正な切断

正常なClosing Handshakeを経ずにTCP接続が突然切断された場合は、Receiveメソッドの中でWebSocketException例外が投げられ エラーコードは WebSocketError.ConnectionClosedPrematurely となる。