WebSokcet、Socket通信を使用して簡易的なChatアプリを作ってみました
websocketを使用すれば、クライアントからajaxを使用し、WebAPIをコールした際よりもレスポンスが早くなる可能性を感じ、そしてネイティブアプリなどで行っているSocket通信がWebブラウザから使用可能になるものと思い調査しつつ、Chatアプリを作成しました。
こちらが今回作成したコードになります。
https://github.com/yuki-yanagawa/WebSocketChatTest
目次
- ブラウザでWebSocketオブジェクトを作成し、 HTTPプロトコルを覗いてみる
- WebSocket通信を確立する(ハンドシェイク部分の実装)
- クライアントからのメッセージをデコードする
- 受け取ったメッセージを送信する
1. ブラウザでwebsocketを作成し、 HTTPプロトコルを覗いてみる
今回Javaを使用し、簡易的なHTTPサーバを用意しました。
クライアントサイドでwebsocketを生成した際、どのようなHTTPリクエストを送るのか確認してみました。
HTTPサーバ
ExecutorService executorService = null;
try(ServerSocket svrsock = new ServerSocket()) {
svrsock.bind(new InetSocketAddress("127.0.0.1", 9998));
executorService = Executors.newFixedThreadPool(10);
while(true) {
Socket socket = svrsock.accept();
executorService.submit(new HTTPClientHandle(socket));
}
} catch(IOException e) {
.......
websocketを生成
var websocket = new Websocket("ws://127.0.0.1:9998");
WebSocketオブジェクトを作成、HTTPリクエストした内容(HTTPリクエストヘッダの情報を確認)
@Override
public void run() {
try(InputStream is = socket_.getInputStream();
OutputStream os = socket_.getOutputStream()) {
byte[] buf = new byte[1024];
int readSize = is.read(buf);
String requestHeader = new String(buf,"UTF-8");
String[] requestHeaderLine = requestHeader.split("\r\n");
for(String r : requestHeaderLine) {
System.out.println(r);
}
......
Java で WebSocket サーバーを記述する
こちらに記載されている内容より、WebSocket通信を確立するにはハンドシェイクが必要で
HTTPヘッダに含まれている Upgrade、Sec-WebSocket-Keyを用いてハンドシェイクを実現する模様。
2. WebSocket通信を確立する(ハンドシェイク部分の実装)
こちらで上記で確認した内容からハンドシェイク部分を実装し、WebSocket通信を確立してみようと思います。
以下のような処理を実装しました。
① リクエストヘッダをヘッダを確認し、Upgradeヘッダが存在し、内容が「WebSocket」である場合、WebSocketを用いた通信のリクエストと判断
② ①で「WebSocket」の通信要求と判断した場合、Sec-WebSocket-Keyヘッダからkeyを取得し、
レスポンスヘッダの作成にうつる。
以下のようにレスポンスヘッダを作成しました。
String connectionSetting = "HTTP/1.1 101 Switching Protocols\r\n";
connectionSetting += "Connection: Upgrade\r\n";
connectionSetting += "Upgrade: websocket\r\n";
connectionSetting += "Sec-WebSocket-Accept: ";
connectionSetting += Base64.getEncoder()
.encodeToString(MessageDigest.getInstance("SHA-1")
.digest((this.updateKey_ + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
.getBytes("UTF-8"))) + "\r\n\r\n";
os.write(connectionSetting.getBytes());// os is OutputStream
・HTTP/1.1 101 Switching Protocols
・Connection: Upgrade
・Upgrade: websocket
上記は定型のもので、
・Sec-WebSocket-Acceptヘッダに関しては、取得したkeyにより可変のものとなる。
取得したkeyに 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 (マジック文字列)を連結し、SHA-1ハッシュ値を算出し、Base64でエンコードする。
▼クライアントからWebSocketを作成し、無事コネクションが確立したことを確認しました。
3. クライアントからのメッセージをデコードする
やったー。繋がったーと思ったのも束の間、どうやらSocket通信のようにメッセージのやりとりを平文的な感覚で行えるのかと思ったら、(クライアントから)メッセージを受信したらデーコードする必要があるようでした。。
下記のような形でメッセージが送信されてくるようです。
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 ... |
+---------------------------------------------------------------+
中々これだけだとイメージがつかめず、こちらのサイトで詳細を確認させて頂きました。
WebSocketの仕様について
実際に受信した内容をデコードしてみようと思います。
private String messageDecoder(DataInputStream ds) throws IOException {
byte first = ds.readByte();
byte second = ds.readByte();
byte opCode = (byte) (first & 0x0F);
// Check the opCode value
if (opCode == 1) {
System.out.println("Start Decode");
} else if (opCode == 8) {
return "";
}
// Mask Check
boolean masked = ((second & 0x80) != 0);
if (!masked) {
return "";
}
// Data Length Check
long length = second & 0x7F;
if (length == 126) {
length = ds.readUnsignedShort();
} else if (length == 127) {
length = ds.readLong();
}
System.out.println("Message Length: " + length);
// Read mask
byte[] mask = new byte[4];
ds.read(mask);
// decoder
byte[] encodedCharArray = new byte[(int) length];
ds.read(encodedCharArray);
StringBuilder decoded = new StringBuilder();
for (int i = 0; i < encodedCharArray.length; i++) {
char decodedChar = (char) (encodedCharArray[i] ^ mask[i % 4]);
decoded.append(decodedChar);
}
System.out.println("Decode Result " + decoded.toString());
return decoded.toString();
}
① 1バイト目の情報から(4~8ビット)opcodeを確認
② 2バイト目の情報から(1ビット)maskを確認
③ 2バイト目の情報からデータ(2~8ビット)の長さを確認
・125以下のであればそれがデータの長さになる
・126であれば3バイト目から2バイト取得したデータにクライアントから送られてきたデータの長さが設定されている
・127であれば3バイト目から4バイト取得したデータにクライアントから送られてきたデータの長さが設定されている
④ 次の4バイトがデコードの為に必要な内容となる。取得した内容とエンコードされているデータを排他的論理和(XOR)でエンコードする
どうやら、これでエンコードができたことを確認しました。
4. 受け取ったメッセージを送信する
最後に受け取ったメッセージを返信してみようと思います。マスクは設定せず、受信し、デコードしたメッセージをそのままリターンしようと思います。
String decodedMess = messageDecoder(dis);
dos.writeByte(0x81);
if(decodedMess.length() < 126) {
dos.writeByte(decodedMess.length());
} else if(decodedMess.length() <= (Short.MAX_VALUE - Short.MIN_VALUE)) {
dos.writeByte(126);
dos.writeShort(decodedMess.length());
} else if (decodedMess.length() <= Long.MAX_VALUE) {
dos.writeByte(127);
dos.writeLong(decodedMess.length());
}
dos.write(decodedMess.getBytes("UTF-8"));
dos.flush();
① 先頭1バイトは0x81(0x1000 0001 = FIN bit = 1 and opCode bit = 1)
② 2バイト目はマスクなしの為、データの長さに関連する内容を設定する(3章の③と同じ形式)
③ メッセージを設定する
データ取得したこと確認できました!!
今後
ローカルエリアネットワーク内でWebSocketを用いて音声通話、videoチャットを作成していければと思っています。。
参考
Java で WebSocket サーバーを記述する
WebSocket についてまとめてみる
WebSocketの仕様について