在计算机网络中,传输层承上启下,一边对接应用层的各种服务,一边利用网络层提供的主机到主机通信能力,最终实现端到端的进程间数据传输。
说到传输层,绕不开的两个核心协议就是UDP和TCP------一个追求极致效率,一个强调可靠交付。
一、传输层
1.1 定位
网络层解决的是主机之间的通信问题,IP地址可以唯一标识一台主机。
*但一台主机上同时运行着多个网络应用,数据送到主机后,该交给哪个程序处理?*这就是传输层要回答的问题。

可以看到,传输层位于应用层和网络层之间,负责将应用层的数据按照不同协议的要求封装后交给网络层传输,同时将网络层收到的数据分发给对应的应用程序。
1.2 端口号
端口号(Port)是传输层用来标识主机上不同应用程序的编号。
可以把IP地址理解为一栋楼的地址,端口号就是楼里不同房间的门牌号。数据到达主机后,根据端口号就能找到对应的应用程序。

上图中,一台主机上同时运行着FTP、SSH、SMTP等多个服务器程序,每个程序绑定了不同的端口号。
当IP层把数据送到这台主机后,传输层根据目的端口号,就能准确地将数据交给对应的应用程序处理。
端口号是一个16位的整数,取值范围是0到65535。这个范围又被划分为两部分:
-
0 - 1023:知名端口号,分配给广泛使用的应用层协议,如HTTP的80端口、HTTPS的443端口、SSH的22端口等。这些端口号由IANA统一管理和分配。
-
1024 - 65535:动态端口号,由操作系统动态分配给客户端程序。当我们用浏览器访问网站时,浏览器会从这个范围中获得一个临时端口号,用来和服务器的80或443端口通信。
知名端口号:
| 协议 | 端口号 | 说明 |
|---|---|---|
| FTP | 21 | 文件传输协议 |
| SSH | 22 | 安全远程登录 |
| Telnet | 23 | 远程终端协议 |
| HTTP | 80 | 超文本传输协议 |
| HTTPS | 443 | 安全超文本传输协议 |
| DNS | 53 | 域名系统 |
| SMTP | 25 | 简单邮件传输协议 |
在Linux系统中,可以通过 cat /etc/services命令 查看系统中记录的知名端口号列表。编写自己的网络程序时,应当避开这些知名端口号,防止和系统服务产生冲突。
1.3 五元组
在TCP/IP协议中,一条通信由五个要素唯一确定,这就是所谓的"五元组":
-
源IP地址
-
源端口号
-
目的IP地址
-
目的端口号
-
协议号

服务器的IP地址是172.20.100.32,端口号是80。客户端B(172.20.100.33)使用端口2001访问服务器,客户端A(172.20.100.34)使用端口2002也访问同一个服务器。虽然目的IP和目的端口都相同,但源IP和源端口不同,因此是两条独立的通信。
可以用 netstat -n 命令查看当前系统中的网络连接,每一行记录都包含了这五个要素。
二、UDP协议
UDP(用户数据报协议)是传输层的另一个重要协议。与TCP相比,UDP的设计理念不同------简单、快速,不可靠。
2.1 报文格式
UDP的报文格式非常简洁,首部只有8个字节,包含四个字段:

