概述

最近在接手一个 Web 推送的网关,主要功能其实就是通过 Socket.IO 协议接收前端的连接,然后推送消息给对应的前端,但是,在调试的时候,遇到了一个不能成功连接的问题,这里做一个总结。

本文使用的 golang lib 为:https://github.com/googollee/go-socket.io
node lib 为:https://github.com/socketio/socket.io

问题重现

具体的问题就是,当我用 Postman (是的,没错,Postman 开始支持 Websocket/Socket.IO 的功能了)连接我们的 Gateway 时,发现处于一直 Connecting 的状态,如图:

图 1:Socket.IO 处于 Connecting 状态

然后同事发现,如果我去掉 URL 中的 query param 就可以成功连接了:

图 2:去除 query param 后成功连接

问题定位

发现去除 query param 可以正常连接之后,我就感觉不应该是我的问题,但是,出于严谨,还是得一步一步地来,首先需要排除是我的业务代码有问题,于是我就用对应版本的 Golang Socket.IO 库写了一个最简单的 Hello World 程序,然后发现能复现问题,ok 这个就排除了我的业务代码的问题。

然后我看了一下,目前项目使用的版本还比较旧(1.0.1),然后就升级到最新版本(1.7.0),发现接口都变了(这个值得吐槽一下啊,同个大版本你居然变接口?),但是没关系,就修改一下 Hello World 的代码,然后再用 Postman 试一遍,发现不能复现,也就是说最新的版本是正常的。

处于对同个大版本修改接口的不信任,我尝试用官方原生的代码来尝试一下,看下标准的协议是如何实现的(Socket.IO 官方没有明确的相关文档说明),于是我就用了 Node Socket.IO 的 2.5.0 版本,发现同样的也是无法复现,也就是正常连接,那么最终可以认定是项目中使用的这个版本有 bug。

问题解析

既然知道是使用的版本代码有问题,那么就直接跟踪一下代码,发现问题出在 Connect 的数据包处理阶段,Server 端看上去是正常处理了,但是,返回给 Postman 之后,Postman 却不认为正常,从而忽略了这个 Connect 的响应包,从而导致连接的状态一直处于 Connecting 中。

从代码中,可以看到问题代码在这里

  1. [root@liqiang.io]# cat parser.go
  2. if next[0] == '/' {
  3. path, err := reader.ReadBytes(',')
  4. if err != nil && err != io.EOF {
  5. return err
  6. }
  7. pathLen := len(path)
  8. if pathLen == 0 {
  9. return fmt.Errorf("invalid packet")
  10. }
  11. if err == nil {
  12. path = path[:pathLen-1]
  13. }
  14. v.NSP = string(path)
  15. if err == io.EOF {
  16. return nil
  17. }
  18. }

Golang 的这个实现在解析 Connect 数据包的时候,Namespace 直接就使用请求的 Path,注意,这里的 Path 是原始未处理的,所以它是带 query param 的,也就是说它将 query param 也作为 namespace 的一部分;但是对于标准的 Node 实现,它的 Namespace 却是解析后的 URL Path,并且去除了 query param 的,源码为这里

  1. [root@liqiang.io]# cat lib/client.js
  2. Client.prototype.ondecoded = function(packet) {
  3. if (parser.CONNECT == packet.type) {
  4. this.connect(url.parse(packet.nsp).pathname, url.parse(packet.nsp, true).query);
  5. } else {
  6. var socket = this.nsps[packet.nsp];
  7. if (socket) {
  8. process.nextTick(function() {
  9. socket.onpacket(packet);
  10. });
  11. } else {
  12. debug('no socket for namespace %s', packet.nsp);
  13. }
  14. }
  15. };

可以发现这里解析 Namespace 的方式为:url.parse(packet.nsp).pathname,不包含 Query Param。所以 Golang 和 Node 的差异就在这里了。

一些疑问

首先我的第一个疑问就是我们的前端是可以正常连接带 Query Param 的,如果后端有这个问题的话,那么前端是不是也是可以处理这个问题?还是得从代码中寻找原因:源码,然后事实就是 Client 段的代码也是有特殊处理这段逻辑的。

那么 Postman 为什么不能正确处理?这个因为 Postman 的源码没有开源,从文档中也没有看到它使用的是开源的 Library 还是自己实现的,但是从问题上来看,应该也是不能正确地处理 Namespace 的问题。所以我创建了一个 ticket 来跟踪后续。

关键词