网络 | UDP与TCP协议讲解 | TCP可靠性是怎样实现的?

news/2024/5/17 15:14:14 标签: 网络协议, tcp

文章目录

    • 前置知识
      • 查看网络状态的工具
      • 查看进程id
    • UDP协议
      • 协议格式
      • UDP只有接收缓冲区
      • 基于UDP的应用层协议
    • TCP协议
      • 流的理解
      • 协议格式
      • 确认应答机制
      • 缓冲区
      • 序号的作用
      • 流量控制
      • 超时重传机制
      • 6位标志位
        • 紧急数据的处理
      • 三次握手
        • listen的第二个参数
      • 全连接和半连接队列都维护了什么信息?
      • 全连接和半连接的优缺点
      • 为什么半连接可以阻止SYN洪水攻击?
      • 四次挥手
        • COLSE_WAIT状态与TIME_WAIT状态
      • 滑动窗口
        • 快重传
      • 拥塞控制
      • 延迟应答
      • 捎带应答
      • tcp异常处理

前置知识

IP+端口号可以确定网络中指定计算机上的指定进程,传输层负责维护端口,网络层负责维护IP,作为传输层协议的TCP和UDP,肯定包含了端口号的相关信息。而一些端口号是我们要避免使用的,它们被称为知名端口号

知名端口号:0~1023
非知名端口号:1024~65535

一些广为人知的协议:SSH,HTTP,FTP,这些应用层协议都会使用固定的端口,所以我们不能随意使用0~1023之间的端口号,以免产生冲突。

ftp服务端,21端口
ssh服务端,22端口
telnet服务端,23端口
http服务端,80端口
https服务端,443端口

在/etc/services目录下,存储了常见协议的端口,可以使用指令

cat /etc/services

了解这些端口。在0 ~ 1023之外的端口号,我们就能随意使用了

查看网络状态的工具

netstat
n 拒绝显示别名,将别名显示成数字
l 仅列出在listen状态的程序
p 显示建立相关连接的程序名
t 仅查看tcp选项
u 仅查看udp相关选项
a 显示所有选项,默认不显示listen相关

较经常使用的选项是netstat -nlpt

查看进程id

pidof 进程名,查看该进程名相关的pid
在这里插入图片描述

UDP协议

协议格式

学习一个协议,需要从两个方面入手:1.基本传输单元如何封装与解包?2.基本传输单元如何分用(向上交付)?要搞清楚这两个问题,就要从协议的格式入手
在这里插入图片描述
数据段的宽度为32b,4个字节,前8个字节为报头,剩余字节为有效载荷。在报头中,前4字节的前两个字节表示源端口,后两字节表示目的端口。后4字节的前两字节表示整个数据段的长度,后两字节是用来校验数据段的数据。UDP是传输层协议,维护端口号的信息再正常不过了,正是由于目的端口号的维护,数据段的分用被很好的解决了,解包得到的有效数据将交付给上层,哪个上层?端口号都告诉你了,当然是找对应端口号的进程了,这是分用。那么封装和解包呢?这里先解释一下数据报的概念,信封总是一封一封的邮递的,没有邮递半封信的说法吧。正常情况下,我给你邮10封信,你也会收到10封信。每封信都有信纸隔开,信和信之间有明显的界限。一次UDP数据段的发送就像信封的发送一样,每次发送的数据段都是报头+有效载荷。报头中有2字节的字段表示了整个数据段的大小(最大为64k),上层将有效数据交付给传输层时,传输层只要计算有效数据的长度,填入源端口和目的端口,校验和字段,将完整的报头添加到有效数据的首部,就完成了数据段的封装。至于解包,只要取出数据段的前8字节,解析报头,得到数据段的总长度,就能精准的找到有效数据的具体范围,取出有效数据交付给上层

UDP只有接收缓冲区

UDP只有接收缓冲区,没有发送缓冲区,可以理解为UDP数据段较小,并且UDP本身就是简单的协议,所以UDP直接将数据段交付给下层网络层,由网络层对其进行处理。而接收UDP数据段时,系统可能被抢占,暂时无法直接读取数据段,所以这时下层的网络层会将数据段放到UDP的接收缓冲区中,达到系统能够处理这些数据时,才会将数据读取。要是缓冲区满了,后续的数据段将丢失,并且UDP是不可靠协议,所以不会进行数据段的重传。由此可见,UDP协议是全双工的,我们可以创建两个线程,一个用UDP进行数据发送,一个读取UDP接收到的数据

基于UDP的应用层协议

  • NFS:网络文件传输协议
  • TFTP:简单文件传输协议
  • DHCP:动态主机配置协议
  • BOOTH:启动协议(用于无盘设备启动)
  • DNS:域名解析协议

TCP协议

流的理解

