vivatoviva / Interview-Frontend-2020

欢迎star、在对应的ussues沉淀知识
17 stars 2 forks source link

TCP连接 #28

Open vivatoviva opened 5 years ago

vivatoviva commented 5 years ago

OSI七层网络模型

  1. 应用层(Application) 提供网络与用户应用软件之间的接口服务
  2. 表示层(Presentation) 提供格式化的表示和转换数据服务,如加密和压缩
  3. 会话层(Session) 提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制
  4. 传输层(Transimission) 提供建立、维护和取消传输连接功能,负责可靠地传输数据(PC)
  5. 网络层(Network) 处理网络间路由,确保数据及时传送(路由器)
  6. 数据链路层(DataLink) 负责无错传输数据,确认帧、发错重传等(交换机)
  7. 物理层(Physics) 提供机械、电气、功能和过程特性(网卡、网线、双绞线、同轴电缆、中继器)

TCP四层协议

TCP是实际使用的是四层分层,现实中我们为了更好的学习TCP协议,通常将TCP分为5层来进行学习,TCP被称之为传输控制层协议,是传输层的一种协议,和UDP协议的最大的不同就是:TCP是面向连接的,也就是在传输数据之前,必须和对方建立可靠连接,并且保证每次传输过程中报文的传输正确。UDP是面向数据包的,也就是说传输数据不需要建立可靠的连接,可以直接将报文进行发送。

TCP和UDP的应用场景

TCP的应用场景:

一般用于文件传输等对数据准确性要求很高的场景,比如说远程登录(ssh)、邮件发送

UDP的应用场景:

UDP一般用于及时聊天等应用中,对数据的准确性要求和丢包要求都比较低的情况中,目前的网络质量还是可以的,如果对数据的准确性没有特殊的要求的话就可以使用UDP协议进行数据传输。

TCP和UDP的编程方式

TCP编程的一般步骤:

  1. 创建一个socket,用函数socket();
  2. 设置socket属性,用函数setsockopt(); * 可选
  3. 绑定IP地址、端口等信息到socket上,用函数bind();
  4. 开启监听,用函数listen();
  5. 接收客户端上来的连接,用函数accept();
  6. 收发数据,用函数send()和recv(),或者read()和write();
  7. 关闭网络连接;
  8. 关闭监听

UDP编程的一般步骤:

  1. 创建一个socket,用函数socket();
  2. 设置socket属性,用函数setsockopt();* 可选
  3. 绑定IP地址、端口等信息到socket上,用函数bind();* 可选
  4. 设置要连接的对方的IP地址和端口等属性;
  5. 连接服务器,用函数connect();
  6. 收发数据,用函数send()和recv(),或者read()和write();
  7. 关闭网络连接;

TCP协议

TCP如何建立可靠连接

TCP通过“三次握手建立连接,四次挥手中断连接”这种方式来建立一种可靠的连接,首先我们了解这个过程:

  1. 第一次握手:主机A通过向主机B 发送一个含有同步序列号的标志位的数据段给主机B,向主机B 请求建立连接,通过这个数据段, 主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我。
  2. 第二次握手:主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:我已经收到你的请求了,你可以传输数据了;你要用那个序列号作为起始数据段来回应我
  3. 第三次握手:主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:"我已收到回复,我现在要开始传输实际数据了,这样3次握手就完成了,主机A和主机B 就可以传输数据了。
  4. 第一次挥手: 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求 ;
  5. 第二次挥手: 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1;
  6. 第三次挥手: 由B 端再提出反方向的关闭请求,将FIN置1 ;
  7. 第四次挥手: 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束.。

但是这些连接的过程都不是重点,重点是为什么建立可靠连接需要进行这些步骤?

为什么需要进行三次握手?

前面两次进行握手是必有原因,但是为什么需要进行第三次握手,即客户端收到服务端的响应之后为什么还需要再次向服务器端发送一次请求确认呐,这主要是为了防止已失效的请求报文突然有传递到服务产生错误的误判。

