WiresharkでWebSocket通信をパケットキャプチャしてみた(WebSocketの切断)

前々回前回で、接続時、テキストデータの送受信時のパケットを見たので、今回は切断時のパケットを見てみることにする。

ブラウザ側のJavaScriptでclose()メソッドを呼び出したときのデータが以下。
(パケットをキャプチャしたときのブラウザはGoogle Chrome 24.0.1312.52)
なお、TCPヘッダなどを取り除き、WebSocketの部分だけを抽出して16進で表示している。

88 80 50 1a 6d dc

Wiresharkで見るとこうなる。

Opcodeが接続の切断を示す "1000" になっている。そしてPayloadにデータは何もないが、クライアントからサーバに送っているのでMasking-Keyが設定されている。
(OpecodeやMasking-Keyの意味は前回の記事を参照)

そして、これに対するサーバの返信。

88 00

Wiresharkで表示したところ。

こちらもOpcodeが "1000" になっている。ただし、サーバからの送信なのでMasking-Keyはない。Masking-KeyもPayloadもないため、2バイトとものすごく短い。

ところで、これはWebSocketと全く関係ないが、WiresharkでWebScoketの切断のパケットを表示させるとこんなことも表示された。
クライアント→サーバの場合

サーバ→クライアントの場合

Malformedと言われてしまっているが、Wiresharkが期待しているパケットと違っているのかな?それともWiresharkのバグ?
よくわからないが、WebSocketの通信自体は正常にできているようなので、気にしなくていい、、、のかな??

WiresharkでWebSocket通信をパケットキャプチャしてみた(テキストデータの送受信)

前回からの続き。
前回はコネクションを接続するときを覗いたので、今回はテキストデータを送受信するときのパケットを覗いてみる。

とりあえず、まずブラウザから"test"という4バイトのテキストを送信したときのパケットキャプチャを見てみる。
TCPヘッダなどを取っ払い、WebSocketの部分だけを抜き出して16進で表示したのが以下。

81 84 cc 2a af ce b8 4f  dc ba

詳細を確認していこう。
RFC6455の「5.2. Base Framing Protocol」を見るとフレームの構造はこんな感じらしい。

これを見ながら解析するのだが、実は深く考えないでもWiresharkならわかりやすく表示してくれる。

それぞれの意味は以下を参照。

Fin
0  分割されたデータの途中(まだ続きのデータがあるということ)。
1  最後のデータ。特に分割されなければ最初のデータが最後のデータになる。

Reserved
全てのビットは"0"でないといけない。"0"以外のビットがあると受信側でエラーとなる。
(ただし、接続時にここを使う拡張を両方のエンドポイントで合意できれば使うことも可能みたい)

Opecode
0000(0x0)  継続フレーム
0001(0x1)  テキストフレーム
0010(0x2)  バイナリフレーム
0011(0x3)  非制御フレーム用に予約済み
0100(0x4)  非制御フレーム用に予約済み
0101(0x5)  非制御フレーム用に予約済み
0110(0x6)  非制御フレーム用に予約済み
0111(0x7)  非制御フレーム用に予約済み
1000(0x8)  接続の切断
1001(0x9)  ping
1010(0xA)  pong
1011(0xB)  制御フレーム用に予約済み
1100(0xC)  制御フレーム用に予約済み
1101(0xD)  制御フレーム用に予約済み
1110(0xE)  制御フレーム用に予約済み
1111(0xF)  制御フレーム用に予約済み

Mask
0  Payloadデータがマスクされていない
1  Payloadデータがマスクされている。Masking-Keyにマスクしたときのキーが設定される。クライアントからサーバへ送信するデータは全てマスクされる。

Payload length
Payloadデータが何バイトあるかを示す。上の例では 100 となっているが、16進数の100は10進数の4なので、4バイトのデータということ。
(Payloadデータが126バイト以上ある場合は、さらに2〜8バイトの領域をExtended Payload lengthとして使い、Payloadデータの長さを表現する)

Masking-Key
Payloadデータをマスクするときに使用したキー。これはクライアントがランダムに設定した32ビットの値で、予測不可能なものとしなければならない。
Payloadデータがマスクされていないときはこの値は存在しない。

Payload
送信するデータそのもの。上の例ではマスクされているため、"test"ではなく"b84fdcba"となっている。これは元々のデータ"test"とマスキングキーの"cc2aafce"とのXORで求められた値。
今回はたまたまどちらも4バイトでわかりやすいが、送信データ4バイトでないことの多いはず。マスキングデータは次のようにして求められる。

