首頁 >php框架 >Swoole >學習在Swoole原始碼中查詢 Websocket 的連線問題

學習在Swoole原始碼中查詢 Websocket 的連線問題

coldplay.xixi
coldplay.xixi轉載
2020-11-17 17:00:043080瀏覽

Swoole教學欄位介紹如何查詢Websocket的連線問題。

學習在Swoole原始碼中查詢 Websocket 的連線問題

#推薦:swoole教學

#問題

我們專案的Websocket伺服器的Swoole,最近在搭建測試環境的時候發現Websocket協議雖然升級成功了,但是使用會出現定時重連,心跳、數據也一直沒有發送。項目的生產環境和測試環境一致,但是生產環境確實沒有這個問題。

定位問題

為了方便調試Swoole,以下測試是在本地環境下進行。

查看PHP日誌

在PHP 日誌裡,發現一條錯誤日誌: ErrorException: Swoole\WebSocket\Server::push(): theconnected client of connection[47] is not a websocket client or close

,說明Websocket連線已經關閉了。

抓包既然連線被關掉掉了,那我們來看看是誰主動關閉的連線。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知道主動斷開了連接,但它是什麼時候斷開的,又為什麼要斷開呢?就讓我們從源碼一探究竟。#########從包抓結果看,發出響應報文到關閉連接的時間很短,所以猜測是握手階段引發的問題。從回應封包可以看出,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 的連線問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除