「前言」文章内容大致是传输层协议,TCP协议讲解,续上篇UDP协议。
「归属专栏」网络编程
「主页链接」个人主页
「笔者」枫叶先生(fy)
目录
- 一、TCP协议介绍
- 二、TCP协议
- 2.1 解包与分用
- 2.2 谈谈可靠性
- 2.3 TCP的工作模式
- 2.4 确认应答(ACK)机制
- 2.5 16位序号与确认序号
- 2.6 16位窗口大小
- 2.7 六个标志位
- 2.7.1 SYN
- 2.7.2 FIN
- 2.7.3 ACK
- 2.7.4 PSH
- 2.7.5 URG
- 2.7.6 RST
- 2.8 TCP超时重传机制
一、TCP协议介绍
TCP(
Transmission Control Protoco
l)是一种面向连接的、可靠的传输协议,TCP全称为
"传输控制协议”,TCP人如其名,要对数据的传输进行一个详细的控制。
它位于传输层,用于在计算机网络中传输数据
TCP概述:
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
二、TCP协议
TCP协议格式如下:
TCP报头当中各个字段的含义如下:
- 16位源端口号:表示数据从哪一个进程来。
- 16位目的端口号:表示数据要到哪一个进程去(对端主机的进程)。
- 32位序号:用于标识TCP数据包中的字节流的序列号,用于实现有序传输。后序详谈
- 32位确认序号:用于确认接收方已经成功接收到的字节流序列号,用于实现可靠传输。后序详谈
- 4位首部长度:表示TCP报头的长度,以4字节为单位。
- 保留6位:保留字段,TCP暂时没有使用的字段。不谈
- 6位控制标志位:包括URG、ACK、PSH、RST、SYN、FIN,这里只学六个,后序详谈
- 16位窗口大小:用于流量控制,指示发送方可以发送的数据量。后序详谈
- 16位校验和:用于检测TCP报头和数据是否有错误。不谈
- 16位紧急指针:标识哪部分数据是紧急数据。
- 选项:可选字段,用于扩展TCP协议的功能。不谈
报头下来就是数据,即上层扔下来的数据,数据包括上层协议报头+数据,这个也不谈。
2.1 解包与分用
TCP如何将报头与有效载荷进行分离?即如何解包
TCP从下层获取到一个TCP报文后,虽然TCP不知道报头的具体长度,但报文的前20个字节是TCP的基本报头,并且这20字节当中涵盖了4位的首部长度。
TCP解包的过程如下:
- TCP获取到一个报文后,首先读取报文的前20个字节,
- 并从中提取出4位的首部长度,此时便获得了TCP报头的大小
- 注意:TCP基本报头的长度是20字节,无脑读取20字节
- TCP报头当中的4位首部长度只有4个比特位,即4位首部长度的取值范围是
0000 ~ 1111
,即最大长度是15,又因为4位首部长度的基本单位是4字节,所以15*4=60
字节 - 即TCP报头的最大长度是60字节,基本长度是20字节,即报头的取值范围是
[20 ~ 60]
- 拿到报头长度之后,就可以直接与有效载荷进行分离了
注意:不同的协议层对数据包有不同的称谓,在传输层叫做数据段(segment)
,在网络层叫做数据报 (datagram)
,在链路层叫做数据帧(frame)
TCP如何决定将有效载荷交付给上层的哪一个协议?即如何分用
应用层的每一个网络进程都必须绑定一个端口号。
- 服务端进程必须显示绑定一个端口号(端口号是公开的)。
- 客户端进程由系统动态绑定一个端口号。
TCP的报头中涵盖了目的端口号,因此TCP可以提取出报头中的目的端口号,找到对应的应用层进程,进而将有效载荷交给对应的应用层进程进行处理,分用完毕
如何解包与分用已经解决,封装就是逆过来
有一个问题,TCP报头问什么没有有效载荷的大小??
- 因为TCP是面向字节流的,即以字节流的形式传输数据的,TCP收到数据就直接交给上层了(TCP已经保证数据有序到达,即提供可靠性),数据怎样用怎么解析,是你上层的工作,与我TCP无关
面向字节流和可靠性下面再谈
收到一个报文,是如何找到曾经绑定特定端口号的进程?网络协议栈与文件是什么关系
- Linux内核中用哈希的方式维护了端口号与进程PCB之间的映射关系,因此可以通过目的端口号的方式快速找到其对应的进程PCB,进而找到对应的进程
- 这也印证了一个端口号为什么自能绑定一个进程
- 曾经套接字代码里,bind特定的port,实际上就是建立进程PCB与port之间的关系
找到进程了,如何把数据给给进程??
- 进程PCB里面有指向
filles_struct
表的指针,该表里面有上层打开的文件对应的文件描述符,通过文件符可以找到该文件,然后就可以找到该文件的缓冲区,然后把数据放入文件缓冲区里面,上层再从该缓冲区里面读取数据
注:以上只是简单概述,实际更加复杂
如何理解报头?
在上一篇UDP已经谈过,不再赘述
下面开始学习TCP的可靠性
2.2 谈谈可靠性
谈可靠性之前就先要谈什么是不可靠
为什么网络传输中,会存在不可靠?
- 计算机的各个硬件并不是孤立的,都是有密切联系的
- 内存和外设之间是用“线”连接起来的,这个线叫,IO总线
- 内存与CPU直接连接也是用“线”连接的,这个线叫系统总线
- 其实,内存与外设之间也有自己的协议,因为有协议的存在,就可以通过协议控制外设,即每个设备都有自己的标准文档。所以就产生了嵌入式相关的东西
- 这里存在一个问题,为什么没有听过内存与外设之间这类协议的可靠性问题?
- 因为传输距离近,这种问题根本不存在
但是如果是网络的话,因为两台主机之间相距太远,数据在经过某些中间设备可能会丢失,信号衰减导致比特位丢失、比特位翻转等等现象,即传输过程干扰因素增多。
出现这种问题仅仅是传输距离变远了
即所有的网络问题本质都是传输距离变远了,即网络传输出现不可靠现象的原因就是传输距离变远了
网络传输不可靠现象有哪些?
- 数据丢包、数据乱序、校验错误、数据重复等
下面都是围绕这些问题去解决的
如何传输距离变长了,存不存在绝对的可靠性??
比如有A、B两个人,相距500米进行对话
A:B你吃饭了么?(A不能保证这句话被B收到了)
只有B进行应答,A才能确认他讲的话被B收到了,
但是B不确定自己的话被A收到了,只有A再次应答才能确认
B看到A回复的内容,就已经确认自己的上一句话被A收到了
但是存在一个问题,最新的内容因为没有应答,无法确认对方是否收到
所以,我们认为:
- 只有收到了应答,历史的信息就能
100%
被对方收到了——确认应答了,可靠 - 双方通信,一定存在最新数据没有应答——即最新消息无法保证可靠性
所以,绝对的可靠性是不存在的,但是存在相对的可靠性,即一个报文只要收到了应答,就可以保证该报文的可靠性
2.3 TCP的工作模式
TCP的工作模式有两种
第一种(不是TCP真正的工作模式)
每一个发送的数据段,都要给一个确认应答,收到应答后再发送下一个数据段(串行)。这样做有一个比较大的缺点,就是性能较差
第二种(TCP真正的工作模式)
一次发送多条数据段(并行),对端再进行应答,这样就可以大大的提高性能
第二种情况是TCP的真正工作模式,即主流,但是也会存在第一种工作模式,第一种情况是很少的
无论是客户端发给服务端,还是服务端发给客户端,双方都需要应答,即TCP客户端和服务端双方的地位是对等的
这里只是浅浅提一下,下面才开始讲解,这里是为了方便理解序号和确认序号
2.4 确认应答(ACK)机制
TCP将发送出去的每个字节数据都进行了编号,这个编号叫做序列号。
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发,每次的ack都是上次收到上次数据序列号加1
注:ack也是一个TCP报文,只不过没有带数据,只有报头
2.5 16位序号与确认序号
32位序号
双方在进行网络通信时,允许一方向另一方连续发送多个报文数据,只要保证发送的每个报文都有对应的应答,就能保证这些报文被对方收到了
下面为了方便,下以单发送单应答为例:
- 在连续发送多个报文时,由于各个报文在进行网络传输时选择的路径可能是不一样的,因此这些报文到达对端主机的先后顺序也就可能和发送报文的顺序是不同的。
- 报文有序也是可靠性的一种,因此TCP报头中的32位序号的作用之一实际就是用来保证报文的有序性的。
TCP将发送出去的每个字节数据都进行了编号,这个编号叫做序列号。这个上面已经说过了
- 假设发送端要发送2000字节的数据,每次发送1000字节,会分为两个TCP报文来发送。发送端会为每个TCP报文设置序列号,对于第一个报文,序列号会填写1,对于第二个报文,序列号会填写1001。
- 接收端在接收到这两个TCP报文后,会根据TCP报头中的序列号对报文进行顺序重排(该动作在传输层进行),从而与发送端发送报文的顺序保持一致,再将其放入TCP的接收缓冲区中。
需要注意的是,TCP协议对于乱序报文的处理是在传输层完成的,而不需要应用层进行干预。
接收端收到的数据会根据序列号的顺序进行处理和组装,以确保数据的完整性和正确性。这样可以解决数据传输过程中的乱序问题,确保数据按照发送端的顺序传输到应用层进行处理。
32位确认序号
TCP报头当中的32位确认序号是告诉对端,我当前已经收到了哪些数据,你的数据下一次应该从哪里开始发,每一个ACK
都带有对应的确认序列号
根据上面的例子,假设主机B已经成功收到主机A的序列号为1的报文和序列号为1001的报文,则主机B会发送两个ACK确认报文:
- 第一个ACK报文的确认序号为
1001
,表示已经成功接收了序列号1-1000
的字节数据。 - 第二个ACK报文的确认序号为
2001
,表示已经成功接收了序列号1001-2000
的字节数据。
ack确认应答和确认序号:保证接收方已经收到了ack序号之前所有的报文
传输过程中报文丢失怎么办?
通过序号和确认序号还可以判断某个报文是否丢失,怎么办后序谈
为什么要用两套序号机制?
发送方有一套序号和确认序号,接收方也有一套序号和确认序号,为什么
因为TCP是全双工的,地位对等的,双方可能存在同时想给对方发送消息的情况:
- 双方发出的报文当中,不仅需要填充32位序号来表明自己当前发送数据的序号。
- 还需要填充32位确认序号,对对方上一次发送的数据进行确认,告诉对方下一次应该从哪一字节序号开始进行发送
2.6 16位窗口大小
TCP本身是具有接收缓冲区和发送缓冲区的(全双工):
- 接收缓冲区用来暂时保存接收到的数据。
- 发送缓冲区用来暂时保存还未发送的数据。
- 这两个缓冲区都是在TCP传输层内部实现的。
这个在上一篇UDP已经谈论过,不再赘述
窗口大小
- 当发送端要将数据发送给接收端时,它会将数据放入自己的发送缓冲区。但是发送缓冲区有一定的大小限制。
- 如果接收端处理数据的速度小于发送端发送数据的速度,就会出现一个问题:接收端的接收缓冲区会被填满。
- 这种情况下,发送端再次发送数据时,接收端的接收缓冲区已经没有空间了,这就会导致数据丢失,并引发一系列的问题,比如丢包重传。
为了解决这个问题,TCP的报头中有一个称为窗口大小的字段,它占用了16个比特位。这个窗口大小表示接收端当前接收缓冲区中的剩余空间大小,也即接收端当前能够接收数据的能力
接收端可以通过窗口大小字段告诉发送端自己的接收缓冲区还有多少空间。发送端可以根据这个窗口大小字段来调整发送数据的速度:
- 如果窗口大小字段较大,说明接收端的接收能力较强,此时发送端可以提高发送数据的速度。
- 如果窗口大小字段较小,说明接收端的接收能力较弱,此时发送端可以减小发送数据的速度。
- 如果窗口大小字段的值为0,说明接收端的接收缓冲区已经满了,此时发送端应该停止发送数据
注意:自己发送数据就填自己的窗口大小,对端发送数据也是填对端的窗口大小,这样双方就交换了自己缓冲区的接收能力
总之,窗口大小字段在TCP中起到了调控发送端和接收端之间数据传输速度的作用,确保数据传输过程中的流量控制。
2.7 六个标志位
六个标志位的作用:
URG(Urgent)
:表示紧急指针字段是否有效。如果设置了这个标志位,那么紧急指针字段的值将被解释为一个偏移量,指示紧急数据的结束位置。ACK(Acknowledgment)
:表示确认序号字段是否有效。如果设置了这个标志位,那么确认序号字段的值将被解释为期望收到的下一个数据的序号。
-PSH(Push)
:表示接收端应该尽快将数据交给应用层,而不需要等待缓冲区填满。该标志位通常在发送端需要立即将数据发送给接收端时设置。RST(Reset)
:表示重置连接。当接收端收到一个无法识别的序号或者收到一个不符合当前连接状态的报文时,会发送一个带有RST标志位的报文来重置连接。SYN(Synchronize)
:表示建立连接。在建立TCP连接时,发送端和接收端都会发送一个带有SYN标志位的报文来进行握手。FIN(Finish)
:表示结束连接。当发送端没有数据要发送了,或者接收到FIN报文后已经完成了数据的接收,就会发送一个带有FIN标志位的报文来结束连接
这六个标志位全部都是一个比特位,为1则说明该标志位被设置,为0则为假
在开始讲解前,先明确一个:
TCP报文是有类型的!
- TCP服务端给一个TCP客户端提供服务时,TCP服务端也在给其他许许多多的客户端提供服务
- 这时TCP就会收到大量的TCP报文,比如建立连接报文、断开连接报文、ack确认报文、重置连接报文等
- 而服务端要根据TCP报文的类型有不同的动作,比如是建立连接的报文,服务端就要进入三次握手的状态,而不是读取该报文
这些TCP报文该如何区分??
通过设置六个标记为即可完成对TCP报文进行区分
2.7.1 SYN
SYN(Synchronize)
:表示建立连接。
- 报文当中的SYN被设置为1,表明该报文是一个连接建立的请求报文。双方进入三次握手状态
- 只有在连接建立阶段,SYN才被设置,正常通信时SYN不会被设置。
三次握手后序详谈
2.7.2 FIN
FIN(Finish)
:表示结束连接。
- 报文当中的FIN被设置为1,表明该报文是一个连接断开的请求报文。双方进入四次挥手状态
- 只有在断开连接阶段,FIN才被设置,正常通信时FIN不会被设置。
四次挥手后序详谈
2.7.3 ACK
ACK(Acknowledgment)
:表示确认序号字段是否有效。
- 报文当中的ACK被设置为1,表明该报文可以对收到的报文进行确认。
- 一般除了第一个请求报文没有设置ACK以外,其余报文基本都会设置ACK,因为发送出去的数据本身就对对方发送过来的数据具有一定的确认能力,因此双方在进行数据通信时,可以顺便对对方上一次发送的数据进行应答。
SYN、FIN和ACK这三个后序详谈
2.7.4 PSH
PSH(Push)
,PSH被设置为1,表示发送端告诉接收端应该尽快将数据交给应用层
- 当发送端设置了PSH标志位,它会在发送的TCP报文段中将PSH标志位置为1。接收端收到带有PSH标志位为1的报文段后,会立即将该报文段中的数据交给应用层进行处理,而不会等待缓冲区填满或者等待一定的延迟时间。
read/recv读取数据问题
- 使用read/recv函数从缓冲区中读取数据时,如果缓冲区中有数据,read/recv函数会立即读取并返回数据。如果缓冲区中没有数据,read/recv函数会阻塞等待,直到缓冲区中有数据才会读取并返回。
- 然而,实际上接收缓冲区和发送缓冲区都有一个水位线的概念。水位线是一个阈值,只有当缓冲区中的数据量达到或超过水位线时,read/recv函数才会读取数据并返回。这是为了避免频繁的读取和返回操作,从而提高读取数据的效率。
- 举个例子,假设TCP接收缓冲区的水位线是100字节。只有当接收缓冲区中的数据量达到100字节时,read/recv函数才会读取这100字节的数据并返回。如果接收缓冲区中只有一点数据,read/recv函数不会立即读取并返回,而是等待更多的数据到达缓冲区。
- 另外,当报文中的PSH(Push)标志被设置为1时,实际上是告知对方操作系统尽快将接收缓冲区中的数据交付给上层,即使数据量还没有达到水位线。
总结起来,read/recv函数在读取数据时需要缓冲区中的数据量达到一定量(水位线)才能进行读取,并且PSH标志可以影响数据的交付时机。
2.7.5 URG
URG(Urgent)
:表示紧急指针字段是否有效。
16位紧急指针:标识哪部分数据是紧急数据。
- 双方在进行网络通信的时候,由于TCP是保证数据按序到达的,因为TCP可以通过32位序号来对这些TCP报文进行顺序重排
- 有时候发送端可能需要发送一些“紧急数据”,这些数据需要让对方上层快速处理。这时候就需要设置URG为1,表示紧急指针生效。
- 16位紧急指针可以找到紧急数据在哪里,即有效载荷中的偏移量
- 紧急指针只有一个,因此一次只能标识一个偏移量,而不是一个连续的区域。这意味着紧急数据只能是一个字节。
- 当接收端收到带有紧急指针生效的报文时,它会立即处理紧急数据,不会等待后续数据的到达。
实际上,在现代的TCP协议中,紧急指针的使用已经不常见了,99%的情况下都用不到
recv/send函数就有设置URG的参数
recv函数的第四个参数flags有一个叫做MSG_OOB的选项可供设置,其中OOB是带外数据(out-of-band)的简称,带外数据就是一些比较重要的数据,因此上层如果想读取紧急数据,就可以在使用recv函数进行读取,并设置MSG_OOB选项
与之对应的send函数的第四个参数flags也提供了一个叫做MSG_OOB的选项,上层如果想发送紧急数据,就可以使用send函数进行写入,并设置MSG_OOB选项。
2.7.6 RST
RST(Reset)
:表示重置连接。
- 当接收端收到一个无法识别的序号或者收到一个不符合当前连接状态的报文时,会发送一个带有RST标志位的报文来重置连接。
- 比如,服务器突然断电了,客户端与服务器没有进行四次挥手,此时客户端依旧认为与服务端建立着连接。服务器重启后,服务器会收到客户端发来的TCP报文。服务端就很疑惑,你都没有和我建立连接,你怎么要跟我通信,这时服务端就会把RST设置为1,把报文发送给客户端,让客户端与服务端进行重新连接。
- 反过来也是如此
在访问某些网站时,会出现以下字样,代表服务端寄了
以上就是TCP的基本报头
2.8 TCP超时重传机制
在谈论这个话题之前,先谈一个问题:
如何看待TCP的接收缓冲区?
- 这个接收缓冲区是字节流的,即可以把这个缓冲区看成是一个
char buffer[N]
,N就是缓冲区的大小,即可以把接收缓冲区看成固定大小的数组,即只要在数据里面的数据(每个字节)天然有了序号 - 这个数组序号给 32位序号和确认序号 提供了极大便利(该数据序号对32位序号和确认序号非常重要)
- 32位序号用于标识每个TCP报文段的起始字节在整个数据流中的位置,以便将其正确放置在接收缓冲区中(按照数组序号的需要进行放置)
- 这也意味着接收缓冲区对于接收到的数据没有分组或分段的概念,而是将所有字节按照到达的顺序存储在缓冲区中
- 这个缓冲区的大小是64KB,由16位窗口大小决定。如果发送的数据大于64KB,上层需要自己进行处理,将数据分成多个部分发送,每个数据均要小于64KB
TCP的超时重传机制
TCP的超时重传机制是为了确保数据能够可靠地传输而设计的。当发送端发送一个数据段后,会启动一个定时器。如果在定时器超时之前,发送端收到了对应的确认,那么定时器会被取消。如果在定时器超时之后,发送端还没有收到确认,那么就会重新发送该数据段。
TCP的超时重传机制可以分为以下几个步骤:
- 发送数据段:发送端将数据段发送给接收端,并启动一个定时器。
- 等待确认:发送端等待接收端发送确认。如果在定时器超时之前,发送端收到了确认,那么定时器会被取消。
- 定时器超时:如果在定时器超时之后,发送端还没有收到确认,那么就会认为该数据段丢失了,需要进行重传。
- 重传数据段:发送端重新发送丢失的数据段,并启动一个新的定时器。
- 等待确认:发送端等待接收端发送确认。如果在定时器超时之前,发送端收到了确认,那么定时器会被取消。
这个过程会一直重复,直到发送端收到了确认或者达到了最大重传次数。
丢包的两种情况
- 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发
但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了
- 当出现丢包时,发送方是无法辨别是发送的数据报文丢失了,还是对方发来的响应报文丢失了,因为这两种情况下发送方都收不到对方发来的响应报文,此时发送方就只能进行超时重传。
- 需要注意的是,当发送缓冲区当中的数据被发送出去后,该数据会在缓冲区暂存一段时间,以免需要进行超时重传,直到收到该数据的响应报文后,发送缓冲区中的这部分数据才可以被覆盖。(这个后序谈滑动窗口详谈)
如果是对方的应答报文丢失而导致发送方进行超时重传,此时接收方就会再次收到一个重复的报文数据。
如何处理这个重复的报文?
- 但此时也不用担心,接收方可以根据报头当中的32位序号来判断曾经是否收到过这个报文,从而达到报文去重的目的,去重也就是直接丢弃重复的报文
数据超时的时间如何确定??即定时器的时间有多长??
- 最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”
- 但是这个时间的长短,随着网络环境的不同,是有差异的
- 如果超时时间设的太长,会影响整体的重传效率
- 如果超时时间设的太短,有可能会频繁发送重复的包
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间
- Linux中(
BSD Unix
和Windows
也是如此),超时以500ms
为一个单位进行控制,每次判定超时重发的超时时间都是500ms
的整数倍 - 如果重发一次之后,仍然得不到应答, 等待
2*500ms
后再进行重传 - 如果仍然得不到应答,等待
4*500ms
进行重传,依次类推,以指数形式递增 - 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接
文章内容太多,下一篇见(以上就已经有一万字了,TCP内容真多…)
--------------------- END ----------------------
「 作者 」 枫叶先生
「 更新 」 2023.7.21
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
或有谬误或不准确之处,敬请读者批评指正。