在 Java 中编写 WebSocket 服务器

本示例展示了如何使用 Oracle Java 创建 WebSocket API 服务器。

虽然可以使用其他服务器端语言来创建 WebSocket 服务器,但本示例使用 Oracle Java 来简化示例代码。

此服务器符合 RFC 6455,因此它只处理来自 Chrome 版本 16、Firefox 11、IE 10 及更高版本的连接。

第一步

WebSockets 通过 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()

返回此套接字的输入流。

java.net.Socket.getOutputStream()

返回此套接字的输出流。

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);

创建响应比理解为什么要这样做更容易。

您必须,

  1. 获取 Sec-WebSocket-Key 请求头的值,不包含任何前导和尾随空格
  2. 将其与 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 连接
  3. 计算它的 SHA-1 和 Base64 代码
  4. 将其写回作为 Sec-WebSocket-Accept 响应头的值,作为 HTTP 响应的一部分。
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 操作码
    1 0 0 0 0x1=0001
    FIN:您可以分帧发送消息,但现在保持简单。操作码 0x1 表示这是一个文本。 操作码的完整列表
  • 134:如果第二个字节减去 128 在 0 到 125 之间,这就是消息的长度。如果它是 126,则接下来的 2 个字节(16 位无符号整数),如果是 127,则接下来的 8 个字节(64 位无符号整数,最高位必须为 0)是长度。

    注意:它可以取 128,因为第一位始终为 1。

  • 167、225、225 和 210 是解码密钥的字节。它每次都会改变。
  • 其余的编码字节是消息。

解码算法

解码字节 = 编码字节 XOR(编码字节位置 按位与 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();
    }
  }
}