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


Posted in PHP onAugust 30, 2020

问题

我们项目的 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 连接建立成功,但是又定时重连的问题。

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!.r
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
    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.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 终于正常了~

总结

到此这篇关于Swoole源码中如何查询Websocket的连接问题的文章就介绍到这了,更多相关Swoole源码查询Websocket连接问题内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

PHP 相关文章推荐
建立文件交换功能的脚本(一)
Oct 09 PHP
利用PHP实现与ASP Banner组件相似的类
Oct 09 PHP
更改localhost为其他名字的方法
Feb 10 PHP
一个PHP针对数字的加密解密类
Mar 20 PHP
PHP实现加密的几种方式介绍
Feb 22 PHP
详解Window7 下开发php扩展
Dec 31 PHP
详解ThinkPHP3.2.3验证码显示、刷新、校验
Dec 29 PHP
PHP实现向关联数组指定的Key之前插入元素的方法
Jun 06 PHP
PHP迭代与递归实现无限级分类
Aug 28 PHP
PHP-X系列教程之内置函数的使用示例
Oct 16 PHP
PHP实现QQ、微信和支付宝三合一收款码实例代码
Feb 19 PHP
PHP新手指南
Apr 01 PHP
PHP常用header头定义代码示例汇总
Aug 29 #PHP
PHP isset()及empty()用法区别详解
Aug 29 #PHP
PHP实现简单日历类编写
Aug 28 #PHP
PHP实现文件上传与下载
Aug 28 #PHP
PHP实现计算器小功能
Aug 28 #PHP
PHP实现简易图形计算器
Aug 28 #PHP
PHP实现简单的计算器
Aug 28 #PHP
You might like
php select,radio和checkbox默认选择的实现方法
2010/05/15 PHP
php 如何获取数组第一个值
2013/08/06 PHP
PHP 面向对象程序设计(oop)学习笔记 (二) - 静态变量的属性和方法及延迟绑定
2014/06/12 PHP
PHP笛卡尔积实现算法示例
2018/07/30 PHP
jquery构造器的实现代码小结
2011/05/16 Javascript
获取当前点击按钮的id用this.id实现
2014/03/17 Javascript
jquery 自定义容器下雨效果可将下雨图标改为其他
2014/04/23 Javascript
NodeJS Express框架中处理404页面一个方式
2014/05/28 NodeJs
深入理解javascript原型链和继承
2014/09/23 Javascript
js的toUpperCase方法用法实例
2015/01/27 Javascript
js与jquery实时监听输入框值的oninput与onpropertychange方法
2015/02/05 Javascript
jquery实现的Accordion折叠面板效果代码
2015/09/02 Javascript
Bootstrap每天必学之滚动监听
2016/03/16 Javascript
Ionic实现页面下拉刷新(ion-refresher)功能代码
2016/06/03 Javascript
一种基于浏览器的自动小票机打印实现方案(js版)
2016/07/26 Javascript
JQuery实现文字无缝滚动效果示例代码(Marquee插件)
2017/03/07 Javascript
深入理解vue Render函数
2017/07/19 Javascript
Vue组件间通信方法总结(父子组件、兄弟组件及祖先后代组件间)
2019/04/17 Javascript
JavaScript计算正方形面积
2019/11/26 Javascript
Vue如何获取数据列表展示
2019/12/11 Javascript
[00:37]DOTA2上海特级锦标赛 Secert 战队宣传片
2016/03/03 DOTA
Django密码系统实现过程详解
2019/07/19 Python
Python绘制股票移动均线的实例
2019/08/24 Python
pip install python 快速安装模块的教程图解
2019/10/08 Python
tensorflow使用range_input_producer多线程读取数据实例
2020/01/20 Python
基于Tensorflow:CPU性能分析
2020/02/10 Python
基于python调用jenkins-cli实现快速发布
2020/08/14 Python
Python中qutip用法示例详解
2020/10/02 Python
Python headers请求头如何实现快速添加
2020/11/03 Python
详解pycharm自动import所需的库的操作方法
2020/11/30 Python
python绘图pyecharts+pandas的使用详解
2020/12/13 Python
详解CSS3新增的背景属性
2019/12/25 HTML / CSS
为中国消费者甄选天下优品:网易严选
2016/08/11 全球购物
JBL澳大利亚官方商店:扬声器、耳机和音响系统
2018/05/24 全球购物
Topshop美国官网:英国快速时尚品牌
2019/05/16 全球购物
公司停电通知
2015/04/15 职场文书