各字段的含义:
-
16位源端口号:发送方的端口号,标识数据从哪个进程来。
-
16位目的端口号:接收方的端口号,标识数据要到哪个进程去。
-
16位UDP长度:整个UDP数据报的长度,包括首部和数据部分。
-
16位UDP校验和:用于检测数据在传输过程中是否出错。
2.2 特点
UDP的传输过程类似于寄信------写上地址直接投进邮筒,对方能不能收到、什么时候收到,寄信人都不管。UDP有三个特点:
(1)无连接
知道对端的IP和端口号就直接进行传输,不需要建立连接;
(2)不可靠
没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;
(3)面向数据报
不能够灵活地控制读写数据的次数和数量。
2.3缓冲区
UDP的缓冲区设计:
-
发送缓冲区:UDP没有真正意义上的发送缓冲区。调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。
-
接收缓冲区:UDP具有接收缓冲区。当数据到达时,先放入接收缓冲区,应用程序调用recvfrom时从缓冲区中读取。
**注意:**UDP的接收缓冲区不能保证收到的报文顺序和发送的顺序一致。因为IP网络本身是不保证按序交付的,UDP也不做排序处理。
另外: UDP的socket既能读也能写,这个概念叫做**全双工****:**同一个socket可以同时进行发送和接收操作。
2.4 注意事项
UDP首部中有一个16位的长度字段,这意味着一个UDP数据报的最大长度是64KB(包含UDP首部)。
在当今互联网环境下,64KB实在是一个很小的数字。一张高清图片、一段视频数据都远远超过这个大小。
如果需要传输的数据超过64KB,就需要在应用层手动分包,多次发送,并在接收端手动拼装。
2.5 应用层协议
虽然UDP不可靠,但它的简单和高效在很多场景下不可替代。以下是基于UDP的应用层协议:
-
DNS:域名解析协议。每
-
DHCP:动态主机配置协议。
-
TFTP:简单文件传输协议。
-
NFS:网络文件系统。
-
BOOTP:启动协议。
-
SNMP:简单网络管理协议。
当然,也包括你自己写UDP程序时自定义的应用层协议。很多在线游戏、实时视频传输也会选择UDP作为底层传输协议,然后在应用层实现必要的可靠性保证。
三、TCP协议
TCP(传输控制协议)是传输层中最复杂、功能最强大的协议。从名字就能看出来,它的核心是"控制"------对数据传输的方方面面进行精细的控制,以确保可靠、高效地交付。
3.1 报文段格式
TCP的报文段格式比UDP复杂得多。首部默认长度是20字节,如果包含选项字段,最长可以达到60字节。

源端口号和目的端口号:各16位,和UDP一样,用于标识发送和接收的进程。
32位序号:TCP将每个字节的数据都编了号,序号字段表示本报文段所发送数据的第一个字节的编号。
32位确认号:表示期望收到对方下一个报文段的第一个字节的序号。
4位首部长度:表示TCP首部有多少个32位字。
6位保留位:预留字段,目前未使用,置为0。
6位标志位:这是TCP报文中非常重要的字段,每个位都有独立的含义:
| 标志位 | 名称 | 含义 |
|---|---|---|
| URG | 紧急指针有效位 | 为1时表示紧急指针字段有效,有紧急数据需要优先处理 |
| ACK | 确认有效位 | 为1时表示确认号字段有效,建立连接后所有报文的ACK都为1 |
| PSH | 推送位 | 为1时提示接收端应用程序立刻从TCP缓冲区把数据读走 |
| RST | 复位位 | 为1时表示要求重新建立连接,携带RST的称为复位报文段 |
| SYN | 同步位 | 为1时表示请求建立连接,携带SYN的称为同步报文段 |
| FIN | 结束位 | 为1时表示通知对方本端要关闭了,携带FIN的称为结束报文段 |
16位窗口大小:用于流量控制,表示接收方当前还能接收多少字节的数据。窗口越大,网络吞吐量越高。
16位校验和:用于检测传输错误。但TCP的校验和是必须的,而且不仅包含首部,也包含数据部分。
16位紧急指针:只有当URG标志为1时才有效,标识紧急数据的末尾在报文段中的位置。
选项字段:最多40字节,用于支持一些额外的功能,如最大段大小、窗口扩大因子、时间戳等。
TCP首部的C语言定义,可以和上面的格式图对照着看:
cpp
// linux kernel include/linux/tcp.h
struct tcphdr {
__be16 source; // 源端口
__be16 dest; // 目的端口
__be32 seq; // 序号
__be32 ack_seq; // 确认号
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4, // 保留位
doff:4, // 首部长度
fin:1, // FIN标志
syn:1, // SYN标志
rst:1, // RST标志
psh:1, // PSH标志
ack:1, // ACK标志
urg:1, // URG标志
ece:1, // ECE标志
cwr:1; // CWR标志
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window; // 窗口大小
__sum16 check; // 校验和
__be16 urg_ptr; // 紧急指针
};
除了上面介绍的6个标志位,内核定义中还多了CWR和ECE两个标志位。这两个用于ECN(显式拥塞通知)机制,是对TCP拥塞控制的改进。
3.2 确认应答机制
TCP可靠性的核心机制之一就是确认应答(ACK)。简单来说,就是发送方发了数据之后,接收方要回复一个确认,表示"我收到了"。

