通俗理解三握四挥与数据传输笔记

352 阅读18分钟

前言

上次在从url到显示页面的笔记中写的三握四挥并没有让我觉得完全理解了:能回答的为什么不多。所以就把它独立出来,再来复习一遍。

三次握手

意义

三次握手其实就是建立一个tcp连接,需要客户端和服务器总共发送三个包。进行三次握手的主要作用就是为了确认双方的接受能力和发送能力是否正常,指定自己的初始化序列号为后面的可靠性传送做准备。实质上就是连接服务器指定端口,建立tcp连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。

通俗过程

Image3.png

这张图能更好的帮我理解这个过程,

首先我们来用一个很简单易懂的过程来模拟一下三次握手:

我理解的握手核心就是用加法来确认双方的收发能力和协商初始序列号:

  1. 我客户端给你一个x,表示我要建立连接且能发送能力正常。
  2. 服务端回我一个x+1和他给出的y表示他的发送和接受能力都正常。
  3. 这个时候服务端还不知道我客户端的接收能力正不正常,所以我回一个y+1告诉服务端我的接收能力正常。(注意这里才知道客户端接收能力是否正常,这是解释为什么要三次握手而不是两次的主要原因)

这样就能确认双方的收发能力是否正常了。

而协商序列号,也很简单,在第2步服务端发出x+1到客户端,且第三步客户端也发出x+1到服务端的时候,这个过程,其实就是协商初始序列号了,协商结果为x+1,说明了数据流就从这个数字(x+1)开始,然后按照后面数据交换过程中的len字段发送len字段长度的数据就好了。

规范过程

ok,上面那段话很好理解,但那也总归是通俗解释,还是得规范的来走一遍流程。

首先先明确一开始的状态,客户端是主动,服务端是被动(listen)。

1.第一次握手,客户端---->服务端,客户端要给服务端发送一个SYN包,其中syn = 1,client_seq = x,这个时候客户端已经发送SYN包(syn = 1),所以客户端进入syn_send状态

其中syn为1的报文就是表示发起一个新的链接,并且在报文中指明客户端的初始序列号seq是随机的,seq的作用为标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,则TCP用序号对每个字节进行计数。在握手的过程中也有确认双方的收发能力和协商初始序列号的作用。

2.第二次握手,服务端---->客户端,服务端收到客户端发送的SYN包,因为syn = 1,得知客户端想要建立连接,所以服务器从原来的被动状态(listen)变为了syn_rcvd(syn received),然后向客户端发送一个SYN包,其中syn = 1, 并且在SYN包中指定自己的初始化序列号server_seq = y,且会把客户端的client_seq值x +1作为ack的值。

第二次握手中服务端发送的SYN包的内容包括:syn = 1, server_seq = y, ack = x + 1。其中syn = 1表示服务端也想建立连接,server_seq = y的作用为等客户端下次发送回y+1,以确认客户端的接收能力;ack = x + 1作用为回应客户端,表示自己接收能力没问题,且开始协商数据字节流的初始序列号。

3.第三次握手,客户端---->服务端,客户端收到了服务端发送的SYN包,知道了服务端也想建立连接,所以发送一个ACK报文,其中ack = y + 1,client_seq = x + 1,表示已经收到报文,这时客户端进入established(已建立)状态。服务端收到ack包,确认客户端接收能力,也确认数据流初始序列号,也进入established(已建立)状态

第三次握手中,客户端收到了服务端的SYN包,得知了服务端的收发能力没问题,所以客户端要再发送一个ACK包,其中ack = y + 1,client_seq = x + 1,ack = y + 1表示收到了服务端的序列号,确认了客户端的接收能力;client_seq = x + 1表示同意用一开始的客户端生成的初始序列号seq + 1来作为数据开始的序列号。

握手时候相关问题

1.为什么要三次握手两次不行吗?

