目录
1.TCP协议
1.1.什么是TCP协议
- TCP 是面向连接的运输层协议。应用程序在使用 TCP 协议之前,必须先建立 TCP 连接。在传送数据完毕后,必须释放已经建立的 TCP 连接
- 每一条 TCP 连接只能有两个端点,每一条 TCP 连接只能是点对点的(一对一)
- TCP 提供可靠交付的服务。通过 TCP 连接传送的数据,无差错、不丢失、不重复,并且按序到达
- TCP 提供全双工通信。TCP 允许通信双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓存和接受缓存,用来临时存放双向通信的数据
- 面向字节流。TCP 中的“流”指的是流入到进程或从进程流出的字节序列
1.2.为什么TCP叫传输控制协议
我们之前创建的TCP套接字,实际上sockfd会指向一个操作系统给分配好的socket file control block(socket文件控制块),而这个socket文件控制块内部会维护网络发送和网络接收的缓冲区,我们调用的所有网络发送函数,write send sendto等实际就是将数据从应用层缓冲区拷贝到TCP协议层,也就是操作系统内部的发送缓冲区,而网络接收函数,read recv recvfrom等实际就是将数据从TCP协议层的接收缓冲区拷贝到用户层的缓冲区中,而实际双方主机的TCP协议层之间的数据发送是完全由TCP自主决定的,什么时候发?发多少?发送时出错了怎么办?
这些全部都是由TCP协议自己决定的,这是操作系统内部的事情,和我们用户层没有任何瓜葛,这也就是为什么TCP叫做传输控制协议的原因,因为传输的过程是由他自己所控制决定的。
c->s和s->c之间发送使用的是不同对的发送和接收缓冲区,所以c给s发是不影响s给c发送的,这也就能说明TCP是全双工的,一个在发送时,不影响另一个也再发送,所以网络发送的本质就是数据拷贝。
- 应用层缓冲区是什么?
说应用层缓冲区怕大家感觉到抽象,其实所谓的应用层缓冲区就是我们自己定义的buffer,可以看到下面的6个网络发送接收接口都有对应的buf形参,我们在使用的时候肯定要传参数进去,而传的参数就是我们在应用层所定义出来的缓冲区。
这里多说一句,上面的六个接口在进行网络发送和网络读取数据的时候,都会做网络字节序和主机字节序之间的转换,recvfrom和sendto是程序员自己显示做转换,其余的四个接口是操作系统自动做转换,这是铁铁的事实!
1.2.TCP是面向字节流的
- 为什么 TCP 是面向字节流的协议?
当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。
这时,接收方的程序如果不知道发送方发送的消息的长度,也就是不知道消息的边界时,是无法读出一个有效的用户消息的,因为用户消息被拆分成多个 TCP 报文后,并不能像 UDP 那样,一个 UDP 报文就能代表一个完整的用户消息。
举个实际的例子来说明。
发送方准备发送 「Hi.」和「I am Xiaolin」这两个消息。
在发送端,当我们调用 send 函数完成数据“发送”以后,数据并没有被真正从网络上发送出去,只是从应用程序拷贝到了操作系统内核协议栈中。
至于什么时候真正被发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件。也就是说,我们不能认为每次 send 调用发送的数据,都会作为一个整体完整地消息被发送出去。
如果我们考虑实际网络传输过程中的各种影响,假设发送端陆续调用 send 函数先后发送 「Hi.」和「I am Xiaolin」 报文,那么实际的发送很有可能是这几种情况。
第一种情况,这两个消息被分到同一个 TCP 报文,像这样:
第二种情况,「I am Xiaolin」的部分随 「Hi」 在一个 TCP 报文中发送出去,像这样:
第三种情况,「Hi.」 的一部分随 TCP 报文被发送出去,另一部分和 「I am Xiaolin」 一起随另一个 TCP 报文发送出去,像这样。
类似的情况还能举例很多种,这里主要是想说明,我们不知道 「Hi.」和 「I am Xiaolin」 这两个用户消息是如何进行 TCP 分组传输的。
因此,我们不能认为一个用户消息对应一个 TCP 报文,正因为这样,所以 TCP 是面向字节流的协议。
当两个消息的某个部分内容被分到同一个 TCP 报文时,就是我们常说的 TCP 粘包问题,这时接收方不知道消息的边界的话,是无法读出有效的消息。
要解决这个问题,要交给上层的应用层。
2.TCP协议段格式
图中,每一行有 4 个字节(32位),解包步骤如下:
提取报头:
- 除了选项之外的报头叫做标准报头,一共 20 字节。
- 提取选项:根据 4 位首部长度获取报头的整体大小,减去 20 字节的标准报头(固定报头),得到选项。如果没有选项的话就能直接得到有效载荷。
提取有效载荷:有效载荷 = 报文-报头 (-选项)
- 16位源端口:发送方主机的应用程序的端口号
- 16位目的端口:目的主机的应用程序的端口号
- 32位TCP序列号:表示本报文段所发送数据的第一个字节的编号
- 32位TCP确认序号:接收方期望收到发送方下一个报文段的第一个字节数据的编号
- 4位TCP首部长度:数据偏移是指数据段中的“数据”部分起始处距离TCP报文段起始处的字节偏移量。确定TCP报文的报头部分长度,告诉接收端应用程序,数据(有效载荷)从何处开始
- 6位保留字段:为TCP将来的发展预留空间,目前必须全部为0
- 6位标志位:共有6个标志位,每个标志位占1个bit
- 16位窗口大小:表示发送该TCP报文的接受窗口还可以接受多少字节的数据量。该字段用于TCP的流量控制
- 16位校验和字段:用于确认传输的数据有无损坏 。发送端基于数据内容校验生成一个数值,接收端根据接受的数据校验生成一个值。两个值相同代表数据有效,反之无效,丢弃该数据包。校验和根据 伪报头 + TCP头 + TCP数据 三部分进行计算
- 16位紧急指针字段: 仅当标志位字段的URG标志位为1时才有意义。指出有效载荷中为紧急数据的字节数。当所有紧急数据处理完后,TCP就会告诉应用程序恢复到正常操作。即使接收方窗口大小为0,也可以发送紧急数据,因为紧急数据无须缓存
- 选项字段:长度不定,但长度必须是32bits的整数倍。内容可变,因此必须使用首部长度来区分选项的具体长度
TCP 的报头是变长的,包括固定的 20 字节和变长的选项。其中,“数据偏移”也叫做“首部长度”,它占固定 4 位,作用是保存报头整体的长度,以便接收端能够正确解析报文中的字段。
值得注意的是,虽然首部长度占 4 个比特位,4 个比特位能表示的范围 0~15。但是它的单位并不是1字节,而是4字节,所以它实际能表示的范围就是[0,60]字节。
而我们看到,选项上面的报头是固定字节大小,也就是20字节,那么说明选项最大可以有40字节。但是我们今天不谈选项,因为固定首部20字节,所以选项字段最长40字节。当没有选项字段时,首部长度字段为5(20 = 5*4),即0101。
每层协议的学习,我们都应该掌握这两个问题:
- 协议报头和有效载荷如何分离?
- 有效载荷如何向上交付?
- 第一个问题,协议报头和有效载荷如何分离。
我们看到,TCP首部包括20字节的固定长度首部和选项字段,同时固定首部中有一个首部长度字段(4bits)。
值得注意的是,虽然首部长度占 4 位,但是它的单位是 4 个字节,那么 4 个比特位能表示的范围 0~15,就能表示 0~60 字节。
首部长度最大可表示15(因为4位比特位最大是1111,即是15),但是它的单位是 4 个字节,即TCP首部最长为15 x 4 =60字节。因为固定首部20字节,所以选项字段最长40字节。当没有选项字段时,首部长度字段为5(20 = 5*4),即0101。
提取报头:
- 除了选项之外的报头叫做标准报头,一共 20 字节。
- 提取选项:根据 4 位首部长度获取报头的整体大小,减去 20 字节的标准报头(固定报头),得到选项。如果没有选项的话就能直接得到有效载荷。
提取有效载荷:有效载荷 = 报文-报头 (-选项)
这样子就 通过首部长度,我们就可以将TCP首部和有效载荷分离。
- 第二个问题,有效载荷如何向上交付。
TCP是传输层的,上层是应用层。而应用层程序会绑定端口号,TCP首部中有16位目的端口号,根据端口号做到向上交付。
接下来我们要进入TCP协议的深入学习,下面会出现例如这样子的图
这个SYN和ACK是哪里的东西啊?
我们一定要注意:
客户端和服务端基于TCP协议进行通信,每次互发消息的时候,发送的可是完整的tcp报文,即一定携带完整的TCP报头。
2.1.流量控制——窗口大小(16位)
说到可靠传输,那么不可靠传输是什么样的呢?
比如数据传输重复,出现乱序,出现丢包等等情况,都是不可靠。因此可靠的数据传输,一定要规避这些情况,
在TCP协议段格式中,在标准报头中,我们看到有个16位窗口大小,这是什么呢?
我们先补充一点知识来,
- 什么是流量控制
我们知道,TCP协议有发送缓冲区和接收缓冲区,无论是用户端还是服务端都是。
假设有一天,服务端太忙了,客户端在向服务端发送数据,但是服务端来不及调用read或者recv这样的接口来拿取数据,但是客户端并不清除服务端那边的情况,就一直向服务端发,服务端已经被写满了,依旧再发的话,那么就会导致出现大面积丢包现象。
因此,为了规避这种情况,当服务端的接收缓冲区空间紧张的时候,我们应该想办法让用户端发送数据的速度慢点,或者直接不发了。
所以,这种通过控制客户端发送数据的速度,以便能让服务端来得及处理数据,从而规避大面积丢包的情况,这种策略就叫做流量控制。
另外说下,TCP协议其实还有一个策略叫做数据重传,也就是如果数据丢包了,那么就重新传一份。
- 那我们丢包了重传不行吗?为什么还要流量控制呢?
因为我们也知道,一个TCP协议的报文,光是标准表头就是固定20字节大小,还不算选项和有效载荷,如果出现大面积丢包,重传确实可以解决问题,但是浪费的资源太多,效率太低效。
大家还需要理解一下:双方通信发送的每一条消息里面都会有TCP协议报头
- 如何实现流量控制?
答案就是服务端在返回给客户端的响应中,16位窗口大小就是服务端的接收缓冲区当前还剩多少空间。客户端就可以根据这个剩余的空间来制定合理的发送数据的速度。
并且,我们要能想到,服务端也可能会给客户端发消息,那么客户端也会给服务端响应,那么此时这个响应中的16位窗口大小就是客户端的接收缓冲区还剩多少空间。
说白了这个16位窗口大小就是对方的接收缓冲区还剩多少空间
也就是说,双方都可以进行流量控制。
2.2.确认应答机制
2.2.1.什么是确认应答机制
TCP保证数据安全传输的时候最基本的一个特点就是确认应答机制。
- 实际上为什么网络传输中会存在不可靠问题呢?
本质原因还是因为传输距离过长。
比如我在内蒙给广东的网友发送消息,那数据包其实是要经过很多的路由器结点进行数据包转发,穿过很多的局域网,在局域网内部经过双绞线(以太网技术常用的物理介质)传输,还要经过运营商的基站,数据包在如此之长的传输距离中很有可能会丢失,数据里面的比特位翻转,又或是数据包中的字节乱序,又或是数据包重复发送给我的广东网友(发送方可能以为数据包丢失了)。
- TCP应该如何解决网络传输时的不可靠问题呢?
需要确认应答(acknowledgement)机制
客户端在向客户端发送数据的时候,每次发送完一个数据,客户端不会马上接着发数据,而是会等待从服务端传来的应答后,再发送数据,这样就能避免数据传输中不可靠的问题
虽然数据包在网络中传输的距离过长,但只要我发送给我网友的消息有回复,有应答,那我就能判断我发的数据一定到达了我网友的主机上。
比如我问我网友,你TCP/IP学的怎么样啊最近?我网友给我回复说,我最近正学TCP的确认应答机制呢!那我立马就可以肯定我发送的数据经过网络传输后,我的网友一定收到了,因为网友对我发送的消息做出了回复。同样的,如果我没回复我的网友,那网友也不敢确定他说的话,我一定收到了,因为我还没有给他发送的消息做出回复呢!而这就是典型的确认应答机制!
但其实你可以发现,我和我的网友在发消息时,总会有最后一条消息是没有被确认的,无论最后一条消息是他发还是我发,所以我们可以得出结论,TCP并没有绝对的可靠性,只有相对的可靠性!事实上,不只是TCP,所有的协议都是没有绝对的可靠性!
所以TCP的可靠性永远不谈最新的消息,只谈论历史的消息,因为一定存在最新的一条消息是没有被应答的。
但是话又说回来,我们其实只要保证客户端到服务端的数据完好就可以了,服务端没必要非要知道自己的响应是否完好送达,因此客户端发送数据后,只要一段时间内没收到来自服务端的响应,那么无论什么原因,都认为这次发送失败了,就会进行重发。
总之,最新的一条消息(也就是应答),是没有应答的(看起来有点绕,也就是应答不需要应答)。
5.2.2.推导确认应答机制
我们直接来看看我们预想的是怎么做的?
这样子可靠性就保证了 ,但是这样子效率太低了。
难道我说一句话,你都要先回一句:“我听到了”,然后才说你想回应的话吗?这这个效率太低下了,正确的做法应该是将应答和想回复的消息一起发送回去
应该是下面这个
我们不保证最新一条信息的可靠性,我们只保证历史消息的可靠性!
但是现在又有问题了,难道我发信息之前都要等你的应答吗?没有你的应答我是不发了吗?
不可能。真实的TCP是发送消息时,会一次性发送一批数据段,确认应答时,也会一次性发送一批确认数据段。
发送方可以同时发送多条数据,接收方根据收到的消息给出回复,但存在“后发先至”问题,接收方收到的数据顺序被打乱,这可能引起严重的歧义。
例如下面这种情况,双方最终会造成误解。
针对“后发先至”问题,TCP的解决办法是给传输的数据和应答报文都进行编号。
5.3.2.确认号和序列号
客户端在接收到响应之前,还是会把数据存在缓冲区里。
首先,我们客户端要发送的数据,已经存在TCP的发送缓冲区(内核里面的那个)中了,因为TCP是面向字节流的,这个缓冲区我们可以看作是char类型的大数组,那么每一个空间就是一个字节,并且还有对应的下标,那么也就是说,每一个字节天然就有自己的编号。
我们拷贝在缓冲区里的数据是按顺序存储的。
我们只需要在缓冲区里的数据是按顺序存储的即可!!!
序列号
- 含义:序列号是指一个TCP报文段中第一个字节的数据序列标识。它表示在一个TCP连接中,该报文段所携带的数据的开始位置。序号是用来保证数据传输的顺序性和完整性的。
- 作用:在TCP连接建立时,双方各自随机选择一个初始序列号(ISN)。随后传输的每个报文段的序号将基于这个初始值递增,其增量为该报文段所携带的数据量(字节数)。通过这种方式,接收方可以根据序号重组乱序到达的数据片段,确保数据的正确顺序和完整性。如果接收到的报文段不连续,接收方可以通过TCP的重传机制请求发送方重新发送缺失的数据。
例如,如果一个报文段被赋予了序号100,并且它包含100字节的数据,那么这个报文段就代表了从序号100到199的数据。随后的报文段将继续这个序列。继续上面的例子,下一个报文段可能会开始于序号200,如果它包含50字节的数据,那么它就代表了从序号200到249的数据。
我们可以简单的理解为数据里面每个字节都有唯一的标识——序列号。
补充知识
- 在TCP中,当发送端的数据达到接收主机时,接收端主机会返回一个已收到消息的通知,这个消息叫做ACK(确认应答,PositiveAcknowlegement)
- 在TCP中,当发送端的数据达到接收主机时,接收端主机会返回一个已收到消息的通知,这个消息叫做ACK(确认应答,PositiveAcknowlegement)
- 每一个 ACK(Acknowledge应答) 都带有对应的确认序列号,意思是告诉发送者,我已经收到了确认号之前的所有数据,下一次你从确认号开始发.
- 服务器和客户端之间需要有办法能区分出, 当前这个报文是普通报文, 还是确认应答报文. 标志位中的 ACK 就可以解决, ACK 为 0 时, 表示这是一个普通的报文, 此时只有 32 位序号是有效的, 当 ACK 为 1 时, 表示这是一个应答报文, 这个报文的序号和确认序号都是有效的.
确认号
确认序号的定义却是这样的,确认序号的值表示接收方收到了确认序号之前的所有报文,而且是连续的报文,比如确认序号的值是11,那就代表接收方收到了10号及之前所有序号的报文,发送端下次从第11序号开始发送报文就可以了,所以确认序号的值从发送方的角度来理解,可以理解为发送方下一次发送报文时,报文的序号的值。
- 含义:确认应答号是接收方期望从发送方接收到的下一个报文段的序号。它实质上是接收方告诉发送方:“我已经成功接收到了哪个序号之前的所有数据,请从这个序号开始发送后续的数据。”
- 作用:确认应答号用于实现可靠性传输。当一个报文段被接收方正确接收时,接收方会发送一个ACK报文,其中包含的确认应答号是接收到的数据加上1(即接收方期望接收的下一个数据的序号)。通过检查这个确认应答号,发送方能够知道其发送的数据是否已被接收方正确接收,并据此决定是否需要重传某些数据段。
但是这里我们可以思考一个问题,
- 在这个场景下,我们其实只需要一个32位序号就可以解决问题了,没必要再使用一个确认序号,那TCP为什么要设计两个序号呢?
可以从两个场景来理解:
- 1.有时候,一个报文可能有双重身份,比如服务器除了返回应答,它还想给客户端传送数据,这种既是应答又是数据的响应,叫做捎带应答,那么此时确认序号就是为了告诉客户确认序号的前面的数据都已经收到了,序列号就是告诉客户端,从服务器发来的数据的第一个字节的序列号是多少的。
- 2.有时候,并不是总是客户端给服务器发消息,服务器也会给客户端发消息,双方地位是对等的,如果只用一个序号位,就搞不清谁发谁收,因此还是需要两种序号位。
另外我们要知道,服务端一般都是一批数据连着发,如果是发一个数据,还得等服务器返回一个应答之后再继续发,那样效率太低了。
但是这里有一个问题,就是这批数据本来是按一定顺序发送的,但是服务器却不一定按相应顺序接收的。这就是数据乱序问题。UDP是没有办法的,毕竟是不可靠传输,那么TCP是如何解决的呢?
这里还是得靠TCP报头中的32位序号,它除了能够确认应答,还能保证数据按序到达!
序列号和确认序号的作用:
- 将请求和应答一一对应起来;
- 确认序号表示的是它之前的数据已经全部收到;
- 允许部分确认应答丢失,或者不发送确认应答;
- 保证了 TCP 的全双工通信。
2.3.六位标志位
标志位在哪里啊?
标志位字段共6bit,有6个标志位,每个标志位1bit,即只有0和1两种状态。
有以下这6位标志位
TCP标志位 | 中文意思 | 作用 |
SYN | 同步标志 | 用于建立连接 |
ACK | 确认标志 | 用于确认收到数据 |
FIN | 结束标志 | 用于关闭连接 |
RST | 重连标志 | 用于重置连接 |
PSH | 催促标志 | 用于立即传输数据 |
URG | 紧急标志 | 用于指示紧急数据 |
接下来来详细介绍一下
1.ACK 表示本报文前面的确认号字段是否有效:只有当ACK=1时,前面的确认号字段才有效;
在TCP确认回复机制中,客户端和服务端任意一方发送数据后,另一方都需要给予应答以表明自己收到数据。在应答的报文中该标记位需要置1,同时应答的报文也可以携带数据。
TCP规定,建立连接后,ACK必须为1
2. SYN 请求建立连接。携带SYN标识的称为同步报文段。
作为服务端,我怎么知道客户端是想发信息还是想和我建立连接呢?那就需要这个标志位了。
SYN表示同步标记位,其实就是申请建立连接的标记位。
TCP是面向连接的协议,双方在正常通信之前需要先建立连接,建立连接是一个三次握手的过程。
- 第一次,Client 给Server发送请求连接,报文中携带SYN标记位来表明当前报文是和建立连接相关的。
- 第二次,Server接受Client的连接,并询问何时建立连接,报文中也会携带SYN标记位,同时会携带ACK来回复上一条请求。
- 第三次,Client 回复 Server,立马建立连接,这里只是单纯的回复,只要设置ACK即可。
那么问题来了,站在Client的角度,什么时候认为连接已经建立起来了呢?答案是第二次握手以后,即收到Server的回复。
然而站在Server的角度,建立连接的时候是在第三次握手,因为第二次握手只是Server端单方面同意了,Client端有没有同意是在第三次握手才知道。
注意:TCP虽然保证可靠性,但是TCP允许连接建立失败。
- client向server请求建立连接:当SYN=1,ACK=0时,表示该报文为请求建立连接的报文;
- server向client请求建立连接:当SYN=1,ACK=1时,表示同意建立连接;
只有在建立连接的前两次请求中SYN才为1。该报文称为同步报文段
3.FIN FIN表示结束标记位,可以理解为Finish,其实就是断开连接的标记位。
断开连接是一个四次挥手的过程。
- 为什么会有四次呢?
Client 单方面断开连接需要两次;Server端单方面断开连接需要两次,加起来就是四次。
谁先断开连接,这个没有限制,下面就假设Client先断开。
- 第一次挥手,Client 通知 Server 自己单方面断开连接,Client发送的报文中就会携带FIN标记位。( 注意:Client单方面断开连接以后,只是断开Client ->Server方向上的数据传输,此时Server可以给Client发送数据,反过来不行。)
- 第二次挥手,Server收到Client的通知请求,并给Client发送应答报文,报文中携带ACK标记位。
- 第三次挥手,Server 通知 Client 自己单方面断开连接,Server发送的报文中会携带FIN标记位。
- 第四次挥手,Client收到Server的通知请求,并给Server发送应答报文,报文中携带ACK标记位。
4.RST RST表示复位标记位。代表着重新建立连接。
三次握手建立连接并不一定能够成功建立连接,没人说三次握手一定能够成功,同样四次挥手也一样,就算连接建立成功了,那也是有可能断开的,比如单方面的将服务器主机电源拔掉,那连接不就会自动断开吗?
等服务器重启的时候,服务器不认为连接建立成功,但client还认为连接存在着,所以client就会给服务器一直发消息,服务器就会感觉很奇怪,连接都已经不存在了,你为什么还要和我通信呢?
所以此时服务器就会给client发送一个复位报文段,其报头中的RST标志位被置为1,告诉client说,你别再给我发消息了,连接早就异常断开了,你再重新发起三次握手,重新和我建立连接吧。
所以复位标志位用于通信双方中,任何一方认为建立连接不一致时,认为连接异常的一方会发送复位报文段,告知对方我们需要重新建立连接。
我们看几个例子
- 1.服务端过载导致重连
客户端与服务器通过三次握手成功建立连接,但是正常通信时服务器端的操作系统资源满载,导致服务器无法对客户端做出应答,由于服务端建立连接后也要管理连接,操作系统描述管理这些连接数据结构,服务端OS为了解决资源满载的问题可能会释放掉建立的连接,服务端端必须重新发送RST标识为1的报文给对应客户端请求重新建立连接才可以进行通信。
就像下面这样子。
- 2.三次握手时第3次握手失败导致重连
client在三次握手的时候,认为只要把三次握手中第3次报文发出,连接就建立好了!!!
但是要是真的第3次握手的时候失败了,就会是下面这种情况
5.PSH PSH表示催促标记位,可以理解为Push。
Client在不断发数据,Server在不断接收数据同时在给Client发送应答,应答报文中包含了16位窗口大小的字段,其实就是在告诉Client自己接收缓冲区剩余空间的大小,以便于Client及时调整自己的发送速度。
但是,Server接收缓冲区快满了或者说已经满了,此时Client给在发送的报文里设置PSH标记位来催促对方尽快取走缓冲区的数据。(让对方上层应用程序立即把数据从TCP接收缓冲区读取,保证TCP接收缓冲区有能力接收新数据或清空TCP接收缓冲区)
补充:
- (1) 为什么缓冲区会满了?
缓冲区满了可能是因为Server应用层还在处理上一条数据,导致没有时间调用read接口函数来取走缓冲区里的数据。
- (2) 如何理解“OS催促上层尽快取走数据”?
缓冲区存在低水位和高水位标记,OS 催促上层取数据的依据便来源于此,低水位就代表当前数据太少了,先别急着读;高水位就代表缓冲区里的数据太多了,赶紧来取走。
一般OS是让上层每次都读取一批数据,而不是每次只读取一个字节。因为上层是调用read接口函数来从缓冲区读取数据的,如果每次都只读取一个字节,那就需要频繁的调用read函数,也就需要频繁的在用户态和内核态之间切换,这样会降低效率。
6.URG
URG表示紧急标记位。表示本报文中发送的数据(有效载荷)是否包含紧急数据:URG=1时表示有紧急数据;当URG=1时,后续的16位紧急指针字段才有效。
当发送方希望一些数据尽快被接收方的上层拿到的时候,就需要用到这个标记位。通常需要搭配16位紧急指针使用,要传递的紧急数据混在普通数据里,16位紧急指针指明了紧急数据在普通数据中的具体位置。
URG的紧急指针字段在这里
16位紧急指针表示的是紧急数据在有效载荷中的偏移量,TCP规定死紧急数据只能有1字节,当URG标志位被置为有效时,紧急指针就会派上用场,接收方在读取TCP报头之后,会首先从紧急指针表示的偏移量处读取1字节的紧急数据,然后再重新从有效载荷的起始位置读取完成剩余的数据,这1字节的紧急数据我们一般称为带外数据。
实际上紧急指针和URG标志位我们在99.99%的情景下都用不到,如果真要是用带外数据,可能运维的一些人员会用到,比如服务器现在压力非常大,可以发送1字节的带外数据用于询问当前服务器的状态怎么样,有没有恢复到健康状态,服务器可以返回1字节的带外数据,用1字节的数据来对应状态码,返回服务器是因为什么原因而导致过载,因为带外数据不用经过冗长的数据流,可以直接在应用层读取。
所以带外数据实际上并不在正常的数据流中,一般用带外数据的也就是UDP和TCP协议了。如果想要读取带外数据,可以将recv的flags标志位按位或上MSG_OOB,这样就可以读取带外数据了。
这些标志位的组合和状态变化规则定义了TCP连接的建立、维护和关闭过程,以及在数据传输中的一些特定行为,确保了TCP连接的可靠性和稳定性。
3.TCP的可靠策略
3.1.确认应答机制
确认应答机制是发送方确保自己的数据要被对方收到。
发送方发出数据以后,接收方要给发送方一个回复,确认自己已经收到数据了,这样的回复称为ACK,确认应答报文。
而发送方收到确认回复之后也知道自己的数据被对方收到了。
发送方可以同时发送多条数据,接收方根据收到的消息给出回复,但存在“后发先至”问题,接收方收到的数据顺序被打乱,这可能引起严重的歧义。
例如下面这种情况,双方最终会造成误解。
针对“后发先至”问题,TCP的解决办法是给传输的数据和应答报文都进行编号。
TCP是面向字节流传输的,在编号的时候是给每个字节都编上序号,假设每次传输1000个字节,那么第一个字节的序号就是1,第二个字节的序号就是2...最后一个字节的序号就是1000。
由于这1000个字节是属于同一个TCP数据报的,TCP报头就只记录第一个字节的序号。
上述说到的编号就需要用到TCP报文格式中的32位序号和32位确认序号,发送方利用32位序号记录发送的数据的第一个字节的序号,接收方读完数据之后,返回给发送方32位确认序号,即告之对方下一个数据的第一个字节应该从哪个序号开始。
在TCP协议中,每一条数据都是有序号的,包括应答报文,一条数据是否是应答报文就要根据ACK标志位来确定,如过ACK为1那就是应答报文了,如果为0则不是应答报文了。
TCP的可靠传输主要就是通过确认应答机制来保证的,通过应答报文就能让发送方清楚的知道传输是否成功,引入32位序号和32位确认序号进一步确保对多条数据有效的传输。
3.2.超时重传机制
我们得知道一台主机发了一条消息出去,他自己是不知道有没有发送成功的,是靠对方发回的回应知道自己上传发送的消息成功发送了的。
确认应答机制讨论的前提是每条数据都能顺利传输的情况,那么在丢包(对方没有收到我发的信息或者我没有收到对方的回复)的情况下该如何应对呢,应对方法就是超时重传(隔一段时间重新发送)。
丢包存在两种情况,发的数据丢包了,返回的ACK丢包了,不管是哪种情况,只要没有收到ACK,都会在到达某个时间阈值之后进行重传。
- 补充知识1——丢包是小概率事件
数据丢包是一个概率事件,假设一条数据在传输的过程中丢包的概率是5%,传输成功的概率是95%,那么第一次传输丢包,第二次重传也丢包的概率是5%*5%=0.25%,第三次重传也丢包的概率可以忽略不计,在实际情况中,丢包的概率是一个非常小的数字,而上述假设的5%已经是一个很大的数字了,如果连续多次重传还是丢包的情况下,那么就要考虑是否是网线断了或是其他情况了。
- 补充知识2——自己这里认为发出,但是没有收到对方回复的消息会先保存到滑动窗口
我们知道发送的数据段是有可能没有收到ACK的,所以被发出的数据不应该立马被移除(计算机上的移除其实就是数据覆盖),应该先保存一段时间,如果发送的数据丢包了,则可以将保存的数据再重新发送,而像这样已经发送但没有收到ACK的数据,其实是存放在滑动窗口里面的,这个后面会讲,现在先提一下。
- ACK丢包的情况下,其实接收端已经收到发送端传输过来的数据,发送端再次发送相同的数据过来该如何处理呢?
TCP有一个特殊的处理功能,去重,TCP存在一个“接收缓冲区”的存储空间,接收端会将读到的数据放到对应的缓冲区,根据数据的序号,TCP就能识别是否有两条重复的数据,如果重复就把后面的这条数据给丢弃了。
- 特定的时间间隔怎么定呢?TCP是否会进行无限次的重传?
其实这个时间应该是随着网络情况动态变化的,如果网络情况好,超时时间设定的非常长,这其实就会影响网络传输的效率,因为数据包发送的速度非常快,可能数据包来回一次共需要50ms,但你将超时时间设定为500ms,那中间的450ms的时间就会被平白无故浪费掉,如果网络情况特别差,超时时间设定的非常短,那更离谱了,数据包正在传输的过程当中就被判定为丢包了,这同样也会影响数据传输的效率。
所以最理想的情况,就是找出一个最短的时间,保证绝大部分的网络情况下,数据包都可以在这个最短的时间窗口内,发送过去,同时ACK报文能够发回来。
在linux(unix和windows也一样)中,超时实际上是以500ms作为基本单位来进行控制的,如果第一次重发后,还没有得到确认,则会以2的指数幂×500ms的方式来逐渐增大超时的时间窗口,累计达到一定重传次数,则TCP会强制关闭双方建立的连接。
3.3.连接管理机制
3.3.1.如何理解TCP连接
- 1.如何理解TCP连接?
事实上,我们说的TCP的三次握手建立的这个连接,其实是端到端的这个连接,也就是客户端的应用层到服务端的应用层的连接,只要我客户端的应用层没和服务端的应用层连接上,我们就可以说这个TCP连接是失败的。
当上层(如应用层)调用
connect
函数时,它实际上是请求传输层(TCP协议层)来建立TCP连接。TCP协议层随后会进行三次握手过程,以在客户端和服务端之间建立连接。在这个过程中,SYN报文是由客户端的TCP协议层(传输层)发出的,用于发起连接请求。需要注意的是,虽然我们说TCP连接是端到端的,但在实际的网络传输过程中,数据会经过多个网络设备和协议层的处理。然而,这些处理对上层应用来说是透明的,它们只需要关注TCP连接是否成功建立,以及数据是否能够在应用层之间可靠地传输。
- 因此,我们在下面如果看到了,明明一方和另一方断开连接了(实质是它们之间的应用层断开连接),但是其他层还可能存在联系,后面还能发SYN这类报头的,都是传输层在发的
- 2.TCP连接是要维护的,会消耗资源的
我们知道,TCP 在『端对端』之间建立的信道,为上层『端』对应的进程提供服务,它由客户端和服务端的套接字(socket)以及它们之间交换的数据包(segment)组成。TCP 连接的建立、维持和终止都需要遵循一定的协议和状态机制。
另外,中心化的 Client-Server 模式使得大量不同的 Client 将会与同一台 Server 建立连接,那么 Server 端势必要对这些来源不同的连接进行管理。
从数据结构的角度理解:我们知道,TCP 是处于传输层的协议,也就是说,TCP 的各种逻辑由操作系统(特指 Linux)维护,那么这些数据就得按照操作系统的规则组织,即先描述,后组织。
现在我们知道了,这些连接在操作系统眼里,只不过内核中的数据结构类型(通过结构体组织),当连接成功被建立时,内存中就会创建对应的『连接对象』。管理不同的连接,即对这些连接对象进行增删查改等操作。
既然组织连接相关的数据结构需要操作系统维护,那么维护是需要成本的,主要是CPU 和内存资源。这是许多网络攻击方式的切入点。
当然,TCP 连接需要维护一些状态信息和参数,例如序号、确认号、窗口大小、重传计时器等。这些信息和参数被存储在一个称为传输控制块(Transmission Control Block,TCB)的数据结构中。每个 TCP 连接都有一个唯一的 TCB 与之对应,操作系统用一张表来存储所有的 TCB。TCB 中的信息和参数会随着连接的状态变化而更新。
- 为什么说在学习网络之前一定要先学好操作系统呢?
最重要的原因就如刚才所说,两个具有代表性的协议:TCP 和 UDP 都是传输层的协议,而传输层由操作系统内核维护,那么协议的实现必须符合操作系统中的规则。
另外,在 Linux 中,传输控制块(Transmission Control Block,TCB)和线程控制块(Thread Control Block,TCB)或者进程控制块(Process Control Block,PCB)之间的关系是不同的,它们分别属于不同的层次(前者是传输层,后两者是内核),它们之间的联系是:
- 一个进程可以创建多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。因此,线程控制块中有一个指针指向所属进程的进程控制块。
- 一个进程或者线程可以创建多个套接字,这些套接字用于与其他进程或者线程进行通信。因此,进程控制块或者线程控制块中有一个文件描述符表,其中包含了指向套接字对应传输控制块的指针。
TCP是面向有连接的,
- 要在连接的情况下进行数据的传输,通常情况下要进行三次握手建立连接
- 断开连接时,要进行四次挥手断开连接。
3.3.2.连接——三次握手
- 怎么触发三次挥手?
用户端调用connect函数,然后操作系统就会自动在内部完成3次握手
TCP是面向连接的协议,双方在正常通信之前需要先建立连接,建立连接是一个三次握手的过程。
- 第一次,Client 给Server发送请求连接,报文中携带SYN标记位来表明当前报文是和建立连接相关的。
- 第二次,Server接受Client的连接,并询问何时建立连接,报文中也会携带SYN标记位,同时会携带ACK来回复上一条请求。
- 第三次,Client 回复 Server,立马建立连接,这里只是单纯的回复,只要设置ACK即可。
有人可能会疑问,客户端最开始不是没和服务端建立连接吗?那客户端为什么能发送SYN给服务端呢?
事实上,我们说的TCP连接,其实是端到端的这个连接,也就是客户端的应用层到服务端的应用层的连接,只要我服务端的应用层没和服务端的应用层连接上,我们就可以说这个TCP连接是失败的。
当上层(如应用层)调用
connect
函数时,它实际上是请求传输层(TCP协议层)来建立TCP连接。TCP协议层随后会进行三次握手过程,以在客户端和服务端之间建立连接。在这个过程中,SYN报文是由客户端的TCP协议层(传输层)发出的,用于发起连接请求。需要注意的是,虽然我们说TCP连接是端到端的,但在实际的网络传输过程中,数据会经过多个网络设备和协议层的处理。然而,这些处理对上层应用来说是透明的,它们只需要关注TCP连接是否成功建立,以及数据是否能够在应用层之间可靠地传输。
- 因此,我们在下面如果看到了,明明一方和另一方断开连接了(实质是它们之间的应用层断开连接),后面还能发SYN这类报头的,都是传输层在发的
那么问题来了,站在Client的角度,什么时候认为连接已经建立起来了呢?答案是第二次握手以后,即收到Server的回复。
然而站在Server的角度,建立连接的时候是在第三次握手,因为第二次握手只是Server端单方面同意了,Client端有没有同意是在第三次握手才知道。
这些包使用 TCP 首部用于控制的字段来管理 TCP 连接,建立一个 TCP 连接需要发送 3 个包,形象的称为“三次握手”。
注意:
- 图中虽然以 SYN 等标记位请求和应答(包括下文常用标志位代替报文),但实际上两端交换的是报文,而不是标记位。报文可能携带数据,也可能只含有报头。理论上在建立连接时,每个报文都应该有回应(就像打电话一样),在奇数次握手中,最后一个报文在连接建立之前一定没有回应的。
- 客户端和服务端都要向对方发送建立连接的请求(SYN),并且需要接收到对方的确认应答(ACK)后,才能认为『这个方向』的通信信道建立成功。这是因为 TCP 要实现『全双工』通信,就必须要保证双方通信的信道是畅通的。
- 有的时候把这个建立连接的过程叫做“四次握手”,这是因为这种说法把第二次握手 ACK+SYN 拆分成了两次握手,实际上都是一样的。
有人想说,TCP只能保证历史消息的可靠性,永远不谈最新一条消息的可靠性,这句话没问题,比如三次握手的最后一次握手,这个消息的确就是不可靠的,因为客户端发送的ACK报文段,server是否收到,客户端是不知道的!
但这没有关系,就算ACK报文段丢失了,那server不会认为连接建立成功,此时如果client给server发送消息,则server会感觉很奇怪,既然连接不建立成功,你还给我发消息,那就说明我们双方产生了认为连接建立不一致的情况,那server就会给client发送复位报文段,请求重新三次握手,重新建立连接,因为我们现在连接的建立是不一致的,client认为连接建立成功,但server不认为成功。
或者还有另一种情况,server发送的SYN报文段超时没有确认应答,则server就会进行超时重传,当client收到重复的捎带应答报文段时,client就会意识到自己给server发送的确认应答报文段可能丢失了,此时client就会重发ACK报文段。
- 为什么是三次握手?不是一次或者二次……
这是一个经典的问题,有很多不同的解释和角度。可以从几个方面来回答,在这里仅从效率角度讨论,在『再次理解“三次握手”中』会从多个角度回答这个问题。
- 为什么不能用 1 次呢?
如果只用 1 次,那么客户端发送一个 SYN 后就认为连接建立成功,但是如果这个 SYN 丢失了或者被延迟了,那么服务器端就无法知道客户端的请求,也无法给客户端发送数据。除此之外,每次连接都会占用服务端一定的 CPU 和内存资源,只用 1 次握手就认为建立连接成功,那么当服务端在短时间内接收到大量 SYN 连接请求,会造成服务端异常,即 SYN 洪水攻击。
- 为什么不用2次呢?
那么如果是两次握手就建立连接的情况下,当服务器将第二次的握手的信息发出去之后,默认连接已经建立,并且开辟空间资源等待接收数据。但如果第二次握手的信息丢包了呢,服务端认为二次握手已经完成,建立了连接,而客户端呢,由于什么也没有收到,所以会进行超时重传,当服务器又收到连接请求,认为有新的客户端发起连接,于是同意连接请求,并开辟空间资源等待接收数据,如果出现大量上述情况,便会造成服务端的崩溃。这种情况下,如果客户端重复地向服务端发送 SYN 请求,也会造成服务端的 SYN 洪水。
- 为什么用3次呢?
a.三次握手能够以最小成本验证全双工通信。全双工通信指的是server和client各自都能够进行数据的接收和发送,发送和接收是解耦的,client可以发送SYN报文段,也能接收来自server的ACK报文段,server可以发送SYN报文段,也能接收来自client的ACK报文段,话说回来,四次握手可以验证全双工通信吗?当然也可以,但既然三次握手行,为什么要四次握手呢?第四次握手不是平白无故的消耗网络资源吗?所以三次握手是以最小成本来验证全双工通信的。
b.三次握手可以防止产生单主机对服务器SYN洪水攻击的漏洞。
在三次握手中,server单方面对client建立连接的前提是client已经单方面向server建立好连接了。而客户端在服务端第一次返回SYN+ACK的时候,就client已经单方面向server建立连接,而这需要消耗客户端内部资源。在这之后客户端向服务端发送ACK后,服务端才会消耗资源来维护连接。这样子连接失败的成本嫁接到客户端上了。这样 SYN 洪水攻击也就失效了,因为三次握手会让发出 SYN 的一方(即服务端)接收等量的 ACK 响应,而大部分情况下服务器的配置要比client高很多,所以如果双方在不停的以相同成本进行消耗,那也一定是client先扛不住,而不是server,所以单主机的情况下,client想要SYN洪水攻击服务器,这是不现实的!这样服务端就能承担最小程度的连接失败成本。
- 为什么不用 4 次或更多呢?
前面我们说过三次握手已经可以最小成本验证全双工通信了,那四次,五次握手肯定也可以。
但四次握手也存在SYN洪水攻击的漏洞,最后一次握手是server发送给client的,让client来建立连接的,但client可以忽略掉这个报文段,只让server自己建立连接,去承担维护连接需要的成本。
同时五次握手由于最后一次是client发送给server的,所以server建立连接之前,client也会建立连接,双方是同等成本的消耗,则可以避免单主机的SYN洪水攻击。
所以三次握手之后的其他握手,偶数次依旧存在SYN洪水攻击,奇数次不存在SYN洪水攻击,但他们都不是最小成本验证全双工通信信道,所以使用三次握手,而不是其他握手!
其实从理论上讲,只要最后一次是客户端发送一个 ACK 给服务器端就行(为啥?因为要嫁接连接失败成本)就可以,即 5/7/9 次。
… 但是这样做没有必要,因为第三次握手已经足够保证双方的同步和确认信息了,再多发送一次或多次只会增加网络开销和延迟。也就是说,三次握手是验证双方通信信道连接成功的最小次数。
TCP 的三次握手,主要是为了在保证连接可靠性和双向性的同时,尽量减少网络开销和延迟。
3.3.3.断开连接——四次挥手
- 怎么触发四次挥手?
建立连接是由一方主动发起,大部分都是客户端主动向服务器发起连接请求,但断开连接可以由任意一方主动发起,发起的上层条件,其实就是调用close()关闭套接字文件描述符sockfd。
断开连接是一个四次挥手的过程。
- 为什么会有四次呢?
Client 单方面断开连接需要两次;Server端单方面断开连接需要两次,加起来就是四次。
谁先断开连接,这个没有限制,下面就假设Client先断开。
- 第一次挥手,Client 通知 Server 自己单方面断开连接,Client发送的报文中就会携带FIN标记位。( 注意:Client单方面断开连接以后,只是断开Client ->Server方向上的数据传输,此时Server可以给Client发送数据,反过来不行。)
- 第二次挥手,Server收到Client的通知请求,并给Client发送应答报文,报文中携带ACK标记位。
- 第三次挥手,Server 通知 Client 自己单方面断开连接,Server发送的报文中会携带FIN标记位。
- 第四次挥手,Client收到Server的通知请求,并给Server发送应答报文,报文中携带ACK标记位。
注意:所谓的TCP连接,其实是端到端的这个连接,也就是客户端的应用层到服务端的应用层的连接,只要我客户端的应用层没和服务端的应用层连接上,我们就可以说这个TCP连接是失败的。但是其他层还可能保持着联系。
我们说断开连接,也就是断开客户端应用层和服务端应用层的连接。我们下面说断开连接之后,还能发ACK等信息,都是传输层(操作系统)在发送。
我们这里所说的不发送消息,指的是不发应用层的数据了,并不代表传输层自己不能发送该层的管理报文段,例如FIN,ACK等等报文段。
可能有读者会敏锐的发现, 这里的 “四次挥手” 过程似乎与 “三次握手” 的过程差别不大, 三次握手可以将中间的 ACK 和 SYN 合并为一次, 为什么这里的四次挥手没有将 ACK 和 FIN 合并为一个步骤呢?
我们直接公布答案, 四次挥手有的时候确实可以三次完成, 但是大部分时候都是分为四次完成的, 因为中间这两次挥手过程, 不一定能够被合并.
与握手不同, 三次握手的过程都是在内核中, 系统自行控制的, 可以说是中间的 SYN 和 ACK 是瞬发的, 而我们四次挥手时发送的 FIN 则是由用户代码来控制的. 只有调用 close 方法, 或者用户进程结束, 才会触发 FIN, 相比之下, ACK 是由内核控制, 第一次收到 FIN 时, 就会立马返回 ACK.
未完待续……