浅析nginx实现websocket原理

前言:

​ 传统的HTTP协议是一种无状态的请求/响应协议,每次请求都需要重新建立连接。在一些特殊的业务场景下,服务端需要主动发送数据到客户端,例如行情推送、监控告警推送等。然而,HTTP协议不支持双向通信,因此需要将HTTP协议“升级”为WebSocket协议。WebSocket协议可以在建立连接后保持连接状态,双方可以通过一个持久的连接通道进行实时通信。WebSocket连接在建立时通过HTTP协议进行握手,之后的数据传输就可以使用WebSocket协议进行。

Nginx作为中间层的Web服务器,支持使用多种协议与上下游进行通信,包括TCP、HTTP、WebSocket等协议,如下图所示。

图1

1、nginx升级http为websocket的过程

​ HTTP/1.1提供了一种特殊的机制,这一机制允许将一个已建立的连接升级成新的、不相容的协议。具体过程如下:

图2

1.客户端发起 WebSocket 连接请求到 Nginx,Nginx 作为反向代理服务器,将请求转发给上游 WebSocket 服务器。客户端发送的请求类似于下图所示:

1
2
3
4
5
6
7
GET ws://10.40.xx.xx:58088/wss/socket.io HTTP/1.1
Host: 10.40.xx.xx:58088
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: 3baOagQNXoc1Cd1dJ4pBiA==
Sec-WebSocket-Extensions:permessage-deflate;client_max_window_bits

2.上游 WebSocket 服务器响应连接请求并完成握手协议,如果允许升级,将响应状态码101返回给 Nginx。服务端的响应类似于下图所示:

1
2
3
4
5
6
7
HTTP/1.1 101 Switching Protocols
Server:xxx
Date:Wed,22 Feb 2023 06:23:49 GMT
connection:upgrade
upgrade:websocket
Sec-WebSocket-accept:VVN2Pd9jkG7b8ur3otAk+Ah3bsg=
Sec-WebSocket-Extensions:permessage-deflate

3.Nginx 收到上游 WebSocket 服务器的响应结果后,将其转发给客户端,建立起客户端与上游 WebSocket 服务器的连接。

4.客户端和上游 WebSocket 服务器之间开始进行实时数据传输。

5.当客户端或上游 WebSocket 服务器需要发送数据时,数据将通过 WebSocket 协议封装成帧(frame)并发送到对方。

6.数据通过 Nginx 进行转发时,Nginx 会根据实际情况选择合适的负载均衡算法,将数据传输到适当的上游 WebSocket 服务器。

7.上游 WebSocket 服务器收到数据后,解析数据帧并处理数据,然后将响应结果封装成帧并发送回客户端。

8.客户端收到上游 WebSocket 服务器的响应结果后,解析数据帧并处理数据,完成一次数据交互。

2、nginx核心代码实现

2.1、连接升级

​ 当服务端同意升级为 WebSocket 时,会将响应状态码设置为 “101 Switching Protocols”,表示服务器正在切换协议。如下代码所示,在处理 HTTP 协议时,首先会检查连接是否已升级到另一个协议。其中,NGX_HTTP_SWITCHING_PROTOCOLS 是一个宏,值为 101。然后,会检查客户端的请求是否携带 Upgrade 头部。如果请求携带了该头部,就会将 u->upgrade 的值设为 1,表示该连接是一个升级连接。

1
2
3
4
5
6
7
8
//检查响应的状态码
if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS) {
u->keepalive = 0;
//客户端请求头是否含有upgrade头部
if (r->headers_in.upgrade) {
u->upgrade = 1;
}
}

2.2、上下游数据处理

​ nginx基于事件驱动模型,当有事件触发时,就会调用对应的回调函数。

下游:

1
2
3
4
5
if (ev->write) {
r->write_event_handler(r);
} else {
r->read_event_handler(r);
}

上游:

1
2
3
4
5
if (ev->write) {
u->write_event_handler(r, u);
} else {
u->read_event_handler(r, u);
}

​ 每个连接都根据类型(http、websocket等)的不同,设置不同的回调,下面的代码展示了当连接的u->upgrade = 1,即为websocket协议时,设置的上下游读写回调函数。当此连接再有读写事件时,就会回调下面设置的函数