基本过程:主机A向主机B发送了1-1000字节的数据,主机B收到后回复一个确认应答,确认号是1001,意思是"1000号之前的数据我都收到了,下一次你从1001开始发"。然后主机A再发送1001-2000字节,主机B再确认下一个是2001,以此类推。
TCP将每个字节的数据都进行了编号,这就是序列号。确认应答也是基于序列号的。

第1字节到第1000字节对应序号1,第1001字节到第2000字节对应序号1001,第2001字节到第3001字节对应序号2001。每个TCP报文段的序号字段就是该段第一个字节的编号。
例子:唐僧讲经,每讲一段就要问徒弟"听懂了吗?",徒弟说"听懂了",唐僧才继续讲下一段。这个一问一答的过程,就是确认应答机制。

确认应答机制保证了数据的可靠交付------只要收到了ACK,就说明对方确实收到了数据。
但如果ACK丢了怎么办?或者数据本身丢了怎么办?这就需要超时重传机制来解决。
3.3 超时重传机制
TCP通过超时重传来以上情况:发送方发送数据后,启动一个定时器,如果在特定时间内没有收到确认应答,就认为数据丢失了,重新发送一遍。

主机A发送了1-1000字节的数据,但数据在传输过程中丢失了。主机B根本没收到,自然也不会回复ACK。经过一段时间后,主机A的定时器超时了,于是重新发送一遍数据。这次数据成功到达,主机B回复了确认应答。
但还有另一种情况:数据成功到达了,但ACK丢了。

主机A等不到ACK,超时后重传了数据。这时候主机B会收到重复的数据,TCP需要能够识别出重复的报文并丢弃。
怎么识别重复报文呢?利用序列号,接收方根据序号就能判断哪些数据已经收到过,重复的直接丢弃。
超时时间应该设多长呢?这是需要权衡的问题:
-
设得太长:重传不及时,整体传输效率低。
-
设得太短:可能频繁重传,浪费网络带宽。
CP采用了动态调整超时时间的策略:
-
Linux中,超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
-
如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传。
-
如果仍然得不到应答,等待4*500ms进行重传。依次类推,以指数形式递增。
-
累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
3.4 连接管理机制
TCP是面向连接的协议,通信之前需要建立连接,通信结束后需要释放连接。
建立连接通过三次握手完成,释放连接通过四次挥手完成。

