文章目录
前言
争取一下把网络的全部给更新出来,之后可能就不怎么会更新博客了,但也说不定,做博客的目的本来就是为了对抗遗忘,之前的内容比如说 C++ 、数据结构 、Linux系统与网络编程这些重要性高且容易遗忘
但是能做的话还是要做的,后端要学的真的好多,后期还打算学一下 Redis、Docker 等等
本篇我们来学习一下 TCP 的有关内容,它的这个 "可靠性" 真的使得它知识点巨多,当然它的应用也很广泛,我们前面学得 HTTP 应用层协议也是基于 TCP 的,乃至于 HTTPS、FTP、SSH 也是,多说无益,就让我们现在来好好学一学吧!
一、什么是可靠性?
为什么需要可靠性
我前言就提到了这个概念,这个概念我们在网络协议才有提起,为什么之前没有呢?
首先还是那个概念,现在大多数计算机都是基于 冯诺依曼结构 的

虽然这里的 输入设备、输出设备、内存、CPU 都在一台机器上,但这几个硬件设备是彼此独立的。如果它们之间要进行数据交互,就必须要想办法进行通信,因此这几个设备实际是用"线"连接起来的,其中连接内存和外设之间的"线"叫做 I/O 总线,而连接内存和 CPU 之间的"线"叫做系统总线。由于这几个硬件设备都是在一台机器上的,因此这里传输数据的"线"是很短的,传输数据时出现错误的概率也非常低
但如果要进行通信的各个设备相隔千里,那么连接各个设备的"线"就会变得非常长,传输数据时出现错误的概率也会大大增高,此时要保证传输到对端的数据无误,就必须引入可靠性
总之,网络中存在不可靠的根本原因就是,长距离数据传输所用的"线"太长了,数据在长距离传输过程中就可能会出现各种各样的问题,而 TCP 就是在此背景下诞生的, TCP 就是一种保证可靠性的协议。

原理是一样的,但是因为一个距离够近,所以就没啥问题,一个距离很长,所以就很容易出问题,就需要引入很多可靠性的机制(确认应答(个人认为理解TCP可靠性的最基础、最重要的概念)、超时重传、连接管理、滑动窗口等),就像你跟她的感情一样~
与UDP的不可靠对比
你可能会想一个可靠一个不可靠,那 UDP 是不是没啥用处?
其实不然,这个你不能代入现实当中,这个跟形容人可靠和不可靠是不一样的,人有褒贬,在这里没有,是一个中性词, TCP 协议是可靠的协议,也就意味着 TCP 协议需要做更多的工作来保证传输数据的可靠,并且引起不可靠的因素越多,保证可靠的成本(时间+空间)就越高,而 UDP 因为不可靠,所以无论是理解掌握原理还是实际使用对比之下都显得格外的简单
所以没有一个谁更好谁更差的概念,具体还是看上层的应用场景,如果应用场景严格要求数据在传输过程中的可靠性,那么就必须采用 TCP 协议,如果应用场景允许数据传输出现少量丢包,那么肯定优先选择 UDP 协议,因为 UDP 协议足够简单
二、TCP协议格式
我们先从协议的格式开始了解,这是前提

