1. 程式人生 > 程式設計 >Swoole原始碼中如何查詢Websocket的連線問題詳解

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 連線建立成功,但是又定時重連的問題。

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,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,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("Sec-WebSocket-Accept"),sec_buf,sec_len,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連線問題內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!