マスク前データの i 番目のオクテット("original-octet-i")
  と 
マスキングキーの「iを4で割った余りの数」番目のオクテット("masking-key-octet-j")
  のXORが
変換後のデータの i 番目のオクテット("transformed-octet-i")

式で書くとこんな感じ。

j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j

どっちもわかりにくいけど。。。

Unmask Payload
一つ上のPayloadがマスクされていてわかりにくいから、Wiresharkが気を利かせてマスクを解除したデータを表示してくれている。実際のWebSocketのフレームの中にはUnmask Payloadなんてものはないので注意。


次に、サーバが受信した"test"というテキストデータをそのままブラウザに送信したときのパケットキャプチャ。

81 04 74 65 73 74

Wiresharkで見たのがこれ。

意味は上で書いたとおり。サーバから送信するデータはマスクされていないことがわかる。

WiresharkでWebSocket通信をパケットキャプチャしてみた(WebSocketの接続)

Wiresharkはバージョン1.8.0から対応プロトコルにWebSocketが追加されていて、WebSocketの通信はWebSocketと表示される

そんなわけで、早速パケットを覗いてみる。
使った環境は「WebSocketでEchoServerをつくる(Jetty8編)」のもの。
つまり、以下。
サーバ:Jetty 8.1.8.v20121106
クライアント:Google Chrome 24.0.1312.52

まずは、WebSocketのコネクション接続時。と言っても、ここはHTTP通信で行われるので、普通にHTTPリクエストとHTTPレスポンスだったりする。

HTTPリクエストはこんな感じ。

GET /WebSocket_Jetty/EchoServlet HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: 192.168.1.5:8080
Origin: http://192.168.1.5:8080
Pragma: no-cache
Cache-Control: no-cache
Sec-WebSocket-Key: 30NeuZ+L6XOBboj/l7m/tg==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: x-webkit-deflate-frame

今回の接続先は "ws://192.168.1.5:8080/WebSocket_Jetty/EchoServlet" でJavaScript

var ws = new WebSocket("ws://localhost:8080/WebSocket_Jetty/EchoServlet");

と書くと上のようなHTTPリクエストが送信される。
では、HTTPリクエストの内容をもうちょっと見てみる。

GET /WebSocket_Jetty/EchoServlet HTTP/1.1
Host: 192.168.1.5:8080
Pragma: no-cache
Cache-Control: no-cache

このあたりは通常のHTTPと変わらない。普通のGET。
"no-cache"の指定がされているが、RFC6455ではキャッシュについての記述は見当たらないので、あってもなくてもいいのではないかと。

Upgrade: websocket
Connection: Upgrade

この2つは必須のヘッダ。"Upgrade"には"websocket"、"Connection"には"Upgrade"を必ず指定する。

Origin: http://192.168.1.5:8080

ブラウザからのリクエストの場合は必須。ブラウザ以外の場合はあってもなくてもよい。超簡単に言うと、このWebSocket接続のリクエストを投げたJavaScriptをダウンロードしたオリジン。
今回は、"http://192.168.1.5:8080/WebSocket_Jetty/index.html" の中に書かれたJavaScriptがWebSocketの接続をしているので、上の値が設定されている。オリジンの詳細についてはRFC6454 "The Web Origin Concept"を参照。

Sec-WebSocket-Key: 30NeuZ+L6XOBboj/l7m/tg==

これも必須。ランダムに4バイトの値を作成し、さらにbase64エンコードした値が設定される。ランダム値なので、この値は毎回違うものが使用される。

Sec-WebSocket-Version: 13

WebSocketのバージョンで、必須のヘッダ。現在のRFC6455では、必ず"13"を指定。

Sec-WebSocket-Extensions: x-webkit-deflate-frame

オプションのヘッダ。どうもChromeは "x-webkit-deflate-frame" を入れるらしい。
"x-webkit-deflate-frame"はWebSocketのフレームを圧縮するものだが、少なくともJettyやTomcatでは使えないので無視しておこう。

他にも "Sec-WebSocket-Protocol" なんていうオプションのヘッダもRFC6455にはあるが、今回は使ってないので、また今度。

以上でHTTPリクエストは終了し、HTTPレスポンスはこんな感じでやってくる。

HTTP/1.1 101 Switching Protocols
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: fdJzSVExohGlkkwXcc59Jq1yEkE=