TCP报头当中各个字段的含义如下:
- 源/目的端口号:表示数据是从哪个进程来,到发送到对端主机上的哪个进程
- 32位序号/32位确认序号:分别代表TCP报文当中每个字节数据的编号以及对对方的确认,是TCP保证可靠性的重要字段
- 4位TCP报头长度:表示该TCP报头的长度,以4字节为单位
- 6位保留字段:TCP报头中暂时未使用的6个比特位
- 16位窗口大小:保证TCP可靠性机制和效率提升机制的重要字段
- 16位检验和:由发送端填充,采用CRC校验。接收端校验不通过,则认为接收到的数据有问题。(检验和包含TCP首部+TCP数据部分)
- 16位紧急指针:标识紧急数据在报文中的偏移量,需要配合标志字段当中的URG字段统一使用。
选项字段:TCP报头当中允许携带额外的选项字段,最多40字节
TCP报头当中的6位标志位:
- URG:紧急指针是否有效。
- ACK:确认序号是否有效。
- PSH:提示接收端应用程序立刻将TCP接收缓冲区当中的数据读走。
- RST:表示要求对方重新建立连接。我们把携带RST标识的报文称为复位报文段。
- SYN:表示请求与对方建立连接。我们把携带SYN标识的报文称为同步报文段。
- FIN:通知对方,本端要关闭了。我们把携带FIN标识的报文称为结束报文段。
TCP 报头在内核当中本质就是一个位段类型,给数据封装 TCP 报头时,实际上就是用该位段类型定义一个变量,然后填充 TCP 报头当中的各个属性字段,最后将这个 TCP 报头拷贝到数据的首部,至此便完成了 TCP 报头的封装。
TCP如何将报头与有效载荷进行分离?
我前面说了,TCP 有个 4位字段 叫做首部长度,它以 4字节 为单位,我们可以依靠这个很快判断 TCP 的报头长度了
当 TCP 从底层获取到一个报文后,虽然 TCP 不知道报头的具体长度,但报文的 前20个字节 是 TCP 的基本报头,并且这 20字节 当中涵盖了 4位 的首部长度
因此TCP是这样分离报头与有效载荷的:
当 TCP 获取到一个报文后,首先读取报文的 前20个字节 ,并从中提取出 4位 的首部长度,此时便获得了 TCP 报头的大小 size
如果 size 的值大于 20字节 ,则需要继续从报文当中读取 size − 20 字节的数据,这部分数据就是 TCP 报头当中的选项字段
读取完 TCP 的基本报头和选项字段后,剩下的就是有效载荷了
需要注意的是, TCP报头 当中的 4位首部长度 描述的基本单位是 4字节 ,这也恰好是报文的宽度。 4位首部长度 的取值范围是 0000 ~ 1111 ,因此 TCP 报头最大长度为 15 × 4 = 60 字节,因为基本报头的长度是 20字节 ,所以报头中选项字段的长度最多是 40字节
如果 TCP 报头当中不携带选项字段,那么 TCP 报头的长度就是 20字节 ,此时报头当中的 4位首部长度 的值就为 20 ÷ 4 = 5 ,也就是 0101
TCP 如何决定将有效载荷交付给上层的哪一个协议?
应用层的每一个网络进程都必须绑定一个端口号
服务端进程必须显示绑定一个端口号
客户端进程由系统动态绑定一个端口号
而 TCP 的报头中涵盖了目的端口号,因此 TCP 可以提取出报头中的目的端口号,找到对应的应用层进程,进而将有效载荷交给对应的应用层进程进行处理
说明一下: 内核中用哈希的方式维护了 端口号 与 进程ID 之间的映射关系,因此传输层可以通过端口号快速找到其对应的 进程ID ,进而找到对应的应用层进程
序号与确认序号
在进行网络通信时,一方发出的数据后,它不能保证该数据能够成功被对端收到,因为数据在传输过程中可能会出现各种各样的错误,只有当收到对端主机发来的响应消息后,该主机才能保证上一次发送的数据被对端可靠的收到了,这就叫做真正的可靠

图中实现的表示数据能被靠谱地收到,而虚线不能保证
但 TCP 要保证的是双方通信的可靠性,虽然此时 主机A 能够保证自己上一次发送的数据被 主机B 可靠的收到了,但 主机B 也需要保证自己发送给 主机A 的响应数据被 主机A 可靠的收到了。因此 主机A 在收到了 主机B 的响应消息后,还需要对该响应数据进行响应,但此时又需要保证 主机A 发送的响应数据的可靠性...,这样就陷入了一个死循环

