WebSocket(Sec-WebSocket-Version:13)-実装の詳細、特にPHP

実際、このトピックを研究している間、多くのサイトが検索されましたが、実際には何も説明されていなかったか、情報が古いプロトコルに基づいていました。 これは、このHowToを作成するための一種のキックとなりました。 これは、考えられるすべての問題の詳細な分析ではなく、誰かにとって些細な理論と説明の少しですが、誰かが(私のような)困難を引き起こし、解決策を見つけるのに時間を浪費します。 私はすぐにあなたに警告しなければなりません-それはPHPでソケットサーバーを上げる方法を考慮しません、そのような情報はインターネットで大量に。 ソケットサーバーが既に存在し、WebSocketを介して通信する方法を教える必要があるという事実から進めます。

さて、これでポイントは十分です。



ちょっとした理論。


握手


Webソケットを介して接続する場合、ヘッダーはHTTPヘッダー、いわゆるハンドシェイク、または私たちの意見では「ハンドシェイク」のように交換されます。

クライアントは同様のコンテンツのヘッダーを送信します。

GET /チャットHTTP / 1.1

ホスト:server.example.com

アップグレード:websocket

接続:アップグレード

Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ ==

起源: example.com

Sec-WebSocket-Protocol:チャット、スーパーチャット

Sec-WebSocket-バージョン:13

サーバーが彼に答えるべきこと:

HTTP / 1.1 101スイッチングプロトコル

アップグレード:websocket

接続:アップグレード

Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK + xOo =

Sec-WebSocket-Protocol:チャット



これは文献(WebSocketプロトコルRFC 6455)に書かれています。 それは複雑に思えます:受信-答えた。 しかし、ここで最初の問題がありました。 サーバーはクライアントからヘッダーを受信して​​応答しましたが、クライアント(この場合はブラウザー)に関係なく、クライアントは応答しませんでした。 私は脳に十分なものをすべて試しましたが、何も助けませんでした。 ここにヒントが見つかりまし 。 私のエラーの意味は、ブラウザーがヘッダーを空白の最後の行で受け入れ、それを送信しなかったので(ドキュメントでその言葉を見つけられなかったため)、ブラウザーはヘッダーを待機し続け、「Webソケットが接続されました(WebSocket.onopen) )”ブラウザでは発生しませんでした。 最終的に、私の答えは次のとおりでした。

$answer = "HTTP/1.1 101 Switching Protocols\r\n" ."Upgrade: websocket\r\n" ."Connection: Upgrade\r\n" ."Sec-WebSocket-Accept: ".$hash."\r\n" ."Sec-WebSocket-Protocol: chat\r\n\r\n"
      
      



そして、クライアントはついに彼を見ました。



サーバーヘッダー


そして、サーバーの応答に含まれるものにスムーズに移行します。

最初の行: 「HTTP / 1.1 101スイッチングプロトコル」 。 ここで何も変更する必要はありません。 101以外のステータスコードは、「ハンドシェイク」が完了していないことを意味します。



アップグレード接続の行で、 「websocket」「Upgrade」を適宜入力しない場合、クライアントは切断する必要があります。 つまり、私たちもまたそのままになります。 たとえば、gelisは「接続:キープアライブ、アップグレード」という見出しで送られましたが、おそらく彼は同じように答えられるかもしれませんが、これまでのところ、私はこの必要性を見つけていません。



次は、おそらく、 Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK + xOo = "のようにする必要がある唯一の行です。 この行は、サーバーが接続を受け入れていることを通知し、クライアントがSec-WebSocket-Keyに送信したキーから計算されたハッシュを特別な方法で報告します。

必要なハッシュを計算するには:

  1. クライアントキーと事前定義されたGUIDの連結。 ドキュメントによると、GUIDは「258EAFA5-E914-47DA-95CA-C5AB0DC85B11」という行です。 すでにクライアントキーを抽出し、変数$キーに格納していると仮定します(変数に何らかの理由で入った場合、先頭と末尾のスペースを削除することを忘れないでください)
     $hash = $key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
          
          



  2. 受信した文字列からsha1を計算し、結果は20文字のバイナリ文字列の形式である必要があります。
     $hash = sha1($hash,true);
          
          



  3. そして最後-base64メソッドを使用したハッシュエンコーディング
     $hash = base64_encode($hash);
          
          





クライアントに送信する次のサーバーヘッダー行に移りましょう。 「Sec-WebSocket-Protocol:chat」はオプションのパラメーターであり、サーバーが通信するサブプロトコルをクライアントに通知します。 このサブプロトコルはクライアントによってサポートされる必要があり、同じパラメーターでクライアントから送信される必要がありますが、接続時にfireとchromeはヘッダーでそのようなパラメーターを送信しませんでした。



ドキュメントで出会った別のおいしい瞬間があります。 サーバーは、サポートするプロトコルのバージョンをクライアントに伝えることができます。

たとえば、クライアントは次を送信します。
 GET /chat HTTP/1.1 ... Sec-WebSocket-Version: 25
      
      





サーバーはそれに応答します。
 HTTP/1.1 400 Bad Request ... Sec-WebSocket-Version: 13, 8, 7
      
      



それは可能です。
 HTTP/1.1 400 Bad Request ... Sec-WebSocket-Version: 13 Sec-WebSocket-Version: 8, 7
      
      





その後、クライアントはハンドシェイクを繰り返しますが、サーバーが通知したプロトコルバージョンを使用します。
 GET /chat HTTP/1.1 ... Sec-WebSocket-Version: 13
      
      





