问题
我们项目的 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 ......."htmlcode">// // swoole_websocket_server.cc int 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.cc bool 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.cc void 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 终于正常了~
总结
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?