TCP 协议

引言

前面已经讲了传输层的UDP协议,UDP整个协议很简洁,应用也很广泛(详见UDP 协议)。位于传输层的第二个协议便是TCP协议,也是TCP/IP协议栈的核心内容(毕竟名字叫TCP/IP而不是UDP/IP)。

通信协议其本质上就是不同设备之间的数据交互,既然存在数据交互那么自然对传输可靠性有要求,在某些场景下我们是不能容忍丢包、乱序、重复等通信异常现象的:你比如ssh,在进行远程登录和控制时优先要保证数据可靠性,否则在文件传输以及关键命令执行时很可能会因为传输出错导致进入一些异常中间状态;平常我们使用最多的HTTP网页协议在传输时也需要一个可靠的协议进行连接管理和网页数据流传输。

在这些场景下UDP是达不到要求的,因为UDP在数据传输时并不提供可靠性保证。这时候就需要使用TCP协议,因为TCP面向连接,具备连接管理能力、能保证数据有序无重复、同时也有一套健壮的数据传输机制,可靠性有保证。

TCP协议十分复杂,本篇文章只简单描述其是什么,不深入任何实现机制。细节部分在其余文章中单独叙述。

TCP 协议报文格式

下图是TCP协议的报文头部结构。可以看到比起UDP的8字节头部,TCP报文结构更复杂,多了不少控制字段。

字段 长度 作用
源端口 16 位 发送方端口
目标端口 16 位 接收方端口
序列号 32 位 本报文数据第一个字节的编号
确认号 32 位 期望收到的下一个字节编号
数据偏移 4 位 头部长度,以 4 字节为单位(最小 5 = 20 字节)
保留 3 位 保留,必须为 0
标志位 9 位 见下表
窗口大小 16 位 接收方还能接收多少字节(用于流量控制)
校验和 16 位 覆盖 TCP 头 + 数据 + 伪首部
紧急指针 16 位 指向紧急数据的位置(很少用)
选项 可变 MSS、窗口缩放等

你可以把TCP头部看作是UDP的Plus版本,里面涵盖了UDP也有的基础控制字段。但是这里你可能会发现:UDP头部还有个16位的长度字段(指示数据段长度有多少),但是TCP并没有。按理来说长度应该是必要字段,为什么TCP可以没有?

这里就涉及到了UDP与TCP的本质区别之一:UDP是面向报文的协议,而TCP是字节流协议!这里展开讲讲

  • UDP之所以需要长度字段,是因为UDP是报文协议,每一次udp_send()的调用发送的都是一个独立UDP报文。这个报文多大?到哪里结束?因此就需要这个字段来划定报文边界。让接收方知道这包UDP数据多大,直接取走。
  • 而TCP作为字节流协议,它并不需要长度字段、不需要去保持边界,对面应用层写了几次、每条消息从哪到哪,TCP一概不管。只要接收缓冲区有数据它就按顺序逐字节往下处理就完事了。

不过这里对于TCP要强调一点:TCP不需要知道消息边界,但是需要知道每个报文段的长度。这里可能有点难以理解,但是必须搞清楚。因为TCP虽然只针对字节流,但是协议栈需要统计本段报文的总大小,以便更新自身的接收序列号以及缓冲区窗口,否则无法发出ACK、流控机制也会失效。

TCP头部并没有长度字段,因此接收到的这个报文段数据长度有多大,需要从IP头部进行推算:

IP包总长 - IP头部 - TCP协议头部 = 有效数据长度

如此一来接收到的该段TCP报文数据总长就知道了。到这里你可能会有个疑问:既然TCP可以这么算,那我同样的公式减去UDP头部不就可以算出UDP数据长度了吗?是的,没错。UDP自身的长度字段其实是冗余设计,不过好处是拿到头部后可直接使用,无需保存IP那边的信息,算是解耦设计吧(其实是设计出来后发现冗余再删已经不好删了[笑])。


