WebSocketを使用し、サーバ側のSocketの状態を確認してみる

#### WebSokcet、Socket通信を使用して簡易的なChatアプリを作ってみました
websocketを使用すれば、クライアントからajaxを使用し、WebAPIをコールした際よりもレスポンスが早くなる可能性を感じ、そしてネイティブアプリなどで行っているSocket通信がWebブラウザから使用可能になるものと思い調査しつつ、Chatアプリを作成しました。

`完成イメージ`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530697/5748c586-16ca-158d-dc62-ce071f5542ad.png)

こちらが今回作成したコードになります。
https://github.com/yuki-yanagawa/WebSocketChatTest


## 目次
1. [ブラウザでWebSocketオブジェクトを作成し、 HTTPプロトコルを覗いてみる](#1-ブラウザでwebsocketを作成し-httpプロトコルを覗いてみる)
2. [WebSocket通信を確立する（ハンドシェイク部分の実装）](#2-websocket通信を確立するハンドシェイク部分の実装)
3. [クライアントからのメッセージをデコードする](#3-クライアントからのメッセージをデコードする)
4. [受け取ったメッセージを送信する](#4-受け取ったメッセージを送信する)


## 1. ブラウザでwebsocketを作成し、 HTTPプロトコルを覗いてみる
今回Javaを使用し、簡易的なHTTPサーバを用意しました。
クライアントサイドでwebsocketを生成した際、どのようなHTTPリクエストを送るのか確認してみました。

##### HTTPサーバ
```java: HttpReceiver.java
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を生成
```javascript: main.js
var websocket = new Websocket("ws://127.0.0.1:9998");
```


##### WebSocketオブジェクトを作成、HTTPリクエストした内容(HTTPリクエストヘッダの情報を確認)
```java: HTTPClientHandle.java
@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);
		}
        ......
````

↓console上に出力された内容
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530697/76aa30e5-5a4e-ffd0-aaaf-7ddcfd74d899.png)

[Java で WebSocket サーバーを記述する](https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_a_WebSocket_server_in_Java)
こちらに記載されている内容より、WebSocket通信を確立するにはハンドシェイクが必要で
HTTPヘッダに含まれている ***Upgrade***、***Sec-WebSocket-Key***を用いてハンドシェイクを実現する模様。

## 2. WebSocket通信を確立する（ハンドシェイク部分の実装）
こちらで上記で確認した内容からハンドシェイク部分を実装し、WebSocket通信を確立してみようと思います。

以下のような処理を実装しました。
① リクエストヘッダをヘッダを確認し、***Upgrade***ヘッダが存在し、内容が「WebSocket」である場合、WebSocketを用いた通信のリクエストと判断
② ①で「WebSocket」の通信要求と判断した場合、***Sec-WebSocket-Key***ヘッダからkeyを取得し、
レスポンスヘッダの作成にうつる。

以下のようにレスポンスヘッダを作成しました。
```java: WebSocketHandle
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を作成し、無事コネクションが確立したことを確認しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530697/a76a4774-2fe6-80fd-5b4a-91fdd4d7155e.png)


## 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の仕様について](https://create-it-myself.com/research/study-websoket-spec/)

実際に受信した内容をデコードしてみようと思います。
```java: WebSocketHandle.java
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. 受け取ったメッセージを送信する
最後に受け取ったメッセージを返信してみようと思います。マスクは設定せず、受信し、デコードしたメッセージをそのままリターンしようと思います。

```java: WebSocketHandle.java
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章の③と同じ形式)
③ メッセージを設定する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530697/5516c44d-f794-270d-2684-40b1cb4bcc53.png)

データ取得したこと確認できました！！

## 今後
ローカルエリアネットワーク内でWebSocketを用いて音声通話、videoチャットを作成していければと思っています。。



## 参考
[Java で WebSocket サーバーを記述する](https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_a_WebSocket_server_in_Java)
[WebSocket についてまとめてみる](https://qiita.com/att55/items/da663f6e713c3bd073e8)
[WebSocketの仕様について](https://create-it-myself.com/research/study-websoket-spec/)