顧客ヘッダー


OriginおよびHostパラメーターを除き、クライアントのタイトルについては何も言うことはありません。

ホストには、Webソケットが接続されているサーバーのアドレスとポートが含まれています。

Origin-通常はブラウザで使用されるオプションのフィールド。 サーバーに接続するためにJavaScriptが起動されたWebサーバーの名前が含まれています(IMHO、チェックしませんでした)。



パケット交換


ここで、もちろん、彼らは非常にスマートです。 ドキュメントでは、フレームは次のようになります。

  0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
      
      





私はすぐに怠inessがどういうわけか理解し始めたのを見ました...しかし、そうでした。 ちなみに、これを詳細に理解したい人のために、 このページにはわかりやすいロシア語の説明がありますが 、もちろん、元のドキュメントで完全に理解する方が良いです。 フレームをデコードおよびエンコードするために、 ここで既製の関数hybi10Decode()およびhybi10Encode()を見つけまし 。これらは適切に機能していることを示しています。 ハンドシェイク()関数は、クライアントヘッダーパラメータを取得する方法も説明します。

また、ハンドシェイク後、クライアントはマスクされたフレームのみをサーバーに送信し、サーバーはマスクされていないフレームのみをクライアントに送信します。つまり、ビットMASK = 0です。



その過程で、私は別の問題にぶつかりました。「hello」クライアントに握手し、サーバーの応答をした後、chromeは以下を生成しました。

「ws://example.com:10001 / test」へのWebSocket接続に失敗しました:サーバーは、クライアントに送信するフレームをマスクしてはなりません。
つまり、ブラウザが次のメッセージがマスクされていることを確認しましたが、サーバーがマスクされていないフレームを送信したことは確かでした。 カブをひっかいてから、WebソケットとFlash用ActionScriptソケットを介した交換プロトコルの互換性の問題であることが判明しました。 私のコードでは、フラッシュの必要に応じて各メッセージの最後にゼロバイト「\ 0」が挿入され、したがってこのバイトは各フレームまたはヘッダーの最後に挿入されました。ブラウザはフレームの正確な長さまたは終了位置を知っているため、すでに次の開始点として読み取りましたヘッダー。 したがって、次のヘッダーの最初のバイトは「\ 0」であり、実際の最初のバイトは2番目にシフトされ、ブラウザーがbrowserしていました。



今のところすべてです。 結論として、HTML5全般のようなWebソケットは、ブラウザがFlash松葉杖なしでこれまで手に入れられなかったことを独立して実行できる素晴らしいツールであると言いたいと思います。



更新する

コメントでは、仕様に従って、すべてのメッセージがutf-8エンコードで送信されることに注意されました。 これも重要なポイントですが、言及するのを忘れました。



更新17-05-13

もう1つの問題に直面しました:ブラウザーは2つのフレームを連続して送信できます。上記のhybi10Decode()関数は、フレームで送信されたペイロードの長さではなく、フレーム全体の終わりまで行を読み取るため、1つのフレームとして処理します。 いくつかの変更後、関数は次のようになります。

クリックして
 function decode($data){ $payloadLength = ''; $mask = ''; $unmaskedPayload = ''; $decodedData = array(); // estimate frame type: $firstByteBinary = sprintf('%08b', ord($data[0])); $secondByteBinary = sprintf('%08b', ord($data[1])); $opcode = bindec(substr($firstByteBinary, 4, 4)); $isMasked = ($secondByteBinary[0] == '1') ? true : false; $payloadLength = ord($data[1]) & 127; if($isMasked === false) $this->close(1002);// close connection if unmasked frame is received switch($opcode) { case 1: $decodedData['type'] = 'text'; break;// text frame case 8: $decodedData['type'] = 'close'; break;// connection close frame case 9: $decodedData['type'] = 'ping'; break;// ping frame case 10: $decodedData['type'] = 'pong'; break;// pong frame default: $this->close(1003); break;// Close connection on unknown opcode } if($payloadLength === 126) { $mask = substr($data, 4, 4); $payloadOffset = 8; $dataLength = sprintf('%016b', ord($data[2]).ord($data[3])); $dataLength = base_convert($dataLength,2,10); } elseif($payloadLength === 127) { $mask = substr($data, 10, 4); $payloadOffset = 14; $dataLength = ''; for ($i=2;$i<8;$i++) $dataLength .=sprintf('%08b',ord($data[$i])); $dataLength = base_convert($dataLength,2,10); } else{ $mask = substr($data, 2, 4); $payloadOffset = 6; $dataLength = base_convert(sprintf('%08b',ord($data[1]) & 63),2,10); } if($isMasked === true) { for($i = $payloadOffset; $i < $dataLength+$payloadOffset; $i++){ $j = $i - $payloadOffset; $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; } $decodedData['payload'] = $unmaskedPayload; } else{ $payloadOffset = $payloadOffset - 4; $decodedData['payload'] = substr($data, $payloadOffset); } $decodedData['offset'] = $payloadOffset; return $decodedData; } //       ($frame -      ) $recieved = 0; while(strlen($frame)> 0) { $msg = decode($frame); $recieved += strlen($msg['payload'])+ $msg['offset']; $frame = substr($frame,$recieved); }
      
      



この関数は断片化されたフレームをサポートしないことに注意してください。



参照資料

RFC6455

Javascript Websocket

プロトコルのバージョン13で動作するGitHubプロジェクト



All Articles