TCP 协议核心特性

TCP协议引入了很多可靠性机制,例如:超时重传、拥塞控制、流量控制、按序传输等等,这里每一个都是UDP没有的,UDP那边如果想要实现得自行进行定制,而TCP标准已经规定并集成了这些机制。我们不需要自行去实现,但是需要其运作机制。

这里对于这些特性只进行简单的介绍,了解有这个玩意儿。更深入的细节不在此处讨论。

面向连接

UDP是无连接的,随发随收。但是TCP不能这么干,要进行一次TCP通信双方必须先建立连接,且必须拥有可用IP地址。TCP维护了一个复杂明确的连接状态机,在连接与断开连接的过程中存在多个状态之间的转换——也就是我们所说的三次握手(连接),四次挥手(断连)。

同样因为连接特性,相比起UDP,TCP的连接控制块十分复杂,其内部维护了各个机制所需的各种各种关键变量和定时器与状态机。总之记住——要进行TCP通信需要先和对方通过三次握手建立连接信道,通信完毕后双方需要通过四次挥手干净的断开连接。每条TCP连接双端都配置有一个复杂的TCP连接控制块。


字节流协议、传输有序可靠

UDP协议有自身独立的报文边界,不管你是发1字节,还是发1000字节,在应用层来看两次不同的各自独立的发送调用,协议栈是绝对不可能将其数据包进行融合的;而TCP不同,TCP是字节流协议,以单个字节作为通信的数据流单元。当然,TCP在数据传输过程中肯定不会是一个字节一个字节的往网络链路上放,那也太笨了。协议栈会将待发送的数据整体打包到一个数据段中,以段为单位进行发送。

这里要厘清TCP的报文段和UDP的数据包之间的区别:UDP数据包独立有边界,哪怕上层下发一个字节的数据,协议栈也会老老实实将这一个字节按UDP报文形式打包后发出;而对于TCP,由于涉及到Nagle算法以及窗口控制,TCP有时会将多次tcp_send()调用合并到一个段内部进行运输:

1
2
3
4
5
6
7
第一次:tcp_send() 数据AAABBCC  --> 挂入发送队列(假设发送队列为空):AAABBCC
第二次:tcp_send() 数据DDEEFF --> AAABBCCDDEEFF
第三次:tcp_send() 数据GHIGHI --> AAABBCCDDEEFFGHIGHI

由于内部机制原因导致三次发送被合并:AAABBCCDDEEFFGHIGHI <-整体被打包放入一个段中

接收方:一次性收到 AAABBCCDDEEFFGHIGHI.

分段机制可以灵活切分、并且根据当前本机和对端情况进行合理切分,从而提高通信效率。不过这也引入一个问题:多次独立发送被整合打包,消息边界不清晰,也就是我们所说的粘包。以一个本机设备向某TCP服务器通信为例(无上层协议):假如某次本机向服务器发布数据,由于分段特性两条发送时刻相近的命令被打包到一个段中发出了,那么这两条命令就废掉了。因为云端收到的是一整段,而没有消息边界根本不知道第一条命令从哪结束,第二条从哪开始,只好丢弃。

当然,粘包问题是可以通过软件逻辑进行解决的,最简单直接的办法就是给每条数据使用特殊字符\n等划清界限。关于粘包问题的详细解决,此处不展开。

要注意:UDP是不存在所谓的粘包的。


TCP的可靠性体现在这几个维度:序列号、确认号、超时重传、快速重传。

在通信中,每一个字节都被序列号进行单独编号,且前后字节流之间是连续编号。每发送一段数据,发送方都必须拿到对方返回的对这一段数据的ACK包。并在此等待期间启动超时定时器,若超时后依然没收到对方的ACK,则重发该段。且在连续收到3个重复ACK时,不用等待超时立刻重传该段,这就是快速重传。

作者

R6bandito

发布于

2026-10-14

更新于

2026-10-24

许可协议

评论

音乐
0:000:00