这个问题就是上面讲过的坑了。第三次的握手是需要确认客户端的接受能力正常。如果只有两次握手,就会出现下面这种情况:比如说我客户端发起了第一次请求,但是这次请求因为某个网络节点卡住长时间滞留了,于是客户端又发了第二次请求,这个请求被确认,建立了连接。数据传输完毕后,就释放了连接。如果在释放连接后,那个因为卡住的请求到达了服务器,那么服务端会以为客户端又发了一个请求,于是就发确认报文到客户端同意连接(第二次握手),如果没有第三次握手的话,这个时候就算建立连接了,此时客户端因为自己没有发请求,所以忽略服务端发起的确认,也不发送数据,则导致服务端一直等待客户端发送数据,浪费资源。

2.初始序列号(inital sequence number)是固定的吗

不是,如果ISN是固定的,攻击者很容易猜出后续的确认号。所以ISN是动态生成的。ISN有时序性,会随着时间变化,这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它做错误的解释。

ISN = M + F(localhost, localport, remotehost, remoteport)

其中M是计时器,每4ms加一,F是哈希算法,根据源IP、目的IP、源端口、目的端口生成一个随机数值。要保证hash算法不能被外部轻易推算得出。

3.三次握手过程中可以携带数据吗

结论:第一次不可以,第三次可以。

为什么:如果第一次握手可以携带数据的话,如果有人要恶意攻击服务器的话,那他每次在第一次握手中的SYN报文放入大量数据,且不理会服务器接受能力发送能力是否正常的话,这样会让服务器花更多时间,空间来处理这些报文。

也就是说,第一次握手是不能带数据得,因为可以的话很容易受到攻击。那么为什么第三次可以?

对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。

4.SYN攻击是什么?

服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。

如何防范?

常见的防御 SYN 攻击的方法有如下几种:

缩短超时(SYN Timeout)时间 增加最大半连接数 过滤网关防护 SYN cookies技术

5.第一次握手,客户端SYN包丢失

当客户端发起的 TCP 第一次握手 SYN 包,在超时时间内没收到服务端的 ACK,就会在超时重传 SYN 数据包,每次超时重传的 RTO 是翻倍上涨的,直到 SYN 包的重传次数到达tcp_syn_retries 值后,客户端不再发送 SYN 包。

6.第二次握手,syn,ack包丢失

通过实验二的实验结果,我们可以得知,当 TCP 第二次握手 SYN、ACK 包丢了后,客户端 SYN 包会发生超时重传,服务端 SYN、ACK 也会发生超时重传。客户端 SYN 包超时重传的最大次数,是由 tcp_syn_retries 决定的,默认值是 5 次;服务端 SYN、ACK 包时重传的最大次数,是由 tcp_synack_retries 决定的,默认值是 5 次。

7.第三次握手,ack包丢失

在建立 TCP 连接时,如果第三次握手的 ACK,服务端无法收到,则服务端就会短暂处于 SYN_RECV 状态,而客户端会处于 ESTABLISHED 状态。由于服务端一直收不到 TCP 第三次握手的 ACK,则会一直重传 SYN、ACK 包,直到重传次数超过 tcp_synack_retries 值(默认值 5 次)后,服务端就会断开 TCP 连接。而客户端则会有两种情况:如果客户端没发送数据包,一直处于 ESTABLISHED 状态,然后经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接,于是客户端连接就会断开连接。如果客户端发送了数据包,一直没有收到服务端对该数据包的确认报文,则会一直重传该数据包,直到重传次数超过 tcp_retries2 值(默认值 15 次)后,客户端就会断开 TCP 连接。

中间传输数据过程

过程

建立连接后,两台主机就可以开始相互传输数据了,下图是数据传输的示意图:

Image.png

上面的图给出了客户端(左)与服务器(右)传输数据的过程 ,应该这样解释:

在三次握手后(就是紧接着客户端发ACK报文后,这里是第四行),客户端A发出携带有效数据的PSH包,其中客户端client_seq为握手时候确定的序列号,这里假设为client_seq = 1,包的数据长度len为725。这里我们发起的是一个get请求,所以请求中带有参数数据,参数数据长度为725.

