簡単なJavaサーバー側の例を実装しました。これを見てみましょう。ポート2005で接続をリッスンするServerSocketを作成することから始めます
public class WebsocketServer {
public static final int MASK_SIZE = 4;
public static final int SINGLE_FRAME_UNMASKED = 0x81;
private ServerSocket serverSocket;
private Socket socket;
public WebsocketServer() throws IOException {
serverSocket = new ServerSocket(2005);
connect();
}
private void connect() throws IOException {
System.out.println("Listening");
socket = serverSocket.accept();
System.out.println("Got connection");
if(handshake()) {
listenerThread();
}
}
WebSocketプロトコルのRFC標準で定義されているように、クライアントがWebSocketを介して接続する場合は、ハンドシェイクを実行する必要があります。それでは、handshake()メソッドを見てみましょう。かなり醜いので、段階的に説明します。最初の部分では、クライアントのハンドシェイクを読み取ります。
private boolean handshake() throws IOException {
PrintWriter out = new PrintWriter(socket.getOutputStream());
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//This hashmap will be used to store the information given to the server in the handshake
HashMap<String, String> keys = new HashMap<>();
String str;
//Reading client handshake, handshake ends with CRLF which is again specified in the RFC, so we keep on reading until we hit ""...
while (!(str = in.readLine()).equals("")) {
//Split the string and store it in our hashmap
String[] s = str.split(": ");
System.out.println(str);
if (s.length == 2) {
keys.put(s[0], s[1]);
}
}
RFC(セクション1.2)によると、クライアントのハンドシェイクは次のようになります(これは、chromeがバージョン22.0.1229.94 mに与えたものです)。
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: localhost:2005
Origin: null
Sec-WebSocket-Key: PyvrecP0EoFwVnHwC72ecA==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: x-webkit-deflate-frame
これで、キーマップを使用して、ハンドシェイクプロセスで対応する応答を作成できます。RFCからの引用:
ハンドシェイクが受信されたことを証明するために、サーバーは2つの情報を取得し、それらを組み合わせて応答を形成する必要があります。最初の情報は|Sec-WebSocket-Key|から取得されます クライアントハンドシェイクのヘッダーフィールド。このヘッダーフィールドの場合、サーバーは値を取得し、これを文字列形式のグローバル一意識別子「258EAFA5-E914-47DA-95CA-C5AB0DC85B11」と連結する必要があります。これは、 WebSocketプロトコル。次に、この連結のSHA-1ハッシュ(160ビット)(base64でエンコード)がサーバーのハンドシェイクで返されます。
それが私たちがしなければならないことです!Sec-WebSocket-Keyをマジックストリングと連結し、SHA-1ハッシュ関数でハッシュし、Base64でエンコードします。これは、次の醜いワンライナーが行うことです。
String hash;
try {
hash = new BASE64Encoder().encode(MessageDigest.getInstance("SHA-1").digest((keys.get("Sec-WebSocket-Key") + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes()));
} catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
return false;
}
次に、「Sec-WebSocket-Accept」フィールドに新しく作成されたハッシュを使用して、期待される応答を返します。
//Write handshake response
out.write("HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: " + hash + "\r\n"
+ "\r\n");
out.flush();
return true;
}
これで、クライアントとサーバーの間にWebSocket接続が正常に確立されました。ならどうしよう?どうすれば彼らを互いに話させることができますか?サーバーからクライアントにメッセージを送信することから始めることができます。NB!この時点から、HTTPを使用してクライアントと通信することはなくなります。次に、純粋なバイトの送信を通信し、着信バイトを解釈する必要があります。では、これをどのように行うのでしょうか?
サーバーからのメッセージは、RFC-セクション5.6で指定されているように、「フレーム」と呼ばれる特定の形式である必要があります。サーバーからメッセージを送信する場合、RFCは、最初のバイトでフレームの種類を指定する必要があると述べています。値が0x81のバイトは、「単一フレームのマスクされていないテキストメッセージ」を送信していることをクライアントに通知します。これは、基本的にはテキストメッセージです。後続のバイトは、メッセージの長さを表す必要があります。これに続いて、データまたはペイロードがあります。さて、わかりました...それを実装しましょう!
public void sendMessage(byte[] msg) throws IOException {
System.out.println("Sending to client");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedOutputStream os = new BufferedOutputStream(socket.getOutputStream());
//first byte is kind of frame
baos.write(SINGLE_FRAME_UNMASKED);
//Next byte is length of payload
baos.write(msg.length);
//Then goes the message
baos.write(msg);
baos.flush();
baos.close();
//This function only prints the byte representation of the frame in hex to console
convertAndPrint(baos.toByteArray());
//Send the frame to the client
os.write(baos.toByteArray(), 0, baos.size());
os.flush();
}
したがって、クライアントにメッセージを送信するには、sendMessage( "Hello、client!"。getBytes())を呼び出すだけです。
そんなに難しくなかった?クライアントからのメッセージの受信はどうですか?まあ、それはもう少し複雑ですが、そこにぶら下がってください!
クライアントからのフレーム送信は、サーバーからのフレーム送信とほぼ同じように構成されています。最初のバイトはメッセージのタイプであり、2番目のバイトはペイロードの長さです。次に違いがあります。次の4バイトはマスクを表します。マスクとは何ですか。また、クライアントからのメッセージがマスクされているのに、サーバーのメッセージはマスクされていないのはなぜですか。RFC-セクション5.1から、次のことがわかります。
...クライアントはサーバーに送信するすべてのフレームをマスクする必要があります...サーバーはクライアントに送信するフレームをマスクしてはなりません。
簡単な答えは次のとおりです。なぜ私たちはしなければならないのですか、あなたは尋ねるかもしれませんか?RFCを読むように言わなかったのですか?
次に進むと、フレーム内の4バイトのマスクの後、マスクされたペイロードが続きます。そしてもう1つ、クライアントはフレームの左から9番目のビットを1に設定して、メッセージがマスクされていることをサーバーに通知する必要があります(RFCのセクション5.2のきちんとしたASCIIアートフレームを確認してください)。左端の9番目のビットは、2番目のバイトの左端のビットに対応しますが、これがペイロードの長さのバイトです。これは、クライアントからのすべてのメッセージのペイロード長が0b10000000 =0x80+実際のペイロード長に等しいことを意味します。したがって、実際のペイロード長を見つけるには、フレームの2番目のバイトであるペイロード長バイトから0x80、128、または0b10000000(またはその他の任意の記数法)を減算する必要があります。
うわー、わかりました..それは複雑に聞こえます...あなたにとって「TLDR」-みんな、要約:ペイロードの長さを取得するために2番目のバイトから0x80を引く...
public String reiceveMessage() throws IOException {
//Read the first two bytes of the message, the frame type byte - and the payload length byte
byte[] buf = readBytes(2);
System.out.println("Headers:");
//Print them in nice hex to console
convertAndPrint(buf);
//And it with 00001111 to get four lower bits only, which is the opcode
int opcode = buf[0] & 0x0F;
//Opcode 8 is close connection
if (opcode == 8) {
//Client want to close connection!
System.out.println("Client closed!");
socket.close();
System.exit(0);
return null;
}
//Else I just assume it's a single framed text message (opcode 1)
else {
final int payloadSize = getSizeOfPayload(buf[1]);
System.out.println("Payloadsize: " + payloadSize);
//Read the mask, which is 4 bytes, and than the payload
buf = readBytes(MASK_SIZE + payloadSize);
System.out.println("Payload:");
convertAndPrint(buf);
//method continues below!
メッセージ全体を読んだので、ペイロードの意味を理解できるように、メッセージのマスクを解除します。マスクを解除するために、マスクとペイロードを引数として取り、デコードされたペイロードを返すメソッドを作成しました。したがって、呼び出しは次のように行われます。
buf = unMask(Arrays.copyOfRange(buf, 0, 4), Arrays.copyOfRange(buf, 4, buf.length));
String message = new String(buf);
return message;
}
}
今、unMaskメソッドはかなり甘くて小さいです
private byte[] unMask(byte[] mask, byte[] data) {
for (int i = 0; i < data.length; i++) {
data[i] = (byte) (data[i] ^ mask[i % mask.length]);
}
return data;
}
getSizeOfPayloadについても同じことが言えます。
private int getSizeOfPayload(byte b) {
//Must subtract 0x80 from (unsigned) masked frames
return ((b & 0xFF) - 0x80);
}
それで全部です!これで、純粋なソケットを使用して両方向で通信できるようになります。完全を期すために、完全なJavaクラスを追加します。WebSocketを使用してクライアントとメッセージを送受信することができます。
package javaapplication5;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import sun.misc.BASE64Encoder;
/**
*
* @author
* Anders
*/
public class WebsocketServer {
public static final int MASK_SIZE = 4;
public static final int SINGLE_FRAME_UNMASKED = 0x81;
private ServerSocket serverSocket;
private Socket socket;
public WebsocketServer() throws IOException {
serverSocket = new ServerSocket(2005);
connect();
}
private void connect() throws IOException {
System.out.println("Listening");
socket = serverSocket.accept();
System.out.println("Got connection");
if(handshake()) {
listenerThread();
}
}
private boolean handshake() throws IOException {
PrintWriter out = new PrintWriter(socket.getOutputStream());
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
HashMap<String, String> keys = new HashMap<>();
String str;
//Reading client handshake
while (!(str = in.readLine()).equals("")) {
String[] s = str.split(": ");
System.out.println();
System.out.println(str);
if (s.length == 2) {
keys.put(s[0], s[1]);
}
}
//Do what you want with the keys here, we will just use "Sec-WebSocket-Key"
String hash;
try {
hash = new BASE64Encoder().encode(MessageDigest.getInstance("SHA-1").digest((keys.get("Sec-WebSocket-Key") + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes()));
} catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
return false;
}
//Write handshake response
out.write("HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: " + hash + "\r\n"
+ "\r\n");
out.flush();
return true;
}
private byte[] readBytes(int numOfBytes) throws IOException {
byte[] b = new byte[numOfBytes];
socket.getInputStream().read(b);
return b;
}
public void sendMessage(byte[] msg) throws IOException {
System.out.println("Sending to client");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedOutputStream os = new BufferedOutputStream(socket.getOutputStream());
baos.write(SINGLE_FRAME_UNMASKED);
baos.write(msg.length);
baos.write(msg);
baos.flush();
baos.close();
convertAndPrint(baos.toByteArray());
os.write(baos.toByteArray(), 0, baos.size());
os.flush();
}
public void listenerThread() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
System.out.println("Recieved from client: " + reiceveMessage());
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
t.start();
}
public String reiceveMessage() throws IOException {
byte[] buf = readBytes(2);
System.out.println("Headers:");
convertAndPrint(buf);
int opcode = buf[0] & 0x0F;
if (opcode == 8) {
//Client want to close connection!
System.out.println("Client closed!");
socket.close();
System.exit(0);
return null;
} else {
final int payloadSize = getSizeOfPayload(buf[1]);
System.out.println("Payloadsize: " + payloadSize);
buf = readBytes(MASK_SIZE + payloadSize);
System.out.println("Payload:");
convertAndPrint(buf);
buf = unMask(Arrays.copyOfRange(buf, 0, 4), Arrays.copyOfRange(buf, 4, buf.length));
String message = new String(buf);
return message;
}
}
private int getSizeOfPayload(byte b) {
//Must subtract 0x80 from masked frames
return ((b & 0xFF) - 0x80);
}
private byte[] unMask(byte[] mask, byte[] data) {
for (int i = 0; i < data.length; i++) {
data[i] = (byte) (data[i] ^ mask[i % mask.length]);
}
return data;
}
private void convertAndPrint(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
System.out.println(sb.toString());
}
public static void main(String[] args) throws IOException, InterruptedException, NoSuchAlgorithmException {
WebsocketServer j = new WebsocketServer();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.println("Write something to the client!");
j.sendMessage(br.readLine().getBytes());
}
}
}
そしてhtmlの単純なクライアント:
<!DOCTYPE HTML>
<html>
<body>
<button type="button" onclick="connect();">Connect</button>
<button type="button" onclick="connection.close()">Close</button>
<form>
<input type="text" id="msg" />
<button type="button" onclick="sayHello();">Say Hello!</button>
<script>
var connection;
function connect() {
console.log("connection");
connection = new WebSocket("ws://localhost:2005/");
// Log errors
connection.onerror = function (error) {
console.log('WebSocket Error ');
console.log(error);
};
// Log messages from the server
connection.onmessage = function (e) {
console.log('Server: ' + e.data);
alert("Server said: " + e.data);
};
connection.onopen = function (e) {
console.log("Connection open...");
}
connection.onclose = function (e) {
console.log("Connection closed...");
}
}
function sayHello() {
connection.send(document.getElementById("msg").value);
}
function close() {
console.log("Closing...");
connection.close();
}
</script>
</body>
</html>
これが何かをクリアし、私がそれに光を当てることを願っています:)