4、TCP/IP协议栈(上):TCP协议特点、三次握手与四次挥手、滑动窗口与流量控制
好,咱们今天聊聊TCP。说实话,在串口转以太网模块里,TCP是最核心的传输层协议。我做了这么多年嵌入式,见过太多因为TCP没搞明白导致项目翻车的情况。今天咱们就把TCP的几个关键点掰开揉碎了讲清楚。
4.1 TCP协议的特点
TCP是个啥?说白了,它是一个面向连接、可靠、基于字节流的传输层协议。你想想看,UDP就像寄明信片,扔进邮筒就不管了;TCP呢,像寄挂号信,每一封都要回执。
TCP的几个核心特点,我列一下:
- 面向连接:通信前必须先建立连接,通信完要释放连接
- 可靠传输:保证数据不丢、不乱、不重复
- 流量控制:防止发送方太快,接收方来不及处理
- 拥塞控制:防止网络太堵,大家互相谦让
- 全双工通信:两边可以同时收发数据
重要提醒:在嵌入式系统中,TCP的可靠性是有代价的。每个数据包都要确认,这增加了延迟和开销。我在做低功耗设备时,就不得不在TCP和UDP之间反复权衡。
4.2 三次握手:建立连接的过程
为什么叫三次握手?因为客户端和服务器要交换三个报文段才能建立连接。我刚开始学的时候也觉得奇怪,两次不行吗?咱们一步步看。
三次握手的流程是这样的:
- 第一次握手:客户端发送SYN报文,告诉服务器「我想跟你建立连接」
- 第二次握手:服务器回复SYN+ACK报文,表示「收到,我也准备好了」
- 第三次握手:客户端发送ACK报文,确认「好的,开始通信吧」
用代码表示一下这个过程:
// 伪代码:三次握手过程
// 客户端
send(SYN, seq=100); // 第一次握手
// 服务器
recv(SYN, seq=100);
send(SYN+ACK, seq=200, ack=101); // 第二次握手
// 客户端
recv(SYN+ACK, seq=200, ack=101);
send(ACK, seq=101, ack=201); // 第三次握手
我的经验:在串口转以太网模块里,三次握手失败最常见的原因是MTU不匹配。我曾经遇到一个设备,因为TCP MSS设置太大,导致握手包被分片,连接死活建不起来。后来把MSS改成1460就解决了。
你可能会问,为什么非要三次?两次不行吗?嗯,这里要注意:两次握手无法防止历史连接的干扰。比如客户端发了个SYN,网络延迟了,客户端以为丢了就重发。结果第一个SYN又到了服务器,如果只有两次握手,服务器就直接建立连接了,但客户端其实已经不需要了。三次握手可以避免这种「半连接」状态。
4.3 四次挥手:断开连接的过程
断开连接比建立连接复杂一点,需要四次挥手。为什么?因为TCP是全双工的,两边都要单独关闭自己的数据流。
四次挥手的流程:
- 第一次挥手:主动方发送FIN报文,表示「我这边没数据要发了」
- 第二次挥手:被动方回复ACK,表示「收到,但我可能还有数据要发」
- 第三次挥手:被动方发送FIN报文,表示「我这边也发完了」
- 第四次挥手:主动方回复ACK,表示「知道了,关闭吧」
避坑指南:我曾经在项目中遇到一个问题,设备频繁断开重连,导致系统资源耗尽。查了半天,发现是四次挥手时主动方没有进入TIME_WAIT状态,直接关闭了。结果被动方重传的FIN包没人处理,连接一直处于半关闭状态。记住:主动关闭方一定要进入TIME_WAIT状态,等待2MSL时间。
TIME_WAIT的时间一般是2分钟(2MSL),这个时间是为了确保被动方收到了最后的ACK。如果ACK丢了,被动方会重传FIN,主动方还能再回复一次。
4.4 滑动窗口与流量控制
滑动窗口是TCP实现流量控制的核心机制。说白了,就是接收方告诉发送方:「你一次最多发这么多,别把我撑爆了」。
滑动窗口的工作原理:
- 接收方在ACK报文里带上自己的窗口大小(win字段)
- 发送方根据窗口大小决定发送多少数据
- 窗口大小会动态变化,接收方处理得快,窗口就变大;处理得慢,窗口就变小
我画个简单的示意图:
发送方缓冲区:
| 已发送已确认 | 已发送未确认 | 可发送 | 不可发送 |
|--------------|--------------|--------|----------|
↑ ↑ ↑
已确认边界 窗口左边界 窗口右边界
窗口大小 = 窗口右边界 - 窗口左边界
关键点:滑动窗口的大小不是固定的。接收方可以通过减小窗口大小来告诉发送方「慢点发」,甚至可以把窗口设为0,让发送方暂停发送。这就是流量控制的核心思想。
在嵌入式系统里,滑动窗口特别重要。我记得有个项目,串口转以太网模块的接收缓冲区只有2KB,但对方服务器一次发来4KB数据。如果没有流量控制,数据直接溢出,丢得一塌糊涂。后来我在TCP握手时把窗口大小设为2048,服务器就乖乖地分两次发送了。
4.5 实际应用中的注意事项
讲完理论,咱们聊聊实际中怎么用。在串口转以太网模块里,TCP协议栈的实现通常有两种方式:
- 硬件协议栈:比如W5500、CH395这些芯片,TCP/IP协议栈固化在芯片里,MCU只需要发指令就行
- 软件协议栈:比如lwIP、uIP,在MCU上跑协议栈代码,灵活性高但占用资源多
我个人习惯用硬件协议栈做产品,稳定可靠,开发周期短。但如果你要做定制化功能,比如修改TCP参数、增加自定义选项,那就得用软件协议栈了。
我的建议:不管用哪种方式,一定要关注TCP的保活机制(Keep-Alive)。在串口转以太网场景下,串口数据可能很久才来一次,TCP连接可能被中间路由器断开。开启Keep-Alive,每隔一段时间发个探测包,能及时发现连接断开。
最后总结一下今天的内容:
- TCP是面向连接的可靠协议,适合对数据完整性要求高的场景
- 三次握手建立连接,四次挥手断开连接,每个步骤都有其必要性
- 滑动窗口实现流量控制,防止接收方被数据淹没
- 实际应用中要根据资源情况选择合适的协议栈实现方式
下一节咱们讲TCP的拥塞控制和超时重传,这两个在嵌入式系统里也经常踩坑。到时候我会分享一个因为RTO设置不当导致设备频繁断线的案例,很有意思。