零、前言
本章主要讲解传输层协议UDP及TCP相关的内容
一、UDP协议
- UDP协议端格式:
- 说明:
- 16位源端口号:表示数据从哪里来
- 16位目的端口号:表示数据要到哪里去
- 16位UDP长度:表示整个数据报(UDP首部+UDP数据)的长度
- 16位UDP检验和:如果UDP报文的检验和出错,就会直接将报文丢弃
注:端口号大部分都是16位的,其根本原因就是因为传输层协议当中的端口号就是16位的
- UDP如何将报头与有效载荷进行分离:
UDP报头是一种定长报头,UDP在读取报文时读取完前8个字节(报头)后剩下的就都是有效载荷了
- UDP如何将有效载荷交付给上层协议:
获取到一个报文后从该报文的前8个字节中提取出对应的目的端口号,通过目的端口号找到对应的上层应用层进程进行交付
- UDP的特点:
- 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接
- 不可靠: 没有确认机制, 没有重传机制,无法保证接收报文的正确顺序; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息
- 面向数据报: 不能够灵活的控制读写数据的次数和数量
- 对于面向数据报的理解:
- 应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并
- 用UDP传输100个字节的数据:如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节
- UDP的发送和接收缓冲区:
- UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作
- UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃
- 如果没有接收缓冲区,那么就要求上层及时将获取到的报文读取上去,如果没有及时被读取,那么新获取的报文数据就会被迫丢弃;UDP会维护接收缓冲区将接收到的报文暂时的保存起来,供上层读取
- UDP是全双工通信:
UDP的socket在读(写)的同时也能进行写(读),也就是说不会像管道这种半双工通信读写是阻塞进行的(无法同时进行)
- UDP使用注意事项:
- UDP协议首部中有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)
- 如果我们需要传输的数据超过64K, 就需要在应用层手动的分包,多次发送,并在接收端手动拼装
- 基于UDP的应用层协议:
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
- 关于可靠性的理解:
- 计算机中的硬件设备是之间的数据传输是依靠“线”进行的,而这些硬件设备都在一台机器上,因此传输数据的“线”是很短的,传输出错的概率也非常低
- 如果要在网络中进行通信(距离远),此时需要维护传输的“线”就非常长,传输出错的概率增大,要保证传输的可靠性就需要相应的做更多的事情,而TCP就是一种保证可靠性的协议
- UDP协议存在的意义:
- TCP协议是可靠的协议,也就意味着TCP协议需要做更多的工作来保证传输数据的可靠,此时需要的成本相比于UDP更高
- UDP协议是不可靠的协议,也就意味着UDP协议不需要考虑数据传输时可能出现的问题,但UDP无论是使用还是维护都足够简单
- 虽然TCP复杂,但TCP的效率不一定比UDP低,TCP当中不仅有保证可靠性的机制,还有保证传输效率的其他机制
- 网络通信时具体采用TCP还是UDP完全取决于上层的应用场景。如果应用场景严格要求数据在传输过程中的可靠性,那么就必须采用TCP协议,如果应用场景允许数据传输出现少量丢包,那么肯定优先选择UDP协议,因为UDP协议足够简单
二、TCP协议
TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制
- TCP协议段格式:
- 说明:
- 16位源端口号:表示数据从哪里来
- 16位目的端口号:表示数据要到哪里去
- 32位序号:表示发送的TCP报文的编号
- 32位确认序号:表示对接收到对方曾经发送过的TCP报文的确认
- 4位TCP报头长度:表示该TCP报头的长度,以4字节为单位(表示范围20-60字节:固定的字段就有20字节,如果带有选项的话,最大报头可以为15*4=60字节)
- 6位保留字段:TCP报头中暂时未使用的6个比特位
- 16位窗口大小:保证TCP可靠性机制和效率提升机制的重要字段
- 16位检验和:由发送端填充,采用CRC校验;接收端校验不通过,则认为接收到的数据有问题(检验和包含TCP首部+TCP数据部分)
- 16位紧急指针:标识紧急数据在报文中的偏移量,需要配合标志字段当中的URG字段统一使用
- TCP报头当中的6位标志位:
URG:紧急指针是否有效
ACK:确认序号是否有效
PSH:提示接收端应用程序立刻将TCP接收缓冲区当中的数据读走
RST:表示要求对方重新建立连接。我们把携带RST标识的报文称为复位报文段
SYN:表示请求与对方建立连接。我们把携带SYN标识的报文称为同步报文段
FIN:通知对方,本端要关闭了。我们把携带FIN标识的报文称为结束报文段
- TCP如何将报头与有效载荷进行分离:
获取到TCP报文后,首先读取报文的前20个字节,并从中提取出4位的首部长度,此时便获得了TCP报头的大小,读取完TCP的基本报头和选项字段后,剩下的就是有效载荷了
- TCP具有发送和接收缓冲区:
- 通过发送缓冲区存储发送的数据,当收到对应的应答后(也就是对应的数据成功被对端接收),对应的数据才会‘取出’,也就保证了发送的可靠性;通过接收缓冲区储存接收到的数据,保证数据能够可靠的被上层取出
- 调用write/send这样的系统调用接口时,实际不是将数据直接发送到了网络当中,而是将数据从应用层拷贝到了TCP的发送缓冲区当中;当上层调用read/recv这样的系统调用接口时,实际也不是直接从网络当中读取数据,而是将数据从TCP的接收缓冲区拷贝到了应用层这样能够使得应用层能够和传输层进行解耦,上层只需要将数据交给TCP或者从TCP中将数据取出,TCP则是负责数据的发送和接收的问题
- 16位窗口大小:
- 当发送端要将数据发送给对端时,本质是把自己发送缓冲区当中的数据发送到对端的接收缓冲区当中,但缓冲区是有大小的,不能无限制的接收,所以需要对发送进行控制
- 而16位窗口大小当中填的是自身接收缓冲区中剩余空间的大小,当对端接收到窗口信息时可以根据这个字段来调整自己发送数据的速度,避免发送速度过快造成缓冲区满足,进而可能引起丢包重传
- 用read/recv函数从套接字当中读取数据时,会因为套接字当中没有数据而被阻塞住,本质是因为TCP的接收缓冲区当中没有数据了,实际是阻塞在接收缓冲区当中了;调用write/send函数往套接字中写入数据时,会因为套接字已经写满而被阻塞住,本质是因为TCP的发送缓冲区已经被写满了,实际是阻塞在发送缓冲区当中了
- 六个标志位:
- TCP报文除了正常通信时发送的普通报文,还需要其他类型的报文来表示其他的一些需求,例如连接,断开连接等等;TCP使用报头当中的六个标志字段来进行区分的报文类型,这六个标志位都只占用一个比特位,为0表示假,为1表示真
- SYN:报文当中的SYN被设置为1,表明该报文是一个连接建立的请求报文;只有在连接建立阶段,SYN才被设置,正常通信时SYN不会被设置
- ACK:报文当中的ACK被设置为1,表明该报文可以对收到的报文进行确认;一般除了第一个请求报文没有设置ACK以外,其余报文基本都会设置ACK,因为发送出去的数据本身就对对方发送过来的数据的确认
- FIN:报文当中的FIN被设置为1,表明该报文是一个连接断开的请求报文;只有在断开连接阶段,FIN才被设置,正常通信时FIN不会被设置
- URG:TCP是保证数据按序到达的,对于若干个TCP报文进行发送,最终到达接收端时这些数据也都是有序的,对于发送“紧急数据”就需要让对方上层提取进行加急处理;当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选项
6、PSH:报文当中的PSH被设置为1,是在告诉对端尽快将缓冲区里的数据进行取出
7、RST:报文当中的RST被设置为1,表示需要让对方重新建立连接;双方在连接未建立好一方发数据,另一方要求对方重新建立连接,建立好的连接出现了异常也会要求重新建立连接
1、应答机制
- 示图:
- 概念及介绍:
- 在进行网络通信时,一方发出的数据后,它不能保证该数据能够成功被对端收到,因为数据在传输过程中可能会出现各种各样的错误,只有当收到对端主机发来的响应消息后,该主机才能保证上一次发送的数据被对端成功的收到了,这就叫做真正的可靠,而这种策略在TCP当中就叫做确认应答机制
- 在实际的网络通信中,最后一次的数据传输是无法确认对端是否成功收到,但实际没有必要保证所有消息的可靠性,只要传输的核心数据都有对应的响应就可以,一些无关紧要的数据没有必要保证它的可靠性
注:对端如果没有收到这个响应数据,会判定上一次发送的报文丢失了,此时对端可以将上一次发送的数据进行重传
2、序号机制
- 序号和确认序号:
- TCP将每个字节的数据都进行了编号(TCP具有发送缓冲区,缓冲区的下标即为编号),即为序列号
- 每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发
- 示图:
- 序号概念:
- 在双方实际进行网络通信时,为了提高数据传输的效率,允许一方向另一方连续发送多个报文数据,只要保证发送的每个报文都有对应的响应消息就行了
- 在连续发送多个报文时,由于各个报文在进行网络传输时选择的路径可能是不一样的,因此这些报文到达对端主机的先后顺序也就可能和发送报文的顺序是不同的,而TCP报头中的32位序号的作用之一实际就是用来保证报文的有序性的
- 接收端收到多个TCP报文后,就可以根据TCP报头当中的32位序列号对这多个报文进行顺序重排,重排后将其放到TCP的接收缓冲区当中,此时接收端这里报文的顺序就和发送端发送报文的顺序是一样的
- 当进行报文重排时,会根据当前报文的32位序号与其有效载荷的字节数,进而确定下一个报文对应的序号,通过检验序号查看是否有数据丢失,进而进行重发
- 确认序号概念:
- 报头当中的确认序号实际就是,接收缓冲区中接收到的最后一个有效数据的下一个位置所对应的下标
- TCP报头当中的32位确认序号是告诉对端,当前已经收到了之前的哪些数据,你的数据下一次应该从哪里开始发
示例:
- 解释:
当主机B收到主机A发送过来的32位序号为1的报文时,由于该报文当中包含1000字节的数据,因此主机B已经收到序列号为1-1000的字节数据,于是主机B发给主机A的响应数据的报头当中的32位确认序号的值就会填成1001…
- 为什么要用两套序号机制:
- 一般来说双方通信发送数据和应答数据,只用一套序号就可以了;TCP为了保证效率,在应答对方之前发送的数据的同时可能想给对方发送数据
- 双方发出的报文当中,不仅需要填充32位序号来表明自己当前发送数据的序号,还需要填充32位确认序号,对对方上一次发送的数据进行确认,告诉对方下一次应该从哪一字节序号开始进行发送
3、超时重传机制
- 概念及介绍:
- 双方在进行网络通信时,发送方发出去的数据在一个特定的事件间隔内如果得不到对方的应答,此时发送方就会进行数据重发,这就是TCP的超时重传机制
- 超时重传机制实际就是发送方在发送数据后开启了一个定时器,若是在这个时间内没有收到刚才发送数据的确认应答报文,则会对该报文进行重传
- 发送的数据报文丢失:
此时发送端在一定时间内收不到对应的响应报文,就会进行超时重传
- 示图:
说明:主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发
- 对方的响应报文丢失:
此时发送端也会因为收不到对应的响应报文,而进行超时重传
- 示图:
说明:主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉,通过序列号就可以很容易做到去重的效果
注:当发送缓冲区当中的数据被发送出去后,操作系统不会立即将该数据从发送缓冲区当中删除或覆盖,而会让其保留在发送缓冲区当中,以免需要进行超时重传,直到收到该数据的响应报文后,发送缓冲区中的这部分数据才可以被删除或覆盖
- 超时重传的等待时间:
- 超时重传的时间设置的太长,会导致丢包后对方长时间收不到对应的数据,进而影响整体重传的效率;超时重传的时间设置的太短,会导致对方收到大量的重复报文,可能对方发送的响应报文还在网络中传输而并没有丢包,但此时发送方就开始进行数据重传了,而发送大量重复报文会是对网络资源的浪费
- 超时重传的时间需要保证“确认应答一定能在这个时间内返回”,同时这个时间的长短是与网络环境有关的,会根据网络状况进行相应的变化
- Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍,如果重发一次之后,仍然得不到应答,下一次重传的等待时间就是2 × 500 2\times5002×500ms,如果仍然得不到应答,那么下一次重传的等待时间就是4 × 5004\times5004×500ms,以此类推以指数的形式递增
- 当累计到一定的重传次数后,TCP就会认为是网络或对端主机出现了异常,进而强转关闭连接
4、连接管理机制
- 概念及介绍:
- TCP是面向连接的,在进行TCP通信之前需要先建立连接,保证传输数据的可靠性
- 面向连接是TCP可靠性的一种,一台机器上可能会存在大量的连接,此时操作系统就需要对这些连接进行管理
- 建立连接,实际就是在操作系统中用该结构体定义一个结构体变量,然后填充连接的各种属性字段,最后将其插入到管理连接的数据结构当中即可;断开连接,实际就是将某个连接从管理连接的数据结构当中删除,释放该连接曾经占用的各种资源
- 示图:
- 服务端状态转化:
- [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接
- [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文
- [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了
- [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT
- [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
- [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接
- 客户端状态转化:
- [CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段
- [SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据
- [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1
- [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段
- [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK
- [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态
三次握手
- 示图:
- 三次握手过程:
- 第一次握手:客户端向服务器发送的报文当中的SYN位被设置为1,表示请求与服务器建立连接
- 第二次握手:服务器收到客户端发来的连接请求报文后,紧接着向客户端发起连接建立请求并对客户端发来的连接请求进行响应,此时服务器向客户端发送的报文当中的SYN位和ACK位均被设置为1
- 第三次握手:客户端收到服务器发来的报文后,得知服务器收到了自己发送的连接建立请求,并请求和自己建立连接,最后客户端再向服务器发来的报文进行响应
- 需要注意的是,客户端向服务器发起的连接建立请求,是请求建立从客户端到服务器方向的通信连接,而TCP是全双工通信,因此服务器在收到客户端发来的连接建立请求后,服务器也需要向客户端发起连接建立请求,请求建立从服务器到客户端方法的通信连接
- 为什么是三次握手,不是其他次数:
- 三次握手是验证双方通信信道的最小次数
TCP是全双工的,连接建立的核心要务是验证双方的通信信道是否是良好的(从服务端到客户端,从客户端到服务端的信道),而三次握手恰好是验证双方通信信道的最小次数(此时服务端和客户端都进行了数据的发送和接收)
- 三次握手能够保证连接建立时的异常连接挂在客户端
- 我们知道最后一次的数据发送是无法知道是否成功被对端接收的,也就是说最后一次的握手是存在丢包的风险的。一方发起最后一次握手前会进行连接的建立,而另一方在接收到最后一次握手时才会进行连接的建立,如果最后一次握手丢包,那么发起方会存有一个异常的连接
- 而连接的维护是需要时间成本和空间成本的,同时服务端和客户端是一个1:n的数量状况,如果异常连接的维护是在服务端的话,当存在大量的异常连接就会影响服务器的性能;发起连接请求是客户端访问服务端,当奇数次握手时异常连接是挂在客户端的,而不会影响到服务器
注:建立连接失败时的异常连接不会一直维护下去。如果服务器端长时间收不到客户端发来的第三次握手,就会将第二次握手进行超时重传,此时客户端就会重新发出第三次握手;或者当客户端认为连接建立好后向服务器发送数据时,此时服务器会发现没有和该客户端建立连接而要求客户端重新建立连接(设置RST标志位)
- 上层调用和连接之间的关系:
- connect函数不参与底层的三次握手,connect函数的作用只是发起三次握手。当connect函数返回时,要么是底层已经成功完成了三次握手连接建立成功,要么是底层三次握手失败
- 如果服务器端与客户端成功完成了三次握手,此时在服务器端就会建立一个连接,但这个连接在内核的等待队列当中,服务器端需要通过调用accept函数将这个建立好的连接获取上来
- 当服务器端将建立好的连接获取上来后,上层就可以通过调用read/recv函数和write/send函数进行数据交互了
四次挥手
- 示图:
- 四次挥手过程:
- 第一次挥手:客户端向服务器发送的报文当中的FIN位被设置为1,表示请求与服务器断开连接
- 第二次挥手:服务器收到客户端发来的断开连接请求后对其进行响应
- 第三次挥手:服务器收到客户端断开连接的请求,且已经没有数据需要发送给客户端的时候,服务器就会向客户端发起断开连接请求
- 第四次挥手:客户端收到服务器发来的断开连接请求后对其进行响应
- 为什么是四次挥手:
- TCP是全双工的,建立的连接是双向的,所以断开连接需要将服务端对客户端的连接以及客户端对服务端的连接都给断开,每两次挥手对应就是关闭一个方向的通信信道,因此断开连接时需要进行四次挥手
- 四次挥手当中的第二次和第三次挥手不能合并在一起,第三次握手是服务器端想要与客户端断开连接时发给客户端的请求,服务器不一定会马上发起第三次挥手,服务器可能还有某些数据要发送给客户端(此时服务端对客户端的连接还没有断开)
- 上层调用和断开连接之间的关系:
客户端发起断开连接请求,对应就是客户端主动调用close函数,服务器发起断开连接请求,对应就是服务器主动调用close函数,一个close对应的就是两次挥手,双方都要调用close,因此就是四次挥手