(1)三次握手
三次握手的过程:
第一次握手:客户端向服务器发送SYN报文(SYN=1),请求建立连接。此时客户端的状态从CLOSED变为SYN_SENT。报文中包含客户端的初始序列号(ISN)。
第二次握手:服务器收到SYN后,回复一个SYN+ACK报文(SYN=1, ACK=1)。ACK表示确认收到了客户端的SYN,SYN表示服务器也请求建立连接。此时服务器的状态从LISTEN变为SYN_RCVD。报文中包含服务器的初始序列号。
第三次握手:客户端收到SYN+ACK后,回复一个ACK报文(ACK=1),确认收到了服务器的SYN。此时客户端的状态变为ESTABLISHED。服务器收到这个ACK后,状态也变为ESTABLISHED。连接建立完成。
(2)四次挥手
四次挥手的过程:
第一次挥手:客户端发送FIN报文(FIN=1),表示"我没有数据要发送了,请求关闭连接"。此时客户端状态从ESTABLISHED变为FIN_WAIT_1。
第二次挥手:服务器收到FIN后,回复一个ACK报文,表示"我收到了你的关闭请求"。此时服务器状态从ESTABLISHED变为CLOSE_WAIT。客户端收到这个ACK后,状态变为FIN_WAIT_2。
注意,此时连接处于半关闭状态------客户端已经不能发送数据了,但服务器还可以继续发送数据。服务器可能还有一些数据没发完,需要继续发送。
第三次挥手:服务器发完所有数据后,也发送一个FIN报文,表示"我也发完了,可以关闭了"。此时服务器状态从CLOSE_WAIT变为LAST_ACK。
第四次挥手:客户端收到FIN后,回复一个ACK报文,表示确认。此时客户端状态从FIN_WAIT_2变为TIME_WAIT。服务器收到ACK后,状态变为CLOSED。客户端需要等待2MSL的时间后,才会变为CLOSED状态。
3.5 TCP状态转换
下面是TCP状态转换的汇总:

较粗的虚线表示服务端的状态变化,较粗的实线表示客户端的状态变化。
CLOSED是一个假想的起始点,不是真实状态。
3.6 TIME_WAIT状态
TIME_WAIT是TCP状态中最值得深入理解的一个状态。主动关闭连接的一方在发送最后一个ACK后,会进入TIME_WAIT状态,等待2MSL的时间才会回到CLOSED状态。
MSL(报文最大生存时间)是TCP报文在网络中能够存在的最长时间。RFC 1122中规定MSL为2分钟,但实际操作系统的实现通常更短,比如CentOS 7和Ubuntu上默认是60秒。
通过以下命令查看系统的tcp_fin_timeout值:

为什么需要TIME_WAIT?
原因有两个:
-
保证迟到的报文段消失:网络中可能存在一些迟到的报文段。如果连接立刻关闭,这些迟到的报文段可能被新的连接(使用相同的五元组)误接收,导致数据错误。
-
保证最后一个ACK可靠到达:假设最后一个ACK丢失了,服务器会重发FIN。如果客户端已经关闭了,就无法重发ACK了。
3.7 CLOSE_WAIT状态
和TIME_WAIT不同,CLOSE_WAIT是被动关闭方的状态。当被动关闭方收到对方的FIN后,回复ACK,就进入了CLOSE_WAIT状态。
CLOSE_WAIT状态的含义是:等待被动关闭方的应用程序调用close关闭连接。收到FIN只是表示对方不再发送数据了,但被动关闭方自己可能还有数据要发送。等数据发完了,调用close发送FIN,才会从CLOSE_WAIT进入LAST_ACK状态。
如果服务器上出现大量的CLOSE_WAIT状态,通常意味着服务器没有正确关闭socket------收到了对方的FIN,但自己没有调用close。这是一个BUG,会导致资源泄漏。加上对应的close调用即可。
3.8 滑动窗口
确认应答机制保证了可靠性,但如果每发一段数据就要等一个ACK,那传输效率也太低了。尤其是在网络延迟比较大的情况下,大部分时间都在等待。

停等协议的工作方式:发一段,等一个ACK,再发下一段。可以看到,数据传输的时间和等待ACK的时间是交替的,大量时间浪费在等待上。
那能不能一次多发几段数据,让多个段的等待时间重叠在一起呢?这就是滑动窗口的思路。

图中的窗口大小是4000个字节(四个段)。发送前四个段的时候,不需要等待任何ACK,直接发送。收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据,依次类推。
窗口越大,网络的吞吐率就越高。因为可以同时有更多的数据在网络中传输,等待时间被重叠了。
操作系统内核为了维护滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答。只有确认应答过的数据,才能从缓冲区删掉。

窗口分为已确认的部分、窗口内已发送未确认的部分、以及窗口内未发送的部分。当收到ACK后,窗口的左边界向右移动,新的数据可以被发送。
过几天再补先写到这。。。。