转载

java面试系列(一)--- Tcp协议精准剖析

TCP/IP协议(传输控制协议/互联网协议)不是简单的一个协议,而是 一组特别的协议 ,包括:TCP,IP,UDP,ARP等,这些被称为子协议。

1.2分层

此图来自: @知乎 仇诺伊

java面试系列(一)--- Tcp协议精准剖析

1.3 TCP

1.3.1简介

TCP(Transmission Control Protocol 传输控制协议)是一种 面向连接 的、 可靠 的、基于字节流的 传输层 通信协议。

1.3.2 连接

面向连接意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据包之前 必须先建立一个TCP连接 。这一过程与打电话很相似,先拨号振铃,等待对方摘机说“喂”,然后才说明是谁。

1.3.2.1 三次握手

如何建立一个连接呢?

TCP的三次握手:“三次握手”的意思是建立TCP连接“需要三个步骤才能建立连接的机制”。三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换 TCP 窗口大小信息。

在socket编程中,客户端执行connect()时,将触发三次握手。

java面试系列(一)--- Tcp协议精准剖析

位码即tcp标志位,有6种标示:

  • SYN(synchronous建立联机)
  • ACK(acknowledgement 确认)
  • PSH(push传送)
  • FIN(finish结束)
  • RST(reset重置)
  • URG(urgent紧急)
  • Sequence number(seq 顺序号码)
  • Acknowledge number(ack 确认号码)

1)主机A发送标志syn=1,随机产生 seq=x 的数据包到服务器,主机B由syn=1知道,A要求建立连接; 此时状态A为SYN_SENT,B为LISTEN。

2)主机B收到请求后要确认连接信息,向A发送ack=(主机A的seq+1),标志syn=1,ack=1,随机产生seq=y的包,此时状态A为ESTABLISHED,B为SYN_RCVD。

3)主机A收到后检查ack 是否正确,即第一次发送的seqnumber+1,以及位码ack是否为1,若正确,主机A会再发送ack =(主机B的seq+1),标志ack=1,主机B收到后确认seq值与ack=1则连接建立成功。此时A、B状态都变为ESTABLISHED。

TCP为什么不是两次连接?而是三次握手?

答:如果A与B两个进程通信,如果仅是两次连接。可能出现的一种情况就是:A发送完请求报文以后,由于网络情况不好,出现了网络拥塞,即B延时很长时间后收到报文,即此时A将此报文认定为失效的报文。B收到报文后,会向A发起连接。此时两次握手完毕。B会认为已经建立了连接可以通信,B会一直等到A发送的连接请求,而A对失效的报文回复自然不会处理。因此会陷入B忙等的僵局,形成了死锁,造成资源的浪费。

1.3.2.2 SYN攻击

什么是 SYN 攻击(SYN Flood)?

在三次握手过程中,服务器发送 SYN-ACK 之后,收到客户端的 ACK 之前的 TCP 连接称为半连接(half-open connect)。此时服务器处于 SYN_RCVD 状态。当收到 ACK 后,服务器才能转入 ESTABLISHED 状态.

SYN 攻击指的是,攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送SYN包,服务器回复确认包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,导致目标系统运行缓慢,严重者会引起网络堵塞甚至系统瘫痪。

SYN 攻击是一种典型的 DoS/DDoS 攻击。

如何检测 SYN 攻击?

检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击。

如何防御 SYN 攻击?

SYN攻击不能完全被阻止,除非将TCP协议重新设计。我们所做的是尽可能的减轻SYN攻击的危害,常见的防御 SYN 攻击的方法有如下几种:

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

1.3.3 数据传输

建立连接后,两台主机就可以相互传输数据了。如下图所示:

java面试系列(一)--- Tcp协议精准剖析

上图给出了主机A分2次(分2个数据包)向主机B传递200字节的过程。

  1. 首先,主机A通过1个数据包发送100个字节的数据,数据包的 Seq 号设置为1200。

  2. 主机B为了确认这一点,向主机A发送 ACK 包,并将 Ack 号设置为 1301。

为了保证数据准确到达,目标机器在收到数据包(包括SYN包、FIN包、普通数据包等)包后必须立即回传ACK包,这样发送方才能确认数据传输成功。

此时 Ack 号为 1301 而不是 1201,原因在于 Ack 号的增量为传输的数据字节数。

假设每次 Ack 号不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分,比如只传递了80字节。

