本机没监听的端口,Windows 让我等了 2 秒才说「连不上」

举报
yd_48584989 发表于 2026/10/31 08:11:39 2026/10/31
【摘要】 实测本机三种「连不上」:拒绝连接要 2039 毫秒、404 却带 application/json 头、JSON 解析抛 Expecting value。同一个 except 把它们压成一个 False,而三种情况该做的恢复动作完全不同。

早上跑健康检查,探测本机 9242 的 DevTools 接口,结果只有一句「不通」。翻回代码, 判断逻辑总共三行:请求 /json/version,成功就返回 True, except Exception 就返回 False。

这三行把至少三种完全不同的故障压成了同一个 False:端口没监听、 进程活着但接口卡住、接口返回的东西根本不是 JSON。三者的恢复动作分别是「把浏览器拉起来」、 「等一会儿」、「对面不是我以为的那个服务」,但在代码里它们长得一模一样。

上午我花十分钟把它们分开量了一遍,结果有两个意外。

实测数据

探测目标 结果 耗时
127.0.0.1:9242 TCP 连接 成功 2.68 ms(首次)、0.58~0.88 ms(后续三次)
127.0.0.1:9297~9301 TCP(五个都没监听) ConnectionRefusedError,errno 10061 2032~2060 ms(10 次全部)
GET /json/version 200,423 字节,application/json 75 ms(首次)、15.0~15.5 ms(后续)
GET /json/list 200,2350 字节,3 个 page target 14~16 ms
GET /json/nope 404,21 字节 15.4 ms

意外一:拒绝连接不是「立刻」

按常理,端口没人监听时内核应该回 RST,客户端毫秒级拿到 ECONNREFUSED。 我这台 Windows 上实测是 2039 毫秒,换五个端口、连测十次,全部落在 2032~2060 ms, 非常整齐。整齐说明它不是在等一个随机的东西,而是在等 Windows 的 SYN 重传间隔。

也就是说,发往 loopback 的 SYN 被丢掉了,而不是被拒绝。为什么会这样我只有一个推测: 机器上装的包过滤驱动选择丢包而不是回 RST。我没有去拆它,但后果很具体—— 任何 timeout=1 的端口探测,在「服务没起」这个最常见的场景下会先超时。

你拿到的是 socket.timeout,不是 ConnectionRefusedError。 这两个错误名指向同一个事实(服务没起来),但代码里的分支会走反: 超时容易被读成「它只是慢,再等等」,拒绝才被读成「肯定没起,直接拉起」。 于是最常见的故障走了最慢的恢复路径,而真正慢的情况又没有得到额外耐心。

# 这台机器上的实测,同一个事实两种错误
$ python -c "import socket;s=socket.socket();s.settimeout(6);s.connect(('127.0.0.1',9299))"
ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝,无法连接。
# 从 connect() 发起到抛异常:2039 ms

意外二:content-type 只是对面的一句自述

DevTools 的 HTTP 接口在遇到不认识的命令时返回 404,21 个字节的 body 是纯文本 Unknown command: nope,可响应头里明明白白写着 content-type: application/json; charset=UTF-8。

照着响应头去 json.loads(),得到的是这句:

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

这句报错今天出现了第二次,在另一个完全不相干的接口上,日志里是 活动 5a7/activity 异常: Expecting value: line 1 column 1 (char 0)。 那次是接口返回了空 body。两处的共同点是:同一个异常名,掩盖了两种不同的事实 ——一个是「404 文本」,一个是「200 空体」。想区分只能把状态码和字节数一起打出来, 单看异常类型永远分不开。

把一次探测拆成三层

def probe(port, timeout=4.0):
    # 第 1 层:TCP。区分「没人听」和「有东西在听」
    s = socket.socket()
    s.settimeout(timeout)
    try:
        s.connect(("127.0.0.1", port))
    except ConnectionRefusedError:
        return "down"        # 明确没起,可以直接拉起
    except socket.timeout:
        return "unknown"     # SYN 被吞:可能在启动,也可能被挡
    finally:
        s.close()

    # 第 2 层:HTTP 状态码。别用 content-type 判断
    r = urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=timeout)
    if r.status != 200:
        return "degraded"

    # 第 3 层:能不能解析。解析不了说明对面不是我以为的服务
    try:
        json.loads(r.read())
    except ValueError:
        return "wrong_service"
    return "up"

三层的区别在于「要不要动手」:down 立刻拉起;unknown 等十秒再判一次, 不要连发;degraded 和 wrong_service 都不能靠重启解决—— 对面还占着端口,重启只会多一个「数据目录已被占用」的新错误。

三条可复用的结论

  1. 本地探测的 timeout 也要按最坏情况给。我原来写 1 秒,理由是「本机还能慢到哪去」。 实测本机回一个「我不在」要 2 秒,1 秒的探测在正常场景下就必然误报。
  2. 把「连不上」拆成至少三种,并让每种对应一个动作。只返回布尔值的探测函数, 等于把后续所有决策都建立在猜测上。
  3. 判断服务身份靠 200 加可解析的 body,不靠 content-type。响应头是服务端的一句话, 不是一条承诺;我今天就看到一个 404 的纯文本响应自称是 JSON。

顺带记录一下这次探测的正面结果:接口热了之后 /json/version 稳定在 15 毫秒、 /json/list 稳定在 14~16 毫秒,423 字节和 2350 字节的响应体量级也一直没变。 这些数字本身没什么用,但它们构成了一条基线——哪天这两条路径突然变成 300 毫秒, 不用猜,直接去看是不是有人在同一台机器上抢 IO。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。