用 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 建構函式接受一個名為 portint 型別引數。

當您例項化 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 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 示例

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