> For the complete documentation index, see [llms.txt](https://note.niefee.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://note.niefee.com/directory-3/websocket.md).

# WebSocket

* [WebSocket](/directory-3/websocket.md#websocket)
  * [TCP的三次握手与四次分手](https://note.niefee.com/directory-3/pages/-LjTb6fUG2FjcTu6-1ot#tcp的三次握手与四次分手)
    * [三次握手](https://note.niefee.com/directory-3/pages/-LjTb6fUG2FjcTu6-1ot#三次握手)
    * [四次分手：](https://note.niefee.com/directory-3/pages/-LjTb6fUG2FjcTu6-1ot#四次分手)
  * [WebSocket简介](https://note.niefee.com/directory-3/pages/-LjTb6fUG2FjcTu6-1ot#websocket简介)
  * [实例](https://note.niefee.com/directory-3/pages/-LjTb6fUG2FjcTu6-1ot#实例)
  * [WebSocket原生事件](https://note.niefee.com/directory-3/pages/-LjTb6fUG2FjcTu6-1ot#websocket原生事件)
  * [WebSocket原生方法](https://note.niefee.com/directory-3/pages/-LjTb6fUG2FjcTu6-1ot#websocket原生方法)
  * [readyState](/directory-3/websocket.md#readystate)

## WebSocket

### TCP的三次握手与四次分手

#### 三次握手

TCP是主机对主机层的传输控制协议，提供可靠的连接服务，采用三次握手确认建立一个连接:

位码即tcp标志位,有6种标示:

1. SYN(synchronous建立联机)
2. ACK(acknowledgement 确认)
3. PSH(push传送)
4. FIN(finish结束)
5. RST(reset重置)
6. URG(urgent紧急)
7. Sequence number(顺序号码)
8. Acknowledge number(确认号码)

握手过程：

第一次握手：建立连接。客户端发送连接请求报文段，将`SYN`位置为1，`Sequence Number`为x；然后，客户端进入`SYN_SEND`状态，等待服务器的确认； 第二次握手：服务器收到`SYN`报文段。服务器收到客户端的`SYN`报文段，需要对这个`SYN`报文段进行确认，设置`Acknowledgment Number`为`x+1(Sequence Number+1)`；同时，自己还要发送`SYN`请求信息，将`SYN`位置为1，`Sequence Number`为y；服务器端将上述所有信息放到一个报文段（即`SYN+ACK`报文段）中，一并发送给客户端，此时服务器进入`SYN_RECV`状态； 第三次握手：客户端收到服务器的`SYN+ACK`报文段。然后将`Acknowledgment Number`设置为y+1，向服务器发送`ACK`报文段，这个报文段发送完毕以后，客户端和服务器端都进入`ESTABLISHED`状态，完成`TCP`三次握手；

![TCP/IP](/files/-LjTb9P6suFKZyxNjPlT)

#### 四次分手：

由于TCP连接是全双工的，因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个`FIN`来终止这个方向的连接。收到一个 `FIN`只意味着这一方向上没有数据流动，一个`TCP`连接在收到一个`FIN`后仍能发送数据。首先进行关闭的一方将执行主动关闭，而另一方执行被动关闭。

操作：

1. 客户端A发送一个`FIN`，用来关闭客户A到服务器B的数据传送。
2. 服务器B收到这个`FIN`，它发回一个`ACK`，确认序号为收到的序号加1。和`SYN`一样，一个`FIN`将占用一个序号。
3. 服务器B关闭与客户端A的连接，发送一个`FIN`给客户端A。
4. 客户端A发回`ACK`报文确认，并将确认序号设置为收到序号加1。

![TCP/IP](/files/-LjTb9P8OyB6FPQ5wlZI)

> 参考： 1. <http://www.cnblogs.com/Jessy/p/3535612.html> 2. <https://www.zhihu.com/question/24853633>

### WebSocket简介

WebSocket协议是基于`TCP`的一种新的网络协议。它实现了浏览器与服务器全双工(`full-duplex`)通信——可以通俗的解释为服务器主动发送信息给客户端。 只需要经过一次`HTTP`请求，就可以做到源源不断的信息传送了（在程序设计中，这种设计叫做回调，即：你有信息了再来通知我）。

客户端请求：

```
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
```

`Upgrade: websocket`、`Connection: Upgrade`表示希望将http协议升级到Websocket协议。 `Sec-WebSocket-Key`是一个经过base64编码的随机字节。 `Sec-WebSocket-Protocol`表示客户端支持的协议列表。 `Sec-WebSocket-Version`:是告诉服务器所使用的Websocket Draft（协议版本）。 `Origin`字段是可选的，表示在浏览器中发起此Websocket连接所在的页面。

服务端响应：

```
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
```

`Sec-WebSocket-Accept`:是把`Sec-WebSocket-Key`加上一个特殊字符串“`258EAFA5-E914-47DA-95CA-C5AB0DC85B11`”，然后计算**SHA-1**摘要，之后进行**BASE-64**编码，将结果做为`Sec-WebSocket-Accept`头的值，返回给客户端。

> 参考： 1.[知乎](https://www.zhihu.com/question/20215561) 2.[维基百科](https://zh.wikipedia.org/wiki/WebSocket)

### 实例

服务器端：

```javascript
const WebSocket = require('ws');

const WebSocketServer = WebSocket.Server;

const wss = new WebSocketServer({
    port: 3000
});

//启动服务链接
wss.on('connection', function (ws) {
    console.log(`[SERVER] connection()`);
    //监听有否有请求信息
    ws.on('message', function (message) {
        console.log(`[SERVER] Received: ${message}`);
        setTimeout(() => {
            //定时给客户端发送信息
            ws.send(`What's your name?`, (err) => {
                if (err) {
                    console.log(`[SERVER] error: ${err}`);
                }
            });
        }, 1000);
    })
});

console.log('ws server started at port 3000...');
```

客户端：

```javascript
const WebSocket = require('ws');
let count = 0;
let ws = new WebSocket('ws://localhost:3000/ws/chat');
//打开链接，给服务器发送一条信息
ws.on('open', function () {
    console.log(`[CLIENT] open()`);
    ws.send('Hello!');
});
//监听服务器是否有信息发送过来
ws.on('message', function (message) {
    console.log(`[CLIENT] Received: ${message}`);
    count++;
    if (count > 3) {
        ws.send('Goodbye!');
        ws.close();
    } else {
        setTimeout(() => {
            ws.send(`Hello, I'm Mr No.${count}!`);
        }, 1000);
    }
});
```

运行后：

```
ws server started at port 3000...
[SERVER] connection()
[CLIENT] open()
[SERVER] Received: Hello!
[CLIENT] Received: What's your name?
[SERVER] Received: Hello, I'm Mr No.1!
[CLIENT] Received: What's your name?
[SERVER] Received: Hello, I'm Mr No.2!
[CLIENT] Received: What's your name?
[SERVER] Received: Hello, I'm Mr No.3!
[CLIENT] Received: What's your name?
[SERVER] Received: Goodbye!
[SERVER] error: Error: not opened

end'
```

> 参考：[websocket](http://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/0014727922914053479c966220f47da91991fa9c27ac3ea000)

### WebSocket原生事件

| 事件      | 事件处理程序           | 描述            |
| ------- | ---------------- | ------------- |
| open    | Socket.onopen    | 连接建立时触发       |
| message | Socket.onmessage | 客户端接收服务端数据时触发 |
| error   | Socket.onerror   | 通信发生错误时触发     |
| close   | Socket.onclose   | 连接关闭时触发       |

```javascript
var ws = new WebSocket('ws://localhost:3000/ws/chat');
ws.onopen=function () {
    ws.send('Hello!');
};
```

### WebSocket原生方法

| 方法             | 描述       |
| -------------- | -------- |
| Socket.send()  | 使用连接发送数据 |
| Socket.close() | 关闭连接     |

### readyState

| 特性常量                 | 取值 | 状态             |
| -------------------- | -- | -------------- |
| WebSocket.CONNECTING | 0  | 连接正在进行中，但还未建立  |
| WebSocket.OPEN       | 1  | 连接已建立，消息可以开始传递 |
| WebSocket.CLOSING    | 2  | 连接正在进行关闭       |
| WebSocket.CLOSED     | 3  | 连接已关闭          |

#### 连接池

每一个连接都放到一个数组中，组成连接池。

```javascript
// 连接池
var clients = [];

wss.on('connection', function(ws) {
    // 将该连接加入连接池
    clients.push(ws);
    ws.on('message', function(message) {
        // 广播消息
        clients.forEach(function(ws1){
            if(ws1 !== ws) {
                ws1.send(message);
            }
       })
    });
});
```

为了区分每个连接，可以添加`UUID` ，通用唯一识别码（Universally Unique Identifier）

> <http://www.jianshu.com/p/ea0a9a6311cf>