面向字节流是tcp协议的特点,关于流的概念,我们可以理解位水龙头中的水流,水龙头的打开和关闭决定了水的流量,水的流量由我们控制。tcp也是如此,报头中没有指明报文长度的字段,我们无法通过报头确定一次通信中tcp的字节大小,所以关于两次tcp数据包的分割需要由上层协议进行具体的定制。对于面向数据报的udp,上层只要调用一次read,就可以像收信封一样接收一次udp报文,因为udp报头中有具体的报文长度,并且报文最大长度也有限制(64K)。但是对于面向字节流的tcp,上层只能一直调用read函数,不断的读取tcp报文填充接收缓冲区,然后根据应用层协议(其中定制了分割报文的规则),将不同报文进行分割。

并且使用tcp协议发送数据时,如果发送的数据太少,数据会被存储在发送缓冲区中,不直接发送,等到数据量大的时候再一起发送,但是udp不一样,udp一拿到数据,直接就发送了。当tcp发送的数据量太多时,tcp会将其进行拆分,将其拆分为多个tcp报文再发送,而udp则是明确的限制了报文的长度,你不能超过这个长度,超了后果自负。

总结一下,tcp像水流,你只管流,我得到的水要怎么使用是我的事,要把水装成几杯,取决于我。而udp像信封,不需要我们考虑如何使用,它的数量是确定的。拿到了信封,只管把它拆开(分离报头和有效载荷)。所以说tcp存在粘包问题,而udp不会发生粘包

协议格式

在这里插入图片描述

确认应答机制

确认应答机制是可靠性的一种实现

协议中有32位序列号和32位确认序列号。这一对序列就涉及到了tcp确认应答机制,如何确认发送的数据被对方接收到了?或者说,怎么保证通信的可靠性?只要对方对这些数据作出有关的响应,我们就知道对方接收到了我们的数据。比如说“你吃饭了吗?”,对方回答“我吃了”,做出了有关的响应,我们就知道了对方接收到了我们的问题。这样的确认应答机制中,最后一次通信肯定无法被确认应答,比如说刚才的对话,对方回答“我吃了”之后,我们就不再回应,对方此时就无法知道我们是否听到了他的话。在这样的机制下,除了最后一次通信之外的数据都是可以被确认应答的,所以我们将重要的数据放到前面,最后进行一次无关痛痒的通信,或者采用额外的机制保证其确认应答

关于确认应答机制的具体实现:tcp将每个字节的数据进行编号,即序列号,比如服务端发送1000字节给客户端,客户端响应1001,即确认序列号,告知服务端我接收到了前1000字节数据,希望下次接收的数据从1001字节开始。这里有一个问题,为什么报头中要有序列号和确认序列号呢?两者使用一个相同字段不就行了?这个问题的本质是:tcp是全双工的,接收的同时还能发送数据,比如我问“你吃了吗?”,你回到“我吃了,你呢?”,这就是一个接收+发送,我不仅确认收到了你的信息,还向你发送新的信息,这样的话,接收和发送就要用不同的字段进行表示,所以tcp报头中有两个序列号字段

由于安全问题,每次tcp通信的序列号都是随机生成的,并且以字节为单位进行递增,如果序列号太大导致溢出了,那么序列号将会回绕,从0开始继续递增

回到报头,确认序列号之后是:

  • 4位的首部长度:其基本单位为4字节,表示报头的长度,最多能表示15*4 = 60字节,由于tcp报头长度=标准报头的20字节+选项的长度,其中标准报头是通信中必须含有的字段,所以首部长度从0101(20)开始
  • 然后是6位的保留位,不表示任何信息
  • 接着是6位的标志位:URG,ACK,PSH,RST,SYN,FIN,这个涉及的东西太多,后面细讲,先跳过

接着是窗口大小,这个展开讲讲,首先要明白IO类函数的本质,像read,write,sendto,recv。这些函数的本质不是发送和接收,而是拷贝,是将用户态的数据拷贝到内核态或者将内核态的数据拷贝到用户态。这里再理解下缓冲区的概念

缓冲区

和udp不同,tcp拥有发送缓冲区和接收缓冲区,由于tcp是传输层协议,而传输层是位于操作系统中的,所以tcp的缓冲区是内核缓冲区,用户没有权限访问。调用write函数时,write只是将我们的数据从用户空间拷贝到内核空间(发送缓冲区),接收函数也是如此,数据到达接收缓冲区,由read将其中的数据拷贝到用户空间,我们才能读到这些数据。

序号的作用

