04.05 TCP 与长连接生命周期:握手、半关闭、心跳与重连
返回第四卷 · 地址与端点:04.02 · HTTP/WS:04.03 · 可靠字节流:04.04
04.05 TCP 与长连接生命周期:握手、半关闭、心跳与重连
DeepTutor 初稿经技术与教学审阅后的静态课程。序列号、连接状态、心跳与响应均为虚构纸上示例;本次没有运行 Go、抓包、联网、IM 服务或站点,也不声称测量了 OpenIM 的连接行为。
一、连接“还在”到底是 TCP、网关还是用户在线
虚构 u-a 打开教学 IM,建立到网关的 WebSocket 连接,向 c-a 提交 m-a,随后网络短暂中断。客户端可能仍显示“在线”,服务器可能还没发现断路,A 可能尝试重连,而旧连接上的写入也可能刚刚完成。04.04 只讲这条连接已建立期间的字节序号与 ACK;本章要给整条连接补上出生、空闲、关闭、异常和重连。
| 层次 | 什么可以成立 | 不能自动推出 |
|---|---|---|
| TCP 连接建立 | 两端 TCP 对序列号与连接状态达成建立条件 | u-a 已登录或有权读 c-a |
| WebSocket 升级成功 | HTTP 升级与 WebSocket 通道已建立 | 每条消息都获授权或 B 已收到 |
| 应用 Session 有效 | 网关可把请求关联到受信主体 | 设备当前一定可达、用户正在看屏幕 |
| 心跳近期有回应 | 某个协议/应用检查点在当时有反馈 | m-a 已持久或已读 |
| 业务回执 | 按明确业务合同完成某阶段 | 其他设备和用户阅读也完成 |
先画出这一条纸上生命线:连接目标→TCP 握手→TLS(若使用)→HTTP/WS 握手→应用认证→收发→空闲/探测→正常关闭或断路→重连。TLS 在 04.07 才系统讲,本章只把它放在正确位置;不能因为应用界面写“在线”,就跨过所有检查点。
二、三次握手先建立字节通道,再谈登录
一条 TCP 连接通常由源 IP/源端口/目标 IP/目标端口组成的四元组识别。服务器监听一个端口,可以接收多条来源不同的连接;u-a 的浏览器与手机即使同一个账号,也可能各有连接,不能拿 user_id 当 TCP 四元组。NAT、代理也可能改变网络端点,应用身份仍由后续认证建立。
用小序号看建立过程(数字只用于教学):
| 步骤 | A 客户端 TCP | 服务端 TCP | 此时新增的事实 |
|---|---|---|---|
| 1 | 发送 SYN, seq=100 | 等待连接 | A 提出初始序号 |
| 2 | 等待 | 回 SYN, seq=500, ACK=101 | B 确认 A 的 SYN,并提出自己的初始序号 |
| 3 | 回 ACK=501 | 收到最终确认 | 双方可按各自方向的后续序号传字节 |
SYN 本身占一个序列号,所以 B 期待 A 的下一序号 101,A 期待 B 的下一序号 501。随后 u-a 才可能发送 TLS/HTTP/WebSocket 握手并完成应用认证。TCP 握手成功不等于 Authorization 或 Cookie 已验证;若服务端返回 401 或拒绝 WebSocket Origin,底层 TCP 连接先前仍可能确实建立过。
HTTP/1.1 可在一条 TCP 连接上先后复用多个请求,WebSocket 则升级为长期双向通道;两者都不让四元组成为稳定的业务消息 ID。连接断开再建立,新的 TCP 序列空间和应用会话处理另起,m-a 的身份应由业务层保存。
三、FIN 是一个方向结束,半关闭仍可读另一方向
正常 TCP 关闭通常由一端发 FIN,意思是“我这一个方向没有更多字节要发送”;它不必表示另一方向也已结束。假设 A 已把请求字节写完,选择只关闭写方向;服务端读完这些字节后可看到该方向的结束,仍可把回应从服务端写方向送回 A。这叫半关闭。Go *net.TCPConn.CloseWrite 是“只关闭写侧”的入口;多数普通应用应让自己的 HTTP/WS 库管理连接关闭,不要为了演示半关闭随意关掉共享连接。
A → 服务端:请求字节 ... FIN (A 不再向此连接写)
A ← 服务端:仍可接收回应字节 (服务端写方向尚未关闭)
A ← 服务端:服务端之后也发 FIN (两个方向才都结束)FIN 与数据一样占序列空间;若 A 的最后数据字节到 104、FIN 使用 105,B 可确认到 106。应用读到 EOF 通常表示对端该方向有序结束,不等于对端数据库提交成功;应用还需解析已收到的完整协议正文并核对业务状态。CloseRead 则是 Go TCPConn 的读侧控制,与 CloseWrite 不同;大多数初学者先用正常的 Close 与框架生命周期,遇到明确的单向结束需求再用半关闭。
正常关闭后,主动发起关闭的一侧常经历 TIME_WAIT,用一段等待时间处理迟到的旧段和最终确认等问题。RFC 9293 用 2×MSL 描述协议等待;具体操作系统状态和可观察时长不应在此填成一个通用常数。TIME_WAIT 不等于“聊天用户还在线”,也不能凭服务器上有 TIME_WAIT 条目推断哪条 m-a 已发送成功。
四、RST 与断路:别把连接错误翻译成“消息未处理”
RST(reset)表示连接被异常重置/中止,和 FIN 的有序结束不同。它可能来自端点异常、连接状态不匹配或其他网络路径;Go 读写可能看到重置错误,但这个错误发生什么时候非常关键。如果 A 的请求字节已经到达并触发服务端写入,随后回应连接被重置,A 看到 RST 仍不能说数据库一定没保存;若请求根本未到服务端,又是另一种情况。需要业务消息身份、服务端阶段记录和可核验查询。
更难的是半开连接:一端进程崩溃或网络路径失效,另一端尚未观察到 FIN/RST,连接对象暂时还显示建立状态。只有后续读写、期限、TCP 探测或应用心跳让故障显现,才可做更强判断;不能把某个内存布尔值 connected=true 当成 B 设备当前可达。即便 TCP 最终报错,具体错误也不足以说明 m-a 是在请求解析前、事务提交前还是回应途中失败。
| 纸上现象 | 当前可知 | 当前未知 |
|---|---|---|
A Write 完成 | A 这次本地写调用已按接口返回 | 服务端应用是否已读、是否受理 |
A Read 得 EOF | 对端在该方向结束字节流,或该调用看见流结束 | 是否带了完整业务回应、B 是否送达 |
| A 看到 RST/连接重置 | 这条连接不能继续正常使用 | m-a 此前是否已在服务端提交 |
| A 等待超时 | 截止前没得到所需反馈 | 对端是否仍在处理、结果以后会怎样 |
状态不同,下一证据也不同。客户端可在新连接上沿同一个稳定消息 ID查询或按后续幂等合同处理;绝不能把网络错误统一改写为“肯定没有消息,所以生成新 ID 再发”。
五、TCP Keepalive、WebSocket Ping/Pong 与业务心跳
长连接空闲时,谁来判断另一端还可达?有三种常被叫“心跳”的机制,回答的问题并不一样:
| 探测 | 谁参与 | 一次成功最多说明 | 一次失败不宜立刻推出 |
|---|---|---|---|
| TCP Keepalive | TCP 协议栈的可选探测 | 对端 TCP 路径/协议栈近期有反馈 | 用户已退出、设备永久离线 |
| WebSocket Ping/Pong | WebSocket 协议端点的控制帧 | WS 对端在当时对 Ping 作了协议回应 | IM 业务处理循环已提交消息 |
| 应用业务心跳 | 客户端与服务端应用约定的消息 | 指定应用检查点近期可响应 | 用户正在看屏幕、m-a 已读 |
RFC 1122 将 TCP Keepalive 描述为可选机制,单次探测没有回应不应被当作连接必然死亡。RFC 6455 定义 WebSocket Ping/Pong 与 Close 控制帧;某些库可能自动回应 Pong,即使上层消息处理正拥塞。因此 Pong 不能代替“发送 m-a 已处理”的业务回执。若应用层设计自己的心跳,可携受控会话或设备关联,并明确间隔、等待期限、连续失败阈值和撤销后的权限检查;它仍只能给一个带时间窗的近期可达判断。
心跳越频繁,断路可能越早被发现,但会增加网络、CPU、电量与网关负载;太稀疏则会让在线展示更陈旧。中间代理也可能设置自己的空闲期限,导致 TCP 层探测/WS 心跳与外部关闭交错。这里不推荐一个通用秒数,后续要按实际移动网络、代理和产品在线定义测量选择。
六、重连是新连接,旧业务状态不随之消失
断路后 A 重连,至少会有新 TCP 建立、新 WebSocket 握手/应用认证、新连接身份。服务器上旧连接可能尚未察觉断开,出现短暂“两条连接都登记为 u-a”的情景;旧连接可能刚处理了 m-a,新连接又发出同一用户意图。应用要定义每设备连接标识、代际或替换政策,以及旧连接上的消息和订阅何时作废;不能仅以“新连接建好了”推断旧请求全部失败。
t0 旧连接提交 m-a,回应在网络上丢失
t1 A 认为断开,开始重连;旧连接在网关还未清理
t2 新 TCP/WS 连接完成,A 的登录身份再次验证
t3 A 查询 m-a 的受理状态,或按明确的重试合同继续这里的 message_id=m-a 是跨连接的业务身份,会话内 seq 是业务顺序,旧/新 TCP 序列号只属于各自连接。09.02 当前教学接口对重复 ID 返回 409,并不提供 S5 的“重试取得第一次结果”语义;因此 t3 的真正产品方案仍需稳定身份与查询/幂等设计。直接给重试生成一个 m-b,可能让一条用户意图变两条消息。
在线状态也要选主体粒度:浏览器 Tab、手机设备、用户账号、会话订阅可能各有不同在线定义。A 手机在线不代表电脑在线;网关刚有一个连接也不证明用户已读 m-a。只有先定义“哪个设备在多长时间窗内按何种证据可达”,在线数字才有可解释含义。多设备同步与真正送达在 S5 深化。
七、Go 连接资源与期限:谁建、谁关、谁等待
Go net 包的 Dial 建立客户端连接,Listener.Accept 接受服务端连接;Conn.Read/Write 处理字节,Close 释放连接资源。*net.TCPConn 还提供 CloseRead/CloseWrite,而 SetReadDeadline/SetWriteDeadline 可给阻塞读写设期限。这些是后续个人网络实践的 API 入口,本章不运行套接字,也不把示例当现有 IM 服务代码。
| 资源/动作 | 典型责任者 | 必须想清的失败 |
|---|---|---|
| 监听器 | 启动服务的应用组件 | 停机时停止 Accept,避免新连接继续进入 |
| 每条底层连接 | 创建/接收它的连接管理器 | 正常退出、异常重置、超时均要释放 |
| 连接上的读写任务 | 持有连接的协作任务 | 一侧结束后另一侧怎样退出,不留永久阻塞任务 |
| Read/Write deadline | 该操作的资源预算设计者 | 超时不证明对端未处理,复用连接时要更新期限 |
| 应用 Session/订阅 | 身份与业务状态管理 | 底层连接关闭后不留下可继续授权的旧订阅 |
net.Conn 的读写本身不是“传入 context.Context 就自动取消”的 API。若上层请求取消,必须由实际连接管理设计把取消关联到 deadline、关闭或任务退出;否则 goroutine 仍可能阻塞。反过来,随手在 handler 里关闭一个由 HTTP/WS 框架共享管理的连接也可能破坏其他操作。先画清谁拥有连接、谁能关闭、哪些任务依赖它,04.08 再做带帧与短读写的 Go 网络 I/O 实践。
八、交付连接生命线与分层练习
本章交付:一张 SYN/SYN-ACK/ACK 建立表,一张 FIN 半关闭与最终结束表,一张“m-a 响应丢失→RST/超时→新连接”的业务时序表,以及 TCP Keepalive、WS Ping/Pong、应用心跳的证据对照。每张表必须写明 A、网关 TCP、网关应用、B 设备各知道什么与不知道什么。所有数字和状态是纸上推演,不是实测抓包。
分层练习:先答,再展开反馈
1. TCP 四元组由哪四项组成?
源 IP、源端口、目标 IP、目标端口。
2. 同一 u-a 的浏览器和手机一定共用 TCP 连接吗?
不一定。账号身份与网络连接不是同一对象。
3. A 发 SYN seq=100,B 为何 ACK=101?
SYN 占一个序列号,B 下一步期待 101。
4. B 发 SYN seq=500,A 下一 ACK 应指向多少?
501。
5. 三次握手成功证明 A 有权读 c-a 吗?
不能。应用登录和资源授权在更高层。
6. FIN 表示两个方向同时都不再有数据吗?
不表示。它结束发送者的一个方向,另一方向仍可继续。
7. A 关闭写方向后还能读取服务端回应吗?
在半关闭连接中可以,前提是另一读方向仍开放。
8. FIN 后 A 读到 EOF 就代表数据库提交了吗?
不能。EOF 是字节流方向结束,业务结果须另核对。
9. TIME_WAIT 可直接解释为“聊天用户仍在线”吗?
不能。它是 TCP 关闭状态,不是用户在线证据。
10. TIME_WAIT 时长对所有操作系统都是固定同一秒数吗?
不应这样宣称;规范用 2×MSL 描述,实际观察还受实现影响。
11. RST 与 FIN 的核心差别是什么?
RST 中止/重置连接,FIN 表示一个方向有序结束。
12. A 看见连接重置能确定 m-a 没被受理吗?
不能。重置可能发生在服务端处理之后。
13. 对端崩溃后本端立刻一定看到 CLOSED 吗?
不一定。半开状态可持续到后续读写、超时或探测。
14. TCP Keepalive 一次探测无回应即可认定设备永久离线吗?
不应如此。探测可丢失,结论需按策略与多次证据限定。
15. WebSocket Pong 能证明 m-a 已写库吗?
不能。Pong 是协议端点对 Ping 的回应。
16. 应用心跳成功能证明人正在读屏幕吗?
不能,只说明相应应用检查点近期有回应。
17. 心跳越频繁,是否只有好处?
不是。它会增加网络、CPU、电量和网关负载。
18. 新连接建好后,旧连接一定已在服务器清理吗?
不一定。旧连接可能尚未被察觉断开。
19. 重连后的 TCP 序列号能用来查旧 m-a 吗?
不能。业务消息 ID 应跨连接稳定,TCP 序号只属连接。
20. 超时后换新 message_id 直接再发安全吗?
不一定,首次请求可能已经受理,会造成重复业务消息。
21. Go `context` 取消会自动取消任意 `net.Conn.Read` 吗?
不能假定。需要连接管理设计通过期限、关闭或任务协作响应取消。
22. 一条连接在线可代表所有设备已收到 m-a 吗?
不能。在线粒度、设备交付与已读都需独立定义和证据。
来源与下一步
- RFC 9293:TCP:三次握手、FIN、RST、半关闭和 TIME_WAIT。
- RFC 1122:TCP Keepalive与RFC 6455:WebSocket:可选传输探测及 Ping/Pong/Close 控制帧。
- Go
net包:连接、半关闭与期限的 API 入口。
按学习路线,下一章04.06 流量与拥塞控制将区分慢接收方与路径拥塞,并用带宽时延积解释有限缓冲和尾延迟;04.07 再补 TLS。离开本章前,要能分别说清这条 TCP 连接在哪个阶段、登录身份是否有效、哪台设备近期可达、m-a 到了哪个业务确认点。