Java面试系列(一)--- TCP协议精准剖析

2,486 阅读23分钟

0.面试高频题目

  • 顺丰面试题:解释下Tcp的三次握手与四次挥手的过程?
  • 顺丰面试题:osi分层,tcp/ip分层,以及各层的作用?
  • 阿里面试题:TCP的连接建立和断开的过程,如何保证TCP发送的信息是正确的,且保证其先后顺序不被篡改?
  • 阿里面试题:TCP连接中的三次握手和四次挥手,四次挥手的最后一个ack的作用是什么,为什么要time wait,为什么是2msl?
  • 今日头条面试题:解释下TCP拥塞控制和流量控制?

本篇,将彻底剖析上述问题,让面试官对你欲罢不能。

1.TCP/IP

1.1简介

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

1.2分层

osi 七层模型

此图来自: @知乎 仇诺伊

tcp/ip 四层模型

网络接口层

负责监视数据在主机和网络之间的交换;与OSI参考模型中的物理层和数据链路层相对应。事实上,TCP/IP本身并未定义该层的协议,而由参与互连的各网络使用自己的物理层和数据链路层协议,然后与TCP/IP的网络接入层进行连接。

网际互连层

是整个体系结构的关键部分,其功能是使主机可以把分组发往任何网络,并使分组独立地传向目标。这些分组可能经由不同的网络,到达的顺序和发送的顺序也可能不同。高层如果需要顺序收发,那么就必须自行处理对分组的排序。互联网层使用因特网协议(IP,Internet Protocol)。TCP/IP参考模型的互联网层和OSI参考模型的网络层在功能上非常相似。

传输层

使源端和目的端机器上的对等实体可以进行会话。在这一层定义了两个端到端的协议:传输控制协议(TCP,Transmission Control Protocol)和用户数据报协议(UDP,User Datagram Protocol)。TCP 是面向连接的协议,它提供可靠的报文传输和对上层应用的连接服务。为此,除了基本的数据传输外,它还有可靠性保证、流量控制、多路复用、优先权和安全性控制等功能。UDP 是面向无连接的不可靠传输的协议,主要用于不需要TCP的排序和流量控制等功能的应用程序。

应用层

包含所有的高层协议,包括:虚拟终端协议(TELNET,TELecommunications NETwork)、文件传输协议(FTP,File Transfer Protocol)、电子邮件传输协议(SMTP,Simple Mail Transfer Protocol)、域名服务(DNS,Domain Name Service)、网上新闻传输协议(NNTP,Net News Transfer Protocol)和超文本传送协议(HTTP,HyperText Transfer Protocol)等。TELNET 允许一台机器上的用户登录到远程机器上,并进行工作;FTP 提供有效地将文件从一台机器上移到另一台机器上的方法;SMTP 用于电子邮件的收发;DNS 用于把主机名映射到网络地址;NNTP 用于新闻的发布、检索和获取;HTTP 用于在 WWW 上获取主页。

1.3 TCP

1.3.1简介

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

1.3.2 连接

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

1.3.2.1 三次握手

我们先看一下tcp的数据报文的结构:

带阴影的几个字段需要重点说明一下:

  1. 序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。

  2. 确认号:Ack(Acknowledge Number)确认号占32位,客户端和服务器端都可以发送,Ack = Seq + 1。

  3. 标志位:每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:

  • SYN(synchronous建立联机)
  • ACK(acknowledgement 确认)
  • PSH(push传送)
  • FIN(finish结束)
  • RST(reset重置)
  • URG(urgent紧急)

如何建立一个连接呢?

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

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

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 数据传输

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

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

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

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

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

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

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

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

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

上图表示通过 Seq 1300 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1300 的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短,那么,这个慢启动就一点也不慢。下图说明了这个过程:

注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。

这样,如果在超时重传定时器溢出之前,接收到连续的三个重复冗余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后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

  • 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.cn/post/684490…

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

1.3.5 tcp状态装换图

2 总结

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

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