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つの部分に分かれています握手 データ転送 握手する
クライアントが要求を送信
サーバ応答
握手するとき、クライアントはランダムなSec-WebSocket-Keyを送信し、サービス側はこのkeyに基づいていくつかの処理を行い、Sec-WebSocket-Acceptの値をクライアントに返し、具体的な原理は後述する.
データ転送
これはWebsocketのデータ伝送プロトコルで、チャット情報は一般的にこのプロトコルの規則に従って伝送され、下図の全体は1つのデータフレームと呼ばれ、データフレームのフレーム形成と解析はこのプロトコルを処理する際に最も面倒な一部である.具体的にはこの表はどう見ても参照できる
プロトコルのパッケージングと転送
1.握手プロトコルのパッケージングと転送
Handshakeクラスはリクエストヘッダに基づいて
このリクエストヘッダのフィールド順序が勝手なので、mapで格納し、メッセージを送信するときにSocketの出力ストリームに書き込むことができます.
次はHandshakedataクラスで、文章が読みやすいようにコードを簡略化しています
握手リクエストヘッダを初期化し、コードが分かりやすいようにJava-Websocketのコードを少し修正しました.
データフレームを生成するには、Socketを介してメッセージを送信するため、最終的に送信されたコンテンツをSocketのOutputStreamに書き込むには、握手メッセージをbytebufferに変換する方法が必要であり、このbytebufferを介してストリームに書き込む
最後にSocketのストリームに書き込み
以上は,クライアントが握手プロトコルを送信するプロセスである.
クライアント受信サービス側応答
サービス側はクライアントの握手要求を受信した後、応答を返す必要がある.
この応答を受信すると、クライアントは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を同じ符号化して比較する.
2.データのパッケージングと転送
....続きを待つ
チャットシステムは複雑でも複雑ではありませんが、安定したシステムを実現するには、非常に多くのことを考えています.最も基本的なのはチャットプロトコルの処理です.よく見られるインスタント通信プロトコルにはXMPP、Websocketがあり、大手企業はテンセント、網易などのプロトコルを自分で定義し、彼らは自分のプロトコルを使っています.私が見たソースコードはLeancloudのインスタント通信コンポーネントで、彼らのチャットはWebsocketに基づいているので、このブログのテーマはWebsocketです.LeancloudのAndroidインスタント通信コンポーネントでは、WebsocketのパッケージはGithub上のオープンソースプロジェクト、Nathan RajlichのJava-Websocketを使用しており、これは「100%Javaが書かれた極めてシンプルな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.データのパッケージングと転送
....続きを待つ