可以假设这样一个例子,如果客户端发送一个连接请求但是在网络传输过程中滞留了,客户端超时等待后又重新向服务器发送一个请求,这时候这个请求及时被服务器端响应建立连接,然后客户端和服务器端进行数据传输,传输完成之后两者断开连接,但是这时候如果第一次滞留的那个请求连接报文重新发送到服务器上,这时候服务器会因为客户端要求建立连接,会立即发送响应报文给客户端,如果只有两次握手,那么肯定这次连接是无效的,浪费资源的,所以为了解决这个问题,这时候要求客户端向服务器端发送一个响应报文来确定建立连接

为什么需要四次挥手断开连接?

为什么连接释放需要四次挥手?

  1. 确保数据能够完成传输,关闭连接的时候,当收到对方的FIN报文通知时候,它仅仅表示对方没有数据发送给你了,但未你所有的数据全部发送给对方,所以你未必能马上关闭socket,即就是你可能还需要发送一些数据给对方之后,在发送FIN报文给对方就可以关闭连接了

为什么主机A需要等待2MSL时间?

  1. 第一原因是保证A发的最后一个报文段可以到达B,因为当A收到B的确认关闭连接的报文时候,将会响应一个请求告知服务器B,但是这个报文有可能丢失,所以为了可靠性需要等待2MSL,MSL称之为最长报文寿命(任何报文在网络上存在的最长的最长时间,超过这个时间报文将被丢弃),这样可以保证报文传输到服务器B,也能B因为超时的请求,重新发送的请求报文可以传输到客户端A,
  2. 第二个原因就是防止已失效的请求连接出现在本连接中,在连接处于2MSL等待时,任何迟到的报文将会被丢弃,因为处于2MSL等待时间内,该socket在这段时间内将不能再次被使用,这样就可以使下一个新的连接中不会出现这种旧的连接之前延迟的报文段。
TCP连接碰撞/断开碰撞的情况

正常情况下,连接是由一方发起的也是由一方终止的,但是也有可以是两边同时要求建立连接或者两边同时释放连接,这种情况就会发生连接碰撞,下面分别是两种连接碰撞的情况。

TCP其他可靠机制

TCP为了实现可靠连接并不是仅仅做了上述控制,其实还做了其他相关步骤的控制,下面这些是了解内容:

实现TCP可靠传输的基础

停止等待协议就是每次发送一个分组,就等待对方确定该分组,在收到确认后再发送最后一个下一个分组

这个协议称之为滑动窗口协议,因为如果要采用停止等待协议,如果只是一个分组一个分组发送的话,这样的话将会极大的浪费资源,为了解决这个问题,TCP提出滑动窗口的概念的,可以进行批量进行发送分组,TCP在滑动窗口的基础提出拥塞控制和流量控制,为了减轻网络传输压力,接收方一般都采用累积确定的方式,这就是说,接收方不必针对收到的分组进行逐个确定,只需要对按序到达的最后一个分组发送确认,这就标识:到这个分组为止的所有分组都已正确收到,但是这种存在相应的问题就是不能精确确定是哪个报文丢失,如果某次传输失败,需要将失败序号之后的所有分组全部重新传递。

流量控制

首先明确滑动窗口的单位是字节,流量控制就是让发送方的发送速率不要太快,要让接收方来的及接受,通常来里tcp连接之后,会产生一个滑动窗口大小,然后发送方和接收方进行交互,来进行控制流量,比如下面图所示:

![image-20190303000029326](/Users/ligen/Library/Application Support/typora-user-images/image-20190303000029326.png)

通过这种协商,可以保证接收端一直可以处理发送端发送的报文,不会产生来不及的情况。但是现在考虑这样一个情况,如果接受端给发送端发送报文使得滑动窗口大小为0,导致发送方不能再发送报文,只能等待发送方重新发送新的滑动窗口大小,但是如果这个报文丢失了,那这样接收方和发送方不就艳茹死锁的方式,所以为了避免这个问题,TCP为每一个连接设置一个持续计时器,只要tcp的一方收到零窗口通知,就会启动一个持续计时器,若计时器时间到了,就会发送一个零窗口探测报文段,二对方就会响应这个报文段给出现在的窗口值,如果窗口值还是0的话,那么发送方就重新设置一个持续计时器,如果不是零的话,那么这个死锁的僵局就可以打开了

