swoole教程栏目介绍如何查询websocket的连接问题。
推荐:swoole教程问题我们项目的 websocket server 使用的 swoole,最近在搭建 beta 环境的时候发现 websocket 协议虽然升级成功了,但是会出现定时重连,心跳、数据也一直没有发送。项目的生产环境和 beta 一致,但是生产环境确没有这个问题。
定位问题为了方便调试 swoole,以下测试是在本地环境下进行。
查看 php 日志在 php 日志里,发现一条错误日志: errorexception: swoole\websocket\server::push(): the connected client of connection[47] is not a websocket client or closed,说明 websocket 连接已经 close 了。
抓包既然连接被 close 掉了,那我们来看看是谁主动关闭的连接。swoole 监听的端口是 1215,通过 tcpdump -nni lo0 -x port 1215 可以看到,swoole 在发出协议升级的响应报文后,又发出了 fin 报文段,即 swoole 主动断开了连接,所以才会出现浏览器显示 websocket 连接建立成功,但是又定时重连的问题。
10:22:58.060810 ip 127.0.0.1.1215 > 127.0.0.1.53823: flags [p.], seq 1:185, ack 1372, win 6358, options [nop,nop,ts val 1981911666 ecr 1981911665], length 184 0x0000: 4500 00ec 0000 4000 4006 0000 7f00 0001 e.....@.@....... 0x0010: 7f00 0001 04bf d23f 9377 304a 6d2f 9604 .......?.w0jm/.. 0x0020: 8018 18d6 fee0 0000 0101 080a 7621 9272 ............v!.r 0x0030: 7621 9271 4854 5450 2f31 2e31 2031 3031 v!.qhttp/1.1.101 0x0040: 2053 7769 7463 6869 6e67 2050 726f 746f .switching.proto 0x0050: 636f 6c73 0d0a 5570 6772 6164 653a 2077 cols..upgrade:.w 0x0060: 6562 736f 636b 6574 0d0a 436f 6e6e 6563 ebsocket..connec 0x0070: 7469 6f6e 3a20 5570 6772 6164 650d 0a53 tion:.upgrade..s 0x0080: 6563 2d57 6562 536f 636b 6574 2d41 6363 ec-websocket-acc 0x0090: 6570 743a 2052 6370 3851 6663 446c 3146 ept:.rcp8qfcdl1f 0x00a0: 776e 666a 6377 3862 4933 6971 7176 4551 wnfjcw8bi3iqqveq 0x00b0: 3d0d 0a53 6563 2d57 6562 536f 636b 6574 =..sec-websocket 0x00c0: 2d56 6572 7369 6f6e 3a20 3133 0d0a 5365 -version:.13..se 0x00d0: 7276 6572 3a20 7377 6f6f 6c65 2d68 7474 rver:.swoole-htt 0x00e0: 702d 7365 7276 6572 0d0a 0d0a p-server....10:22:58.060906 ip 127.0.0.1.53823 > 127.0.0.1.1215: flags [.], ack 185, win 6376, options [nop,nop,ts val 1981911666 ecr 1981911666], length 0 0x0000: 4500 0034 0000 4000 4006 0000 7f00 0001 e..4..@.@....... 0x0010: 7f00 0001 d23f 04bf 6d2f 9604 9377 3102 .....?..m/...w1. 0x0020: 8010 18e8 fe28 0000 0101 080a 7621 9272 .....(......v!.r 0x0030: 7621 9272 v!.r10:22:58.061467 ip 127.0.0.1.1215 > 127.0.0.1.53823: flags [f.], seq 185, ack 1372, win 6358, options [nop,nop,ts val 1981911667 ecr 1981911666], length 0 0x0000: 4500 0034 0000 4000 4006 0000 7f00 0001 e..4..@.@....... 0x0010: 7f00 0001 04bf d23f 9377 3102 6d2f 9604 .......?.w1.m/.. 0x0020: 8011 18d6 fe28 0000 0101 080a 7621 9273 .....(......v!.s 0x0030: 7621 9272 v!.r复制代码
追踪 swoole 源码我们现在知道了是 swoole 主动断开了连接,但它是在什么时候断开的,又为什么要断开呢?就让我们从源码一探究竟。
从抓包结果看,发出响应报文到 close 连接的时间很短,所以猜测是握手阶段出了问题。从响应报文可以看出,websocket 连接是建立成功的,推测 swoole_websocket_handshake() 的结果应该是 true,那么连接应该是在 swoole_websocket_handshake() 里 close 的。
// // swoole_websocket_server.ccint swoole_websocket_onhandshake(swserver *serv, swlistenport *port, http_context *ctx){ int fd = ctx->fd; bool success = swoole_websocket_handshake(ctx); if (success) { swoole_websocket_onopen(serv, ctx); } else { serv->close(serv, fd, 1); } if (!ctx->end) { swoole_http_context_free(ctx); } return sw_ok;}复制代码
追踪进 swoole_websocket_handshake() 里,前面部分都是设置响应的 header,响应报文则是在 swoole_http_response_end() 里发出的,它的结果也就是 swoole_websocket_handshake 的结果。
// swoole_websocket_server.ccbool swoole_websocket_handshake(http_context *ctx){ ... swoole_http_response_set_header(ctx, zend_strl(upgrade), zend_strl(websocket), false); swoole_http_response_set_header(ctx, zend_strl(connection), zend_strl(upgrade), false); swoole_http_response_set_header(ctx, zend_strl(sec-websocket-accept), sec_buf, sec_len, false); swoole_http_response_set_header(ctx, zend_strl(sec-websocket-version), zend_strl(sw_websocket_version), false); ... ctx->response.status = 101; ctx->upgrade = 1; zval retval; swoole_http_response_end(ctx, nullptr, &retval); return z_type(retval) == is_true;}复制代码
从 swoole_http_response_end() 代码中我们发现,如果 ctx->keepalive 为 0 的话则关闭连接,断点调试下发现还真就是 0。至此,连接断开的地方我们就找到了,下面我们就看下什么情况下 ctx->keepalive 设置为 1。
// swoole_http_response.ccvoid swoole_http_response_end(http_context *ctx, zval *zdata, zval *return_value){ if (ctx->chunk) { ... } else { ... if (!ctx->send(ctx, swoole_http_buffer->str, swoole_http_buffer->length)) { ctx->send_header = 0; return_false; } } if (ctx->upgrade && !ctx->co_socket) { swserver *serv = (swserver*) ctx->private_data; swconnection *conn = swworker_get_connection(serv, ctx->fd); // 此时websocket_statue 已经是websocket_status_active,不会走进这步逻辑 if (conn && conn->websocket_status == websocket_status_handshake) { if (ctx->response.status == 101) { conn->websocket_status = websocket_status_active; } else { /* connection should be closed when handshake failed */ conn->websocket_status = websocket_status_none; ctx->keepalive = 0; } } } if (!ctx->keepalive) { ctx->close(ctx); } ctx->end = 1; return_true;}复制代码
最终我们找到 ctx->keepalive 是在 swoole_http_should_keep_alive() 里设置的。从代码我们知道,当 http 协议是 1.1 版本时,keepalive 取决于 header 没有设置 connection: close;当为 1.0 版本时,header 需设置 connection: keep-alive。
websocket 协议规定,请求 header 里的 connection 需设置为 upgrade,所以我们需要改用 http/1.1 协议。
int swoole_http_should_keep_alive (swoole_http_parser *parser){ if (parser->http_major > 0 && parser->http_minor > 0) { /* http/1.1 */ if (parser->flags & f_connection_close) { return 0; } else { return 1; } } else { /* http/1.0 or earlier */ if (parser->flags & f_connection_keep_alive) { return 1; } else { return 0; } }}复制代码
解决问题从上面的结论我们可以知道,问题的关键点在于请求头的 connection 和 http 协议版本。
后来问了下运维,生产环境的 lb 会在转发请求时,会将 http 协议版本修改为 1.1,这也是为什么只有 beta 环境存在这个问题,nginx 的 access_log 也印证了这一点。
那么解决这个问题就很简单了,就是手动升级下 http 协议的版本,完整的 nginx 配置如下。
upstream service { server 127.0.0.1:1215;}server { listen 80; server_name dev-service.ts.com; location / { proxy_set_header host $http_host; proxy_set_header scheme $scheme; proxy_set_header server_port $server_port; proxy_set_header remote_addr $remote_addr; proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for; proxy_set_header upgrade $http_upgrade; proxy_set_header connection $connection_upgrade; proxy_http_version 1.1; proxy_pass http://service; }}复制代码
重启 nginx 后,websocket 终于正常了~
以上就是学习在swoole源码中查询 websocket 的连接问题的详细内容。