深蓝黑背景封面:右侧大标题「SYN 打到没监听的端口,会收到什么应答」,副标题「对方协议栈回一个 RST」;左侧为时序示意图:上方左右各一个圆角方框「客户端」与「服务器(运行正常)」,各自向下引出虚线生命线;客户端向右发出标有「SYN → 端口 80」的青色箭头,服务器生命线上挂一个紫色描边小框标注「端口 80:未监听」,随后服务器向左返回标有「← RST 拒绝连接」的紫色箭头;下方注明「机器没宕机,是内核里没人监听这个端口,协议栈直接回 RST,连接立即失败」;左上标注「笔试复盘 · 网络」,右上圆角胶囊标注「Day 04 · 11-11」 图 1:本文封面,SYN 到达未监听端口,协议栈直接回 RST。

先说明属性:这是一篇笔试真题复盘,题目出自本地历史笔试资料,与任何公司的当前招聘无关。题面:向一台运行正常、但 80 端口没有进程监听的机器发送 TCP SYN 报文,会收到什么响应?答案:对方协议栈回一个 RST 报文,连接立即失败。注意题面里「机器运行正常」这个限定——它排除了主机宕机、网络中断这些干扰项,把问题聚焦到协议栈的行为上。

为什么是 RST:协议规范的直接规定。

TCP 规范 RFC 9293 在「复位生成」一节写得明白:如果连接不存在(处于 CLOSED 状态),对任何到达的报文段(RST 本身除外)都应回一个 RST;不匹配任何已有连接的 SYN,正是用这种方式被拒绝的。机器上没有任何套接字监听 80 端口,对内核来说这个 SYN 就不属于任何连接,于是按规范回 RST。

报文细节也值得说:由于入方向的 SYN 不带 ACK 位,规范要求回包序号置 0、ACK 位置位、确认号填「SYN 序号 + 报文长度」——所以精确地讲,回去的是一个带 ACK 标志的 RST。发送方收到后连接直接进入 CLOSED,connect 调用立刻以「连接被拒绝」返回,不重传、不等待。

一个 SYN 的三种命运。

把题面放进完整图景里看,一个 SYN 发出后只有三种结局。场景一,端口已监听:收到 SYN+ACK,客户端再回 ACK,三次握手完成,连接进入 ESTABLISHED。场景二,端口未监听(本题考点):协议栈回 RST,连接立刻失败——这是对端的明确表态。场景三,防火墙静默丢包:什么回复都没有,客户端只能按指数退避重传 SYN,几次之后超时放弃。

场景二和场景三必须分清:收到 RST 说明「主机在线、协议栈在工作、只是这个端口没人接」;超时无响应说明「包在路上被丢了,或被防火墙拦了」,对端主机可能完全正常。排障时把「拒绝」误读成「不通」,方向就错了。

端口扫描工具正是靠这三种结局给端口分类的:发出 SYN 后,收到 SYN+ACK 判 open,收到 RST 判 closed,迟迟无响应(或收到 ICMP 不可达)判 filtered。理解了这道题,扫描报告里每一行的含义就都能看懂了。

本地实测:拒绝来得有多快。

用 Node.js 在本机对比两种场景:向未监听的 18081 端口发起连接,1 毫秒内收到 ECONNREFUSED 错误——这就是 RST 到达后内核给应用层的答复;再向已起好监听服务的 18080 端口发起连接,握手成功、连接建立。代码如下:

import net from 'node:net';

function probe(port, label) {
  return new Promise((resolve) => {
    const sock = net.connect({ host: '127.0.0.1', port });
    const t0 = Date.now();
    sock.on('connect', () => {
      console.log(`${label}: 三次握手成功,耗时 ${Date.now() - t0}ms`);
      sock.end(); resolve();
    });
    sock.on('error', (err) => {
      console.log(`${label}: ${err.code},耗时 ${Date.now() - t0}ms`);
      resolve();
    });
  });
}

const server = net.createServer((s) => s.end());
await new Promise((r) => server.listen(18080, '127.0.0.1', r));
await probe(18081, '未监听端口');
await probe(18080, '已监听端口');
server.close();

实测输出:未监听端口 1ms 报 ECONNREFUSED,已监听端口握手成功。拒绝与成功的差异,就是对方内核里有没有一个监听套接字的差异。同一个 RST,换到别的语言栈里是 Java 的 ConnectException、Python 的 ConnectionRefusedError——报错文案各不相同,背后的报文是同一个。

还有一点容易误解:RST 并不只出现在建连期。RFC 9293 的复位规则覆盖整个状态机,对已建立连接的异常报文、服务端重启后收到的旧连接数据,同样会触发 RST——它是 TCP 表达「这个报文不属于任何现存连接」的通用手段,本题的「SYN 到未监听端口」只是其中最经典的一种。

深蓝黑背景概念图,标题「一个 SYN 的三种命运」。主体是三张竖向卡片并排对比:场景一「端口已监听」,文字时序为 SYN 发出、收到 SYN+ACK、再发 ACK,最终连接建立 ESTABLISHED;场景二「端口未监听」用青色描边高亮并标注「本题考点」,文字时序为 SYN 发出、对方内核发现没有套接字监听、协议栈直接返回紫色标注的 RST,连接立即失败;场景三「防火墙丢包」,文字时序为 SYN 发出后无响应、客户端重传 SYN、直到超时放弃,注明这与 RST 是完全不同的两种结果。左下方卡片画出 TCP 首部六个标志位 URG、ACK、PSH、RST、SYN、FIN 的格子,其中 SYN 与 ACK 用青色高亮、RST 用紫色高亮,表示本题用到的三个标志位。右下方卡片给出面试答法:收到 RST 说明主机在线但端口未监听;超时无响应说明中间链路丢包或被防火墙拦截;不要把「拒绝」误判成「不通」 图 2:三种命运对照,以及本题用到的三个 TCP 标志位。

面试现场的答题结构。

先一句话给结论:收到 RST(精确说带 ACK 位的 RST),连接立即失败。然后给出规范依据:连接不存在时对到达报文段回 RST,SYN 不匹配任何已有连接即被拒绝。接着主动扩展到三种命运对照,把 RST 与超时区分开——这是这题从「背答案」到「懂排障」的分水岭。最后落到工程语义:健康检查看到 connection refused,含义是「机器活着、端口没开」,而不是网络故障,排查方向应该指向服务进程为什么没起来。你在抓包或排障时遇到过哪些意料之外的 RST?欢迎在评论区分享当时是怎么定位的。