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


创建响应比理解为什么必须以这种方式来创建响应更容易。

你必须：

1. 获取 Sec-WebSocket-Key 请求标头的值，去除头部和尾部的所有空格
2. 追加字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
3. 使用 SHA-1 计算拿到结果值并进行 Base64 编码
4. 将其作为 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();
    }
  }
}
```

相关链接
----

* Writing WebSocket servers (en-US)