第五行,服务器收到了客户端的PSH包,知道了已经开始发送数据了,所以用ack = client_seq(客户端序列号) + len回复客户端,告知他收到了725字节的数据。

第六行,服务器返回http响应,服务端序列号server_seq依然为1,因为服务器在返回该包以前的包里面都不带有有效数据。这个包带有1448长度的数据。这里就是response响应请求了。

第七行,由于上个数据包的发送,TCP客户端的序列号增长至726,从服务端接收了1448字节的数据,客户端的确认号由1增长至1449。在抓包文件的主体部分,我们可以看到上述过程的不断的重复,客户端的序列号一直是726,因为客户端除了最初的725字节数据没有再向服务端发送数据,服务端的序列号则与此相反,由于服务端不断的发送HTTP响应,故其序列号一直在增长序列号为当前端成功发送的数据位数,确认号为当前端成功接收的数据位数,SYN标志位和FIN标志位也要占1位。

tips:seq可以看作是自己发送的数据的序列号,ACK可以看作是对方发送的数据的序列号,像在这两步,服务端收到了客户端发来的725长度的数据,所以通过ACK告诉客户端收到了多少数据(ACK = 726),并在第三步发送1448长度的数据给客户端,客户端收到以后,更新seq = 726,并发送ACK = 1449表示收到了1448长度的数据

如何保证可靠传输?

综述:

  1. 上面流程中的确认收发是一个保证可靠传输的手段,当一段时间没有收到确认后,就会重传。
  2. 数据校检
  3. 数据分片和排序,接收方会缓存未按序到达的数据,重新排序后再交给应用层。
  4. 流量控制:当接收方来不及处理发送方数据时,会提示发送方降低发送速率,防止包丢失。
  5. 拥塞控制:当网络拥塞时,减少数据发送。

稍微了解下

1.通过应答确认收发,通过序列号识别是否接收数据

数据包丢失的情况有两个:一是发送端发送的数据包丢失了,接收方没有收到所以没有发送ACK;二是接收端发送的确认包丢失或者延迟了。

对于这两种情况,发送方都不知道对方有没有收到数据,只能重发。这就是通过应答确认收发

但对于接收方来说,如果数据只是延迟了,但是发送方以为丢失,又发了一次,这时候有两份一样的数据,接收方总不能浪费精力去处理重复的数据包吧,那该怎么办呢?这时就用到了序列号,通过序列号识别是否已经接收数据,又能够判断是否需要接收,这就是通过序列号判断是否接收数据

超过多长时间可以认定包丢失了呢?就是说时间间隔应该是多长呢?

TCP在每次发包的时候,都会计算往返时间RTT(Round Trip Time),及其偏差(RTT的波动),这两者相加,重发的超时时间就是比这两者相加还要大一点的值

2.滑动窗口控制提高速度

因为TCP是以段为单位的,每一段就是一次确认应答,才能进行下一次通信。这样的传输方式有一个缺点,就是如果每一段的应答时间越长,那么效率就越低。这就有点类似浏览器单线程,只能一个一个来,不能并发处理。

为了解决这个问题,TCP引入了窗口概念,确认应答的时候不是由每个分段来确认,而是以更大的单位进行确认。也就是说,发送一段后不必要等待应答再发送下一段,而是继续发送。

Image1.png

这样一口气发送一大段的机制,就是滑动窗口。窗口的大小就是指无需等待确认应答ACK而继续发送的数据最大量

在三次握手中,在第一和第二次的握手里面,双方都会发送自己的最大长度(MSS)作为发送的单位,然后双方取最小值,作为包大小。MSS存储在第一二次握手中的window size里面。这个包大小就是窗口的大小。

四次挥手

建立一次连接需要三次握手,但是终止一个连接需要四次挥手,这是tcp的半关闭造成的,所谓的半关闭,其实就是TCP提供了连接一端在结束它的发送后还能接收到另一端数据的能力。而关闭一端要2次挥手,所以关闭两端就要四次挥手。

还是这张图

Image3.png

通俗过程