因此按如下的公式确认 Ack 号: Ack号 = Seq号 + 传递的字节数 + 1

与三次握手协议相同,最后加 1 是为了告诉对方要传递的 Seq 号。

下面分析传输过程中数据包丢失的情况,如下图所示:

java面试系列(一)--- Tcp协议精准剖析

上图表示通过 Seq 1301 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1301 的ACK确认,因此尝试重传数据。

为了完成数据包的重传, TCP套接字每次发送数据包时都会启动定时器 ,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。

上图演示的是数据包丢失的情况,也会有 ACK 包丢失的情况,一样会重传。

超时时间如何判断是由具体的算法去确认,重传次数根据系统设置的不同而有所区别,在此不多赘述。

1.3.3.1 可靠传输过程

在上述的数据传输过程中,tcp如何确保数据的可靠传输呢?

(1)为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区;

(2)并为每个已发送的数据包启动一个超时定时器;

(3)如在定时器超时之前收到了对方发来的应答信息,则释放该数据包占用的缓冲区;

(4)否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。

(5)接收方收到数据包后,先进行CRC校验(循环冗余校验码,保证数据传输的正确性和完整性),如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。

1.3.3.2 可靠传输之超时重传和快速重传

  • 超时重传:当超时时间到达时,发送方还未收到对端的ACK确认,就重传该数据包。
  • 快速重传:当后面的序号先到达,如接收方接收到了1、 3、 4,而2没有收到,就会立即向发送方重复发送三次ACK=2的确认请求重传。如果发送方连续收到3个相同序号的ACK,就重传该数据包。而不用等待超时 。

1.3.3.3 可靠传输之流量控制

一条TCP连接每一侧主机都为该连接设置了接收缓存。当该TCP连接收到了正确的、按序的字节后,他就将数据放入接收缓存。相关联的应用进程会从该缓存中读取数据。但不必是数据一到达就立即读取。事实上,接收方也许正忙于其他任务,甚至要过很长时间后才读取该数据。 如果某个应用进程读取比较缓慢,但是发送方发送的太多、太快,发送的数据就会很容易地使该连接的接收缓存溢出。

TCP为它的应用程序提供了 流量控制服务(flow-control service) 以消除发送方使接收方缓存溢出的可能性。流量控制因此是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。

TCP通过让发送方维护一个称为 接收窗口(receivewindow) 的变量(TCP报文段首部的接收窗口字段)来提供流量控制。通俗的讲,接收窗口用于给发送方一个指示--该接收方还有多少可用的缓存空间。因为TCP是全双工通信,在连接两端的发送方都各自维护了一个接收窗口。

考虑一种特殊的情况,就是接收方若没有缓存足够使用,就会发送零窗口大小的报文,此时发送放将发送窗口设置为0,停止发送数据。之后接收方有足够的缓存,发送了非零窗口大小的报文,但是这个报文在中途丢失的,那么发送方的发送窗口就一直为零导致死锁。 解决这个问题,TCP为每一个连接设置一个 持续计时器(persistencetimer) 。只要TCP的一方收到对方的零窗口通知,就启动该计时器,周期性的发送一个零窗口探测报文段。对方就在确认这个报文的时候给出现在的窗口大小。

1.3.3.4 可靠传输之拥塞控制

为什么要进行拥塞控制?

要回答这个问题,首先必须知道什么时候TCP会出现拥塞。TCP作为一个端到端的传输层协议,它并不关心连接双方在物理链路上会经过多少路由器交换机以及报文传输的路径和下一条,这是IP层该考虑的事。然而,在现实网络应用中,TCP连接的两端可能相隔千山万水,报文也需要由多个路由器交换机进行转发。交换设备的性能不是无限的!, 当多个入接口的报文都要从相同的出接口转发时, 如果出接口转发速率达到极限,报文就会开始在交换设备的入接口缓存队列堆积。但这个队列长度也是有限的,当队列塞满后,后续输入的报文就只能被丢弃掉了 。对于TCP的发送端来说,看到的就是发送超时丢包了。

网络资源是各个连接共享的,为了大家都能完成数据传输。所以,TCP需要当它感知到传输发生拥塞时,需要 降低自己的发送速率,等待拥塞解除

如何进行拥塞控制?

