0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-08-15

WebSokcet、Socket通信を使用して簡易的なChatアプリを作ってみました

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

完成イメージ
image.png

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

目次

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

1. ブラウザでwebsocketを作成し、 HTTPプロトコルを覗いてみる

今回Javaを使用し、簡易的なHTTPサーバを用意しました。
クライアントサイドでwebsocketを生成した際、どのようなHTTPリクエストを送るのか確認してみました。

HTTPサーバ
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を生成
main.js
var websocket = new Websocket("ws://127.0.0.1:9998");
WebSocketオブジェクトを作成、HTTPリクエストした内容(HTTPリクエストヘッダの情報を確認)
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

Java で WebSocket サーバーを記述する
こちらに記載されている内容より、WebSocket通信を確立するにはハンドシェイクが必要で
HTTPヘッダに含まれている UpgradeSec-WebSocket-Keyを用いてハンドシェイクを実現する模様。

2. WebSocket通信を確立する(ハンドシェイク部分の実装)

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

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

以下のようにレスポンスヘッダを作成しました。

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

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の仕様について

実際に受信した内容をデコードしてみようと思います。

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. 受け取ったメッセージを送信する

最後に受け取ったメッセージを返信してみようと思います。マスクは設定せず、受信し、デコードしたメッセージをそのままリターンしようと思います。

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

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

今後

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

参考

Java で WebSocket サーバーを記述する
WebSocket についてまとめてみる
WebSocketの仕様について

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0