回顾确认应答机制,其中最核心的部分就是序号,双方通过序号进行数据的确认应答。除此之外,序号还有另外两个应用:去重和数据的按序到达。先说去重,很简单,如果发送方发送了相同的数据段(由于未知的原因),那么接收方就可以根据序号判断接收缓冲区是否已经接收过了这个数据段,如果数据段发送过了(数据段位于已接收且确认应答的窗口中),接收方完全可以丢弃它,以此来提高效率。

接着说按序达到:tcp的一大特性就是可靠性,数据段的按序达到是可靠性中的一种。接收方完全可以根据每个数据段的序号对所有的数据段进行排序,或者是采用其他的机制保证数据段的有序。总之有了序号,不同数据段之间的相对关系也就确定了

流量控制

流量控制也是可靠性中的一种实现:其保证了接收方不会因为接收缓冲区的容量限制,使数据溢出,出现浪费网络带宽的现象

缓冲区肯定有容量上限,如果缓冲区满了,数据还是不断的发送,而操作系统又来不及处理这么多的数据,或者说操作系统的处理速度跟不上缓冲区的接收速度,这就会导致数据段的丢失,由于tcp是个可靠协议,数据段丢失了就要重传,为了避免不必要的重传,我们需要控制发送数据的速度,使其能够根据对方接收缓冲区的剩余大小进行灵活的调整(剩的多,就发快些,剩的少,就发慢些)。这就是16位窗口大小的作用,其表示接收缓冲区剩余空间的大小,双方每一次的通信,都可以用16为窗口大小告知对方自己还能接收多少数据,对方根据16位窗口大小及时调整自己的发送速度,以充分利用接收缓冲区的空间

当然16位窗口大小也只是流量控制中的一部分,滑动窗口,拥塞控制会在后续讲解

超时重传机制

超时重传机制也是可靠性中的一种:保证数据的完整传输,将可能丢失的数据重新发送

由于某些原因导致数据段在发送过程中丢失了,为保证tcp传输的可靠性,tcp需要对丢失的数据进行重传。当发送方发送数据后,会启动一个计时器,如果在指定时间内,发送没有收到对方的确认应答(ACK),就会认为该数据段丢失,需要对其进行重发。并且指定时间会延长至之前的两倍,如果在这段时间内发送方还没有收到接收方确认应答,就会再次重发,并且指定时间延长为上次的两倍。不断这样的重复操作,直到发送方接收到了对方的确认应答才会停止。要是重发达到一定次数,这时就会停止tcp连接,即因为某些问题,导致数据段的传输异常,数据总是无法被对方接收,此时要关闭双方的连接,排查可能存在的问题

6位标志位

我们知道tcp面向连接的协议,要进行tcp通信,首先要建立连接,要关闭tcp通信,肯定要关闭连接。这就意味着通信数据有不同的类型,用来请求建立连接的,用来关闭连接的,用来正常通信的,根据不同的类型,我们需要对这些数据进行分类,这个分类就在6位标志位中体现,比如用SYN来建立连接,用FIN来关闭连接,用ACK对数据进行确认应答

之前我写过tcp通信模型,其中的服务端主动调用read从socket的接收缓冲区中拷贝信息,如果接收缓冲区中没有信息,read就会陷入阻塞,直到缓冲区中有信息,read才会被唤醒,进行数据的拷贝。我要说的是:这种IO方式是很低效的,在实际开发中我们很少这样使用,一般都是在报头中携带PSH标志,用来提示上层尽快将数据取走,先不等待缓冲区其他数据的递达,将已递达的数据向上交付,这才是一种高效的IO方式。而一般缓冲区都会有一个最低限度,只有数据量超过了这个最低限度,上层才会将缓冲区中的数据拷贝走

  • URG: 紧急指针是否有效
  • ACK: 确认号是否有效
  • PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
  • RST: 拒绝一个连接,我们把携带RST标识的称为复位报文段
  • SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
  • FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段

紧急数据的处理

URG标志为1,表示16位紧急指针字段有效,此时它表示了从有效载荷的第一个字节开始,有多少个字节为紧急数据,也就是说,紧急指针字段的值减一就是紧急数据最后一个字节的下标。

  • 一般情况下,很少应用程序支持或使用tcp的紧急数据,所以接收方忽略URG标志,将紧急数据与普通数据放在同一缓冲区,一起处理
  • 如果接收方不忽略URG,那么紧急数据就会被提取出来,放到一个单独的缓冲区,作为一种带外数据供应用程序读取,让应用程序自己决定如何处理紧急数据
  • 如果接收方不忽略URG,那么紧急数据就会被提取出来,并且直接交付给应用程序,不经过缓冲区,这是一种比较危险但安全的作用,因为它可能会覆盖普通数据或者使普通数据丢失,但是它可以立即让应用程序收到紧急数据

三次握手

