当前位置:首页 > PHP教程 > php高级应用 > 列表

Swoole源码中如何查询Websocket的连接问题详解

发布: 来源: PHP粉丝网  添加日期:2022-03-24 14:05:11 浏览: 评论:0 

这篇文章主要给大家介绍了关于Swoole源码中如何查询Websocket的连接问题的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。

问题

我们项目的 Websocket Server 使用的 Swoole,最近在搭建 beta 环境的时候发现 Websocket 协议虽然升级成功了,但是会出现定时重连,心跳、数据也一直没有发送,项目的生产环境和 beta 一致,但是生产环境确没有这个问题。

Swoole源码中如何查询Websocket的连接问题详解

定位问题

为了方便调试 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 连接建立成功,但是又定时重连的问题。

  1. 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 
  2.  
  3. 0x0000:  4500 00ec 0000 4000 4006 0000 7f00 0001  E.....@.@....... 
  4.     0x0010:  7f00 0001 04bf d23f 9377 304a 6d2f 9604  .......?.w0Jm/.. 
  5.     0x0020:  8018 18d6 fee0 0000 0101 080a 7621 9272  ............v!.r 
  6.     0x0030:  7621 9271 4854 5450 2f31 2e31 2031 3031  v!.qHTTP/1.1.101 
  7.     0x0040:  2053 7769 7463 6869 6e67 2050 726f 746f  .Switching.Proto 
  8.     0x0050:  636f 6c73 0d0a 5570 6772 6164 653a 2077  cols..Upgrade:.w 
  9.     0x0060:  6562 736f 636b 6574 0d0a 436f 6e6e 6563  ebsocket..Connec 
  10.     0x0070:  7469 6f6e 3a20 5570 6772 6164 650d 0a53  tion:.Upgrade..S 
  11.     0x0080:  6563 2d57 6562 536f 636b 6574 2d41 6363  ec-WebSocket-Acc 
  12.     0x0090:  6570 743a 2052 6370 3851 6663 446c 3146  ept:.Rcp8QfcDl1F 
  13.     0x00a0:  776e 666a 6377 3862 4933 6971 7176 4551  wnfjcw8bI3iqqvEQ 
  14.     0x00b0:  3d0d 0a53 6563 2d57 6562 536f 636b 6574  =..Sec-WebSocket 
  15.     0x00c0:  2d56 6572 7369 6f6e 3a20 3133 0d0a 5365  -Version:.13..Se 
  16.     0x00d0:  7276 6572 3a20 7377 6f6f 6c65 2d68 7474  rver:.swoole-htt 
  17.     0x00e0:  702d 7365 7276 6572 0d0a 0d0a            p-server.... 
  18. 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 
  19.     0x0000:  4500 0034 0000 4000 4006 0000 7f00 0001  E..4..@.@....... 
  20.     0x0010:  7f00 0001 d23f 04bf 6d2f 9604 9377 3102  .....?..m/...w1. 
  21.     0x0020:  8010 18e8 fe28 0000 0101 080a 7621 9272  .....(......v!.r 
  22.     0x0030:  7621 9272                                v!.r 
  23. 10: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 
  24.     0x0000:  4500 0034 0000 4000 4006 0000 7f00 0001  E..4..@.@....... 
  25.     0x0010:  7f00 0001 04bf d23f 9377 3102 6d2f 9604  .......?.w1.m/.. 
  26.     0x0020:  8011 18d6 fe28 0000 0101 080a 7621 9273  .....(......v!.s 
  27.     0x0030:  7621 9272                                v!.r 

追踪 Swoole 源码

我们现在知道了是 Swoole 主动断开了连接,但它是在什么时候断开的,又为什么要断开呢?就让我们从源码一探究竟。

从抓包结果看,发出响应报文到 close 连接的时间很短,所以猜测是握手阶段出了问题。从响应报文可以看出,Websocket 连接是建立成功的,推测 swoole_websocket_handshake() 的结果应该是 true,那么连接应该是在 swoole_websocket_handshake() 里 close 的。

  1. // // swoole_websocket_server.cc 
  2. int swoole_websocket_onHandshake(swServer *serv, swListenPort *port, http_context *ctx) 
  3.   int fd = ctx->fd; 
  4.   bool success = swoole_websocket_handshake(ctx); 
  5.   if (success) 
  6.   { 
  7.     swoole_websocket_onOpen(serv, ctx); 
  8.   } 
  9.   else 
  10.   { 
  11.     serv->close(serv, fd, 1); 
  12.   } 
  13.   if (!ctx->end
  14.   { 
  15.     swoole_http_context_free(ctx); 
  16.   } 
  17.   return SW_OK; 

追踪进 swoole_websocket_handshake() 里,前面部分都是设置响应的 header,响应报文则是在 swoole_http_response_end() 里发出的,它的结果也就是 swoole_websocket_handshake 的结果。

  1. // swoole_websocket_server.cc 
  2. bool swoole_websocket_handshake(http_context *ctx) 
  3.   ... 
  4.  
  5.   swoole_http_response_set_header(ctx, ZEND_STRL("Upgrade"), ZEND_STRL("websocket"), false); 
  6.   swoole_http_response_set_header(ctx, ZEND_STRL("Connection"), ZEND_STRL("Upgrade"), false); 
  7.   swoole_http_response_set_header(ctx, ZEND_STRL("Sec-WebSocket-Accept"), sec_buf, sec_len, false); 
  8.   swoole_http_response_set_header(ctx, ZEND_STRL("Sec-WebSocket-Version"), ZEND_STRL(SW_WEBSOCKET_VERSION), false); 
  9.  
  10.     ... 
  11.  
  12.   ctx->response.status = 101; 
  13.   ctx->upgrade = 1; 
  14.  
  15.   zval retval; 
  16.   swoole_http_response_end(ctx, nullptr, &retval); 
  17.   return Z_TYPE(retval) == IS_TRUE; 

从 swoole_http_response_end() 代码中我们发现,如果 ctx->keepalive 为 0 的话则关闭连接,断点调试下发现还真就是 0,至此,连接断开的地方我们就找到了,下面我们就看下什么情况下 ctx->keepalive 设置为 1。

  1. // swoole_http_response.cc 
  2. void swoole_http_response_end(http_context *ctx, zval *zdata, zval *return_value) 
  3.   if (ctx->chunk) { 
  4.     ... 
  5.   } else { 
  6.     ... 
  7.  
  8.       if (!ctx->send(ctx, swoole_http_buffer->str, swoole_http_buffer->length)) 
  9.     { 
  10.       ctx->send_header = 0; 
  11.       RETURN_FALSE; 
  12.     }  
  13.   } 
  14.  
  15.   if (ctx->upgrade && !ctx->co_socket) { 
  16.     swServer *serv = (swServer*) ctx->private_data; 
  17.     swConnection *conn = swWorker_get_connection(serv, ctx->fd); 
  18.  
  19.     // 此时websocket_statue 已经是WEBSOCKET_STATUS_ACTIVE,不会走进这步逻辑 
  20.     if (conn && conn->websocket_status == WEBSOCKET_STATUS_HANDSHAKE) { 
  21.       if (ctx->response.status == 101) { 
  22.         conn->websocket_status = WEBSOCKET_STATUS_ACTIVE; 
  23.       } else { 
  24.         /* connection should be closed when handshake failed */ 
  25.         conn->websocket_status = WEBSOCKET_STATUS_NONE; 
  26.         ctx->keepalive = 0; 
  27.       } 
  28.     } 
  29.   } 
  30.  
  31.   if (!ctx->keepalive) { 
  32.     ctx->close(ctx); 
  33.   } 
  34.   ctx->end = 1; 
  35.   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 协议。

  1. int swoole_http_should_keep_alive (swoole_http_parser *parser) 
  2.  if (parser->http_major > 0 && parser->http_minor > 0) { 
  3.   /* HTTP/1.1 */ 
  4.   if (parser->flags & F_CONNECTION_CLOSE) { 
  5.    return 0; 
  6.   } else { 
  7.    return 1; 
  8.   } 
  9.  } else { 
  10.   /* HTTP/1.0 or earlier */ 
  11.   if (parser->flags & F_CONNECTION_KEEP_ALIVE) { 
  12.    return 1; 
  13.   } else { 
  14.    return 0; 
  15.   } 
  16.  } 

解决问题

从上面的结论我们可以知道,问题的关键点在于请求头的 Connection 和 HTTP 协议版本。

后来问了下运维,生产环境的 LB 会在转发请求时,会将 HTTP 协议版本修改为 1.1,这也是为什么只有 beta 环境存在这个问题,nginx 的 access_log 也印证了这一点。

那么解决这个问题就很简单了,就是手动升级下 HTTP 协议的版本,完整的 nginx 配置如下。

  1. upstream service { 
  2.   server 127.0.0.1:1215; 
  3.  
  4. server { 
  5.   listen 80; 
  6.   server_name dev-service.ts.com; 
  7.  
  8.   location / { 
  9.     proxy_set_header Host $http_host
  10.     proxy_set_header Scheme $scheme
  11.     proxy_set_header SERVER_PORT $server_port
  12.     proxy_set_header REMOTE_ADDR $remote_addr
  13.     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for
  14.     proxy_set_header Upgrade $http_upgrade
  15.     proxy_set_header Connection $connection_upgrade
  16.     proxy_http_version 1.1; 
  17.  
  18.     proxy_pass http://service; 
  19.   } 

重启 Nginx 后,Websocket 终于正常了~

Tags: Swoole Websocket

分享到: