TCP 协议的特点

TCP 是在不可靠的 IP 层之上实现的可靠的数据传输协议,它主要解决传输的可靠、有序、无丢失和不重复问题。TCP 的主要特点如下:

  1. TCP 是面向连接的传输层协议。
  2. 每条 TCP 连接只能有两个端点,每条 TCP 连接只能是点对点的(一对一)。
  3. TCP 提供可靠的交付服务,保证传送的数据无差错、不丢失、不重复且有序。
  4. TCP 提供全双工通信,TCP 连接的两端都设有发送缓存接收缓存,用来临时存放双向通信的数据。发送缓存用来暂时存放以下数据:
    1. 发送应用程序传送给发送方 TCP 准备发送的数据;
    2. TCP 已发送但尚未收到确认的数据。

接收缓存用来暂时存放以下数据:

  1. 按序到达但尚未被接收应用程序读取的数据;
  2. 不按序到达的数据。
  1. TCP 是面向字节流的,虽然应用程序和 TCP 的交互是一次一个数据块(大小不等),但 TCP 把应用程 序交下来的数据仅视为一连串的无结构的字节流。

TCP 报文段

TCP 传送的数据单元称为报文段。TCP 报文段的首部最短为 20B。

gaitubao_TCP_jpg.jpg
TCP 报文段既可以用来运载数据,又可以用来建立连接、释放连接和应答。
各字段意义如下:

  • 源端口和目的端口字段。各占 2B。端口是运输层与应用层的服务接口,运输层的复用和分用功能都要通过端口实现。
  • 序号字段。占 4B。TCP 是面向字节流的(即 TCP 传送时是逐个字节传送的),所以 TCP 连接传送的数据流中的每个字节都编上一个序号。序号字段的值指的是本报文段所发送的数据的第一个字节的序号。
  • 确认号字段。占 4B,是期望收到对方的下一个报文段的数据的第一个字节的序号。若确认号为 N,则表明到序号 N - 1 为止的所有数据都已正确收到。
  • 数据偏移(即首部长度)。占 4 位,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。
  • 保留字段。占 6 位,保留为今后使用。
  • 紧急位 URG。URG = 1 时,表明紧急指针字段有效。它告诉系统报文段中有紧急数据,应尽快传送(相当于高优先级的数据)。但 URG 需要和紧急指针配套使用,即数据从第一个字节到紧急指针所指字节就是紧急数据。
  • 确认位 ACK。只有当 ACK = 1 时确认号字段才有效。当 ACK = 0 时,确认号无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置1。
  • 推送位 PSH(Push)。接收 TCP 收到 PSH = 1 的报文段,就尽快地交付给接收应用程序,而不再等到整个缓存都填满后再向上交付。
  • 复位位 RST(Reset)。RST = 1 时,表明 TCP 连接中出现严重差错(如主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。
  • 同步位 SYN。同步 SYN = 1 表示这是一个连接请求或连接接收报文。当 SYN = 1, ACK = 0 时,表明这是一个连接请求报文,对方若同意建立连接,则在响应报文中使用 SYN = 1, ACK = 1。
  • 终止位 FIN(Finish)。用来释放一个连接。FIN = 1 表明此报文段的发送方的数据已发送完毕,并要求释放传输连接。
  • 窗口字段。占 2B。用窗口值作为接收方让发送方设置其发送窗口的依据,单位为字节。例如,设确认号是 701,窗口字段是 1000。这表明,从 701 号算起,发送此报文段的一方还有接收 1000B 数据(字节序号为 701~1700)的接收缓存空间。
  • 校验和。占 2B。校验和字段检验的范围包括首部和数据两部分。
  • 紧急指针字段。占 16 位,指出在本报文段中紧急数据共有多少字节(紧急数据放在本报文段数据的最前面)。
  • 选项字段。如最大报文段长度MSS。MSS 是 TCP 报文段中的数据字段的最大长度。
  • 填充字段。这是为了使整个首部长度是 4B 的整数倍。

TCP 连接管理

TCP 连接的建立(三次握手)

gaitubao_三次握手_jpg.jpg
第一步:客户机的 TCP 首先向服务器的 TCP 发送一个连接请求报文段。这个特殊的报文段中不含应用层数据,其首部中的 SYN 标志位被置为 1。另外,客户机会随机选择一个起始序号 seq = x (连接请求报文不携带数据,但要消耗一个序号)。
第二步:服务器的 TCP 收到连接请求报文后,如同意建立连接,就向客户机发回确认,并为该 TCP 连接分配 TCP 缓存和变量。在确认报文中,SYN 和 ACK 位都被置为 1,确认号字段的值为 x+1,并且服务器随机产生起始序号 seq = y(确认报文不携带数据,但也要消耗一个序号)。确认报文段同样不包含应用层数据。
第三步:当客户机收到确认报文段后,还要向服务器给出确认,并且也要给该连接分配缓存和变量。这个报文段的 ACK 标志位被置 1,序号字段为 x+1,确认号字段 ack = y+1。该报文段可以携带数据,若不携带数据则不消耗序号。

注:服务器端的资源是在完成第二次握手时分配的,而客户端的资源是在完成第三次握手时分配的,这就使得服务器易于受到 SYN洪泛攻击。

TCP 连接的释放(四次挥手)

gaitubao_四次挥手_jpg.jpg
第一步:客户机打算关闭连接时,向其 TCP 发送一个连接释放报文段,并停止发送数据,主动关闭 TCP 连接,该报文段的 FIN 标志位被置 1,seq = u,它等于前面已传送过的数据的最后一个字节的序号加 1(FIN 报文段即使不携带数据,也要消耗一个序号)。
第二步:服务器收到连接释放报文段后即发出确认,确认号是 ack = u + 1,而这个报文段自己的序号是 v,等于它前面已传送过的数据的最后一个字节的序号加 1。此时,从客户机到服务器这个方向的连接就释放了,TCP 连接处于半关闭状态。但服务器若发送数据,客户机仍要接收,即从服务器到客户机这个方向的连接并未关闭。
第三步:若服务器已经没有要向客户机发送的数据,就通知 TCP 释放连接,此时其发出的 FIN = 1 的连接释放报文段。
第四步:客户机收到连接释放报文段后,必须发出确认。在确认报文段中,ACK 字段被置为 1,确认号 ack = w + 1,序号 seq = u + 1。此时 TCP 连接还未释放,必须经过时间等待计时器设置的时间 2MSL 后,A 才进入连接关闭状态。

对上述 TCP 连接建立和释放的总结如下:

  1. 连接建立。分为 3 步:
    1. SYN = 1,seq = x。
    2. SYN = 1,ACK = 1,seq = y,ack = x + 1。
    3. ACK = 1,seq = x + 1,ack = y + 1。
  2. 释放连接。分为 4 步:
    1. FIN = 1,seq = u。
    2. ACK = 1,seq = v,ack = u + 1。
    3. FIN = 1,ACK = 1,seq = w,ack = u + 1。
    4. ACK = 1,seq = u + 1,ack = w + 1。

TCP 可靠传输

TCP 提供的可靠数据传输服务保证接收方进程从缓存区读出的字节流与发送方发出的字节流完全一样。TCP 使用了校验、序号、确认和重传等机制来达到这一目的。

序号

TCP 首部的序号字段用来保证数据能有序提交给应用层。
TCP 连接传送的数据流中的每个字节都编上一个序号。序号字段的值是指本报文段所发送的数据的第一个字节的序号。

确认

TCP 首部的确认号是期望收到对方的下一个报文段的数据的第一个字节的序号。

重传

有两种事件会导致 TCP 对报文段进行重传:超时和冗余ACK。

  1. 超时

TCP 每发送一个报文段,就对这个报文段设置一次计时器。计时器设置的重传时间到期但还未收到确认时,就要重传这一报文段。
TCP 采用一种自适应算法计算超时计时器的重传时间。主要是根据报文段的往返时间(RTT)。

  1. 冗余ACK(冗余确认)

TCP 规定每当比期望序号大的失序报文段到达时,就发送一个冗余 ACK,指明下一个期待字节的序号。当发送方收到对同一个报文段的 3 个冗余 ACK 时,就可以立即执行重传,这种技术通常称为快速重传。

TCP 流量控制

TCP 提供流量控制服务来消除发送方使接收方缓存区溢出的可能性,因此可以说流量控制是一个速度匹配服务(匹配发送方的发送速率与接收方的读取速率)。

TCP 提供一种基于滑动窗口协议的流量控制机制。接收方根据自己接收缓存的大小,动态地调整发送方的发送窗口大小,这称为接收窗口 rwnd,同时发送方根据当前网络拥塞程序的估计而确定的窗口值,这称为拥塞窗口 cwnd,其大小与网络的带宽和时延密切相关。

发送方的发送窗口的实际大小取 rwnd 和 cwnd 中的最小值。

传输层和数据链路层的流量控制的区别是:传输层定义端到端用户之间的流量控制,数据链路层定义两个中间的相邻结点的流量控制。另外,数据链路层的滑动窗口协议的窗口大小不能动态变化,传输层的则可以动态变化。

TCP 拥塞控制

拥塞控制,是指防止过多的数据注入网络,以使网络中的路由器或链路不致过载。

拥塞控制与流量控制都是通过控制发送方发送数据的速率来达到控制效果。

拥塞控制与流量控制的区别:

  • 拥塞控制是让网络能够承受现有的网络负荷,是一个全局性的过程,涉及所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是指点对点的通信量的控制,即接收端控制发送端,它所要做的是抑制发送端发送数据的速率,以便使接收端来得及接收。
  • 在流量控制中,发送方发送数据的量由接收方决定,而在拥塞控制中,则由发送方自己通过检测网络状况来决定。

为了更好地对传输层进行拥塞控制,因特网建议标准定义了以下 4 种算法:慢开始、拥塞避免、快重传、快恢复。

慢开始和拥塞避免

(1)慢开始算法
在 TCP 刚刚连接好并开始发送 TCP 报文段时,先令拥塞窗口 cwnd = 1,即一个最大报文段长度 MSS。每收到一个对新报文段的确认后,将 cwnd 加 1,即增大一个 MSS。
使用慢开始算法后,每经过一个传输轮次(即往返时延 RTT),拥塞窗口 cwnd 就会加倍,即 cwnd 的大小指数式增长。这样,慢开始一直把拥塞窗口 cwnd 增大到一个规定的慢开始门限 ssthresh(阈值),然后改用拥塞避免算法。

(2)拥塞避免算法
拥塞避免算法的做法如下:发送端的拥塞窗口 cwnd 每经过一个往返时延 RTT 就增加一个 MSS 的大小,而不是加倍,使 cwnd 按线性规律缓慢增长(即 加法增大),而当出现一次超时(网络拥塞)时,令慢开始门限 ssthresh 等于当前 cwnd 的一半(即 乘法减小)。
根据 cwnd 的大小执行不同的算法,可归纳如下:

  • 当 cwnd < ssthresh 时,使用慢开始算法。
  • 当 cwnd >= ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。

(3)网络拥塞的处理
网络出现拥塞时,无论是在慢开始阶段还是在拥塞避免阶段,只要发送方检测到超时事件的发生(未按时收到确认,重传计时器超时),就要把慢开始门限 ssthresh 设置为出现拥塞时的发送方的 cwnd 值的一半(但不能小于 2)。然后把拥塞窗口 cwnd 重新设置为 1,执行慢开始算法。
gaitubao_慢开始和拥塞避免_jpg.jpg
注:在慢开始(指数级增长)阶段,若 2cwnd > ssthresh,则下一个 RTT 的 cwnd 等于 ssthresh,而不等于 2cwnd,即 cwnd 不能跃过 ssthresh 值。

快重传和快恢复

快重传和快恢复算法是对慢开始和拥塞避免算法的改进。

(1)快重传
当发送方连续收到三个重复的 ACK 报文时,直接重传对方尚未收到的报文段,而不必等待那个报文段设置的重传计时器超时。

(2)快恢复
快恢复算法的原理如下:发送端收到三个冗余ACK(即重复确认)时,执行“乘法减小”算法,把慢开始门限 ssthresh 设置为出现拥塞时发送方 cwnd 的一半。与慢开始(慢开始算法将拥塞窗口设置为 1)的不同之处是,它把 cwnd 的值设置为慢开始门限 ssthresh 改变后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

快恢复的实现过程:
gaitubao_快恢复_jpg.jpg

当发送方检测到超时的时候,就采用慢开始和拥塞避免,当发送方接收到冗余ACK 时,就采用快重传和快恢复。

常见面试题

为什么要三次握手?

三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。

第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常
第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常
第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常
所以三次握手就能确认双发收发功能都正常,缺一不可。

为什么不采用“两次握手”建立连接呢?

这主要是为了防止两次握手情况下已失效的连接请求报文段突然又传送到服务端而产生错误。考虑下面这种情况。客户 A 向服务器 B 发出 TCP 连接请求,第一个连接请求报文在网络的某个结点长时间滞留,A 超时后认为报文丢失,于是再重传一次连接请求,B 收到后建立连接。数据传输完毕后双方断开连接。而此时,前一个滞留在网络中的连接请求到达服务端 B,而 B 认为 A 又发来连接请求,此时若使用“三次握手”,则 B 向 A 返回确认报文段,由于是一个失效的请求,因此 A 不予理睬,建立连接失败。若采用的是“两次握手”,则这种情况下 B 认为传输连接已经建立,并一直等待 A 传输数据,而 A 此时并无连接请求,因此不予理睬,这样造成了 B 的资源白白浪费。


四次挥手为什么客户端要等待 2MSL 后再释放连接?

第一个角度:确保最后一个确认报文能到达,如果服务器没收到来自客户端的 ACK 报文,就会重新发送 FIN 报文到客户端,客户端等待一段时间就是为了处理这种延迟的情况。
第二个角度:等待一段时间是为了让本连接持续的时间内所有报文从网络中消失,使得下一个新连接里不会出现旧的报文。

tcp三次握手的过程,为什么要用随机初始化的序号

可以从两个方面考虑,一是安全性:如果不是随机产生初始序列号,黑客将会以很容易的方式获取到你与其他主机之间通信的初始化序列号,并且伪造序列号进行攻击,这已经成为一种很常见的网络攻击手段。二是历史报文:每次初始化序列号不一样能够很大程度上避免历史报文被下一个连接接收,注意是很大程度上,并不是完全避免了。

什么是TCP粘包拆包?为什么会出现粘包拆包?如何在应用层面解决此问题?

如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题。
1、TCP是基于字节流的,虽然应用层和传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;
2、在TCP的首部没有表示数据长度的字段。
基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。

可能产生 TCP粘包拆包的场景
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

解决
解决问题的关键在于如何给每个数据包添加边界信息
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

推荐阅读:
TCP粘包/拆包的产生原因和解决办法

SYN 泛洪攻击

SYN Flood是当前最流行的 DoS(拒绝服务攻击)与 DDoS(分布式拒绝服务攻击)的方式之一,它是利用 TCP协议缺陷,发送大量伪造的TCP连接请求,从而使得被攻击方资源耗尽(CPU满负荷或内存不足)的攻击方式,最终导致系统或服务器宕机。
SYN Flood攻击利用了 TCP连接的三次握手,假设一个用户向服务器发送了 SYN报文后突然死机或掉线,那么服务器在发出 SYN+ACK应答报文后是无法收到客户端的 ACK报文的(第三次握手无法完成),这种情况下服务器端一般会重试(再次发送 SYN+ACK给客户端)并等待一段时间后丢弃这个未完成的连接,这段时间的长度我们称为 SYN Timeout,一般来说这个时间是分钟的数量级(大约为30秒-2分钟);一个用户出现异常导致服务器的一个线程等待 1 分钟并不会对服务器端造成什么大的影响,但如果有大量的等待丢失的情况发生,服务器端将为了维护一个非常大的半连接请求而消耗非常多的资源。我们可以想象大量的保存并遍历也会消耗非常多的CPU时间和内存,再加上服务器端不断对列表中的 IP进行 SYN+ACK的重试,服务器的负载将会变得非常巨大。如果服务器的TCP/IP栈不够强大,最后的结果往往是堆栈溢出崩溃。相对于攻击数据流,正常的用户请求就显得十分渺小,服务器疲于处理攻击者伪造的TCP连接请求而无暇理睬客户的正常请求,此时从正常客户会表现为打开页面缓慢或服务器无响应,这种情况就是我们常说的服务器端SYN Flood攻击(SYN洪水攻击)。

解决方法

1.缩短SYN Timeout时间
由于SYN Flood攻击的效果取决于服务器上保持的SYN半连接数,这个值 = SYN攻击的频度 x SYN Timeout,所以通过缩短从接收到 SYN报文到确定这个报文无效并丢弃改连接的时间,例如设置为 20秒以下,可以成倍的降低服务器的负荷。但过低的SYN Timeout设置可能会影响客户的正常访问。

2.设置SYN Cookie
给每一个请求连接的 IP地址分配一个 Cookie,如果短时间内连续受到某个 IP的重复 SYN报文,就认定是受到了攻击,并记录地址信息,以后从这个IP地址来的包会被一概丢弃。这样做的结果也可能会影响到正常用户的访问。

上述的两种方法只能对付比较原始的SYN Flood攻击,缩短SYN Timeout时间仅在对方攻击频度不高的情况下生效,SYN Cookie更依赖于对方使用真实的IP地址,如果攻击者以数万/秒的速度发送SYN报文,同时利用SOCK_RAW随机改写 IP报文中的源地址,以上的方法将毫无用武之地。

其他解决方法

1.过滤 TTL
遭到 SYN Flood攻击后,首先要做的是取证,通过在命令行下使用 Netstat –n –p tcp >resault.txt记录目前所有TCP 连接状态是必要的,如果有嗅探器或 TcpDump之类的工具,详细记录 TCP SYN报文会更有助于追查和防御,需要记录的字段有:源地址、IP首部中的标识、TCP首部中的序列号、TTL值(Time to Life,生存周期)等,这些信息虽然很可能是攻击者伪造的,但是用来分析攻击攻击程序不无帮助。特别是TTL值,如果大量的攻击包似乎来自不同的IP但是TTL值却相同,我们往往能推断出攻击者与我们之间的路由器距离,至少也可以通过过滤特定TTL值的报文降低被攻击系统的负荷,确保TTL值与攻击报文不同的用户就可以恢复正常访问。

2.更换 IP地址
基于 SYN Flood攻击代码的一个缺陷,一旦攻击开始,将不会再进行域名解析,我们就是要利用这一点,假设一台服务器在受到 SYN Flood攻击后迅速更换自己的 IP地址,那么攻击者仍在不断攻击的只是一个空的 IP地址,并没有任何主机,而管理员只需将 DNS解析更改到新的 IP地址就能在很短的时间内恢复用户通过域名进行的正常访问,这种做法取决于 DNS的刷新时间。为了迷惑攻击者,我们甚至可以放置一台“牺牲”服务器,对攻击数据流进行牵引。

3.负载均衡
原理也是引流,降低单个服务器的负载,使服务器不容易崩溃,同样可以考虑容灾技术。

TCP中的保活机制

TCP保活机制是一种在不影响数据流内容的情况下探测对方的方式。它由一个保活计时器实现,当计时器被激发,连接一端将发送一个保活探测(简称保活)报文,另一端接收报文的同时会发送一个 ACK作为响应。

保活机制引入的原因:

  • TCP对于非正常断开的连接系统并不能侦测到
  • 长时间没有任何数据发送,连接可能会被中断。这是因为,网络连接中间可能会经过路由器、防火墙等设备,而这些有可能会对长时间没有活动的连接断掉。

保活机制的弊端:

  • 在出现短暂的网络错误的时候,保活机制会使一个好的连接断开;
  • 保活机制会占用不必要的带宽;

延伸:当TCP三次握手以后,客户端突然断开,此时服务端知道对方断开了吗?如果客户端重新连 接又会进行三次握手吗?四次挥手?
如果没有开启保活机制,客户端断开后,服务器端是不知道对方已经断开了。如果此时客户端重新连接,是不需要进行三次握手的 如果开启了报货机制,客户端断开后,服务器端也是不会立刻知道对方已经断开,而是直到保活报文发出去后,没有收到确认,而且是没有收到确认的次数满足保活次数,则服务器端会判定客户端不可达,发送一个reset报文终止这个连接。这个连接是异常终止,不需要进行四次挥手。


大量CLOSE_WAIT

通常,CLOSE_WAIT状态在服务端停留的时间很短,如果发现大量的 CLOSE_WAIT状态,那么意味着被动关闭的一方没有及时发出 FIN包,一般有以下可能:
代码层面忘记 close相应的 socket连接,那么就不会发出FIN报文,从而导致累积 CLOSE_WAIT。或者代码不严谨,出现死循环之类的问题,导致后面写了 close也执行不到。

大量TIME_WAIT

出现场景
在高并发短连接的 TCP服务器上,当服务器处理完请求后立刻按照主动正常关闭连接的场景下,会
出现大量 socket处于 TIME_WAIT状态。如果客户端的并发量持续很高,此时部分客户端就会连接
不上。

解决方法
应用层面:尽量避免频繁的关闭连接,如业务优化、使用长连接
系统层面:缩短MSL时间,增加可用端口数量。