一、初步认识
TCP全称"传输控制协议"(Transmission Control Protocol),是如今互联网应用最为广泛的传输层协议
网络通信存在不可靠性
如今大部分计算机都基于冯诺依曼体系结构
虽然输入设备、输出设备、内存、CPU等都在一台机器上,但各个硬件设备彼此独立。若它们之间要进行数据交互,就必须要进行通信,因此这几个设备实际是用"线"连接起来的,其中连接内存和外设之间的"线"是IO总线,而连接内存和CPU之间的"线"被称为系统总线。由于这几个硬件设备都在同一台机器上,因此这里传输数据的"线"很短,传输数据时出现错误的概率也较低
但若要进行通信的各个设备相隔千里,那么连接各个设备的"线"就会变得很长,传输数据时出现错误的概率也会大幅增高,此时要保证传输到对端的数据无误,就须引入可靠性
注意:单独的一台计算机可以看作一个小型网络,计算机上的各种硬件设备之间实际也是在进行数据通信,并且在通信时也必须遵守各自的通信协议,只不过更多是描述一些数据的含义
UDP协议存在的合理性
TCP协议是一种可靠的传输协议,使用TCP协议能够在一定程度上保证数据传输时的可靠性,而UDP协议是一种不可靠的传输协议,那这种不可靠的协议存在有什么意义呢?
不可靠和可靠仅是中性词,描述的是协议的特点,并没有贬义:
TCP协议是可靠的协议,也意味着TCP协议需要更多的工作来保证传输数据的可靠,并且引起不可靠的因素越多,保证可靠的成本(时间+空间)就越高。如数据在传输过程中出现了丢包、乱序、检验和失败等,都是不可靠的情况
UDP协议是不可靠的协议,也意味着UDP协议不需要考虑数据传输时可能出现的问题,因此UDP无论是使用还是维护都更为简单
虽然TCP复杂,但某些情况下TCP的效率不一定比UDP低,TCP中不仅有保证可靠性的机制,还有提升传输效率的各种机制
UDP和TCP没有谁好,只有谁合适,网络通信时采用TCP还是UDP完全取决于上层的应用场景。若应用场景严格要求数据在传输过程中的可靠性,那么就必须采用TCP协议;若应用场景允许数据传输出现少量丢包,那么优先选择UDP协议,因为UDP协议更为简单、轻量
二、TCP协议格式
2.1 初识协议格式
TCP报头中各个字段的含义如下:
源/目的端口号:表示数据是从哪个进程来、发送到对端主机上的哪个进程
32位序号/32位确认序号:分别代表TCP报文中每个字节数据的编号以及对对方的确认,是TCP保证可靠性的重要字段
4位首部长度:表示TCP报头的长度,以4字节为单位(最大60字节)
6位保留字段:TCP报头中暂时未使用的6个bit位
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从底层获取到一个报文后,报文的前20个字节是TCP的基本报头,并且这20字节中包含了4位首部长度。因此TCP是如下分离报头与有效载荷的:
当TCP获取到一个报文后,首先读取报文的前20个字节,并从中提取出4位首部长度,此时便获得了TCP报头的大小size
若size的值大于20字节,则继续从报文中读取size−20字节的数据,这部分数据就是TCP报头中的选项字段
读取完TCP的基本报头和选项字段后,剩下的就是有效载荷了
注意:
4位首部长度的基本单位是4字节,4位数据的取值范围是0000 ~ 1111,因此TCP报头最大长度为15 × 4 = 60字节。基本报头的长度是20字节,所以报头中选项字段的长度最多为40字节
若TCP报头中不携带选项字段,那么TCP报头的长度就为20字节,此时报头中的4位首部长度的值就为20 ÷ 4 = 5,即0101
由上可知,4位首部长度的取值范围为:0101 ~ 1111
TCP如何将有效载荷交付给上层?
应用层的每一个网络进程都绑定了一个端口号。服务端进程显示绑定端口号;客户端进程由系统动态绑定端口号。而TCP的报头中涵盖了目的端口号,因此TCP可以提取出报头中的目的端口号,找到对应的应用层进程,进而将有效载荷交给对应的应用层进程进行处理
注意:内核中用哈希的方式维护了端口号与进程ID之间的映射关系,因此传输层可以通过端口号快速找到其对应的进程ID,进而找到对应的应用层进程
2.2 序号与确认序号
对于可靠的认识
在进行网络通信时,一方发出的数据后不能保证该数据能够成功被对端收到,因为数据在传输过程中可能会出现各种错误,只有收到对端主机发来的响应消息后,该主机才能保证上一次发送的数据被对端可靠的收到了(只有历史数据可靠)
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位序号与其有效载荷的字节数,进而确定下一个报文对应的序号
32位确认序号
当主机B收到主机A发送过来的32位序号为1的报文时,由于该报文中包含1000字节的数据。主机B已经收到序列号为1-1000的字节数据,于是主机B发给主机A的响应数据的报头中的32位确认序号的值就填成1001
一方面是告诉主机A,序列号在1001之前的字节数据已经收到了
另一方面是告诉主机A,下次发送数据时应该从序列号为1001的字节数据开始进行发送
注意:响应数据与其他数据相同,也是完整的TCP报文。该报文可能不携带有效载荷,但至少具有完整报头
如何处理报文丢失?
主机A发送了三个报文给主机B,其中每个报文的大小都是1000字节,这三个报文的32位序号分别是1、1001、2001。若这三个报文在网络传输过程中出现了丢包,最终只有序号为1和2001的报文被主机B收到了,那么当主机B在对报文进行顺序重排的时候,就会发现只收到了1-1000和2001-3000的字节数据。此时主机B在对主机A进行响应时,其响应报头中的32位确认序号填的就是1001,告诉主机A下次发送数据时应从序列号为1001的字节数据开始进行发送
此时主机B在给主机A响应时,其32位确认序号不能填3001,因为1001-2000是在3001之前的,若直接给主机A响应3001,就说明序列号在3001之前的字节数据全都收到了。因此主机B只能给主机A响应1001,当主机A收到该确认序号后就会判定序号为1001的报文丢包了,此时主机A就可以选择进行重传
因此发送端可以根据对端发来的确认序号,来判断是报文是否在传输过程中丢失
为什么要用两套序号机制?
若通信双方只有一端发送数据,另一端接收数据,那么只用一套序号即可
发送端在发送数据时,将该序号看作是32位序号
接收端在对发送端发来的数据进行响应时,将该序号看作是32位确认序号
但TCP并没有这么做,根本原因就是因为TCP是全双工的,双方可能都想给对方发送消息
双方发出的报文中,不仅需要填充32位序号来表明当前发送数据的序号
还需要填充32位确认序号,对对方上一次发送的数据进行确认,告诉对方下一次应该从哪一字节序号开始进行发送
因此进行TCP通信时,双方都需要有确认应答机制,此时一套序号就无法满足需求了,因此TCP报头中出现了两套序号
总结
32位序号的作用:保证数据的按序到达,同时这个序号也是对端发送报文时填充32位确认序号的根据
32位确认序号的作用:告诉对端当前已经收到的字节数据有哪些,对端下一次发送数据时应该从哪一字节序号开始进行发送
序号和确认序号是确认应答机制的数据化表示,确认应答机制由序号和确认序号来保证
通过序号和确认序号还可以判断某个报文是否丢失
2.3 16位窗口大小
TCP存在接收缓冲区和发送缓冲区
TCP本身是具有接收缓冲区和发送缓冲区的。接收缓冲区用来暂时保存接收到的数据;发送缓冲区用来暂时保存还未发送的数据。这两个缓冲区都在TCP传输层内部实现
TCP发送缓冲区中的数据由上层进行写入。当上层调用write/send等系统调用接口时,实际不是将数据直接发送到了网络中,而是将数据从应用层拷贝到了TCP的发送缓冲区中。
TCP接收缓冲区中的数据最终被应用层读取。当上层调用read/recv等系统调用接口时,实际也不是直接从网络中读取数据,而是将数据从TCP的接收缓冲区拷贝到了应用层而已
类似于调用read和write进行文件读写时,并不是直接从磁盘读取数据,也不是直接将数据写入到磁盘上,而对文件缓冲区进行的读写操作
当数据写入到TCP的发送缓冲区后,对应的write/send函数(本质上是拷贝函数)就可以返回了,至于发送缓冲区中的数据具体什么时候发,怎么发等问题实际都是由TCP决定的。之所以称TCP为传输控制协议,就是因为最终数据的发送和接收方式、时间,以及传输数据时遇到的各种问题如何解决,都是由TCP自行决定的,用户只需要将数据拷贝到TCP的发送缓冲区,以及从TCP的接收缓冲区中读取数据即可
TCP的发送缓冲区和接收缓冲区存在的意义
数据在网络中传输时可能会出现某些错误,此时就可能要求发送端进行数据重传,因此TCP必须提供一个发送缓冲区来暂时保存发送出去的数据,以免数据丢失。只有当发出去的数据被对端可靠的收到后,发送缓冲区中的这部分数据才可以被覆盖
接收端处理数据的速度是有限的,为了保证没来得及处理的数据不会被迫丢弃,因此TCP必须提供一个接收缓冲区来暂时保存未被处理的数据。因为数据传输是需要耗费资源与时间的,不能随意丢弃正确的报文
TCP的数据重排也在接收缓冲区中进行
经典的生产者消费者模型:
对于发送缓冲区来说,上层应用不断往发送缓冲区中放入数据,下层网络层不断从发送缓冲区中拿出数据准备进一步封装。此时上层应用扮演的就是生产者的角色,下层网络层扮演的就是消费者的角色,而发送缓冲区对应的就是"交易场所"
对于接收缓冲区来说,上层应用不断从接收缓冲区中拿出数据进行处理,下层网络层不断往接收缓冲区中放入数据。此时上层应用扮演的就是消费者的角色,下层网络层扮演的就是生产者的角色,而接收缓冲区对应的就是"交易场所"
因此引入发送缓冲区和接收缓冲区相当于引入了两个生产者消费者模型,该生产者消费者模型将上层应用与底层通信细节进行了解耦。生产者消费者模型的引入同时也解决了并发和忙闲不均
窗口大小
当发送端要将数据发送给对端时,本质是把自己发送缓冲区中的数据发送到对端的接收缓冲区中。但缓冲区大小是有限的,若接收端处理数据的速度小于发送端发送数据的速度,那么总有一个时刻接收端的接收缓冲区会被打满,这时发送端再发送数据过来就会造成数据被迫被丢弃,进而引起丢包重传等一系列的连锁反应(浪费资源)
因此TCP报头中就有了16位窗口大小,这个16位窗口大小中填的是自身接收缓冲区中剩余空间的大小,即当前主机接收数据的能力
接收端在对发送端发来的数据进行响应时,就可以通过16位窗口大小告知发送端自己当前接收缓冲区剩余空间的大小,此时发送端就可以根据这个窗口大小字段来调整自己发送数据的速度
窗口大小字段越大,说明接收端接收数据的能力越强,此时发送端可以提高发送数据的速度
窗口大小字段越小,说明接收端接收数据的能力越弱,此时发送端可以减小发送数据的速度
若窗口大小的值为0,说明接收端接收缓冲区已经被打满了,此时发送端就不应该再发送数据了
2.4 六个标志位
标志位存在的意义
TCP报文的种类多种多样,除了正常通信时发送的普通报文,还有建立连接时发送的请求建立连接的报文、断开连接时发送的断开连接的报文等。收到不同种类的报文时需要对应执行动作,如正常通信的报文需要放到接收缓冲区中等待上层应用进行读取,而建立和断开连接的报文本质不是交给用户处理的,而是需要让操作系统在TCP层执行对应的握手和挥手动作
不同种类的报文对应的是不同的处理逻辑,所以要能够区分报文的种类。TCP就是使用报头中的六个标志字段进行区分的,这六个标志位都只占一个bit位
SYN
报文中的SYN被设置为1,表明该报文是一个连接建立的请求报文
只有在连接建立阶段,SYN才被设置,正常通信时SYN不会被设置
ACK
报文中的ACK被设置为1,表明该报文可以对收到的报文进行确认
一般除了第一个请求报文没有设置ACK以外,其余报文基本都会设置ACK。因为发送出去的数据本身就对上一条对方发送过来的数据具有一定确认能力,因此双方在进行数据通信时,可以顺便对对方上一次发送的数据进行响应
FIN
报文中的FIN被设置为1,表明该报文是一个连接断开的请求报文
只有在断开连接阶段,FIN才被设置,正常通信时FIN不会被设置
URG
双方在进行网络通信的时候,由于TCP是保证数据按序到达的,TCP通过序号来对这些TCP报文进行顺序重排,最终就能保证数据到达对端接收缓冲区中时是有序的,对端上层在从接收缓冲区读取数据时也是按顺序读取的。但是有时候发送端可能发送了一些"紧急数据",这些数据需要让对方上层优先提取读取
此时就需要URG标志位,以及TCP报头中的16位紧急指针
当URG标志位被设置为1时,需要通过TCP报头中的16位紧急指针来找到紧急数据,否则一般情况下不关注TCP报头中的16位紧急指针
16位紧急指针代表的就是紧急数据在报文中的偏移量
紧急指针只有一个,只能标识数据段中的一个位置,因此紧急数据只能发送一个字节,而至于这一个字节的具体含义就不展开讨论了
recv函数的第四个参数flags有一个MSG_OOB的选项可供设置,其中OOB是带外数据(out-of-band)的简称,带外数据就是一些比较重要的数据,因此上层若想读取紧急数据,就可以在使用recv函数进行读取,并设置MSG_OOB选项
send函数的第四个参数flags也提供了MSG_OOB的选项,上层若想发送紧急数据,就可以使用send函数进行写入,并设置MSG_OOB选项
PSH
报文中的PSH被设置为1,是在催促对方尽快将接收缓冲区中的数据交付给上层,即使接收缓冲区中的数据还没到达所指定的"水位线"
当使用read/recv从缓冲区中读取数据时,若缓冲区中有数据read/recv函数就能够读到数据进行返回,而若缓冲区当中没有数据,那么此时read/recv函数就会阻塞,直到当缓冲区中有数据时才会读取到数据进行返回。实际这种说法是不太准确的,其实接收缓冲区和发送缓冲区都有一个水位线的概念
若接收缓冲区中有一点数据就让read/recv函数读取返回了,此时read/recv就会频繁的进行读取和返回,进而影响读取数据的效率(在内核态和用户态之间切换有成本)。因此不是接收缓冲区中只要有数据,调用read/recv函数时就能读取到数据进行返回,而是当缓冲区中的数据量达到一定量时(超过水位线)才能进行读取。这就是为什么使用read/recv函数读取数据时,期望读取的字节数和实际读取的字节数不一定是吻合的
RST
报文中的RST被设置为1,表示需要让对方重新建立连接
在通信双方在连接未建立好的情况下,一方向另一方发数据,此时另一方发送的响应报文中的RST标志位就会被置1,表示要求对方重新建立连接。在双方建立好连接进行正常通信时,若通信中途发现之前建立好的连接出现了异常也会要求重新建立连接