拥塞窗口 cwnd:首先需要明确的是,TCP是在 发送端 进行拥塞控制的。TCP为每条连接准备了一个记录拥塞窗口大小的变量 cwnd ,它限制了本端TCP可以发送到网络中的最大报文数量。显然,这个值越大,连接的吞吐量越高,但也更容易导致网络拥塞。所以,TCP的拥塞控制本质上就是根据丢包情况调整 cwnd ,使得传输的吞吐率尽可能地大!而不同的拥塞控制算法就是调整 cwnd 的方式不同!

rwnd与cwnd区别

rwnd(Receiver Window,接收者窗口)与cwnd(Congestion Window,拥塞窗口)的概念: rwnd:是用于流量控制的窗口大小,即上述流量控制中的AdvertisedWindow,主要取决于接收方的处理速度,由接收方通知发送方被动调整(详细逻辑见上)。

cwnd:是用于拥塞处理的窗口大小,取决于网络状况,由发送方探查网络主动调整。

介绍流量控制时,我们没有考虑cwnd,认为发送方的滑动窗口最大即为rwnd。实际上,需要同时考虑流量控制与拥塞处理,则发送方窗口的大小不超过min{rwnd, cwnd}。下述4种拥塞控制算法只涉及对cwnd的调整,同介绍流量控制时一样,暂且不考虑rwnd,假定滑动窗口最大为cwnd;但读者应明确rwnd、cwnd与发送方窗口大小的关系。

四种拥塞控制算法

TCP从诞生至今,已经有了多种的拥塞控制算法,直到现在还有新的在被提出!其中TCP Tahoe(1988) 和TCP Reno(1990) 是最初的两个算法。虽然看上去年代很就远了,但 Reno 算法直到现在还在广泛地使用。

Tahoe 提出了  1)慢启动,2)拥塞避免,3)快速重传
Reno 在Tahoe的基础上增加了 4)快速恢复
复制代码

Tahoe算法的基本思想是:

首选设置一个符合情理的初始窗口值。

当没有出现丢包时,慢慢地增加窗口大小,逐渐逼近吞吐量的上界。

当出现丢包时,快速地减小窗口大小,等待阻塞消除。

拥塞控制算法之慢启动算法

慢启动算法(Slow Start)作用在拥塞产生之前:对于刚刚加入网络的连接,要一点一点的提速,不要妄图一步到位。如下:

连接刚建好,初始化cwnd = 1(当然,通常不会初始化为1,太小),表明可以传一个MSS大小的数据。
每收到一个ACK,cwnd++,线性增长。
每经过一个RTT,cwnd = cwnd * 2,指数增长(主要增长来源)。
还有一个ssthresh(slow start threshold),当cwnd >= ssthresh时,就会进入拥塞避免算法(见后)。
复制代码

因此,如果网速很快的话,Ack返回快,RTT短,那么,这个慢启动就一点也不慢。下图说明了这个过程:

java面试系列(一)--- Tcp协议精准剖析
java面试系列(一)--- Tcp协议精准剖析

注4RFC 2581 已经允许 cwnd 的初始值最大为2, RFC 3390 已经允许 cwnd 的初始值最大为4, RFC 6928已经允许 cwnd 的初始值最大为10

拥塞控制算法之拥塞避免算法

前面说过,当cwnd >= ssthresh(通常ssthresh = 65535)时,就会进入拥塞避免算法(Congestion Avoidance):缓慢增长,小心翼翼的找到最优值。如下:

每收到一个Ack,cwnd = cwnd + 1/cwnd,显然,cwnd > 1时无增长。
每经过一个RTT,cwnd++,线性增长(主要增长来源)。
复制代码

慢启动算法主要呈指数增长,粗犷型,速度快(“慢”是相对于一步到位而言的);而拥塞避免算法主要呈线性增长,精细型,速度慢,但更容易在不导致拥塞的情况下,找到网络环境的cwnd最优值。

拥塞控制算法之快速重传算法

由于TCP采用的是累计确认机制,即当接收端收到比期望序号大的报文段时,便会重复发送最近一次确认的报文段的确认信号,我们称之为冗余ACK(duplicate ACK)。 如图所示,报文段1成功接收并被确认ACK 2,接收端的期待序号为2,当报文段2丢失,报文段3失序到来,与接收端的期望不匹配,接收端重复发送冗余ACK 2。

java面试系列(一)--- Tcp协议精准剖析

这样,如果在超时重传定时器溢出之前,接收到连续的三个重复冗余ACK(其实是收到4个同样的ACK,第一个是正常的,后三个才是冗余的),发送端便知晓哪个报文段在传输过程中丢失了,于是重发该报文段,不需要等待超时重传定时器溢出,大大提高了效率。这便是快速重传机制。

