學習在Swoole源碼中查詢 Websocket 的連接問題

Swoole教程欄目介紹如何查詢Websocket的連接問題。

學習在Swoole源碼中查詢 Websocket 的連接問題

推薦:Swoole教程

問題

我們項目的 Websocket Server 使用的 swoole,最近在搭建 beta 環境的時候發現 Websocket 協議雖然升級成功了,但是會出現定時重連,心跳、數據也一直沒有發送。項目的生產環境和 beta 一致,但是生產環境確沒有這個問題。
學習在Swoole源碼中查詢 Websocket 的連接問題

定位問題

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

查看 PHP 日志

在 PHP 日志里,發現一條錯誤日志: ErrorException: SwooleWebSocketServer::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 終于正常了~

? 版權聲明
THE END
喜歡就支持一下吧
點贊7 分享