Writing a WebSocket server in Java
引言
你可以通过这个例子知道如何用甲骨文的 Java 语言来创建一个 WebSocket 服务。
虽然其他的服务端语言也能创建 WebSocket 服务,但是通过这个例子你可以看到使用 Java 来做这件事会更简单。
这个服务符合协议RFC 6455,所以它只处理 Chrome 版本 16,Firefox 11,IE 10 及更高版本的连接。
第一步
WebSocket 通过TCP(传输控制协议)通信。Java 的ServerSocket 类位于 java.net 包中。
ServerSocket
构造器:
ServerSocket(int port)
实例化 ServerSocket 类时,它将绑定到 port 参数指定的端口号。
实现代码片段一:
java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class WebSocket {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
ServerSocket server = new ServerSocket(80);
try {
System.out.println("Server has started on 127.0.0.1:80.\r\nWaiting for a connection...");
Socket client = server.accept();
System.out.println("A client connected.");
Socket 方法
-
java.net.Socket.getInputStream()
返回该 Socket 的输入流。 -
java.net.Socket.getOutputStream()
返回该 Socket 的输出流。
OutputStream 方法
java
write(byte[] b, int off, int len)
将指定字节数组从偏移量 off
开始的 len
字节写入此输出流。
InputStream 方法
java
read(byte[] b, int off, int len)
从输入流中读取最多 len 字节的数据到一个字节数组中。
代码片段二:
java
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
Scanner s = new Scanner(in, "UTF-8");
握手
当客户端连接到服务器时,它会发送 GET 请求以从简单的 HTTP 请求升级到 WebSocket 的连接。这被称为握手。
java
try {
String data = s.useDelimiter("\\r\\n\\r\\n").next();
Matcher get = Pattern.compile("^GET").matcher(data);
创建响应比理解为什么必须以这种方式来创建响应更容易。
你必须:
- 获取 Sec-WebSocket-Key 请求标头的值,去除头部和尾部的所有空格
- 追加字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- 使用 SHA-1 计算拿到结果值并进行 Base64 编码
- 将其作为 HTTP 响应的一部分写回 Sec-WebSocket-Accept 响应头的值
java
if (get.find()) {
Matcher match = Pattern.compile("Sec-WebSocket-Key: (.*)").matcher(data);
match.find();
byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n"
+ "Connection: Upgrade\r\n"
+ "Upgrade: websocket\r\n"
+ "Sec-WebSocket-Accept: "
+ Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-1").digest((match.group(1) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes("UTF-8")))
+ "\r\n\r\n").getBytes("UTF-8");
out.write(response, 0, response.length);
解码消息
握手成功后,客户端可以向服务器发送消息,但现在这些已经过编码的消息需要解码。
如果客户端发送 "abcdef",我们会拿到这些字节数据:
129 | 134 | 167 | 225 | 225 | 210 | 198 | 131 | 130 | 182 | 194 | 135 |
---|
- 129:
FIN (消息是完整的吗?) | RSV1 | RSV2 | RSV3 | Opcode |
---|---|---|---|---|
1 | 0 | 0 | 0 | 0x1=0001 |
FIN: 你可以分多次发送一个完整的消息。但现在为了简单,操作码 0x1 表示这是一个完整的消息。 Full list of Opcodes
- 134:
如果第二个字节减去 128 在 0 到 125 之间,则这是消息的长度。如果是 126,则后面的 2 个字节(16 位无符号整数),如果是 127,则后面的 8 个字节(64 位无符号整数,最高有效位必须为 0)是长度。
备注: 我可以拿 128,因为第一位总是 1。
- 167, 225, 225 和 210 是要解码的密钥key
的字节。它每次都在变化。
- 剩余的编码字节是消息数据部分。
解码算法
decoded[i] = (byte) (encoded[i] ^ key[i & 0x3]);
Java 例子:
java
byte[] decoded = new byte[6];
byte[] encoded = new byte[] { (byte) 198, (byte) 131, (byte) 130, (byte) 182, (byte) 194, (byte) 135 };
byte[] key = new byte[] { (byte) 167, (byte) 225, (byte) 225, (byte) 210 };
for (int i = 0; i < encoded.length; i++) {
decoded[i] = (byte) (encoded[i] ^ key[i & 0x3]);
}
}
} finally {
s.close();
}
} finally {
server.close();
}
}
}