大家都知道传输层中的TCP协议是面向连接的,提供可靠的连接服务,其中最出名的就是三次握手和四次挥手。
一、三次握手
三次握手的交互过程如下
喜欢钻牛角尖的我在学习三次握手的时候就想到了几个问题:为什么三次握手是三次?不是一次、两次或者更多?如果是两次或者是一次会出现什么情况?带着这个问题我找了好多资料,发现了其中的奥秘。
1.1 握手存在的问题
(1)一次握手的情况:由于TCP是面向连接的,一次很明显时不可能的,因为客户端发出连接消息后,却没有接收到来自服务端的回应,客户端就无法确定服务端接是否收到了连接请求,当然也就不能确定是否连接成功。
(2)两次握手的情况:既然一次客户端接收不到服务端的回应,那就连接两次,接收到回应就说明服务端接收到了连接请求,可以连理连接了。结果并不是这样。
如果客户端想建立连接,给服务端发了一个连接请求(SYN),但是由于网络中种种情况,导致没有及时到达服务端,这就导致客户端在很长一段时间中没有收到回复消息(ACK),这时客户端又给服务端发送一个SYN,这次的发送和接收的很顺利,很快就收到了ACK,但是这时之前的SYN终于到了服务端,服务端规规矩矩的为这个SYN申请资源,然后返回ACK。由于之前的SYN已经失效了,所以客户端也不会去理会这个ACK,但是傻乎乎的服务端并不知道这个SYN已经失效了,一直为他委会着资源,这就造成了资源的浪费。
(3)三次握手的情况:一发一收的两次握手既然不行,那么三次握手就可以了吗?接着往下看。
在两次握手中服务端不知道当前这个SYN是不是有效的,三次握手就很好的解决了这个问题,第三次握手就是客户端给服务端回复第二次握手,这也就是说服务端会等第三次握手的到来,如果第三次握手迟迟不来,服务端就可以识别这个SYN是无效的,就会将他的资源释放了。还有一种情况就是第三次握手由于网络中的种种原因失败了,这时候客户端认为自己已经连接好了,就会给服务端发送数据,服务端由于没有收到第三次握手,就会以RST包对客户端响应,收到RST的的客户端就知道第三次握手没有成功,就会重新连接。在谢希仁著《计算机网络》第四版中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。
四次握手和两次握手的情况一样,五次握手和三次握手的情况一样,以此类推,奇数次握手的情况与三次握手相同,同理偶数次握手与两次握手一样,所以为了更快的连接,就使用三次握手最合适。
1.2 握手失败的措施
第一次握手失败:
如果第一次的SYN传输失败,两端都不会申请资源。如果一段时间后之前的SYN发送成功了,这时客户端只会接收他最后发送的SYN的SYN+ACK回应,其他的一概忽略,服务端也是如此,会将之前多申请的资源释放了。
第二次握手失败:
如果服务端发送的SYN+ACK传输失败,客户端由于没有收到这条响应,不会申请资源,虽然服务端申请了资源,但是迟迟收不到来自客户端的ACK,也会将该资源释放。
第三次握手失败:
如果第三次握手的ACK传输失败,导致服务端迟迟没有收到ACK,就会释放资源,这时候客户端认为自己已经连接好了,就会给服务端发送数据,服务端由于没有收到第三次握手,就会以RST包对客户端响应。但是实际上服务端会因为没有收到客户端的ACK多次发送SYN+ACK,次数是可以设置的,如果最后还是没有收到客户端的ACK,则释放资源。
二、四次挥手
大家都知道TCP是全双工的,再建立连接时的三次握手中的SYN和ACK一起发送,这里就会有疑问,为什么在四次挥手的时候没有将SYN和ACK一起发送呢?
说到四次挥手,顾名思义,就是在关闭连接的时候双方一共要操作四次,来看一下这四次都是怎么操作的:
从图中可以看出来,在四次挥手的时候双方一共进入了六种状态,这六种状态就是理解四次挥手的关键所在,我们来看一下
2.1 四次挥手的状态
-
FIN_WAIT_1:这个状态和FIN_WAIT_2状态都在再等待对方的回复,但是这两种状态是有区别的,FIN_WAIT_1就是主动方在ESTABLISHED状态的时候,想要主动关闭连接,向对方发送FIN报文,这时候就进入了FIN_WAIT_1状态。当他收到对方回复的ACK报文后,就进入了FIN_WAIT_2状态。 但是在实际操作中是很难遇到FIN_WAIT_1状态的,因为无论对方是什么情况都应该立刻回应ACK报文,但是FIN_WAIT_2状态还是可以在主动方中用netstat看到的。
-
FIN_WAIT_2:上面已经对FIN_WAIT_2讲解过了,当主动方进入FIN_WAIT_2时,就表示着半连接状态,也就是主动方还有数据要发给对方,这个数据就是之后的ACK,所有他要等一会儿才关闭连接。
-
CLOSE_WAIT:这个状态从表面也可以看出它的作用,就是等待关闭。当被动方接收到FIN时,会立刻回复一个ACK给对方,接下来就是进入CLOSE_WAIT状态。在这个状态中,被动方需要考虑自己还有没有数据要发送给对方,如果有可以继续发送,如果没有了就可以关闭连接了,发送一个FIN给对方。 这个状态其实也就是给自己一个缓冲的时间,让自己处理完需要处理的事,然后去关闭连接。
-
TIME_WAIT:这个状态就是一段时间后进行一些操作。当主动方收到了对方发来的FIN报文,并发出ACK报文,接下来就等2MSL就可以进入CLOSED状态了。其实,如果主动方在FIN_WAIT_1状态下,收到了对方的FIN+ACK标志的报文,就可以跳过FIN_WAIT_2状态直接进入TIME_WAIT状态了。
-
LAST_ACK:这个状态从表面不难不理解他的意思,这个状态就是被动方发送了FIN报文后,最后等待对方的ACK报文,收到ACK报文后就可以进入CLOSED状态了。
-
CLOSED:上面提到了几次这个状态,相比也猜出来了,这个状态表示的就是连接中断,已经关闭。
在上面的TIME_WAIT状态中有提到过2MSL,那么什么是2MSL呢?那么来详细说一下TIME_WAIT状态和里面的2MSL。
2.2 为什么需要TIME_WAIT?
TIME_WAIT在四次挥手中有着不可替代的位置,如果没有TIME-WAIT,主动方就会直接进入CLOSED状态,(假设主动方时客户端,被动方时服务端)这时候如果立即重启客户端使用相同的端口,如果因为网络中种种原因最后一次ACK丢失了,服务端就会重复FIN请求,这时这个FIN就会被重新启动的客户端接收到,或者新启动的客户端向服务端发起请求的时候,因为服务端正在等待最后一次ACK,因此新连接请求发送的SYN就会被服务端认为时请求码错误,服务端就会回复RET重置连接。所以就需要主动方发送最后一次ACK之后进入TIME_WAIT状态,等待2MSL(两个报文最大生命周期),等待这段时间就是为了如果接收到了重发的FIN请求能够进行最后一次ACK回复,让在网络中延迟的FIN/ACK数据都消失在网络中,不会对后续连接造成影响
2.3 为什么TIME_WAIT的时间是2MSL?
MSL是TCP报文的最大生命周期,因为TIME_WAIT持续在2MSL就可以保证在两个传输方向上的尚未接收到或者迟到的报文段已经消失,否则服务器立即重启,可能会收到来自上一个进程迟到的数据,但是这种数据很可能是错误的,同时也是在理论上保证最后一个报文可靠到达,假设最后一个ACK丢失,那么服务器会再重发一个FIN,这是虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK。
三、传输层基础
3.1 基本概念
传输层负责端与端之间的数据传输,主要有两大知识点:TCP和UDP。
名词解释
- 五元组:在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信(可以通过netstat -n查看);
- 端口号范围
- 0 - 1023: 知名端⼝口号, HTTP, FTP, SSH等这些广为使⽤用的应⽤用层协议, 他们的端⼝口号都是固定的
- 1024 - 65535: 操作系统动态分配的端⼝口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。
3.2 UDP(不连接不可靠面向数据报)
UDP协议格式:源端口、目的端口、数据报长度、校验和
(1)校验和:二进制反码求和
(2)最大长度
UDP数据包最大长度64K(包含报头),如果用户发送的长度大于64K-8就会报错,因为UDP在传输层不会自动进行数据分段,这就意味着如果传输的数据大于64K,就需要用户在应用层就进行数据的分段,但是因为传输层UDP并不保证数据的有序到达,就需要用户在应用层进行包序的管理。
(3)UDP 缓冲区
UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;
(4)UDP粘包问题
面向数据报不会产生粘包问题,因为UDP数据包中定义了数据包的长度
(5)基于UDP的应用层协议
- NFS: 网络文件系统
- TFTP: 简单文件传输协议
- DHCP: 动态主机配置协议
- BOOTP: 启动协议(用于无盘设备启动)
- DNS:域名解析协议
3.3 TCP 传输控制协议
TCP协议段格式
- 源/目的端口:表示数据从哪个进程来,要到哪个进程中
- 32位序号/32位确认序号:用来标识数据的顺序
- 4位TCP报文长度:表示该TCP头部有多少个32位bit,以为就是有多少个4字节,所以TCP头部最大的长度时15*4=64字节
- 6位标志位:
URG:紧急指针是否有效
ACK:确认号是否有效
PSH:提示接收端应用程序立刻从TCP缓冲区把数据取走
RST:对方要求重新建立连接,我们把携带RST标识的称为复位报文段
SYN:请求建立连接,我们把携带SYN标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段 - 6位窗口大小:双方约定的滑动窗口的大小
- 16位校验和:发送端填充,CRC校验,接收端校验不通过,则认为数据是有问题的,此处的校验和不止包含TCP首部,也包含TCP的数据部分
- 16位紧急指针:标识那部分数据是紧急数据
(1)tcp的连接管理
为什么TIME_WAIT的时间是2MSL?
MSL是TCP报文的最大生命周期,因为TIME_WAIT持续在2MSL就可以保证在两个传输方向上的尚未接收到或者迟到的报文段已经消失,否则服务器立即重启,可能会收到来自上一个进程迟到的数据,但是这种数据很可能是错误的,同时也是在理论上保证最后一个报文可靠到达,假设最后一个ACK丢失,那么服务器会再重发一个FIN,这是虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK。
TIME_WAIT功能
如果没有TIME-WAIT,主动关闭方就会直接进入CLOSED,如果立即重启客户端使用相同的端口,在最后一次ACK丢失时,服务端重复FIN请求,就会被重新启动的客户端接收到,或者新启动的客户端向服务端发起请求的时候,因为服务端正在等待最后一次ACK,因此新连接请求发送的SYN就会被认为请求码错误,服务端就会回复RET重置连接,因此需要主动关闭方发送最后一次ACK之后进入一个TIME_WAIT状态,等待一段时间—2MSL(两个报文最大生命周期),等待这段时间就是为了如果接收到了重发的FIN请求能够进行最后一次ACK回复,让在网络中延迟的FIN/ACK数据都消失在网络中,不会对后续连接造成影响
(2)TCP可靠传输
确认应答机制/超时重传机制、协议中的序号/确认序号、校验和
(3)超时重传机制
主机A发送数据给B后,可能是因为网络拥塞或者数据丢失等原因倒是数据无法到达主机B,所以如果主机A心思一个特定的时间间隔中没有受到B发来的确认应答,就会进行数据重发。因此主机B就会收到很多重复的数据,利用序列号就可以很容易的做到去重的效果
(4)时间间隔
最理想的情况下,找到一个最小的时间,保证“确认应答一定可以在这个时间段中返回”,但是这个时间的长短随着网络环境的不同而不同。如果时间设置的太长会影响整体的效率,时间设置的太短了可能会频繁发送重复的包。
(5)时隔方案
TCP为了保证无论任何环境下都能高性能的通信,采取了动态计算这个最大超时时间,Linus、BSD、Unix和Windows中的超时时间以500ms为一个单位进行控制,每次重发的时间都是500ms的整数倍,如果一次重发后得不到应答,就会在2*500ms后继续,依次类推,以2的指数形式递增。但是累积到一定次数的重传,TCP认为网络或者主机出现了异常,会强制关闭连接。
(6)滑动窗口
通信双方通过协议中的窗口字段,来协商能都一次发送的最多数据,然后连续发送多条数据,在socket中使用两个指针维护窗口的前沿和后沿。
- 发送端:发送的起始位置–后沿,前沿就是发送的结束位置,如果窗口中后沿数据没有接收到ack确认,后沿不能向前移动,数据不能从缓冲区中移除,接收到ack确认后窗口的前后沿向后移动
- 接收端:接收数据的起始位置–后沿 接收数据的结束位置–前沿,当接收数据时没有收到第一条数据,后沿就不能向后移动,知道收到数据后,才可以向以后移动
滑动窗口机制通过窗口大小来确定两虚发送多条数据,但是一开始通信的时候,因为不了解网络状态,有可能造成发的越多丢的越多,超时重传就越多,降低了效率。
(7)拥塞窗口
发送窗口=拥塞窗口,但是发送窗口不是一直等于拥塞窗口的,在网络情况好的时候,拥塞窗口不断的增加,发送方的窗口自然也随着增加,但是接受方的接受能力有限,在发送方的窗口达到某个大小时就不在发生变化了。
TCP拥塞控制
(8)慢启动
主机开发发送数据报时,如果立即将大量的数据注入到网络中,可能会出现网络的拥塞。慢启动算法就是在主机刚开始发送数据报的时候先探测一下网络的状况,如果网络状况良好,发送方每发送一次文段都能正确的接受确认报文段。那么就从小到大的增加拥塞窗口的大小,即增加发送窗口的大小。
开始发送方先设置cwnd(拥塞窗口)=1,发送第一个报文段,接收方接收到后返回确认信息,发送方接收到接收方的确认后,把cwnd增加到2,接着发送方发送报文段,发送方接收到接收方发送的确认后cwnd增加到4,慢启动算法每经过一个传输轮次(认为发送方都成功接收接收方的确认),拥塞窗口cwnd就加倍。
(9)快重传
快重传要求接收方收到一个失序的报文段后就立刻发出重复确认,而不要等待自己发送数据时才进行捎带确认。接收方成功的接受了发送方发送来的1-1001并且分别给发送了ACK,现在接收方没有收到1001-2000,而接收到了2001-3000,显然接收方不能确认2001-3000,因为2001-3000是失序的报文段。如果根据可靠性传输原理接收方什么都不做,但是按照快速重传算法,在收到其他报文段的时候,不断重复的向发送方发送1001-2000的ACK,如果接收方一连收到三个重复ACK(防止网络延时又收到数据),那么发送方不必等待重传计时器到期,由发送方尽早重传未被确认的报文段。这时返回的ACK是6001,因为之前的2001-6000已经成功接收,只是存储在了系统的接收缓冲区中。
(10)快恢复
当触发了快重传算法时,接下来就执行乘法减小算法,把慢启动开始门限(ssthresh)减半,但是接下来并不执行慢开始算法,而是把cwnd设置为ssthresh的一半, 然后执行拥塞避免算法,使拥塞窗口缓慢增大。
(11)拥塞避免
为了防止cwnd增加过快而导致网络拥塞,所以需要设置一个慢开始门限ssthresh状态变量。拥塞避免激素hi是让cwnd缓慢的增加而不是加倍的增长,每经历过一次往返时间就使cwnd增加1,而不是加倍,这样使cwnd缓慢的增长,比慢启动要慢的多。
- 当cwnd < ssthresh,使用慢启动算法
- 当cwnd > ssthresh,使用拥塞控制算法,停用慢启动算法。
- 当cwnd = ssthresh,这两个算法都可以。
ack确认丢失的情况
每条数据都要进行回复,并且应该按序逐条回复,如果没有收到第一天,但是都到了第二条,第二条就不能先回复,应该先回复第一条。这样做的好处就是第一条ack丢失后,如果发送端收到第二条回复,也会认为第一条正常接收,第一条就不需要重传了。
延迟应答机制
尽可能的保持窗口的大小,保持网络传输的吞吐率,如果接收端收到数据就立即返回ACK回答,这是后返回的窗口可能比较小,比如说接收端的接收缓冲区是1M,一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了.在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放⼤大一些, 也能处理过来;如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;窗口越大,传输效率就越高。但是不是所有的包都可以延迟应答,有数量限制,每隔N个包就应答一次,还有时间限制,超过最大延迟时间就应答一次,一般N为2,超时时间为200ms。
捎带应答机制
尽量减少不必要的确认应答包,接收端在发送数据的时候顺便对上一次的请求进行一个ack确认,比如说客户端给服务器说了 “How are you”, 服务器也会给客户端回一个 “Fine, thank you”;那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起回给客户端
面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区和一个接收缓冲区。调用write时, 数据会先写入发送缓冲区中,如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去。接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用read从接收缓冲区拿数据.
TCP的连接中, 既有发送缓冲区, 也有接收缓冲区, 那么对于这⼀一个连接, 既可以读数据,也可以写数据. 这个概念叫做 全双工。由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 比如说写100个字节数据时, 可以调用一次write写100个字节, 也可以调⽤用100次write, 每次写一个字节。读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次.
Tcp粘包问题
缓冲区中的数据对于Tcp来说没有边界;造成数据粘连问题---->数据没有边界
解决粘包问题
对于定长的包, 保证每次都按固定大小读取即可;
对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
对于变长的包, 还可以在包和包之间使⽤用明确的分隔符
TCP与UDP对比
TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播;