>  기사  >  PHP 프레임워크  >  Swoole 소스 코드에서 Websocket 연결 문제를 쿼리하는 방법 알아보기

Swoole 소스 코드에서 Websocket 연결 문제를 쿼리하는 방법 알아보기

coldplay.xixi
coldplay.xixi앞으로
2020-11-17 17:00:043028검색

Swoole Tutorial 칼럼에서는 Websocket 연결 문제를 쿼리하는 방법을 소개합니다.

Swoole 소스 코드에서 Websocket 연결 문제를 쿼리하는 방법 알아보기

추천: swoole 튜토리얼

문제

우리 프로젝트의 Websocket 서버는 Swoole을 사용하고 있습니다. 최근 베타 환경을 구축할 때 Websocket 프로토콜이 성공적으로 업그레이드되었지만 예약된 재연결, 하트비트 및 데이터가 전송되지 않았습니다. 프로젝트의 프로덕션 환경은 베타 버전과 일치하지만 프로덕션 환경에는 이러한 문제가 없습니다.

Positioning 문제

Swoole 디버깅을 용이하게 하기 위해 로컬 환경에서 다음과 같은 테스트를 진행합니다.

PHP 로그 확인

PHP 로그에서 오류 로그를 발견했습니다: ErrorException: SwooleWebSocketServer::push(): 연결[47]의 연결된 클라이언트가 웹소켓 클라이언트가 아니거나 닫혔습니다, 웹소켓 연결이 종료되었음을 의미합니다. 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()

패킷 캡처🎜연결이 끊어졌으니, 누가 적극적으로 연결을 끊었는지 확인해보세요. Swoole이 수신하는 포트는 1215입니다. tcpdump -nni lo0 -X port 1215를 통해 Swoole이 프로토콜 업그레이드에 대한 응답 메시지를 보낸 후 Fin 메시지 세그먼트, 즉, Swoole이 적극적으로 연결을 끊습니다. 연결이 열리므로 브라우저에 WebSocket 연결이 성공적으로 설정되었다고 표시되지만 주기적으로 다시 연결됩니다. 🎜
// // 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 소스 코드 추적🎜🎜🎜이제 우리는 Swoole이 적극적으로 연결을 끊었다는 것을 알고 있지만 언제 연결이 끊어졌고 왜 연결이 끊어졌습니까? 소스코드를 통해 알아봅시다. 🎜🎜🎜패킷 캡처 결과를 보면 응답 메시지를 보낸 뒤 연결을 끊는 데 걸리는 시간이 매우 짧은 것을 볼 수 있어 핸드쉐이크 단계에 문제가 있는 것으로 추측된다. 응답 메시지를 보면 Websocket 연결이 성공적으로 이루어졌음을 알 수 있습니다. swoole_websocket_handshake()의 결과가 true이면 연결이 이루어져야 한다고 추측됩니다. swoole_websocket_handshake() 가 닫힙니다. 🎜
// // 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으로 문의하시기 바랍니다. 삭제