在这里插入图片描述
客户端向服务端发送含有SYN的报文,请求与服务端同步,客户端进入SYN_SENT状态。服务端接收SYN后,发送SYN+ACK确认收到客户端的请求并与请求与客户端同步,服务端发送之后进入SYN_RCVD状态。与此同时,服务端会维护一个半连接队列(syn queue),将与客户端连接的套接字放到队列中,维护起来(注意是在SYN+ACK发送后,服务端单方面的将连接放入半连接队列)。客户端收到服务端的ACK+SYN后,发送ACK,表示确认服务端的信息,并进入ESTABUSHED状态。服务端接收ACK后,进入ESTABUSHED状态,将处于半连接状态的套接字移动到(从半连接队列中移除)全连接队列中(accept queue),至此三次握手完成。

listen的第二个参数

在这里插入图片描述
listen是服务端初始化的最后一步:使服务端处于监听状态,其中sockfd是监听套接字的文件描述符fd,listen将使该文件处于监听状态。那么第二个参数backlog呢?它指的是全连接等待队列的长度,在等待队列中排队的是与本地(三次)握手成功的套接字,accept就是取出等待队列中第一个套接字。如果处于listen状态的套接字不accept的话,这个等待队列肯定会满,假设backlog为n,等待队列可以维护的最长链接数为n + 1,也就是说,在不accept的情况下,最多只有n + 1个套接字可以与本地握手成功,第n + 2个套接字请求将被拒绝。

准确的说,全连接队列满了,会发生以下的情况

  • 服务端发送SYN+ACK后忽略客户端的ACK,导致客户端的ACK重传,这样会造成网络拥塞与带宽的浪费
  • 服务端直接发送RST,拒绝与客户端的连接,这样可以避免资源的浪费,但这会导致客户端的不满与疑惑

等待队列的长度应该设置为多少合适?

深入理解一下,accept的速度取决于服务端提供服务的速度,如果服务端服务的客户很多,其资源大部分被占用,能继续对外提供的服务就很少,accept套接字的速度就慢(在资源紧张的情况下,只有处理完一个客户端的服务,才会accept下一个连接),此时全连接等待队列中的连接就是待服务的连接。如果等待队列的长度过长,我们知道维护等待队列也是需要占用资源的,当服务端资源紧张时,肯定不能用太多的资源区维护这样一个队列,要把资源尽可能的分配给客户端使用。但是等待队列的又不能太短,为了保证服务端资源的充分利用,我们不仅要充分使用现在的资源,还要考虑将来资源的使用情况,所以我们要适当的维护等待队列,根据服务端的情况选择一个合适的长度,充分利用服务端资源

最后补充一下:半连接队列满了,会发送怎样的情况?

  • 服务端忽略客户端的SYN包,这会导致客户端的SYN超时重传,同样是会浪费带宽和造成网络拥塞问题
  • 服务端不发送SYN+ACK,直接发送RST显式拒绝客户端的连接,这样不会产生资源的浪费,但是会给客户端带来疑惑

全连接和半连接队列都维护了什么信息?

  1. 半连接队列,也称SYN队列,是服务端用来存储已经接收客户端SYN,但没有接收客户端ACK的TCP套接字。这些套接字处于SYN_RCVD状态
  2. 全连接队列,也称accept队列,是服务端用来存储与客户端三次握手成功的TCP套接字,这些套接字处于ESTABLISHED状态
  3. 两者都维护了服务端与客户端的IP,端口号,确认号,应答号,它们维护的信息都是一样的

tcp只是用两个队列表示套接字的不同状态,如果非要区分它们的不同

  • 全连接队列中的套接字可以进行可靠的双向通信,比如进行拥塞控制,流量控制,而半连接队列中的套接字只能简单的超时重传,不能收发数据包
  • 全连接队列中的套接字可以被accept取走并使用,半连接则不行
  • 全连接队列中的套接字可能因为异常或者对方关闭而断开,并发送FIN或者RST结束会话,而半连接队列中的套接字只会因为超时或者服务端主动或被动拒绝而断开,并发送RST包来清除状态

关于半连接队列中的套接字断开的原因:

  • 客户端发送SYN后主动关闭套接字
  • 网络拥塞导致SYN+ACK丢失,导致超时重传
  • SYN洪水攻击恶意占用服务端资源,影响正常半连接套接字的通信,使正常套接字超时重传

而拒绝的原因有:

  • 客户端的ACK没有携带正确cookie值被服务端拒绝
  • 由于防火墙或其他安全措施,客户端被服务端拒绝
  • 恶意客户端SYN洪水攻击服务端,服务端主动拒绝客户端

全连接和半连接的优缺点

  • 全连接的优点:避免重复握手,提高传输效率
  • 半连接的优点:可以防止SYN洪水攻击
  • 两者的缺点都是:队列满了的情况下,会导致客户端无法与服务端建立连接,即无法完成三次握手

