用 Java 编写 WebSocket 服务器
本示例将演示如何使用 Oracle Java 创建 WebSocket API 服务器。
虽然可以使用其他服务器端语言创建 WebSocket 服务器,但本示例使用 Oracle Java 来简化示例代码。
此服务器符合 RFC 6455,因此它仅处理来自 Chrome 16、Firefox 11、IE 10 及更高版本的连接。
第一步
WebSocket 通过 TCP(传输控制协议)连接进行通信。Java 的 ServerSocket 类位于 java.net 包中。
ServerSocket
ServerSocket 构造函数接受一个名为 port 的 int 类型参数。
当您实例化 ServerSocket 类时,它会绑定到您通过 *port* 参数指定的端口号。
下面是分部分实现的示例
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()-
返回此套接字的输入流。
java.net.Socket.getOutputStream()-
返回此套接字的输出流。
OutputStream 方法
write(byte[] b, int off, int len)
将指定字节数组中从偏移量 off 开始的 len 个字节写入此输出流。
InputStream 方法
read(byte[] b, int off, int len)
从输入流中读取最多 len 字节的数据到字节数组中。
让我们扩展我们的示例。
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
Scanner s = new Scanner(in, "UTF-8");
握手
当客户端连接到服务器时,它会发送一个 GET 请求,以将连接从简单的 HTTP 请求升级为 WebSocket。这称为握手。
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 编码
- 将其作为 *Sec-WebSocket-Accept* 响应头的值写回,作为 HTTP 响应的一部分。
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:您可以将消息分成帧发送,但现在保持简单。Opcode *0x1* 表示这是一个文本消息。完整的 Opcode 列表
-
134:
如果第二个字节减去 128 在 0 到 125 之间,则表示消息的长度。如果为 126,则表示接下来的 2 个字节(16 位无符号整数)是长度;如果为 127,则表示接下来的 8 个字节(64 位无符号整数,最高有效位必须为 0)是长度。
注意:由于第一个位始终为 1,因此它不能是 128。
-
167、225、225 和 210 是用于解码密钥的字节。它每次都会改变。
-
剩余的编码字节是消息。
解码算法
decoded byte = encoded byte XOR (position of encoded byte BITWISE AND 0x3)th byte of key
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();
}
}
}