因为只有当一端收到对方的响应消息后,才能保证自己上一次发送的数据被对端可靠的收到了,但双方通信时总会有最新的一条消息,因此无法百分之百保证可靠性
所以严格意义上来说,互联网通信当中是不存在百分之百的可靠性的,因为双方通信时总有最新的一条消息得不到响应。但实际没有必要保证所有消息的可靠性,我们只要保证双方通信时发送的每一个核心数据都有对应的响应就可以了。而对于一些无关紧要的数据(比如响应数据),我们没有必要保证它的可靠性。因为对端如果没有收到这个响应数据,会判定上一次发送的报文丢失了,此时对端可以将上一次发送的数据进行重传(这也是我们等一下要学习的内容)

这种策略在 TCP 当中就叫做确认应答机制。需要注意的是,确认应答机制不是保证双方通信的全部消息的可靠性,而是只要一方收到了另一方的应答消息,就说明它上一次发送的数据被另一方可靠的收到了
32位序号

但在连续发送多个报文时,由于各个报文在进行网络传输时选择的路径可能是不一样的,因此这些报文到达对端主机的先后顺序也就可能和发送报文的顺序是不同的(也就是可能会乱序接收 )。但报文有序也是可靠性的一种,因此 TCP 报头中的 32位序号 的作用之一实际就是用来保证报文的有序性的
TCP 将发送出去的每个字节数据都进行了编号,这个编号叫做序列号
比如现在发送端要发送 3000字节 的数据,如果发送端每次发送 1000字节 ,那么就需要用三个 TCP 报文来发送这 3000字节 的数字
此时这三个 TCP 报文当中的 32位序号 填的就是发送数据中首个字节的序列号,因此分别填的是 1、1001和2001

此时接收端收到了这三个 TCP报文 后,就可以根据 TCP报头 当中的 32位序列号 对这三个报文进行顺序重排(该动作在传输层进行),重排后将其放到 TCP 的 接收缓冲区 当中,此时接收端这里报文的顺序就和发送端发送报文的顺序是一样的了
接收端在进行报文重排时,可以根据当前报文的32位序号与其有效载荷的字节数,进而确定下一个报文对应的序号。
另外有个为什么一次不能直接传送 3000字节 全部数据的问题,我后面会解答,这里先放个DS大人给的解答,这个是后面数据链路层的知识点了~

32位确认序号
TCP报头当中的32位确认序号是告诉对端,我当前已经收到了哪些数据,你的数据下一次应该从哪里开始发。
以刚才的例子为例,当主机B收到主机A发送过来的32位序号为1的报文时,由于该报文当中包含1000字节的数据,因此主机B已经收到序列号为1-1000的字节数据,于是主机B发给主机A的响应数据的报头当中的32位确认序号的值就会填成1001。
一方面是告诉 主机A ,序列号在1001之前的字节数据我已经收到了
另一方面是告诉 主机A ,下次向我发送数据时应该从 序列号为1001 的字节数据开始进行发送之后 主机B 对 主机A 发来的其他报文进行响应时,发给主机A的响应当中的 32位确认序号 的填法也是类似的道理

响应数据与其他数据一样,也是一个完整的 TCP 报文,尽管该报文可能不携带有效载荷,但至少是一个 TCP 报头
也就是说不要理解成我们就发一个字段了,至少都是一个报文,包括我们后面连接发送标志位的时候,也是这样的道理
报文丢失怎么办?
还是以刚才的例子为例, 主机A 发送了三个报文给 主机B ,其中每个报文的有效载荷都是 1000字节 ,这三个报文的 32位序号 分别是 1、1001、2001
如果这三个报文在网络传输过程中出现了丢包,最终只有序号为 1和2001 的报文被 主机B 收到了,那么当 主机B 在对报文进行顺序重排的时候,就会发现只收到了 1-1000 和 2001-3000 的字节数据。此时 主机B 在对 主机A 进行响应时,其响应报头当中的 32位确认序号 填的就是 1001 ,告诉 主机A 下次向我发送数据时应该从 序列号 为 1001 的字节数据开始进行发送
还是以刚才的例子为例, 主机A 发送了三个报文给 主机B ,其中每个报文的有效载荷都是1000字节,这三个报文的32位序号分别是1、1001、2001。