为什么半连接可以阻止SYN洪水攻击?

半连接队列有一个SYN等待时间,即队列中的套接字经过这个时间没有发送ACK,服务端会将该套接字移除等待队列。所以

  • 缩短SYN等待时间,在恶意服务端SYN攻击时,尽可能快的释放半连接
  • 限制每个IP的最大连接数,过滤掉恶意IP
  • 利用SYN cookies技术,验证客户端是否想真正建立连接

关于SYN cookies技术:

  1. 服务端在收到客户端的SYN请求时,不分配资源给它,而是根据一些信息(时间戳,IP地址,端口号,MSS)计算出一个序列号。将该序列号捎带在SYN+ACK报文中
  2. 客户端接收SYN+ACK后,将序列号+1作为确认序列号,捎带在ACK报文中发送给服务端
  3. 服务端再根据请求中的信息(时间戳,IP地址,端口号,MSS)重新计算一个序列号,将其与客户端确认序列号进行对比,如果两者是对应的。说明客户端合法,此时再保存客户端的信息,分配资源并创建连接,维护全连接队列

所以SYN cookies使得服务端不用为半连接分配资源,验证客户端合法之后才建立连接

四次挥手

主动断开连接的一方发送FIN报文进行单方面的断开连接,被动断开连接的一方(以下简称“主动方”和“被动方”)发送ACK报文对FIN进行响应。要注意的是,此时断开的连接只是单方面的。比如服务端发送FIN,客户端返回ACK,此时说明服务端不再向客户端发送数据(服务端没有了向客户端发送数据的需求),但客户端依然可以向服务端发送数据。所以客户端也要发送FIN到服务端,服务端返回ACK确认之后,四次挥手才算完成

COLSE_WAIT状态与TIME_WAIT状态

主动方发送FIN后会处于FIN_WAIT_1状态,被动方接收到FIN请求后会进行ACK响应并进入CLOSE_WAIT状态。之后,如果被动方不关闭用来通信的套接字文件(不发送FIN给对方),会一直处于CLOSE_WAIT状态,这是一种资源泄漏。主动方接收到被动方的ACK后进入FIN_WAIT_2状态,之后不再主动发送数据给对方。当被动方关闭连接时也会发送FIN给主动方并进入LAST_ACK状态(可以理解为退出CLOSE_WAIT状态),主动方接收后会响应ACK并进入TIME_WAIT状态,经过一段时间后才进入CLOSED状态,被动方接收ACK后直接进入CLOSED状态,但是四次挥手还未完成,只有双方都进入CLOSED状态,四次挥手才算完成。所以,为什么主动方在进入TIME_WAIT状态后为什么要经过一段时间才进入CLOSED,不直接进入CLOSED?为什么要有TIME_WAIT状态存在?一段时间具体是多长?

被动方发送FIN使主动方进入TIME_WAIT状态并使其响应ACK报文,但是ACK报文可能丢失,此时被动方并没有进入CLOSED状态,如果主动方直接进入CLOSED状态完全的断开连接,被动方的状态将永远阻塞在LAST_ACK,此时连接没有完全关闭,被动方没有进入CLOSED,并且永远不会进入,四次挥手失败。

所以,被动方在发送FIN后的一段时间内,如果没有收到主动方的ACK,就可以认为ACK包丢失,此时应当重新发送FIN使主动方再次发送ACK(如果主动方进入了CLOSED,被动方无法重新发送FIN),并在成功接收ACK的情况下使自己进入CLOSED状态。所以主动方需要维护一个TIME_WAIT状态,接收来自被动方重新发送的FIN请求(考虑自己发送的ACK包丢失的情况,也是最后一次通信的可靠性保证)。一般情况下,TMIE_WAIT状态会持续2MSL(maximum segment lifetime,报文最大生存时间,超过这个时间,报文将被丢弃),超过2MSL,主动方将进入CLOSED状态,默认被动方接收到了ACK,早已进入CLOSED状态,此时四次挥手彻底完成。

那么为什么TIME_WAIT的时间是2MSL呢?MSL是一个极端情况,假设主动方的ACK花费了将近MSL时间到达被动方,但是报文却在MSL的最后丢失,因为时间超过了MSL,被动方还没有接收到ACK,所以它会重新发送一次FIN请求,假设这个请求又花费了MSL时间才到达主动方,也就是说,考虑了两次通信的最坏情况,主动方需要2MSL才能收到被动方重新发送的FIN请求。接收到被动方的FIN请求,主动方会重置TIME_WAIT状态的计时并重新发送ACK报文,直到TIME_WAIT状态中没有收到来自被动方的FIN,主动方才能确认被动方收到我的ACK并进入了CLOSED状态,此时主动方才进入CLOSED状态。