では、詳細を見てみよう。

HTTP/1.1 101 Switching Protocols

ステータスコードなので、当然、必須ヘッダ。サーバ側がWebSocketの接続を受け入れる場合のステータスコードは"101"。

Upgrade: WebSocket
Connection: Upgrade

リクエスト同様、必須ヘッダ。

Sec-WebSocket-Accept: fdJzSVExohGlkkwXcc59Jq1yEkE=

必須ヘッダ。この値は以下の手順で作られる

  1. 1. リクエストの中にあった"Sec-WebSocket-Key"の値と、固定の文字列"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" を連結する。
  2. 2. その値のSHA-1ハッシュを算出する。
  3. 3. そこから得られる20バイトをbase64エンコードする。

ここまでのやり取りでWebSocketの接続が完了。
データ送受信のパケットキャプチャは次回にでも。

WebSocketでEchoServerをつくる(Java EE 7編)

Java EE 7でWebSocketに対応するらしいので、Jetty8編Tomcat7編に続き、今回はJava EE 7でやってみることにする。

Java EE 7が使えるAPサーバが必要なので、GlassFish 4.0 Promoted Buildsをここからダウンロードした。
そんなわけで、今回の環境は以下。

サーバ:GlassFish 4.0 Promoted Build 71
クライアント:Firefox 18

で、サーバ側のソースコード

package test;

import javax.websocket.WebSocketClose;
import javax.websocket.WebSocketEndpoint;
import javax.websocket.WebSocketMessage;
import javax.websocket.WebSocketOpen;
import org.apache.log4j.Logger;

@WebSocketEndpoint("echo")
public class EchoServer {
    private static final Logger logger = Logger.getLogger(EchoServer.class);
    
    @WebSocketOpen
    public void openConnection() {
        logger.info("Open");
    }
    
    @WebSocketClose
    public void closeConnection() {
        logger.info("Close");
    }
    
    @WebSocketMessage
    public String echo(String message) {
        logger.info("received:" + message);
        return message;
    }
}

JettyやTomcatと違って、アノテーションだらけ。
簡単に説明するとこんな感じ。

@WebSocketEndpoint
WebSocketのエンドポイント。パラメータはコンテキストパスに続くパス。上のように"echo"を渡している場合は
 ws://ホスト名:ポート番号/コンテキストルート/echo
となる
@WebSocketOpen
コネクション確立時に呼ばれる
@WebSocketClose
コネクション切断時に呼ばれる
@WebSocketMessage
メッセージ受信時に呼ばれる

素直な名前なのでわかりやすい。
そして、下がクライアント側のHTML。前回、前々回とは接続先のみ異なっている。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Test</title>
</head>
<body>
	<div id="status">ステータス:未接続</div>
	<input type="text" id="msg">
	<button onclick="sendMessage()">send</button>
	<br>
	<textarea id="data" rows="10" cols="50" readonly></textarea>
	<br>
	<button onclick="closeConnection()">close</button>

	<script type="text/javascript">
		var con_status = document.getElementById("status")
		var msg = document.getElementById("msg");
		var data = document.getElementById("data");
		var ws = new WebSocket("ws://localhost:8080/WebSocket_JavaEE7/echo");

		ws.onopen = function(e) {
			con_status.textContent = "ステータス:接続済み";
		}

		ws.onmessage = function(e) {
			var message = e.data;
			data.textContent += message + "\n";
		}

		ws.onclose = function(e) {
			con_status.textContent = "ステータス:未接続";
		}

		function sendMessage() {
			ws.send(msg.value);
			msg.value = "";
		}

		function closeConnection() {
			ws.close();
		}
	</script>
</body>
</html>

WebSocketでEchoServerをつくる(Tomcat7編)

Tomcatもバージョン7からWebSocketに対応しているらしい。
そんなわけで、前回はJetty8を使ったが、今回はTomcat7でEchoServerをつくってみる。

なお、TomcatドキュメントのWebSocketのところを見ると「まだ開発終わってないよ」と書かれているが、お試しなので気にしないことにする。

今回、使ったのは以下。

サーバ:Tomcat 7.0.34
クライアント:Google Chrome 24.0.1312.52

で、サーバ側のソースコード

package test;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;

import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.apache.catalina.websocket.WsOutbound;
import org.apache.log4j.Logger;