第一次挥手,客户端:我没有数据要发了,要关闭我到你的连接了,你确认一下。 第二次挥手,服务端:好的,你先关吧,我发完数据再关闭我到你的连接。 第三次挥手,服务端:我的数据也发完了,要关闭我到你的连接了,你确认一下。 第四次挥手,客户端:好的,确认关闭。

详细过程

还是先来

刚开始两边还是处于连接状态established。

第一次挥手,客户端发起一个FIN报文,然后客户端进入fin_wait状态

FIN报文中,内容为fin = 1,client_seq = u,ack = v;其中fin = 1表示自己已经没有数据要传输了,要关闭客户端指向服务器的连接;seq = u是接着上面传输过程中累积下来的(记得吧,这个数据包数据从哪开始),这里服务端会用它+1来作确认作用;ack = v也是接着上面传输过程中累积下来的(收到了多少数据)。这个时候客户端主动关闭客户端到服务端的tcp连接。

第二次挥手,服务端收到客户端的FIN报文,会发送ACK报文确认关闭连接,然后进入close_wait状态,客户端在收到ACK报文后进入fin_wait2状态等待服务端发送连接释放FIN报文段

因为收到客户端的FIN报文,知道客户端没有东西发了,所以发送ACK报文确认关闭客户端到服务端的连接,其中报文内容为:ack = u + 1,server_seq = v。其中ack = u + 1,表示已经收到报文;server_seq = v也是接着上面传输过程始中累计下来的,也能在后面用它(v) + 1作确认作用。然后客户端在收到ACK报文后进入fin_wait2状态等待服务端发送连接释放FIN报文段。前两次挥手断开了客户端到服务器的连接。

第三次挥手,服务端也发完数据了,也想断开连接,就要像第一次握手一样,发一个FIN报文,然后服务端进入last_ack状态,等待客户端确认

第三次挥手,服务端发完数据了,想断开服务器到客户端的连接了,发送的FIN报文内容为:fin = 1, server_seq = v, ack = u + 1;其中fin = 1就是断开连接;server_seq = v也是接着上面传输过程始中累计下来的,在下一次挥手中用它(v) + 1作确认作用;ack因为客户端的连接断了,没有收到客户端的数据了,所以不变。

第四次挥手,客户端收到了服务端的FIN报文,知道服务端发完数据了,想要断开服务端到客户端的连接了,就发一个ACK报文同意断开。然后客户端进入time_wait状态,需要等一阵子以确保服务端收到自己的ACK报文才会进入closed状态。服务端收到ACK报文后就关闭连接了,然后进入close状态。

客户端收到服务端的FIN报文后,所以发送一个ACK报文同意断开连接,其中内容为:ack = v(server_seq) + 1,client_seq = u + 1。其中ack就是确认收到FIN报文,因为服务端的连接也要断了,没有收到服务端的数据了,所以不变。然后客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

为什么是2msl的等待时间。

首先要先明确msl是什么单位,msl全称maximum segment lifetime,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

为什么要等待2个msl呢?

1.因为在客户端最后发送ACK报文后有两种情况:

1. 如果客户端发的ACK丢失了,服务端会重传FIN报文

2. 如果客户端发的ACK没有丢失,那就不会收到任何消息

无论是情况1或者情况2,都需要等待,所以保险起见就要取这两种等待时间的最大值,以防最坏情况发生,最坏情况是什么?ack丢了,重传的fin很慢才到。那么ack丢了多久才知道丢了呢?重传的fin最慢什么时候才能到呢?或者说重传的fin过了多长时间就会是失效呢?

答案就是2msl:ack最长存活时间(1msl)+ fin最长存活时间(1msl)

2.还有一个原因就是

如果不等,释放的端口可能会重连刚断开的服务器端口,这样依然存活在网络里的老的TCP报文可能与新TCP连接报文冲突,造成数据冲突,为避免此种情况,需要耐心等待网络老的TCP连接的活跃报文全部死翘翘,2MSL时间可以满足这个需求(尽管非常保守)。