现在思考这样一个问题,若果接收方发送给发送方的rwnd大小为1,如果一直维持这个滑动窗口,那么这样是不是就退化为交互式发送,并且发送方发送一字节长度的报文,但是经过TCP的包装(20字节TCP头部 + 20字节的IP),报文长度将会扩充到41字节,同时接收方为了响应发送方也会发送一个长度为41字节的报文,这样网络的性能将极大的退化,这个问题被称之为小包问题,为了保证TCP发送数据的过程中的高性能,我们需要考虑一下TCP的传输的效率,也就是考虑一下发送方发送报文的时机从而保证TCP的效率,前面讲到应用进程将数据传输给TCP的发送缓存之后,剩下的任务将交给TCP进程来控制,可以使用不同的机制来控制TCP发送报文的时机,主要有如下三种机制:

在TCP协议中常使用的是Nagle算法避免网路中产生很多小的数据块,算法是这样的:算法的基本定义是在任意时刻都要保证网路中只能存在一个未被确认的小段(这里指一个数据块发送出去后,没有接受到接受方发送的ACK确认报文),除非超时或者FIN包,那么我们可以理解为Nagle算法是一个扩展的停止-等待协议,只不过这种停止等待是基于(包)报文段的,而不是基于字节的停止等待协议,但是这里接受端接受到发送方发送的报文还需要等待200ms左右才开始发送确认ACK报文,为什么需要这样?这种设置是考虑到当接收到一个TCP分组之后不想立刻发送一个空的ACK,而是在将要发送的数据包中捎带ACK,避免空的ACK报文存在于网路中。但是这样也会造成性能问题,如果接收端不发送任何响应的数据,这样就会平白无故多出200ms的延迟,这也是一个性能问题。

拥塞控制

为什么需要拥塞控制,总结为一句话来说就是网络中速度不匹配,我们需要通过通过拥塞控制来协调整个系统中的速度,保证发送方和接受方两者的速度可以达到一个平衡的状态,计算机网络中链路容量(带宽)、交换节点的缓存和处理机等,都是网络的资源,但是在某一段时间内,如果对网络中资源的需求超过该资源所能提供的量,这时候网络将会产生用拥塞,由于计算机网络是一个很复杂的系统,因此可以从控制理论来看待拥塞控制这个问题,从大的方面来看,主要分为两种,一种是开环控制、一种是闭环控制

开环控制: 在设计网络的时候,事先将有关发生拥塞的因素考虑周到,力求网络在工作的时候不产生拥塞

闭环控制:主要是基于反馈回路的概念,主要有下面几种措施

  • 检测网络系统以便检测到拥塞在何时、何地发生
  • 把拥塞发生的信息传递到可采取行动的地方
  • 调整网络系统的运行以解决出现的问题

现在我们来看下在TCP协议中是如何处理网络拥塞,TCP的拥塞控制主要有四种方法,慢开始拥塞避免快重传快恢复

当主机开始发送数据的时候,由于并不清楚网络的情况,如果立即把大量的数据发送到网络中,那么极有可能发生网络拥塞,慢开始是这样控制的,首先探测一下,即由小到大逐渐增大发送窗口,也就是说由小到大逐渐发送窗口的值。慢开始规定,在接受到一个ACK确认报文之后,就把拥塞窗口(cwnd)增加最多一个SMSS的数值,

但是在实际中,只要发送方接受到一个对报文的确认,那么cwnd可以立即加一,并立即发送新的报文段,而不需要等待每个轮次完成之后在开始进行发送,为了防止cwnd 过大引起网络拥塞,我们还需要设置一个慢开始门限(ssthresh),当cwnd < ssthresh 的时候使用满开始算法,当cwnd > ssthresh 我们开始使拥塞控制算法,拥塞控制算法让cwnd处于一种线性的缓慢增长,即每经过一个往返时间RTT就把发送发的拥塞窗口增加1,这种比慢开始的拥塞窗口增长速度慢很多。随着这样的增长,如果发送方判断出现了网络拥塞,发送将会将拥塞窗口设置为1,同时将会重新进行慢启动算法,这里慢开始门限也会发生变化,比如下面这张图:

