Websocketプロトコルの原理と実現(一)

7629 ワード

最近暇で、チャットシステムの構築を少し研究して、その実現原理を深く理解して、ついでに文章に整理しました.私は主にAndroidを書いているので、具体的な分析はモバイル側のチャットシステムの構築を主とします.
チャットシステムは複雑でも複雑ではありませんが、安定したシステムを実現するには、非常に多くのことを考えています.最も基本的なのはチャットプロトコルの処理です.よく見られるインスタント通信プロトコルにはXMPP、Websocketがあり、大手企業はテンセント、網易などのプロトコルを自分で定義し、彼らは自分のプロトコルを使っています.私が見たソースコードはLeancloudのインスタント通信コンポーネントで、彼らのチャットはWebsocketに基づいているので、このブログのテーマはWebsocketです.LeancloudのAndroidインスタント通信コンポーネントでは、WebsocketのパッケージはGithub上のオープンソースプロジェクト、Nathan RajlichのJava-Websocketを使用しており、これは「100%Javaが書かれた極めてシンプルなWebsocketクライアントとサービス側実装」です.
文章の大まかな枠組み
  • Websocketプロトコルの簡単な紹介
  • プロトコルのパッケージングと伝送
  • Websocketクライアントの実装
  • Websocketプロトコルの簡単な紹介
    Websocketは、単一のTCP接続上でフルデュプレクス通信を行うプロトコルであり、デュプレクス(duplex)は、2台の通信装置間で双方向の資料伝送を可能にすることを意味する.フルデュプレクスとは、2台のデバイス間で同時に双方向の資料伝送を許可することを意味する.これは半二重に対して、半二重は同時に双方向に伝送することができず、この間の違いは携帯電話とインターホンの違いに相当し、携帯電話は話をすると同時に相手の話を聞くことができ、インターホンは一人でもう一人しか話せない.
    簡単に言えば、Websocketプロトコルでは、クライアントとサービス側が握手をするだけでチャネルを形成し、両者の間でデータを相互に転送することができます.
    WebSocketプロトコルは2つの部分に分かれています
  • 握手
  • データ転送
  • 握手する
    クライアントが要求を送信
    GET / HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Host: example.com
    Origin: null
    Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
    Sec-WebSocket-Version: 13
    

    サーバ応答
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
    Sec-WebSocket-Origin: null
    Sec-WebSocket-Location: ws://example.com/
    

    握手するとき、クライアントはランダムなSec-WebSocket-Keyを送信し、サービス側はこのkeyに基づいていくつかの処理を行い、Sec-WebSocket-Acceptの値をクライアントに返し、具体的な原理は後述する.
    データ転送
    これはWebsocketのデータ伝送プロトコルで、チャット情報は一般的にこのプロトコルの規則に従って伝送され、下図の全体は1つのデータフレームと呼ばれ、データフレームのフレーム形成と解析はこのプロトコルを処理する際に最も面倒な一部である.具体的にはこの表はどう見ても参照できる
    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 ...                |
    +---------------------------------------------------------------+
    
        bit   
    FIN      1bit          
    RSV 1-3  1bit each            0
    Opcode   4bit    ,    
    Mask     1bit   ,      ,      1 (     )
    Payload  7bit      
    Masking-key      1 or 4 bit   
    Payload data     (x + y) bytes   
    Extension data   x bytes      
    Application data y bytes      
    

    プロトコルのパッケージングと転送
    1.握手プロトコルのパッケージングと転送
    Handshakeクラスはリクエストヘッダに基づいて
    GET / HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Host: example.com
    Origin: null
    Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
    Sec-WebSocket-Version: 13
    

    このリクエストヘッダのフィールド順序が勝手なので、mapで格納し、メッセージを送信するときにSocketの出力ストリームに書き込むことができます.
    次はHandshakedataクラスで、文章が読みやすいようにコードを簡略化しています
    public class Handshakedata
    {
        private byte[] content;                 //   ,          
        private TreeMap map;  //       
    
    }
    

    握手リクエストヘッダを初期化し、コードが分かりやすいようにJava-Websocketのコードを少し修正しました.
    public Handshakedata postProcessHandshakeRequestAsClient(Handshakedata request)
    {
        request.put("Upgrade", "websocket");
        request.put("Connection", "Upgrade");
        request.put("Sec-WebSocket-Version", "8");    
    
        byte[] random = new byte[16];
        this.reuseableRandom.nextBytes(random);     //       Sec-WebSocket-Key
        request.put("Sec-WebSocket-Key", Base64.encodeBytes(random));
    
        return request;
    }
    

    データフレームを生成するには、Socketを介してメッセージを送信するため、最終的に送信されたコンテンツをSocketのOutputStreamに書き込むには、握手メッセージをbytebufferに変換する方法が必要であり、このbytebufferを介してストリームに書き込む
    public ByteBuffer createHandshake(Handshakedata handshakedata) {
        StringBuilder bui = new StringBuilder(100);
        bui.append("GET ");
        bui.append(handshakedata.getResourceDescriptor());
        bui.append(" HTTP/1.1");
        bui.append("\r
    "); Iterator it = handshakedata.iterateHttpFields(); while (it.hasNext()) { String fieldname = (String)it.next(); String fieldvalue = handshakedata.getFieldValue(fieldname); bui.append(fieldname); bui.append(": "); bui.append(fieldvalue); bui.append("\r
    "); } bui.append("\r
    "); byte[] httpheader = Charsetfunctions.asciiBytes(bui.toString()); byte[] content = withcontent ? handshakedata.getContent() : null; ByteBuffer bytebuffer = ByteBuffer.allocate((content == null ? 0 : content.length) + httpheader.length); bytebuffer.put(httpheader); bytebuffer.flip(); return bytebuffer; }

    最後にSocketのストリームに書き込み
    ByteBuffer buffer = (ByteBuffer)WebSocketClient.this.engine.outQueue.take();   //              bytebuffer
    WebSocketClient.this.ostream.write(buffer.array(), 0, buffer.limit());    //this.ostream = this.socket.getOutputStream() Socket    
    WebSocketClient.this.ostream.flush();   //  ,    
    

    以上は,クライアントが握手プロトコルを送信するプロセスである.
    クライアント受信サービス側応答
    サービス側はクライアントの握手要求を受信した後、応答を返す必要がある.
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
    Sec-WebSocket-Origin: null
    Sec-WebSocket-Location: ws://example.com/
    

    この応答を受信すると、クライアントはSec-WebSocket-Accept値を比較する必要があります.この値は、サーバが握手して接続を確立することに同意したことを示しています.クライアントから送信されたSec-WebSocket-Keyが「258 EAFA 5-E 914-47 DA-95 CA-C 5 AB 0 DC 85 B 11」と接続した後、SHA-1で暗号化し、BASE-64符号化を行います.
    クライアントは、Sec-WebSocket-Acceptを受信した後、ローカルのSec-WebSocket-Keyを同じ符号化して比較する.
    public Draft.HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response)
    throws InvalidHandshakeException
      {
        if ((!request.hasFieldValue("Sec-WebSocket-Key")) || (!response.hasFieldValue("Sec-WebSocket-Accept"))) {
          return Draft.HandshakeState.NOT_MATCHED;
        }
    
    //Sec-WebSocket-Key Sec-WebSocket-Accept    
        String seckey_answere = response.getFieldValue("Sec-WebSocket-Accept");
        String seckey_challenge = request.getFieldValue("Sec-WebSocket-Key");
        seckey_challenge = generateFinalKey(seckey_challenge);
    
        if (seckey_challenge.equals(seckey_answere))
          return Draft.HandshakeState.MATCHED;
        return Draft.HandshakeState.NOT_MATCHED;
      }
    
     //  Sec-WebSocket-Accept   
    private String generateFinalKey(String in) { 
        String seckey = in.trim();
        String acc = seckey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
        MessageDigest sh1;
        try {
          sh1 = MessageDigest.getInstance("SHA1");
        } catch (NoSuchAlgorithmException e) {
          throw new RuntimeException(e);
        }
        return Base64.encodeBytes(sh1.digest(acc.getBytes()));
      }
    

    2.データのパッケージングと転送
    ....続きを待つ