1
2
3
4
5
6
7
8
//接收来自upstream的数据 
u->read_event_handler = ngx_http_upstream_upgraded_read_upstream;
//向upstream发送数据
u->write_event_handler = ngx_http_upstream_upgraded_write_upstream;
//接收来自客户端的数据
r->read_event_handler = ngx_http_upstream_upgraded_read_downstream;
//向客户端发送数据
r->write_event_handler = ngx_http_upstream_upgraded_write_downstream;

​ 实际上,这四个回调函数都调用了同一个处理函数。但是,它们分别传入了不同的参数,因此函数的处理方式也不同,对应四种不同的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void
ngx_http_upstream_upgraded_read_upstream(ngx_http_request_t *r,
ngx_http_upstream_t *u)
{
ngx_http_upstream_process_upgraded(r, 1, 0);
}
static void
ngx_http_upstream_upgraded_write_upstream(ngx_http_request_t *r,
ngx_http_upstream_t *u)
{
ngx_http_upstream_process_upgraded(r, 0, 1);
}
static void
ngx_http_upstream_upgraded_read_downstream(ngx_http_request_t *r)
{
ngx_http_upstream_process_upgraded(r, 0, 0);
}
static void
ngx_http_upstream_upgraded_write_downstream(ngx_http_request_t *r)
{
ngx_http_upstream_process_upgraded(r, 1, 1);
}

​ 此处的设计非常巧妙(部分代码),使用了不同参数复用了同一个函数。下面的核心代码展示了 Nginx 如何处理事件。

当数据来自于服务端时,from_upstream 为真,do_write 表示需要发送数据。根据下面的代码,可以看出 src 表示服务端的连接,dst 表示客户端的连接。当 do_write 为 1 且 dst 准备好写入操作(并且需要发送的数据长度不为 0)时,dst 就会发送数据;当 src 准备好读取操作时,src 就会读取数据。

比较有意思的是,当数据来自于客户端时,只需要将 dst 和 src 交换即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static void
ngx_http_upstream_process_upgraded(ngx_http_request_t *r,
ngx_uint_t from_upstream, ngx_uint_t do_write)
{
//客户端连接
downstream = r->connection;
//服务端连接
upstream = r->upstream->peer.connection;

// 如果数据来自于后端服务器
if (from_upstream) {
src = upstream;
dst = downstream;
b = &u->buffer;
// 如果数据来自于客户端
} else {
src = downstream;
dst = upstream;
b = &u->from_client;
}
for ( ;; ) {
// 判断是否需要发送数据
if (do_write) {
// 获取当前要发送的数据长度
size = b->last - b->pos;
// 如果要发送的数据长度不为 0,且连接已经准备好进行写操作
if (size && dst->write->ready) {
// 发送数据
n = dst->send(dst, b->pos, size);
}
}
size = b->end - b->last;
// 如果要接收的数据长度不为 0,且连接已经准备好读操作
if (size && src->read->ready) {
// 接收数据
n = src->recv(src, b->last, size);
}
break;
}
}

3、使用实例

本节将通过 Nginx 和 WebSocket 客户端/服务端的实例,展示如何在实际业务中使用 WebSocket 进行消息推送。

首先,需要对 Nginx进行路由配置,如下图所示。即,所有以 uri 前缀为 wss 的 HTTP 客户端请求都会被升级为 WebSocket。在代理过程中,HSIAR 还需要将 Connection 和 Upgrade 头部携带给后端服务,告知后端需要将 Nginx 与后端的连接升级为 WebSocket。

图3

2、建立连接的过程如下图所示,可以看到与2.1节所述过程一致

图4

3、连接建立成功后,点击订阅,即接收消息的推送

图5

4、后端推送消息

可以看到消息由后端成功推送到了客户端

图6

4、总结

在实际的生产中,消息推送是一种常用的技术。然而,如果在 WebSocket 客户端和服务端之间的链路中加入代理,尤其是多级代理后,情况就会变得更加复杂。为了确保链路上的每一条连接都是 WebSocket 长连接,需要避免中间出现 HTTP 短链接。否则,推送就可能因连接提前断开而失败。了解该技术的原理和细节,可以帮助快速排查问题并进行修复。同时,研究 Nginx 对 WebSocket 的支持技术实现,不仅能够提高对该技术的理解,也能够为今后开发相关系统提供有益的借鉴。


浅析nginx实现websocket原理
https://zjfans.github.io/2024/04/12/浅析nginx实现websocket原理/
作者
张三疯
发布于
2024年4月12日
许可协议