イントロダクション

WebSocket API を使用したい場合は、サーバーを持っている場合に便利です。この記事では、C# で記述する方法を説明します。どんなサーバーサイドの言語でも行うことができますが、わかりやすく理解しやすいように、Microsoft の言語を選択しました。

このサーバーは RFC 6455 に準拠しているため、Chrome バージョン16、Firefox 11、IE 10 以上の接続のみを処理します。

ファーストステップ

WebSocket は TCP (伝送制御プロトコル) 接続を介して通信します。幸いにも、C# には TcpListener クラスがあり、その名前が示すようにします。これは System.Net.Sockets 名前空間にあります。

少なく書くためには名前空間を using キーワードに含めることをお勧めします。毎回完全な名前空間を入力することなく、名前空間のクラスを使用できます。

TcpListener

コンストラクタ:

TcpListener(System.Net.IPAddress localaddr, int port)

localaddr はリスナーの IP を指定し、port はポートを指定します。

string からIPAddress オブジェクトを作成するには、IPAddress の静的 Parse メソッドを使用します。

メソッド:

  • Start()
  • System.Net.Sockets.TcpClient AcceptTcpClient()
    Tcp 接続を待ち、それを受け取り、TcpClient オブジェクトとして返します。

ベアボーンサーバーの実装は次のとおりです。

​using System.Net.Sockets;
using System.Net;
using System;

class Server {
    public static void Main() {
        TcpListener server = new TcpListener(IPAddress.Parse("127.0.0.1"), 80);

        server.Start();
        Console.WriteLine("Server has started on 127.0.0.1:80.{0}Waiting for a connection...", Environment.NewLine);

        TcpClient client = server.AcceptTcpClient();

        Console.WriteLine("A client connected.");
    }
}

TcpClient

メソッド:

  • System.Net.Sockets.NetworkStream GetStream()
    通信チャネルであるストリームを取得します。チャンネルの両側には読み書き機能があります。

プロパティ:

  • int Available
    このプロパティは送信されたデータのバイト数を示します。値は NetworkStream.DataAvailable が true になるまでゼロです。

NetworkStream

メソッド:

  • Write(Byte[] buffer, int offset, int size)
    buffer からバイトを書き込み、offset と size はメッセージの長さを決定します。
  • Read(Byte[] buffer, int offset, int size)
    buffer を読み込みます。offset と size はメッセージの長さを決定します。

私たちの例を拡張してみましょう。

TcpClient client = server.AcceptTcpClient();

Console.WriteLine("A client connected.");

NetworkStream stream = client.GetStream();

//enter to an infinite cycle to be able to handle every change in stream
while (true) {
    while (!stream.DataAvailable);

    Byte[] bytes = new Byte[client.Available];

    stream.Read(bytes, 0, bytes.Length);
}

ハンドシェイキング

クライアントがサーバーに接続すると、単純な HTTP リクエストから WebSocket への接続をアップグレードするための GET リクエストが送信されます。これはハンドシェイキングと呼ばれます。

このサンプルコードはクライアントから GET を検出できます。これは、メッセージの最初の 3 バイトが利用可能になるまでブロックされることに注意してください。運用環境では、代替ソリューションを検討する必要があります。

using System.Text;
using System.Text.RegularExpressions;

while(client.Available < 3)
{
   // wait for enough bytes to be available
}

Byte[] bytes = new Byte[client.Available];

stream.Read(bytes, 0, bytes.Length);

//translate bytes of request to string
String data = Encoding.UTF8.GetString(bytes);

if (Regex.IsMatch(data, "^GET")) {

} else {

}

リクエストは簡単に作成できますが、理解するのは少し難しいかもしれません。サーバーのハンドシェイクの完全な説明は RFC 6455、セクション4.2.2 にあります。私たちの目的のために、簡単なレスポンスを作成します。