拥塞控制算法之快速恢复算法

如果触发了快速重传,即发送方收到至少3次相同的Ack,那么TCP认为网络情况不那么糟,也就没必要提心吊胆的,可以适当大胆的恢复。为此设计快速恢复算法(Fast Recovery),下面介绍TCP Reno中的实现。

回顾一下,进入快速恢复之前,cwnd和sshthresh已被更新:

ssthresh = cwnd /2
cwnd = cwnd /2
复制代码

然后,进入快速恢复算法:

cwnd = ssthresh + 3 * MSS (尝试一步到位)
重传重复Ack对应的Seq
如果再收到该重复Ack,则cwnd++,线性增长(缓慢调整)
如果收到了新Ack,则cwnd = ssthresh ,然后就进入了拥塞避免的算法了
复制代码

1.3.4 断开

连接终止协议(四次握手)

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

java面试系列(一)--- Tcp协议精准剖析
  • A的应用进程先向其TCP发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN-WAIT-1(终止等待1)状态,等待B的确认。
  • B收到连接释放报文段后即发出确认报文段,(ACK=1,确认号ack=u+1,序号seq=v),B进入CLOSE-WAIT(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。
  • A收到B的确认后,进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段。
  • B没有要向A发出的数据,B发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),B进入LAST-ACK(最后确认)状态,等待A的确认。
  • A收到B的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A进入TIME-WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,A才进入CLOSED状态。

为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

1.3.4.1 FIN_WAIT_2 状态

在FIN_ WAIT_2状态我们已经发出了FIN,并且另一端也已对它进行确认。除非我们在实 行半关闭,否则将等待另一端的应用层意识到它已收到一个文件结束符说明,并向我们发一 个FIN来关闭另一方向的连接。只有当另一端的进程完成这个关闭,我们这端才会从 FIN_WAIT_2状态进入TIME_WAIT状态。 这意味着我们这端可能永远保持这个状态。另一端也将处于 CLOSE_WAIT状态,并一直 保持这个状态直到应用层决定进行关闭。 许多伯克利实现采用如下方式来防止这种在FIN_WAIT_2状态的无限等待。如果执 行主动关闭的应用层将进行全关闭,而不是半关闭来说明它还想接收数据,就设置一 个定时器。如果这个连接空闲10分钟75秒,TCP将进入CLOSED状态。在实现代码的注释中确认这个实现代码违背协议的规范。

1.3.4.2 2MSL

什么是2MSL?MSL即Maximum Segment Lifetime,也就是 报文最大生存时间 ,引用《TCP/IP详解》中的话:“它 (MSL)是任何报文段被丢弃前在网络内的最长时间 ,那么,2MSL也就是这个时间的2倍,当TCP连接完成四个报文段的交换时,主动关闭的一方将继续等待一定时间(2-4分钟),即使两端的应用程序结束。

为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

第一:保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。

第二:防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中,即在关闭一个TCP连接后,马上又重新建立起一个相同的IP地址和端口之间的TCP连接,后一个连接被称为前一个连接的化身 (incarnation),那么有可能出现这种情况。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

对服务器的影响?

当某个连接的一端处于TIME_WAIT状态时,该连接将不能再被使用。事实上,对于我们比较有现实意义的是, 这个端口将不能再被使用 。某个端口处于TIME_WAIT状态(其实应该是这个连接)时,这意味着这个TCP连接并没有断开(完全断开),那么,如果你bind这个端口,就会失败。对于服务器而言,如果服务器突然crash掉了,那么它将无法在2MSL内重新启动,因为bind会失败。解决这个问题的一个方法就是设置socket的 SO_REUSEADDR 选项。这个选项意味着你可以重用一个地址。

1.3.4.3 案例与解决方案

1.掘金文章 juejin.im/post/5b59e6…

2.个人博客: lanjingling.github.io/2016/02/27/…

1.3.5 tcp状态装换图

java面试系列(一)--- Tcp协议精准剖析

2 总结

上述文章,讲述了tcp协议从建立到传输数据到断开连接的过程。文章是从网上搜罗多篇文章总结出来的结果,本人的理解也有限,如果大家发现有什么问题请及时指出。

如果有关于tcp的好的面试题请在下方留言给我!!!多谢!!!

原文  https://juejin.im/post/5d103a76f265da1ba431f8a2
正文到此结束
Loading...