前言
传输层的历史渊源可以追溯到计算机网络的早期阶段。在20世纪60年代和70年代,计算机网络主要是由一些简单的点对点连接组成的。这些连接通常使用专用的硬件和协议,例如串行线路和电话线路。在这种情况下,传输层的功能是由这些协议本身来提供的。随着计算机网络的发展,各种传输层协议也随之涌现,保证了即使在网络规模大和网络复杂性高、网络通信双方的距离变得越来越远的环境下也能可靠的进行通信。其中最早的传输层协议之一是UDP协议,它最初是在1980年代初期由David P. Reed设计的。UDP协议的设计旨在提供一种简单的、无连接的传输层协议,适用于广域网上的数据传输。与此同时,TCP协议也在1970年代晚期开始出现,它是由Vint Cerf和Bob Kahn设计的,旨在解决广域网上的数据传输问题。TCP协议在计算机网络中得到广泛应用,成为了传输层协议的事实标准。 随着技术的不断发展,传输层协议也在不断进化。例如,在1990年代晚期和2000年代初期,TCP协议的一些变体,如TCP Vegas和TCP Reno,推出了一系列优化和改进,以提高网络性能和可靠性。此外,一些新的传输层协议,如SCTP协议和QUIC协议,也在出现,以应对新型应用程序和网络环境的需求。
目录
谈谈TCP的可靠性!
要知道TCP的可靠性,那就要先明白网络中为什么会存在不可靠性。我们知道现代的计算机大部分都是基于冯诺依曼体系结构的。
虽然输入、输出、内存、CPU这些设备都被集成一台机器上,但彼此之间还是独立的,它们之间要进行协合数据的交互,就要解决通信问题。在这些设备之间是通过线连接起来的。内存和外设之间的线叫IO总线。内存与CPU之间的线叫系统总线。这些设备被集成一台机器它们之间的通信线是很短的。因此,数据在这些设备之间传输时出现概率的错误是非常低的。
进行网络通信的设备,可能发送方与接收方相隔千里,也就是通信的线变长了,使得数据在传输过程中出现错误的概率也大大增高,TCP就是在此背景下诞生了。TCP就是一种保证可靠性的协议。
TCP协议可靠而UDP协议不可靠,为什么UIDP还会存在?
可靠和不可靠是两个中性词,描述协议的特点。
TCP协议是可靠性的协议,也就是说TCP需要做更多个工作来降低数据在转输过程中出现错误的概率,并且引起不可靠的因素越多保证可靠的成本就越高。
UDP协议是不可靠的协议性,也就意味着UDP协议不需要考虑数据传输时可能出现的问题,因此UDP无论是使用还是维护都足够简单。
不可靠因素包括:丢包,乱序,检验和先败等。
TCP要解决数据传输不可靠的问题,TCP的设计就要比UDP的设计更复杂,并且维护成本高。
需要注意的是:TCP虽然复杂但效率不一定比UDP低,TCP中也有保证传输效率的各种-机制。没有哪个好,只有谁合适。
TCP协议格式:
TCP报头各字段含义如下:
1.源/目的端口号:数据从发送主机的哪个进程到接收主机的哪个进程。
2.32位序号/32位确认序号:分别代表TCP报文当中每个字节数据的编号以及对对方的确认,是TCP保证可靠性的重要字段。
3.4位包头长度:表示TCP报头的长度以4字节为单位。
4.6位保留字段:TCP中暂未使用的6个比特位。
5.16位窗中大小:保证TCP可靠性机制和效率提升机制的重要字段。
6.16位检验和:由发送端填充,采用CRC校验接收端检验不通过,则认为接收到的数据有问题。
7.16位紧急指针:标识紧急数据在报文中的偏移量,和URG字段统一使用。
TCP报头中的6位标志位的含义:
URG:表示紧急指针是否有效。
ACK:确认序号是否有效。
PSH:提示接收端应用程度立刻将TCP接收缓冲区中的数据读走。
RST:表示要求对方重新建立连接,携带RST的报文称为复位报文。
SYN:表示请求与对方建立连接,称为请求链接报文。FIN:通知对方,本端要关闭了。称为请求结束报文。
TCP报头在内核中本质就是一个结构体位段类型,给数据封装TCP报头时,实际上就是用该位段类型定义了一个结构体变量,然后填充TCP报头中的各属性字段。最后将TCP报头拷贝到数据的首部,至此便完成了TCP披头的封装。
TCP如何将报头与有效载荷分离?
当TCP从底层获取一个报文后,虽然TCP不知道报文的具体长度,但报文的前20个字节是TCP的基本报头,并且包含了4位首部长度。因此TCP是这样将报头与有效载荷分离的:
1.TCP获取一个报文后,先读取报文的前20个字节,从中提取出4位的首部长度,得到报头的大小。
2.如果size的大于20字节,则需要从报文当中读取size-20字节的数据,这部分数据就是TCP报头当中的选项字段。
3.剩下的就是有效载荷了。
TCP如何决定将有效载荷交付给上层的哪个协议?
应用层的每一个网络进程都必须绑定一个端口号。
服务端进程必须显示,绑定一个端口号。
客户服进程由系统动态绑有一个端口号。
发送端TCP报头中有目的端口号,因此接收端可以在TCP报头中可以提取出报头中的目的端口号,从而找到对应的进程,进而将有效载荷交付给对应进程进行处理。内核中用哈希表的方式将端口号与进程ID进行映射。
什么是可靠?
在两台机器之间进行通信时,一方将数据发出后,它并不能够保证数据会被对方成功接收,因为数据在传输过程中会有各种各样的情况出现,例如主机A和主机B在进行通信:
主机A将数据发进给主机B,主机B收到数据后不做任何响应,此时对于主机A来说,主机A并不知道主机B有没有收到,主机A只有收到主机B发送来的响应消息后,主机A才能知道上一次给主机B发送的数据它收到了。这才是可靠,即收不收到是能力问题知道收不收到是可靠问题。
TCP要保证双方通信的可靠性在上面的例子中A主机收到了B主机的响应后,A主机知道B主机收到了自己的发送数据,即A主机知道A发送数据的可靠性。B主机在给A主机发送响应数据时,B主机也是需要知道A主机有没有收到自己发送的数据,即B也需要知道通信过程可不可靠,因此A主机收到了B主机的响应消息后,还要给B主机发送响应消息…这样就进入了死循环。
因为只有当一端收到对方的响应消息后,才能保证自己上一次发送的数据被对方可靠的收到了,但双方通信总会有新的消息出现,总不能每条消息都要响应,即通信双方的可靠性不能被百分百保证。即总有一条消息是得不到响应的,但在实际的通信中没有必要保证所有消息的可靠性,只有保证通信双方的发送的核心数据都有响应即可。对无关紧数据,如响应数据,没有必要保证它的代可靠性,这种策略在TCP中叫做确认应答机制,它不是保证通信双方的全部消息的可靠性,而是一方收到了另一方的应答消息,说明它上一次发送的数据被另一方可靠的收到了。
在进行通信时,如果只有收到了对方的响应消息后才发送下一个数据,那么数据就是以串行的方式进行发送的使得通信效率低下。所以在通信过程中,可以允许一方连续向另一方发送多个数据。只需要保证每个数据都能被响应即可,但在连续发送的多个报文中,可能由于某些原因如网络波动或传输路径不同时报文到达对端主机的顺序与发送报文的一方顺序不一致,引起数据紊乱。因此TCP也提供了一种保证数据顺序的机制。TCP将发送出去的每个字节都进行了编号,这个编号叫做序列号。
例如有一台主机A要将3000字节的数据发送结主机B,如果主机A每次发送1000字节,那就需要使用3个TCP报文来进行发送
此时这三个TCP报文中的32位序号中填的就是发送数据中首个节的序号,因此,这三个报文中的内容分别为1、1001,2001。
当接收端收到了这三个TCP报文后,就可以根据TCP报头中的32位序列号对应这三个报文进行顺序重排(在接收端的传输层进行)。重排后将这三个报文放到接收缓冲区。这时顺序就发送顺序一致了。接收端在进行重排时,又可以根据当前报文的32位序列号与其有效载荷的字节数进而确定下一个报文对应的序号。
32位确认序号的作用就是告诉对方,我当前收到了哪些信息,你下一次从哪些数据开始发。接上原例子,当手机B接收到了来自主机A中发过来的报文时,知道到了其序号和有效载荷的大小为1000字节,也就是主机B知道自己接收到了1~1000字节,于是B主机就会在给主机A的响应消息中将32位确认序号填成1001,告诉A主机下次数据从1001字节开始发。在后续的响应消息中,也是如此告诉主机A的发送位置。
注意:响应消息和其他通信数据一样,也是有TCP报头的,也是一个数据包,其中有效载荷可有因无。
为什么要使用序号和确认序号两套机制?
因为TCP是全双工的。在双方发出的报文中,不仅需要填充了序号告诉对方自己当前发送的数据序号,还需要对方对自已接收的信息进行确认,通过对32确认序号进行填充,来告诉对方我接收到了你的那些信息和没有收到你的那些信息,并告诉你下一次数据从那里开始发送在TCP通信中通信双方都要进行确认应答机制,一套序号无法满足。
总结;
32位序号的作用:就是保证数据按序到达,也作为对端发送报文时填充的32位确认序号的依据。
32位确认序号的作用:告诉发送端我接收到了哪此数据,和告诉它下一次从那里开始发送数据。
确认序号和序号是确认应答机制的保证,也是判断报文丢失的依据。
TCP的接收缓冲区和发送缓冲区都是在传输层内部实现的,当我们在使用socket创建网络通信文件时,操作系统会自动维护。
TCP发送缓冲区中的数据,由应用层写入,当应用层调用write/send接口时,是将数据拷贝到发送到缓冲区,再经网络到达接收端的缓冲区。
TCP接收缓冲区中的数据,由应用层读取,当应用层调用read/recv接口时,不是直接从网络中读取的,数据经网络,再到达接收缓冲区,应用层从缓冲区读取。
数据如何到达接收缓冲区以及数据如响何从发送缓冲区中发送,以及数据在传输时遇到的各种问题如何解决,由TCP协议决定。用户只需关心将数据拷贝到TCP发送缓冲区,以及从TCP的接收缓区中读取数据即可。
在通信双方,都有对应的接收缓冲区和发送缓冲区。
TCP的发送缓冲区和接收缓冲区的意义
发送缓冲区:数据在网络传输过程中可能会发生某些错误,此时就可能要求发送端重新发送,因此TCP提供了发送缓冲区来暂保存发送出去的数据,以提供重传。当对端响应可靠接收到后,发送缓冲区的这部分数据才会被刷新。
接收缓冲区:接收端接收和处理数据的速度是有限的,为保证没来得及处理的数据不会丢失,所以TCP提供了接收缓冲区来暂时保存相处理的数据。
TCP的两个缓冲区的工作模型
我们可以想的是生产者消费者模型:
对于发送缓冲区来说:上层应用不断将数据拷贝到发送缓冲区,下层网络不断从发送缓冲区中获取数据,将数据进一步封装发送,此时应用层扮演生产者,网络层扮演消费者,而发送缓冲区就是交易场所。
对于接收缓冲区来说:上层应用不断从接收缓冲区中获取数据,而下层网络不断地向接收缓冲区中放数据。此时上层应用对应消费者,而下层网络对应生产者,接收缓冲区对应交易场所。
当发送端要将数据发送给对端时,本质就是将自己的发送缓冲里的数据拷贝到对端的接收缓冲区。但缓冲区是有大小的,如果接收处理数据的速度小于接收数据的速度,那么总有一刻接收端的接收缓冲区会被打满、这时如果发送端再继续发送数据过来就可能造成丢包,进而引起丢包重传系列的连锁反应。
因此,TCP报头中就有了16位的窗口大小,这个16位窗口大小当中填的就是接收主机接收缓冲区中剩余空间的大小。接收端在对发送端发来的数据进行响应时,就可以将自己接收缓冲区剩余的大小填入16位的窗口大小告诉对方,此时发送端可根据窗口大小进行发送数据的调整。
窗口越大,说明接收端接收数据的能力越强,此时发送端可以提高发送数据的速度。
窗口越小,说明接收端接收数据的能力越弱,此时发送端可以减小发送数据的速度。
窗口大小值为0,说明接收端的接收缓冲区已经满了,此时发送端应停止发送数据。
在TCP套接字编程时,当调用read/recv从socket中读取数据时,如果TCP接收缓冲区中没有数据时读取会被阻塞。当调用write/send向socket中写数据时,如果TCP发送缓冲区中写满了数据时,写入会被阻塞。
为什么会存在标志位?
TCP的报文多种多样,除了正常通信发送的谱通报文,还有建连接时发送的请求建立连接报文,以及断开连后接时发送的请求断开连接的报文等等。收到不同的报文TCP会执行不同的操作,如接收到通信报文,进行有效载荷分离后,再将有效载荷放到接收缓冲区中,接收到请求连接或断开后,TCP执行相应的握手和挥手动作。
TCP根据不同种类的报文执行不同的处理而TCP就是根据报头中的六个标志字段进行报文区分的,每个标志但都只占用一个比特位,0表示无效,1为有效。
TCP报头中的标志位共有6个,它们的含义如下:
SYN(SYNchronize):表示同步,当报文中的SYN被设置为1,表明这是一个请求连接的报文,只有在建立连接阶段,SYN才被设置,正常通信情况不会被设置,当该标志位被设置时,表示TCP连接需要进行同步,即进行TCP三次握手。
ACK(ACKnowledge):当报文中的ACK被设置时,表明对对方上一次发送的报文进行确认,一般除了第一个请求报文的ACK没有被设置,其余报文都会将ACK置为序列号,因为发送出去的数据本身就是对对方发过来的数据具有一定的确引能力,因此双方在通信时,顺便对对方上一次发送的数据序列号进行确认响应。
FIN(FINish):表示结束。当报文中的FIN被置为1时,表明该报文是一个请求断开连接的报文。当该标志位被设置时,表示TCP连接需要结束,即进行TCP四次挥手。只有在断开连接阶段FIN才被置1,正常通信时不会被设置。
URG(URGent):表示紧急数据。当该标志位被设置时,表示TCP包中包含的数据是紧急数据,需要尽快传输。双方在进行通信时,由于TCP要保证数据按序到达,即使TCP将数据进行分包转发,最终到达接收端时这此数据也是有序的,因为TCP可以通过序列号来对这些TCP报文进行顺序重排,以保证数据有序。
TCP接收到的数据有序本身也是我们的目的。应用层从接收缓冲区中读取数据也是按序的。但有是发送端有时也会发送一些需要“紧急处理的数据”,这些数据需要被先先读取。
此时就需要用URG标志位,以及TCP报头中的16位紧急指针。当URG被设置为1时,需要通过TCP报头中的16位紧急指针来找到紧急数据,否则一般不关注TCP报头当中的16位紧急指针。
PSH(PuSH):表示推送。当该标志位被设置时,表示TCP包中携带的数据需要立即被传送,让接收方尽快处理。直观告诉我们当我们调用read/rec从缓冲区中读取数据时,只要缓冲区中有数据就一定能够读取成功。如果没有数据就会被阻塞,直到有数据才会读取返回。
实际上这种说法并不太准确,其实接收缓冲和发送缓冲区都有一个水位线的概念。发送接收缓冲区读取发送读取水位线发送水位线的设置使发送/取效率最大化避免了频繁发送数据或频繁读取数据使得频繁切换内核态和用户态。当报文中的PSH被设置为1时,实际上是告诉对方操他系统尽将调用执行read/recv读取接收爱冲区的数据付给上层, 即使数据量未达到水位线。
RST(ReSeT):表示重置。当报文中的RST被置为1,表示TCP连接出现了某种异常情况,需要强制结束连接,需要让对方重新建立连接。在通信双方未建立好连接的情况下,一方直接向另一方发送数据,此时接收到数据的一方在对接收到的数据进行响应时报文的RST会被置1,要求对方重新建立连接。
报文丢失了怎么办?
在刚才的例子中,如果只有序号1和序号1001的报文被主机B收到了,那么主机B在对报文进行重排时,就会发现2001~3000的字节没有被接收,那么在对主机A进行响应时,32位确认信号填的就是2001。
注意:响应时确认序号不能为3001,因为3001表示前3000字节数据已被接收,而2001-3000在3000之内,并没有被确认接收。当主机A收到确认序号为2001时,就会判断序号为2001的数据包丢失,可以根据选择重传。因此,当发送端接收到的确认序号与序号一致时就会判断报文可能丢失。
超时重传机制
双方在进行网络通信时,发送方发出去的报文在一定的时间内如果发送方得不到对方的应答,此时发送方就会进行数据重发。
TCP的可靠性,一部分是通过TCP报头实现一部分是实现TCP的代码体现的。
超时重传机制实际就发送方在发送数据后开启了一个定时器,如果时间结束发送方没有收到响应报文就会重发数据,这部分是通过TCP代码实现的。
丢包的两种情况
一、发送的数据包丢失了,此时发送端会在一定时间内收不到对方的响应报文,就会进行超时重传。主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
当出现丢包时发送方无法识别是发送的报文丢了还是响应的报文丢了,因为这两种情况下发送方都收不到响应报文,此时发送方只能进行超时重传;
如果是因为响应报文的丢失而引起的超时重传,接收端会根据报头中的32位序号来判断是否重复接收了该报文,从而达到去重。需要注意的是,当发送缓冲区里的数据被发送出去后,操作系统并不是立即将该数据从发送缓冲区当中删除或覆盖,而是会保留;以免需要进行超时重传,直到接收到响应报文,发送缓冲区里的数据才会更新。因此主机B会收到很多重复数据, 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果。
超时重传的等待时间
等待时间太长,导致丢包后,对方长时间收不到对应数据,进而影响整体的通信效率。
等待时间太短,导致接收方收到大量重复报文,可能响应报文还在网络量传输,并没有丢包,但发送端已经开始进行数据重发了,并且发送大量重复的报文也是对网络资源的浪费。因此,超时重传的时间一定要合理,最理想的情况就是找到一个最小的时间,保证应管能在这个时间内送达”这个时间与网络环境有关。
那么, 如果超时的时间如何确定?
最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".但是这个时间的长短, 随着网络环境的不同, 是有差异的。
如果超时时间设的太长, 会影响整体的重传效率;
如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间。Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时,时间都是500ms的整数倍。如果重发一次之后, 仍然得不到应答, 等待 2*500ms后再进行重传。如果仍然得不到应答, 等待 4*500ms 进行重传,依次类推, 以指数形式递增。累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。
连接管理机制
TCP是面向连接的,TCP的可靠性也是基于连接的,一台服务器可能同时要被大量的客户端访问,如果访问服务器没有成本,可能就会被一些不怀好意的人进行不量的访问攻击,就是服务器收到太多的信息、无法从这些信息中识别出哪些信息是真正的需要被服务的,而TCP是基于连接的,在正式通信前,双方要准备好相应的资源,双方在通信中都付有成本。
操作系统对连接的管理
一台服务器会同时被大量的访问,请求服务,此时操作系统就要将这些与客服端建立的连接进行管理,操作系统会为建立好的连接,创建相应的数据结构,将建立好的连接用结构体字段进行描述,再将这些结构体组织成相应数据结构,对连接的管理就变成了对数据结构的增删查改。
通信双方进行TCP通信前需要先建立连接,建立连接的过程称为三次握手。一般客户端会先发送请求连接,因为服务服端不主动请求发送数据。
第一次握手:客户端主动给服务端发送一个SYN被置为1的报文,当服务端收到客户端的报文后,服务可以得出结论:客户端具有发送能力,服务端具有接收能力。
第二次握手:当服务端收到客户端的连接请求后,服务端会给客户端发送一个响应报文报文中的ACK被置1,表示对客户端说你发送来的报文我收到了。其中SYN也要被置1,表示我也向你发送连接请求,当客户端收到该数据报之后,客户端可以得出结论:客户端具有发送和接收能力,服务端具有接收和发送能力。
但第二次握手后,服务端并不能确认客户端是否具有接收能力和服务端是否具有发送能力。所以这时就需要进行第三次握手。
第三次握手:当客户端收到服务端,得知服务器收到了自己发送的建立连接请求,并请求与自己建立连接后,客户端立即向服务端发送一个ACK被置为1的报文进行响应,当客户服务端收到了报文后,服务端可以得知自己:具有发送能力和客户端具有接收能力。
因此三次握手双方才能确认对方的接收和发送能力。
为什么是三次握手?
握手的过程是双方互相验证对方是否具有接收和发送能力的过程,而三次握手是验证的最小次数。
三次握手能保证建立连接时的异常挂在客户端。
1.当客户端收到服务器发来的第二次握手后,客户端就已验证了双方具有通信能力,因此当客户端在发出第三次握手后,这个连接就已在客户端建立。
2.只有在第三次握手后,服端才知道双方都具有通信能力,这时服务端最才会建立对应的连接。
3.报文在网络中传输需要时间,客户端和服务验证双方能力的时间也不一致,如果第三次报文丢了,服务端就不会建立连接,而客户端就需要在一定时间内维护连接。
4.而维护连接是需要成本的,三次握手就能让客户端先付出成本。一个客户正常可能不会建立太多的连接,也就不用维护太多的连接本,而服务端不同,它需要接收千上万个不同的连接请求,就需要维护建立连接的成本。三次握手能够将建立连接时的异常挂在客户端,所以服务端就不必耗费大量资源来维护这些异常连接。
5.由客户端维护的这个客户端建立,而服务端未建的连接异常也不会一直进行下去,如果服务端长时间没有收到第三次握手的报文,就会进行第二次握手超时重传,此时客户端就有机会重新发送第三次握手。或者当客户端认为建立好连接后向服务端发送数据时,此时服务端发现彼此之没有建立好连接而要求客户端重新发起连接请求。
因此可以给出为什么建立连接时为采用三次握手的结论:
1、三次握手是双方验证彼此是否具有通信能力的最小次数,能快速建立连接。
2、三次握手能保证连接建立时的异常连接挂在客户端,由客户端维护。
三次握手过程中的状态变化
刚开始时客户端处于closed的状态,服务出处于listen状态。
第一次握手:客户端给服务端发送一个SYN报文,并指明客户端的初始化ISN(c)序列号后状态由CLOSED变为SYN_Send。
第二次握手:服务端在收到客户端的SYN报文后会以自己的SYN报文作为应答,并指明自己的初始化序列号ISN(s),同时将客户端的ISN(c)+1作为ACK的值,表示已接收到请求,服务器由LISTEN变为SYN_RCVD状态。
第三次握手:客户端在收到服务端响应后,会将服务端的TSN(s)+1作为ACK的值,并将ACK报文发送给服务端,表示客户端已经收到服务端的SYN报文,此时客端由SYN_send状态,变为ESTABLISHED状态。
当服务器收到ACK报文之后,状态也由SYN_RCVD状态转为ESTABLISHED状态。至此握手结束,链接建立完成通信双方可以进行通信了。
关于SYN-ACK包重传次数:服务器发送完SYN-ACK包,如果在一段时间内未收到客户确认包,服务器进行首次重传,如果过一段时间段内仍未收到确认包,服务器进行第二次重传,当重传次数超过系统规定次数,系统将连接信息从半连接队列中删除。
三次握手过程中的数据包可以携带通信数据吗?
第一次第二次不可以,因为此时双方都不能确认对方是否具有接受数据包和发送数据包的能力,第三次握手可以,因为当客户端接收第二次握手时,客户端能够确认服务端具有收发数据的能力。
当通信双方交换完通信数据或不需要再进行通信时, 通信双方就不必再维护连接了,而是将其连接断开,断开连接的过程常称为4次挥手。而服务端一般不会主动与客户端断开链接,一般由客户端先发送断开链接请求。
其过程如下图:
刚开始双方都处于通信establised状态。
第一次挥手:客户端发送一个FIN请求断开连接的报文,报文中会指定一个序列号,此后客户端处于FIN_WAIT1状态。
第二次挥手:服务端收到FIN请求断开连接报文之后会将客户端的序列号值+1作为ACK报文的确认序列号值,后将ACK报文发送给客户端,表明我已收到你的断开连接请求。此时服务端处于CLOSE_WAIT2状态。
而当服务器收到客户端发送的FIN请求断开连接后并立即给予客户端生回应,因为服务器可能还有通信数据发送个客户端,所以服务端并不会立即进行第三次挥手,而是将这些数据发送完成后,才会发起第三次请求挥手。
第三次挥手:服务器也需要断开连接,和第一次挥手一样,服务器发送FIN报文请求断开连接,且指定一个序列号。此时,服务器处于LAST_ACK的状态。
第4次挥手:客户端收到服务发来的FIN包文后会把服务器报文的序列号值+1作为自己ACK报文的序列号值,再将ACK包发送给服务器表示收到请求,此时客户端并不会直接处于关闭,而是处于TIME_WAIT状态,需要过一段时间才会进入CLOSED状态,以保证ACK包能被服务器收到,再一定的时间内如果服务端没有收到客户端的ACK包,服务端就会执行超时重传。经过一段时间后,服务器收到ACK报文后进入CLOSED状态。
需要注意: 客户端收到服务端下FIN报文后,发送ACK报文进行响应后,并没有立即进入CLOSED状态,而是进入TIME-WAIT状态,要过一段时间才进入CLOSED状态,是因为要确服务器收到ACK报文,如果服务器没有收到ACK报文的话,服务器会重发FIN报文给客户端,客户端收到后立即发ACK包回应,TIME_WAIT持续至少一个报文的来回时间,这期这间收不到FIN报文就会进入CLOSED状态。
为什么是四次挥手?
TCP是全双工,使用TCP协议进行通信的双方地位是对等的,双方都要进行断开请求并对彼此的断开清求作出回应,这就是4次挥手了。
流量控制
TCP提供发送端根据接收端的接收能力来控制发送端发送数据的速度。 当在网络环境良好的情况下,接收端处理数据的速度是有限的,如果发送端发送速度快,不考虑据收端的接收能力,那么接收端的接收缓冲区很快就会放满数据,如果此时,发送端仍然发送数据就会造成丢包,发送端因为收不到响应信息而引起丢、包超时重传等一系列连锁反应。
因此,接收端可以将自己未使用的接收缓冲区的大小告诉发送端。让发送端根据自己的空间情况来控制发送速度。
1.接收端将自己剩余的缓冲区大小镇入TCP报头的"16位窗口大小”字段,通过响应信息ACK发送给发送端。
2.窗口大小字段越大,说明发送端可以加快发送速度。
3.窗口大小字段越小,说明发送端应该放慢发送速。
4. 窗口大小为0时,告知发端先停止 发送数据。但会有一个定期发送的窗口探针数据将窗口大小告知发送端。
当发送端知道接收端的接收缓冲区剩余大小为0时会停止发送数据,但会通过以下两种方式来获取何时能继续发送数据。
1、等待接收方主动告知,当接收端的上层将数据从接收缓冲区里读走后,接收端会主动发送一个TCP报文将更新的剩余缓冲区大小知发送端,发送端知到后就会继续发送数据了。
2、主动询问接收端方,发送端会定期间接收端发送一个不带有效载荷的报文,用于询问是否能继续发送数据。
TCP报头中用于表示窗口大小的字段,只有16位,那TCP口大小最大只能是65535了吗?
窗口大小是通过窗口字段的值左移得到的。第一次发送数据时如何知道对方窗口大小?通信双方在通信前进行的三次握手时就获取了对方的窗口信息。所以双方在发送数据时不会出现缓冲区溢出的问题。
使用TCP协议进行通信的双方,在通信时可以同时间对方发选多个数据包,然后将在一个时间段内等待多个响应报文,这使得数据包的发送是并行的,提高通信效
率。
通过流量控制我们知道发送的数据包大小要考虑接收端的窗口大小,也就是发送端不是将缓冲区里的数据一次全部打包发送给对方的。
可以将发送端发送缓冲区里的数据分为三部分:
1.已经发送且已经收到ACK的数据。
2.已经发送但在等待响应ACK的数据。
3.未开始发送的数据。
同时发送,在一时间内等待ACK。
注意:
滑动窗口大小,等于接收端窗口大小和自身拥塞窗口大小中的较小值,因为发送数据时不仅要考虑接收方的接收能力也要考虑网络因素。
这里我们只考虑对方窗口大小,并假设对方窗口大小固定为4000,此时发送亏一次能并发发送4000字节的数据。
现在同时发送1001-2000、2001-3000、3001-4000、4001-5000的数据。
当收到对方发回来的响应ACK为2001时说明1001-2000数据已被对方接收,这时1001-2000的数据应被纳入已发送自已收到ACK的部分。此时会有新的数据刷新进滑动窗口。
滑动窗口越大,说明网络环境越好,对方接收能力强。
等待收到ACK的数据段会在收到ACK后,被归置于滑动窗口左侧,即被刷新出缓冲区。并根据当前滑动窗口的大小决定滑动窗口是否要右侧数据归置到滑动窗口中。
TCP的重传机制要求对未收到响应的数据不能刷出缓冲区,而这部分数据就位于滑动窗口中,只有被确认的数据才会被覆盖或删除。
滑动窗口的实现
TCP接收缓冲区和发送缓冲区可以看作一个字符数组,而滑动窗口际就是两个数组下标限定的一个范围,如用start指向滑动窗口的左侧,end指有滑动窗口右侧,start和end之间的数组区域就是滑动窗口。
当发送立收到对方的响应时,如果ACK确认序号为X,窗口大小为win此时更新start为X,而end更新为start+win。
在进行数据包传输时,难免会出现数据丢失情况。这种情况一般分为两种。
情况一:数据包已经抵达,ACK(确认应答包)被丢了。
这种情况下,如果未使用滑动窗口机制,发送的数据包没有收到确认应答包,那么数据都会被重发;如果使用了滑动窗口机制,即使确认应答包丢失,也不会导致数据包重发,因为可以通过后续的ACK进行确认;
这种情况下指的是前面发送的数据包没有收到对应的确认应答。当收到后面数据包的确认应答包,表示前面的数据包已经成功被接受到了,发送端不需要重新发送前面的数据包了。如下图所示:
窗口在一定程度上较大时,即使有少部分的确认应答丢失也不会进行数据重发,可以通过下一个确认号进行确认。
情况二:数据包直接丢了
这种情况指的是发送端发送的部分数据没有达到接收端。那么,如果接收端收到的数据包,不是本该要接受的数据包,就是给发送端返回消息,告诉发送端自己应该接收的数据包。
如果发送端连续收到3次这样的数据包,就认为该数据包成功发送到接收端,这时就重发该数据包。这种机制叫做高速重发机制。
有以下7个过程:
(1)发送端发送数据包:这里的窗口大小为4,所以发送端1发了四个数据包,如上图所示‘
(2)接收端返回确认应答包:接收端收到这些数据,并给出确认应答包。数据包2001-3001在发送过程中丢失了,没有成功到达接收端,但是它的下一个数据包3001-4000没有丢失成功到达了接收端,但是该数据包不是接收端应该接受到的数据包,因此接收端收到数据包3001-4000以后,第一次返回下一个是2001的数据包的确认应答包;
(3)发送端发送数据包:发送端仍然向接收端发送4个数据包;
(4)接收端返回确认应答包:当接收端收到数据包4001-5000时,发现不是自己应该接受的数据包2001-3000,第二次返回下一个是2001的数据包的确认应答包。当接收端收到数据包5001-6000时,仍然发现不是自己应该接受的数据包,第三次返回下一个是2001的数据包的确认应答包。依次类推直到接受完所有数据包,接收端都返回下一个应该是2001的数据包的确认应答包。
(5)发送端重发数据包:发送端连续3次收到结束端发来的下一个应该发送2001的数据包的确认应答包,认为数据包2001-3000丢失了,就进行重发该数据包。
(6)接收端收到重发数据包:接收端收到重发数据包以后,查看这次是自己应该接受的数据包2001-3000,并返回确认应答包,告诉发送包,下一个该接受8001的数据包了(因为3001-8000接收端其实之前就收到了,被放到了接收端操作系统内核的接受缓冲区)。
(7)发送端发送数据包:发送端收到确认应答包后,继续发送窗口大小为4的数据包。
补充:之所以连接到3次而不是两次的原因是因为,即使数据段的序号被替换两次也不会触发重发机制。
快重传 VS 超时重传
快重传是能够快速进行数据的重发,当发送端连续收到三次相同的应答时就会触发快重传,而不像超时重传一样需要通过设置重传定时器,在固定的时间后才会进行重传。
虽然快重传能够快速判定数据包丢失,但快重传并不能完全取待超时重传,因为有时数据包丢失后可能并没有收到对方三次重复的应答,此时快重传机制就触发不了,而只能进行超时重传。
因此快重传虽然是一个效率上的提升,但超时重传却是所有重传机制的保底策略,也是必不可少的。
为什么会有拥塞控制?
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题。因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵。 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的,TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
两个主机在进行使用TCP协议通信的过程中,出现个别数据包丢包的情况是很正常的,此时可以通过快重传或超时重发对数据包进行补发。但如果双方在通信时出现了大量丢包,此时就不能认为是正常现象了。TCP不仅考虑了通信双端主机的问题,同时也考虑了网络的问题。
流量控制:考虑的是对端接收缓冲区的接收能力,进而控制发送方发送数据的速度,避免对端接收缓冲区溢出。
滑动窗口:考虑的是发送端不用等待ACK一次所能发送的数据最大量,进而提高发送端发送数据的效率。
拥塞窗口:考虑的是双方通信时网络的问题,如果发送的数据超过了拥塞窗口的大小就可能会引起网络拥塞。
双方网络通信时出现少量的丢包TCP是允许的,但一旦出现大量的丢包,此时量变引起质变,这件事情的性质就变了,此时TCP就不再推测是双方接收和发送数据的问题,而判断是双方通信信道网络出现了拥塞问题。
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快,为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍。
此处引入一个叫做慢启动的阈值当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长。
如何解决网络拥塞问题?
网络出现大面积瘫痪时,通信双方作为网络当中两台小小的主机,看似并不能为此做些什么,但“雪崩的时候没有一片雪花是无辜的”,网络出现问题一定是网络中大部分主机共同作用的结果。
如果网络中的主机在同一时间节点都大量向网络当中塞数据,此时位于网络中某些关键节点的路由器下就可能排了很长的报文,最终导致报文无法在超时时间内到达对端主机,此时也就导致了丢包问题。\n当网络出现拥塞问题时,通信双方虽然不能提出特别有效的解决方案,但双方主机可以做到不加重网络的负担。
双方通信时如果出现大量丢包,不应该立即将这些报文进行重传,而应该少发数据甚至不发数据,等待网络状况恢复后双方再慢慢恢复数据的传输速率。
需要注意的是,网络拥塞时影响的不只是一台主机,而几乎是该网络当中的所有主机,此时所有使用TCP传输控制协议的主机都会执行拥塞避免算法。
因此拥塞控制看似只是谈论的一台主机上的通信策略,实际这个策略是所有主机在网络崩溃后都会遵守的策略。一旦出现网络拥塞,该网络当中的所有主机都会受到影响,此时所有主机都要执行拥塞避免,这样才能有效缓解网络拥塞问题。通过这样的方式就能保证雪崩不会发生,或雪崩发生后可以尽快恢复。
虽然滑动窗口能够高效可靠的发送大量的数据,但如果在刚开始阶段就发送大量的数据,就可能会引发某些问题。因为网络上有很多的计算机,有可能当前的网络状态就已经比较拥塞了,因此在不清楚当前网络状态的情况下,贸然发送大量的数据,就可能会引起网络拥塞问题。因此TCP引入了慢启动机制,在刚开始通信时先发少量的数据探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
TCP除了有窗口大小和滑动窗口的概念以外,还有一个窗口叫做拥塞窗口。拥塞窗口是可能引起网络拥塞的阈值,如果一次发送的数据超过了拥塞窗口的大小就可能会引起网络拥塞。刚开始发送数据的时候拥塞窗口大小定义以为1,每收到一个ACK应答拥塞窗口的值就加一。每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送数据的窗口大小,即滑动窗口的大小。每收到一个ACK应答拥塞窗口的值就加一,此时拥塞窗口就是以指数级别进行增长的,如果先不考虑对方接收数据的能力,那么滑动窗口的大家就只取决于拥塞窗口的大小,但指数级增长是非常快的,因此“慢启动”实际只是初始时比较慢,但越往后增长的越快。如果拥塞窗口的值一直以指数的方式进行增长,此时就可能在短时间内再次导致网络出现拥塞。
为了避免短时间内再次导致网络拥塞,因此不能一直让拥塞窗口按指数级的方式进行增长。此时就引入了慢启动的阈值,当拥塞窗口的大小超过这个阈值时,就不再按指数的方式增长,而按线性的方式增长。
当TCP刚开始启动的时候,慢启动阈值设置为对方窗口大小的最大值。
在每次超时重发的时候,慢启动阈值会变成当前拥塞窗口的一半,同时拥塞窗口的值被重新置为1,如此循环下去。如下图:
图示说明:
指数增长:刚开始进行TCP通信时拥塞窗口的值为1,并不断按指数的方式进行增长。
加法增大:慢启动的阈值初始时为对方窗口大小的最大值,图中慢启动阈值的初始值为16,因此当拥塞窗口的值增大到16时就不再按指数形式增长了,而变成了的线性增长。
乘法减小:拥塞窗口在线性增长的过程中,在增大到24时如果发生了网络拥塞,此时慢启动的阈值将变为当前拥塞窗口的一半,也就是12,并且拥塞窗口的值被重新设置为1,所以下一次拥塞窗口由指数增长变为线性增长时拥塞窗口的值应该是12。主机在进行网络通信时,实际就是在不断进行指数增长、加法增大和乘法减小。
需要注意的是,不是所有的主机都是同时在进行指数增长、加法增大和乘法减小的。每台主机认为拥塞窗口的大小不一定是一样的,即便是同区域的两台主机在同一时刻认为拥塞窗口的大小也不一定是完全相同的。因此在同一时刻,可能一部分主机正在进行正常通信,而另一部分主机可能已经发生网络拥塞了。
如果接收数据的主机收到数据后立即进行ACK应答,此时返回的窗口可能比较小。
假设对方接收端缓冲区剩余空间大小为1M,对方一次收到500K的数据后,如果立即进行ACK应答,此时返回的窗口就是500K。
但实际接收端处理数据的速度很快,10ms之内就将接收缓冲区中500K的数据消费掉了。
在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
如果接收端稍微等一会再进行ACK应答,比如等待200ms再应答,那么这时返回的窗口大小就是1M。
需要注意的是,延迟应答的目的不是为了保证可靠性,而是留出一点时间让接收缓冲区中的数据尽可能被上层应用层消费掉,此时在进行ACK响应的时候报告的窗口大小就可以更大,从而增大网络吞吐量,进而提高数据的传输效率。
此外,不是所有的数据包都可以延迟应答。
数量限制:每个N个包就应答一次。
时间限制:超过最大延迟时间就应答一次(这个时间不会导致误超时重传)。
延迟应答具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms。
捎带应答其实是TCP通信时最常规的一种方式,就好比主机A给主机B发送了一条消息,当主机B收到这条消息后需要对其进行ACK应答,但如果主机B此时正好也要给主机A发生消息,此时这个ACK就可以搭顺风车,而不用单独发送一个ACK应答,此时主机B发送的这个报文既发送了数据,又完成了对收到数据的响应,在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收" 的,意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine, thank you"; 那么这个时候ACK就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端这就叫做捎带应答。
捎带应答最直观的角度实际也是发送数据的效率,此时双方通信时就可以不用再发送单纯的确认报文了。
此外,由于捎带应答的报文携带了有效数据,因此对方收到该报文后会对其进行响应,当收到这个响应报文时不仅能够确保发送的数据被对方可靠的收到了,同时也能确保捎带的ACK应答也被对方可靠的收到了。
流当创建一个TCP的socket时,同时在内核中会创建一个发送缓冲区和一个接收缓冲区。
调用write函数就可以将数据写入发送缓冲区中,此时write函数就可以进行返回了,接下来发送缓冲区当中的数据就是由TCP自行进行发送的。
如果发送的字节数太长,TCP会将其拆分成多个数据包发出。如果发送的字节数太短,TCP可能会先将其留在发送缓冲区当中,等到合适的时机再进行发送。
接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区,可以通过调用read函数来读取接收缓冲区当中的数据。
而调用read函数读取接收缓冲区中的数据时,也可以按任意字节数进行读取。
由于缓冲区的存在,TCP程序的读和写不需要一一匹配。
例如:
写100个字节数据时,可以调用一次write写100字节,也可以调用100次write,每次写一个字节。读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次。
实际对于TCP来说,它并不关心发送缓冲区当中的是什么数据,在TCP看来这些只是一个个的字节数据,它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流。
什么是粘包?
首先要明确,粘包问题中的“包”,是指的应用层的数据包。在TCP的协议头中,没有如同UDP一样的“报文长度”这样的字段。捎带应答站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。
但站在应用层的角度,看到的只是一串连续的字节数据。捎带应答那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
要解决粘包问题,本质就是要明确报文和报文之间的边界。
对于定长的包,保证每次都按固定大小读取即可。
对于变长的包,可以在报头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。比如HTTP报头当中就包含Content-Length属性,表示正文的长度。
对于变长的包,还可以在包和包之间使用明确的分隔符。因为应用层协议是程序员自己来定的,只要保证分隔符不和正文冲突即可。
UDP是否存在粘包问题?
捎带应答捎带应答对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在,同时,UDP是一个一个把数据交付给应用层的,有很明确的数据边界。
站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现“半个”的情况。
因此UDP是不存在粘包问题的,根本原因就是UDP报头当中的16位UDP长度记录的UDP报文的长度,因此UDP在底层的时候就把报文和报文之间的边界明确了,而TCP存在粘包问题就是因为TCP是面向字节流的,TCP报文之间没有明确的边界。
TCP异常情况捎带应答进程终止
当客户端正常访问服务器时,如果客户端进程突然崩溃了,此时建立好的连接会怎么样?
当一个进程退出时,该进程曾经打开的文件描述符都会自动关闭,因此当客户端进程退出时,相当于自动调用了close函数关闭了对应的文件描述符,此时双方操作系统在底层会正常完成四次挥手,然后释放对应的连接资源。也就是说,进程终止时会释放文件描述符,TCP底层仍然可以发送FIN,和进程正常退出没有区别。
机器重启
当客户端正常访问服务器时,如果将客户端主机重启,此时建立好的连接会怎么样?
当我们选择重启主机时,操作系统会先杀掉所有进程然后再进行关机重启,因此机器重启和进程终止的情况是一样的,此时双方操作系统也会正常完成四次挥手,然后释放对应的连接资源。
机器掉电/网线断开
当客户端正常访问服务器时,如果将客户端突然掉线了,此时建立好的连接会怎么样?
当客户端掉线后,服务器端在短时间内无法知道客户端掉线了,因此在服务器端会维持与客户端建立的连接,但这个连接也不会一直维持,因为TCP是有保活策略的。
服务器会定期客户端客户端的存在状况,检查对方是否在线,如果连续多次都没有收到ACK应答,此时服务器就会关闭这条连接。
此外,客户端也可能会定期向服务器“报平安”,如果服务器长时间没有收到客户端的消息,此时服务器也会将对应的连接关闭。
其中服务器定期询问客户端的存在状态的做法,叫做基于保活定时器的一种心跳机制,是由TCP实现的。此外,应用层的某些协议,也有一些类似的检测机制,例如基于长连接的HTTP,也会定期检测对方的存在状态。
三次握手和四次挥手过程的状态变化。
如何使UDP具有可靠性?
TCP协议这么复杂就是因为TCP既要保证可靠性,同时又尽可能的提高性能?
TCP如何保证可靠性?
检验和
序列号
确认应答
超时重传
连接管理
流量控制
拥塞控制
TCP如何提高性能?
滑动窗口
快速重传
延迟应答捎带应答
需要注意的是,TCP的这些机制有些能够通过TCP报头体现出来的,但还有一些是通过代码逻辑体现出来的。
TCP定时器
重传定时器:为了控制丢失的报文段或丢弃的报文段,也就是对报文段确认的等待时间。坚持定时器:专门为对方零窗口通知而设立的,也就是向对方发送窗口探测的时间间隔。
保活定时器:为了检查空闲连接的存在状态,也就是向对方发送探查报文的时间间隔。
TIME_WAIT定时器:双方在四次挥手后,主动断开连接的一方需要等待的时长。
理解传输控制协议
TCP的各种机制实际都没有谈及数据真正的发送,这些都叫做传输数据的策略。TCP协议是在网络数据传输当中做决策的,它提供的是理论支持,比如TCP要求当发出的报文在一段时间内收不到ACK应答就应该进行超时重传,而数据真正的发送实际是由底层的IP和MAC帧完成的。
TCP做决策和IP+MAC做执行,我们将它们统称为通信细节,它们最终的目的就是为了将数据传输到对端主机。而传输数据的目的是什么则是由应用层决定的。因此应用层决定的是通信的意义,而传输层及其往下的各层决定的是通信的方式。
基于TCP的应用层协议
常见的基于TCP的应用层协议如下:
HTTP(超文本传输协议)
HTTPS(安全数据传输协议)
SSH(安全外壳协议)
Telnet(远程终端协议)
FTP(文件传输协议)
SMTP(电子邮件传输协议)
包括我们自己写TCP程序时自定义的应用层协议。