やらなければならないのは:

  1. 先行または後続空白なしで "Sec-WebSocket-Key" リクエストヘッダーの値を取得します
  2. それを "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (RFC 6455 で指定された特別な GUID)
  3. 新しい値の SHA-1 および Base64 ハッシュを計算します
  4. HTTP レスポンスの "Sec-WebSocket-Accept" レスポンスヘッダの値としてハッシュを書き戻します

if (new System.Text.RegularExpressions.Regex("^GET").IsMatch(data))
{
    const string eol = "\r\n"; // HTTP/1.1 defines the sequence CR LF as the end-of-line marker

    Byte[] response = Encoding.UTF8.GetBytes("HTTP/1.1 101 Switching Protocols" + eol
        + "Connection: Upgrade" + eol
        + "Upgrade: websocket" + eol
        + "Sec-WebSocket-Accept: " + Convert.ToBase64String(
            System.Security.Cryptography.SHA1.Create().ComputeHash(
                Encoding.UTF8.GetBytes(
                    new System.Text.RegularExpressions.Regex("Sec-WebSocket-Key: (.*)").Match(data).Groups[1].Value.Trim() + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
                )
            )
        ) + eol
        + eol);

    stream.Write(response, 0, response.Length);
}

メッセージのデコード

ハンドシェイクが成功すると、クライアントはエンコードされたメッセージをサーバーに送信します。

"MDN" を送信すると、次のバイトが得られます。

129 131 61 84 35 6 112 16 109

これらのバイトの意味を見てみましょう。

現在 129 の値を持つ最初のバイトは、次のように分解するビットフィールドです。

FIN (Bit 0) RSV1 (Bit 1) RSV2 (Bit 2) RSV3 (Bit 3) Opcode (Bit 4:7)
1 0 0 0 0x1=0001
  • FIN ビット: このビットは完全なメッセージがクライアントから送信されたかどうかを示します。メッセージはフレームで送信されるかもしれませんが、今のところ単純なものにします。
  • RSV1, RSV2, RSV3: エクステンションがネゴシエートされない限り、これらのビットは 0 でなくてはなりません。
  • Opcode: これらのビットは受信したメッセージのタイプを記述します。Opcode 0x1 は、これがテキストメッセージであることを意味します。 Opcode の完全なリスト

現在 131 の値を持つ 2 番目のバイトは、次のように分解する別のビットフィールドです。

MASK (Bit 0) ペイロードの長さ (Bit 1:7)
1 0x83=0000011
  • MASK ビット: "ペイロードデータ" がマスクされているかどうかを定義します。1 に設定すると、マスキングキーが Masking-Key にあり、これは "ペイロードデータ" のマスクを解除するために使用されます。クライアントからサーバーへのすべてのメッセージはこのビットが設定されています。
  • ペイロードの長さ: この値が 0〜125 の場合、メッセージの長さになります。126 の場合、次の 2 バイト (16ビットの符号なし整数) が長さになります。127 の場合、次の 8 バイト (64ビットの符号なし整数) が長さになります。

最初のビットはクライアントからサーバーへのメッセージでは常に 1 なので、このバイトから 128 を引いて MASK ビットを取り除くことができます。

メッセージに MASK ビットが設定されていることに注意してください。これは次の4バイト (61、84、35、および6) がメッセージのデコードに使用されるマスクバイトであることを意味します。これらのバイトはすべてのメッセージとともに変化します。

残りのバイトはエンコードされたメッセージペイロードです。

アルゴリズムのデコード

Di = Ei XOR M(i mod 4)

D は復号されたメッセージ配列、E は符号化されたメッセージ配列、M はマスクバイト配列、i はデコードするメッセージバイトのインデックスです。

C# の例です:

Byte[] decoded = new Byte[3];
Byte[] encoded = new Byte[3] {112, 16, 109};
Byte[] mask = new Byte[4] {61, 84, 35, 6};

for (int i = 0; i < encoded.Length; i++) {
    decoded[i] = (Byte)(encoded[i] ^ mask[i % 4]);
}

関連

 

ドキュメントのタグと貢献者

このページの貢献者: silverskyvicto
最終更新者: silverskyvicto,