也就是说,维护TIME_WAIT状态是为了保证tcp通信中,第四次挥手的可靠性,确保被动方能接收到主动方最后一次发送的ACK报文

滑动窗口

滑动窗口是tcp可靠性机制的又一实现:为了完成流量控制而设计的滑动窗口,并且它会增加了tcp的传输效率

可以将发送缓冲区想象成一个字节数组,每个字节的数据都有属于它们的编号(按字节编号),在确认应答机制中双方可能这样通信:发送方发送一个数据段,等待一次应答,确认应答后,进行下一次数据的发送,再等待应答…虽然一个一个的发送数据段虽然保证了传输的可靠性,但是这样传输极不合理,因为传输效率是很低的。为在保证可靠性的条件下提高传输的效率,tcp一次传输要发送多条数据段,不再串行式的等待
在这里插入图片描述
图片来自网络,下面是滑动窗口的具体介绍:

滑动窗口是缓冲区中的一部分数据,这里以发送缓冲区为例。滑动窗口中的数据段都是等待对方应答的,可以分为已发送但等待ACK和待发送但等待ACK两个部分。其左边是已发送且确认应答的数据段,右边是待发送且未等待对方ACK的数据段。这样的划分有些绕,不好理解,可以这样理解:

滑动窗口经过的区域(左边)都是已经发送且对方确认接收,也就是没有丢包不需要重发的数据段,发送方不再关心,可以被清理。滑动窗口未经过的区域(右边)都是等待发送的数据段,连发送都没发送,所以肯定没有被确认接收,这部分数据不能清理。滑动窗口中的数据都是已发送但等待应答的数据,至于说其中为什么会存在待发送但等待应答这一概念,我是这样认为的:在窗口滑动的过程中,原窗口右边(等待发送的数据段)的数据段被装入了窗口,所以产生了一小段的待发送的数据段,这些数据段进入了滑动窗口,操作系统一看滑动窗口来了新的数据,但是还没有发送,为了保证窗口中的数据都是已发送状态的,所以就会将其打包成数据段进行发送,但是这个过程明显是需要时间的,在这些数据未被发送之前,它们的属性就是未发送(或者说将要被发送)但等待接收的。所以说,滑动窗口的滑动是一个特殊的时间点,窗口的不断滑动产生了一些特殊的数据

至于发送窗口中的已发送和未发送的两个数据段的分界,操作系统中肯定是有字段维护的。滑动窗口是怎样滑动的呢?

假设发送的数据段为32~ 37,38~ 41,42~ 45,接收方返回的ACK报文中显示的最大序号是42,就表明32~ 37,38~ 41这两个数据段(编号之前的数据段)已经收到,希望发送方下次从42编号开始发送数据。此时滑动窗口会向右滑动(41 - 32 + 1)= 10编号的大小。当然了,接收方的ACK报文中还有字段表明了接收缓冲区的剩余窗口大小,滑动窗口的滑动距离会被接收缓冲区的剩余空间所限制,这是流量控制

快重传

快重传是提供传输效率的一种方式

至于说传输过程中丢包的情况,如果发送的数据包都被接收了,但是有些ACK包丢了,此时发送方会根据接收方传回的ACK包中,最大的编号来确定哪些数据段是已经被确认的。总不可能每个ACK包都丢失吧,只要通过收到的ACK中最大的编号,发送方就得知该编号之前的所有数据都被接收了,这是因为ACK中的编号表示:希望发送方下次从这个编号开始发送数据段,换言之,就是编号之前的数据都被接收了。

那么数据段直接丢失了呢?如果发送方收到的ACK报文中的编号总是同一个,就说明该编号代表的数据段丢失了,发送方将重新发送这个数据段。可以这样做的原因是:在大量tcp数据段的发送过程中,其中一个数据段丢失了,但后续的数据段被成功地接收,那么接收方就会发现数据段的不连续,之后发送的ACK包,将表明该数据段的丢失,希望对方重新发送。如果接收方成功接收到的数据段越多,指向同一编号的ACK包就会越多,发送方可以根据这一点判断是否有数据段丢失

这样的重传机制被称为高速重发机制,也称为快重传。与超时重传机制的不同是:快重传的重传发生在MSL中,而超时重传的重传是在MSL后,显然超时重传的速度更慢,这也是高速重发与快重传的名字由来

这里抽象理解一下滑动窗口的滑动过程,假设用start和end维护窗口的最左边的最右边,当接收到ACK报文,将其解析得到下次传输要发送的数据段编号,直接让start等于这个编号(这样左边界就滑过了已经被确认应答的数据,指向了未被应答的数据),这个编号之后的数据段都是已经发送但等待应答或者是等待发送的了。end呢?根据ACK返回的接收缓冲区剩余大小,end + 剩余大小,如果剩余大小为0,说明接收缓冲区满了,此时end + 0,右边界不再滑动