@WebServlet("/EchoServlet")
public class EchoServlet extends WebSocketServlet {
	private static final long serialVersionUID = 1L;
	private static final Logger logger = Logger.getLogger(EchoServlet.class);
       
	@Override
	protected StreamInbound createWebSocketInbound(String subProtocol,
			HttpServletRequest request) {
		return new EchoStreamInbound();
	}

	class EchoStreamInbound extends MessageInbound {
		private WsOutbound wsb;

		@Override
		protected void onOpen(WsOutbound outbound) {
			logger.info("Open");
			wsb = outbound;
		}
		
		@Override
		protected void onClose(int status) {
			logger.info("Close");
		}
		
		@Override
		protected void onBinaryMessage(ByteBuffer message) throws IOException {
			// バイナリデータの場合は何もしない
		}

		@Override
		protected void onTextMessage(CharBuffer message) throws IOException {
			logger.info("received:" + message.toString());
			wsb.writeTextMessage(message);
		}
		
	}
}

継承する親クラスの名前はWebSocketServletでJettyの場合と同じだが、オーバーライドするメソッドは当然異なる。
下のクライアント側のHTMLは前回とほとんど同じで、接続先のURLだけ変えてある。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Test</title>
</head>
<body>
	<div id="status">ステータス:未接続</div>
	<input type="text" id="msg">
	<button onclick="sendMessage()">send</button>
	<br>
	<textarea id="data" rows="10" cols="50" readonly></textarea>
	<br>
	<button onclick="closeConnection()">close</button>

	<script type="text/javascript">
		var con_status = document.getElementById("status")
		var msg = document.getElementById("msg");
		var data = document.getElementById("data");
		var ws = new WebSocket("ws://localhost:8080/WebSocket_Tomcat/EchoServlet");

		ws.onopen = function(e) {
			con_status.textContent = "ステータス:接続済み";
		}

		ws.onmessage = function(e) {
			var message = e.data;
			data.textContent += message + "\n";
		}

		ws.onclose = function(e) {
			con_status.textContent = "ステータス:未接続";
		}

		function sendMessage() {
			ws.send(msg.value);
			msg.value = "";
		}

		function closeConnection() {
			ws.close();
		}
	</script>
</body>
</html>

今回も無事に動いた。

WebSocketでEchoServerをつくる(Jetty8編)

WebSocketを試してみるために、WebSocketを利用したEchoServerをつくってみた。
サーバにはJettyを使うことにする。
なお、今回使ったのは以下。

サーバ:Jetty 8.1.8.v20121106
クライアント:Google Chrome 24.0.1312.52

で、サーバ側のソースコードはこんな感じ。

package test;

import java.io.IOException;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;

@WebServlet("/EchoServlet")
public class EchoServlet extends WebSocketServlet {
	private static final long serialVersionUID = 1L;
	private static final Logger logger = Logger.getLogger(EchoServlet.class);
       
	@Override
	public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
		return new EchoWebSocket();
	}

	private class EchoWebSocket implements WebSocket.OnTextMessage {
		private Connection connection; 

		@Override
		public void onOpen(Connection connection) {
			logger.info("Open");
			this.connection = connection;
		}

		@Override
		public void onClose(int closeCode, String message) {
			logger.info("Close");
		}

		@Override
		public void onMessage(String data) {
			logger.info("received:" + data);
			try {
				connection.sendMessage(data);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
	}
}

こっちはクライアント側のHTMLとJavaScript

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Test</title>
</head>
<body>
	<div id="status">ステータス:未接続</div>
	<input type="text" id="msg">
	<button onclick="sendMessage()">send</button>
	<br>
	<textarea id="data" rows="10" cols="50" readonly></textarea>
	<br>
	<button onclick="closeConnection()">close</button>

	<script type="text/javascript">
		var con_status = document.getElementById("status")
		var msg = document.getElementById("msg");
		var data = document.getElementById("data");
		var ws = new WebSocket("ws://localhost:8080/WebSocket_Jetty/EchoServlet");

		ws.onopen = function(e) {
			con_status.textContent = "ステータス:接続済み";
		}

		ws.onmessage = function(e) {
			var message = e.data;
			data.textContent += message + "\n";
		}

		ws.onclose = function(e) {
			con_status.textContent = "ステータス:未接続";
		}

		function sendMessage() {
			ws.send(msg.value);
			msg.value = "";
		}

		function closeConnection() {
			ws.close();
		}
	</script>
</body>
</html>

手抜きのソースコードだけど、とりあえず動いた!