感觉已经要有点引出滑动窗口的意思了~
- 此时 主机B 在给 主机A 响应时,其 32位确认序号 不能填3001,因为 1001-2000 是在 3001 之前的,如果直接给 主机A 响应 3001 ,就说明序列号在 3001 之前的字节数据全都收到了(注意确认序号的含义,是一定要前面所有都已经发送成功的了)
- 因此 主机B 只能给 主机A 响应1001,当 主机A 收到该 确认序号 后就会判定序号为 1001 的报文丢包了,此时 主机A 就可以选择进行数据重传
因此发送端可以根据对端发来的确认序号,来判断是否某个报文可能在传输过程中丢失了
为什么要有两套序号机制?
一开始可能会疑惑,为什么要分 序号与确认序号 ?诚然,如果一端只是发数据,而另一端只是接收数据的话那当然可以,但是我们 TCP 是 全双工 的,也就是两端地位其实是等同的,两端都可以发,也都可以收
于是就引入了这么两套序号机制:
- 双方发出的报文当中,不仅需要填充 32位序号 来表明自己当前发送数据的序号
- 还需要填充 32位确认序号 ,对对方上一次发送的数据进行确认,告诉对方下一次应该从哪一字节序号开始进行发送
- 32位序号的作用是,保证数据的按序到达,同时这个序号也是作为对端发送报文时填充 32位确认序号 的根据
- 32位确认序号 的作用是,告诉对端当前已经收到的字节数据有哪些,对端下一次发送数据时应该从哪一字节序号开始进行发送
- 序号和确认序号是确认应答机制的数据化表示,确认应答机制就是由 序号和确认序号 来保证的
- 此外,通过序号和确认序号还可以判断某个报文是否丢失
窗口大小
TCP本身是具有接收缓冲区和发送缓冲区的
- 接收缓冲区用来暂时保存接收到的数据。
- 发送缓冲区用来暂时保存还未发送的数据。
- 这两个缓冲区都是在 TCP传输层 内部实现的。
(后期我可能会考虑给大家看 Linux 源码,大家可以敬请期待一下)

- TCP发送缓冲区当中的数据由上层应用层进行写入。当上层调用 write / send 这样的系统调用接口时,实际不是将数据直接发送到了网络当中,而是将数据从应用层拷贝到了 TCP 的发送缓冲区当中
- TCP接收缓冲区当中的数据最终也是由应用层来读取的。当上层调用 read / recv 这样的系统调用接口时,实际也不是直接从网络当中读取数据,而是将数据从 TCP 的接收缓冲区拷贝到了应用层而已
- 就好比调用 read 和 write 进行文件读写时,并不是直接从磁盘读取数据,也不是直接将数据写入到磁盘上,而对文件缓冲区进行的读写操作

当数据写入到 TCP 的发送缓冲区后,对应的 write / send 函数就可以返回了,至于发送缓冲区当中的数据具体什么时候发,怎么发等问题实际都是由 TCP 决定的
我们之所以称 TCP 为传输层控制协议,就是因为最终数据的发送和接收方式,以及传输数据时遇到的各种问题应该如何解决,都是由 TCP 自己决定的,用户只需要将数据拷贝到 TCP 的发送缓冲区,以及从 TCP 的接收缓冲区当中读取数据即可