拥塞控制

拥塞控制是tcp可靠性的一种方式,也是提高传输效率的一种方式

以上的讨论只是理想情况,是建立在网络总是能传输数据的前提下,假设发送了100个数据段,只丢失了2个数据段,这是正常情况。但是丢失了98个数据段就不是正常情况了。如果发生了严重的丢包,极有可能是网络出现了拥塞问题,使用网络的客户端在同一时间发送了大量的数据段,导致网络无法及时处理这些数据,网络陷入了瘫痪,暂时没有发送数据的能力,大部分数据段被丢弃。

所以网络传输不仅要考虑对方的接收能力,还需要考虑网络的接收能力tcp关于网络接收能力的控制,我们称为拥塞控制。当网络出现了拥塞,网络中的客户端不能再发送同样多的数据(不能进行重传),应当减少发送的数据量,这里就引入了慢启动机制与拥塞窗口的概念。tcp传输开始时,拥塞窗口的大小为1,发送方每次发送的数据量为min(拥塞窗口的大小,接收方的剩余窗口大小),每收到一次ACK应答,拥塞窗口的大小都会乘以2,所以随着不断收到的ACK,拥塞窗口的大小将会指数型增长。但是拥塞窗口有一个阈值,超过了这个阈值,窗口的增长不再是指数型增长而是线性增长,因为指数型增长到了后期,速度会非常恐怖,这很可能导致网络拥塞的出现,为了防止可能的拥塞,窗口的增加速度需要放慢,所以后期是线性增长。

当网络出现拥塞,拥塞窗口大小的阈值将减小一半,并且被重置为1,重新开始增长在这里插入图片描述
图片来自网络

总结一下,为了保证最大的效率,只能使每次传输的数据量尽可能的多,但是网络的吞吐量是有上限的,这与处于同一网络下的主机数量有关系,为了防止网络出现瘫痪,减少拥塞现象的发生,tcp采用慢启动方式,引入了拥塞控制,不断的试探网络崩溃的临界值,给网络最大的压力却不使它崩溃,从而让它传输最多的数据

有一个形象的比喻:拥塞控制就好像热恋

延迟应答

延迟应答是提高通信效率的一种方式

要想提高数据传输的效率,就要使每次发送的数据量尽可能的大,但是发送方所发送的数据量是受到接收方的剩余窗口大小影响的。一般情况下,如果接收方剩余窗口大小越大,发送方能发送的数据也就越多。

如果这样进行ACK应答:接收方每次收到报文,都马上返回ACK告知发送方自己的剩余空间大小。假设接收完报文,剩余窗口大小为500K,但是上层处理的速度非常快,可能10ns就将数据取走了,此时的剩余窗口大小为1M,如果现在接收的数据长度为1M,就可以充分利用缓冲区了。但是ACK报文立马返回了500K,下次接收到的数据最大也只是500K,此时缓冲区的空间就被闲置了。

所以为了提高传输的效率,tcp引入了延时应答机制,即接收到报文时,不要马上返回ACK报文,而是延迟一段时间(当然了,不能延迟MSL那么长的时间)才进行ACK。当然了,每次接收报文都延时应答也有些不合理,所以延时应答遵循以下两种限制中的一种

数量限制:每隔N个数据段就延时应答一次
时间限制:每隔一段时间就延时应答一次

操作系统不同,限制的条件就会有差异,一般情况下,N取2,时间取200ms。并且大多数操作系统都采用数量限制,即每隔2个数据段就延时应答一次。最后,延时应答是一种提高传输效率的机制,但是这个机制也有不合理的地方,所以它有一些限制,使它尽可能的合理

总结一下:上层处理数据的速度很快,适当的延长ACK响应的时间可以充分利用接收缓冲区,提供传输的效率,但是只有在满足特定条件的情况下,才会延迟ACK响应

捎带应答

捎带应答也是提高传输效率的一种机制

即接收方的ACK报文可以携带其他信息。如果每次ACK都只是单纯的ACK,当我们需要传输非ACK的数据时,必须要与ACK分开发送吗?显然这样的做法并不合理,所以接收方在发送ACK报文时,可以捎带其他的非ACK数据,ACK就只是报文中的一个比特位,甚至可以说是正常的通信数据中捎带了ACK。最典型的例子就是四次挥手变成三次挥手,如果接收方接收到对方的FIN后,也希望和对方FIN,此时就不需要先发送一个ACK,再发送一个FIN,而是ACK,FIN一起发送,将两次挥手合并为一次

tcp_228">tcp异常处理