在这张图中慢启动和拥塞控制主要处于2号之前,首先进行慢启动开始发送数据,后面当拥塞窗口达到慢开始门阀开始使用拥塞避免算法,当出现超时(也就是接收方判断出现的网络拥塞)接受方将会重新调整拥塞窗口为1,并重新启动慢启动算法,并将慢开始门限调整为发生超时的拥塞窗口的一半。

为什么需要提出快重传:根据上面慢启动和拥塞控制我们知道当报文出现超时的时候,这时候我们会认为这时候插出现了网络拥塞,但是我们考虑这样一种情况就是网路实际上并没有发生网络拥塞,仅仅只是发生了报文段丢失,但是如果我们在这种情况下判断网路的发生拥塞,重新将拥塞窗口调整为1,那么这样对网路的性能影响将是很大的,所以为了解决这个问题,我们需要约定接受方来解决这个问题,所以采用快重传算法的目的就是让发送方尽早知道发生了个别报文段的丢失,快重传首先要求接收方不能等待自己发送数据时候才进行捎带确定,而是要立即发送确认(Nagle算法存在的问题),即使受到了失序的报文段也要立即发送对已收到的报文段的重复确认,比如如下:

通过这种方法,就不会产生超时问题,这样发送方也就不会误认为网络发生拥挤了,当发送方收到重复确认的ACK时,这时候将不会进入慢开始状态,而是返送方重新调整慢开始门阀,从调整后的慢开始地方重新执行拥塞避免算法,保证网络的性能,对于调整这个慢开始门阀,我们有两种调整方式,一种是调整为此时 (cwnd / 2),另一张是调整为(cwnd / 2)+ 3 * MSS,因为当接受方收到3个ACK确认吗的时候,我们可以理解为三个分组已经被接收端收到,那么网络中也就不存在这三个分组,那么我们可以适当的将拥塞窗口扩大点,这种计算方法被称之为AIMD算法,针对TCP的整个拥塞控制我们可以使用下面这张流程图进行概括:

结合上面的流量控制,我们可以理解为:发送方的发送上限 = Min(rwnd, wnd)

主动队列管理

上篇提到TCP的拥塞控制并没有和网络层连接起来,其实网络层针对TCP的拥塞控制影响特别大,比如,针对TCP影响最大的就是分组丢弃策略,简单的情况下路由器都是按照先进先出的规则来处理到来的分组,由于队列长度是有限的,当超出队列的长度之后,后面来的报文就直接进行丢弃,这就称之为尾部丢弃策略。路由器的尾部丢弃将会造成一系列的报文段丢失,但是最严重不适这个,而是可能这条线路上可能存在很多TCP连接,这样的话会导致很多连接的TCP突然进入慢开始状态,这样的话就会造成网络性能变差,这种情况被称之为全局同步,这种情况会突然是的网络通信量突然减少很多,而在网络恢复正常之后,其通信量又突然增加很多,为了避免这种情况产生,我们提出主动队列管理(AQM),所谓主动的意思就是不等待队列长度达到最大值才不得丢弃后面的分组,这样就太被动了,应当在队列长度达到一个定值的时候,就应该主动丢弃到达的分组,使得网络拥塞程度减轻,甚至不出现网络拥挤情况,AQM有不同的实现方法,其中早期流行多年的是一种称之为随机早期检测RED,这种算法通常就是需要维护两个参数,即队列的最小门限和最大门限,当请求报文发送过来之后,将会判断此时的平均队列,如果此时的平均队列长度大于最小门限,则路由器将会根据一定概率将分组进行丢弃,让拥塞控制只在个别的TCP连接上进行,因而避免了发生全局性的拥塞控制。