结合我前面说的 TCP 是 全双工 的,所以 TCP 的两端都是有 发送缓冲区 和 接收缓冲区 的
发送缓冲区 和 接收缓冲区 存在的意义
数据在网络中传输时可能会出现某些错误,此时就可能要求发送端进行数据重传,因此 TCP 必须提供一个发送缓冲区来暂时保存发送出去的数据,以免需要进行数据重传。只有当发出去的数据被对端可靠的收到后,发送缓冲区中的这部分数据才可以被覆盖掉(滑动窗口来实现 )
接收端处理数据的速度是有限的,为了保证没来得及处理的数据不会被迫丢弃,因此 TCP 必须提供一个接收缓冲区来暂时保存未被处理的数据,因为数据传输是需要耗费资源的,我们不能随意丢弃正确的报文。此外, TCP 的数据重排也是在接收缓冲区当中进行的(所以其实你可以想到UDP是没有一个真正的发送缓冲区的)

经典的生产者消费者模型:
- 对于发送缓冲区来说,上层应用不断往发送缓冲区当中放入数据,下层网络层不断从发送缓冲区当中拿出数据准备进一步封装。此时上层应用扮演的就是生产者的角色,下层网络层扮演的就是消费者的角色,而发送缓冲区对应的就是"交易场所"。
- 对于接收缓冲区来说,上层应用不断从接收缓冲区当中拿出数据进行处理,下层网络层不断往接收缓冲区当中放入数据。此时上层应用扮演的就是消费者的角色,下层网络层扮演的就是生产者的角色,而接收缓冲区对应的就是"交易场所"。
- 因此引入 发送缓冲区 和 接收缓冲区 相当于引入了两个生产者消费者模型,该生产者消费者模型将上层应用与底层通信细节进行了解耦,此外,生产者消费者模型的引入同时也支持了并发和忙闲不均。
(这就跟我们之前所学习的内容又照应上了)
当发送端要将数据发送给对端时,本质是把自己发送缓冲区当中的数据发送到对端的接收缓冲区当中。但缓冲区是有大小的,如果接收端处理数据的速度小于发送端发送数据的速度,那么总有一个时刻接收端的接收缓冲区会被打满,这时发送端再发送数据过来就会造成数据丢包,进而引起丢包重传等一系列的连锁反应。
因此 TCP 报头当中就有了 16位的窗口大小 ,这个 16位窗口大小 当中填的是自身接收缓冲区中剩余空间的大小,也就是当前主机接收数据的能力
接收端在对发送端发来的数据进行响应时,就可以通过 16位窗口大小 告知发送端自己当前接收缓冲区剩余空间的大小,此时发送端就可以根据这个窗口大小字段来调整自己发送数据的速度
窗口大小字段越大,说明接收端接收数据的能力越强 ,此时发送端可以提高发送数据的速度
窗口大小字段越小,说明接收端接收数据的能力越弱 ,此时发送端可以减小发送数据的速度
如果窗口大小的值为0,说明接收端接收缓冲区已经被打满了,此时发送端就不应该再发送数据了
你可能会想,既然 TCP16位窗口大小 ,是不是代表着,我们的缓冲区大小上限就是 65535 ?其实不是,可以通过选项 字段来进行缩放因子的调整,从而使得适应更多场合,这就是 TCP 的妙处!
- 在编写 TCP 套接字时,我们调用 read / recv 函数从套接字当中读取数据时,可能会因为套接字当中没有数据而被阻塞住,本质是因为 TCP 的接收缓冲区当中没有数据了,我们实际是阻塞在接收缓冲区当中了
- 而我们调用 write / send 函数往套接字中写入数据时,可能会因为套接字已经写满而被阻塞住,本质是因为 TCP 的发送缓冲区已经被写满了,我们实际是阻塞在发送缓冲区当中了
- 在生产者消费者模型当中,如果生产者生产数据时被阻塞,或消费者消费数据时被阻塞,那么一定是因为某些条件不就绪而被阻塞。(其实你要是这方面疑惑的话,可以倒回去看看之前的生产者与消费者篇章,其实这里的缓冲区就相当于共享资源,也就是之前的阻塞队列)
总结
还是分篇来吧,现在我们先暂且休息一下,下篇我打算照着源码来给大家讲解!!!