tcp通信时,可能突然出现一些特殊状况,比如突然断电,网络突然无法连接,电脑突然死机,这些都可能导致tcp出现异常。这些异常的具体表现是:数据段发送后没有及时得到响应,或者数据段压根没有发送出去,这可能在三次握手,四次挥手或者正常通信中发送。除此之外还有一些异常,比如接收了无效数据或者RST报文,以下是new bing的具体归纳
在这里插入图片描述
这里我再总结一下可能发生的异常

  • 三次握手和四次挥手中,如果一方陷入异常,另一方就会启动超时重传机制(它又不知道对方怎么了,是否具有重新发送数据段的能力,只会认定数据报丢失),若重传次数达到上限,另一方会强制关闭这个连接,并报告错误。
  • 处于半连接状态的tcp连接,一方直接发送数据给另一方,接收方会认为这样的通信是非法的,所以发送RST断开与对方的连接
  • 在任何时候接收到无效数据,这时接收方会自动忽略,并且返回一个描述自己当前状态信息的数据段给对方
  • 在任何时候接收到RST

当然了,还有一种“异常”是程序突然被终止,或者说进程被终止。此时操作系统会释放进程打开的所有文件,其中包括网络套接字文件。关闭套接字时,系统默认会为我们进行四次挥手,这与正常的关闭没什么区别。


如果你看到这了,这是一些问题,检测你的tcp掌握情况:

tcp的三次握手

  1. 全连接与半连接具体维护了什么?有什么不同?
  2. listen的第二个参数,全连接等待队列满了,将发生什么情况?
  3. 三次握手是怎么交换彼此的窗口大小与其实序列号的?

tcp的6位标志位

  1. 为什么read阻塞读取很低效?
  2. PSH是怎样实现的?
  3. URG是怎样实现的?
  4. 缓冲区的最低限度是指什么?
  5. RST清除谁的状态?

http://www.niftyadmin.cn/n/132990.html

相关文章

322. 零钱兑换 279.完全平方数

322. 零钱兑换 完全背包问题&#xff0c;需要注意的是数组的初始值。 class Solution {public int coinChange(int[] coins, int amount) {int[] dp new int[amount1];for(int i 0;i < amount;i){dp[i] Integer.MAX_VALUE;}dp[0] 0;for(int i 0;i < coins.length;i…

C#基础教程16 继承

文章目录 C# 继承基类和派生类C# 多重继承C# 继承 继承是面向对象程序设计中最重要的概念之一。继承允许我们根据一个类来定义另一个类,这使得创建和维护应用程序变得更容易。同时也有利于重用代码和节省开发时间。 当创建一个类时,程序员不需要完全重新编写新的数据成员和成…

CF大陆斗C战士(三)

文章目录[C. Good Subarrays](https://codeforces.com/problemset/problem/1398/C)题目大意题目分析code[C. Boboniu and Bit Operations](https://codeforces.com/problemset/problem/1395/C)题目大意题目分析code[C. Rings](https://codeforces.com/problemset/problem/1562/…

js一键保存当前页面所有图片

<html> <head> <head><meta charset"utf-8" name”viewport”content”widthdevice-width,initial-scale1″/></head> <title>一键保存</title><link rel"shortcut icon" href"n2.ico" type"…

数据分析师常见问题(1)

1&#xff09;.sql三种排序的区别 2&#xff09;.几种连接方式 3&#xff09;.union和union all的区别 4) .drop和delete的区别 5&#xff09;.有关机器学习random forest 和xgboost的区别 6) .SVM原理 SVM是在特征空间上找到最佳的分离超平面&#xff0c;使得训练集上的正负样…

【使用vue init和vue create的区别以及搭建vue项目的教程】

vue init 是vue-cli2.x的初始化方式&#xff0c;可以使用github上面的一些模板来初始化项目 webpack是官方推荐的标准模板名 使用方式&#xff1a;vue init webpack 项目名称 例如使用github上面electron-vue的模板使用方式&#xff1a;vue init electron-vue 项目名称教程目…

使用Tensorflow完成一个简单的手写数字识别

Tensorflow中文手册 介绍TensorFlow_w3cschool 模型结构图&#xff1a; 首先明确模型的输入及输出&#xff08;先不考虑batch&#xff09; 输入&#xff1a;一张手写数字图&#xff08;28x28x1像素矩阵&#xff09; 1是通道数 输出&#xff1a;预测的数字&#xff08;1x10的one…

Spark读取JDBC调优

Spark读取JDBC调优&#xff0c;如何调参一、场景构建二、参数设置1.灵活运用分区列实际问题&#xff1a;工作中需要读取一个存放了三四年历史数据的pg数仓表&#xff08;缺少主键id&#xff09;&#xff0c;需要将数据同步到阿里云 MC中&#xff0c;Spark在使用JDBC读取关系型数…