安琪拉从Broken Pipe网络问题聊到TCP协议

2,624 阅读26分钟

本文是以安琪拉在项目中解决的一个网络异常(Broken Pipe)为出发点写的,我想玩家们应该多少遇到过网络问题,这篇文章也提供了一种解决网络问题的思路,遇到类似网络问题时可以借鉴这个思路。

为了尽量讲的清晰明了,以下面这个流程进行:

  1. 背景:安琪拉遇到网络异常
  2. 小试牛刀:在Wireshark数据包中看三次握手
  3. 预备知识:回顾网络分层、三次握手、四次挥手等网络基础知识
  4. 异常定位:Wireshark 分析定位Broken Pipe 异常原因
  5. 代码修复:调整客户端代码
  6. 扩展阅读:其他网络知识及面试题

1. 背景:安琪拉遇到网络异常

1. 异常信息

安琪拉做的一部分工作是和公司外部机构对接系统,对接方式很多种,有的机构提供封装好的SDK,有的提供统一网关平台、还有一个接口一个HTTP URL等。涉及到的应用层协议主要有https、http、sftp、ftp等。最近对接的一家机构生产环境联调日志出现了网络异常,异常栈如下:

网络异常栈
网络异常栈

第一眼看异常栈直译过来就是管道破裂,为什么会出现管道破裂呢? 这篇文章就由此而来,背后牵涉的是我们常常挂在嘴边,面试也经常被问的httptcp 协议的知识,这里埋个伏笔,后面我们由浅入深,慢慢把这个问题解决,同时了解如何运用网络知识解决实际生产问题。

安琪拉遇到异常是很兴奋的,又可以学知识了,咳咳哈!当然咯,前提是不影响到正常业务。这里现象是每个http 请求发起总是失败,请求重试大概率会成功。

闲扯一段:其实一般应用本地的异常还是比较好排查的,像空指针、内存溢出、数组越界等都比较好搞!网络问题,尤其是跟外部公网打交道的网络问题有时间还是会稍微棘手的,因为公网环境比较复杂,中间涉及的跳转节点比较多,很考验工程师的计网水平。

安琪拉知道运维中有个词叫根故障定位,实际生产中很多异常的根故障最后都会定位到网络这一层。

想说点题外话,安琪拉工作这几年的一点体会,工程师的能力分二种,有一种能力叫掌握知识的能力,也叫学习能力,另一种能力叫解决问题的能力,学生时代前一种能力很容易体现,成绩和分数,而工作以后,公司更关注或者说更看重的是解决问题的能力,因为企业招工程师最终还是为了创造业务价值,创造业务价值就需要不断解决实际业务的一个一个问题,不论是新业务需求还是系统bug都可以看做是面临的问题。当然这二种能力不是完全割裂的,往往相辅相成,解决问题能力强的人学习能力不会弱。只是看个人更偏重哪一方面? 学习能力强的人适合做学术型人才,解决问题能力强的适合做工程型人才。

闲扯扯完了,舒服! 发现写体会、感受比写技术文章轻松流畅多了!技术需要严谨细致,体会感受就很个人,很感性的东西,对错因人而异!

2. 网络抓包

遇到上面Broken Pipe这个问题,不急,第一步企微上撩一下运维大哥,让他帮我抓个包下来看下。

抓包聊天截图
抓包聊天截图

抓包命令如下:

tcpdump tcp port 20004 and host  **.**.com -w brokenpipe.cap        

tcpdump : dump the traffic on a network,根据使用者的定义对网络上的数据包进行截获的包分析工具。

大家也可以边看边自己抓包,常用的命令格式如下:

tcpdump tcp port 端口号 and host 域名 -w  保存文件

如果网络流量高,不要抓太久,不然包会很大,分析的时候加载慢!

tcpdump 完整命令可以用tcpdump --help 查看。

2. 小试牛刀:在Wireshark数据包中看三次握手

之前如果没有用过Wireshark 做过网络包分析的,安琪拉担心直接上来就看包分析有点费劲,下面安琪拉会对wireshark 先做个简单的介绍。

2.1 Wireshark 简介

首先,把运维给的包存在本地, 打开wireshark 导入网络包,开始有趣的网络漫游之旅。

wireshark 是一款非常流行的网络包分析工具,经常是网络工程师/后端工程师用来分析网络包,解决网络问题的利器。先放一张图出来闻闻味:

安琪拉截了一张自己本机装的 wireshark 软件,分为四个部分:

  1. 顶部是菜单栏
  2. 浅绿色背景是过滤器,这个对于分析包很重要,后面会讲
  3. 列表那一栏是包列表信息
  4. 最下面的这个是包详细信息,包含各层协议的内容(应用层、传输层、网络层、数据链路层、物理层)
Wireshark介绍
Wireshark介绍

吐槽一下微信的截图,文字的样式都不能改,太难看!

2.2 Wireshark 过滤器

过滤器单独拿出来说下,因为确实很有用,后面异常分析会用到。

你导入的包可能内容很多,需要使用过滤器筛选一下,过滤器很多种过滤的规则,我列举一下常用的几种:

  • 协议过滤

    如果抓的包有很多种协议类型,可以输入 tcp 回车只看tcp 协议的包

  • IP 过滤

    例如 ip.src == 192.168.1.23 (过滤发起地址ip)ip.dst == 12.8.0.1(过滤目标地址ip) ip == 12.0.0.1(过滤源或目的地址)

  • 端口过滤

    tcp.port == 4980 , 还可以 tcp.port == 4542 or tcp.port == 4528 加入表达式 and、or、in 等等

    例如:tcp.port in {80 443 8080}

  • HTTP 模式过滤

    http.request.method == "GET" 或者 http.request.method == "POST"

  • 报文内容过滤

    tcp.segment_data contains "202005190001" 过滤tcp 报文内容包括 202005190001 的报文

更多wireshark 过滤器可以参考:wireshark-filter

wireshark 的详细使用教程不是今天的重点,就介绍这二部分,后面分析数据包时会穿插着讲,安琪拉觉得大家有兴趣可以自己抽空玩一玩这个软件。

2.3 三次握手初探

安琪拉写的三次握手初探 这部分如果看不懂没关系,这里是为了介绍Wireshark写的三次握手,后面会详细解释,详细到直接从网络协议分层开始讲起,如果你这看不懂可以Diss 安琪拉。

三次握手如果第一次看Wireshark 网络包,会一脸懵逼,看多了就会越看越喜欢。重点看框出来的,前三行就是三次握手的过程:

  1. 上图第一行,客户端向服务端发送SYN 数据包,数据长度len 为0,Seq(随机生成包序列号)为2421858999;
  2. 上图第二行,服务端向客户端回应ACK 数据包,并且发送SYN 数据包,合并一起就是SYN + ACK 数据包,数据长度len 为0,Seq(随机生成包序列号)为1988635269,ack为2421859000 = 第一次握手Seq(2421858999)+1;
  3. 上图第三行,客户端回应客户端的SYN 数据包,发送ACK 确认数据包,Seq 为 二次握手的ack(2421859000),ack为 1988635270= 二次握手的seq(1988635269)+1;

公众号【安琪拉的博客】后面会更新一个网络系列:列举常见的网络问题,解决的思路,wireshark分析包方法等。

3. 回顾网络协议分层、三次握手、四次挥手等网络基础知识

3.1 网络协议分层

在解决文章开头的异常,分析数据包之前,我们需要一些预备知识,需要一丢丢基础的网络知识。

首先在直接看Wireshark 的包信息之前,需要来回顾一下计算机网络的知识,大家知道目前主流使用的TCP/IP 五层协议,而不是国际标准化组织(ISO)出的OSI(Open System Interconnection)七层协议。TCP/IP协议栈如下图所示:

TCP/IP五层协议
TCP/IP五层协议

我们可以看到Wireshark 包详情就是TCP/IP 五层的信息,对比上面的图从下往上看(取每个英文单词首字母就是协议简称,例如 HTTP:Hypertext Transfer Protocol ),如下:

image-20200515005055601
image-20200515005055601

后面我们看 Wireshark 数据报文时,主要看TCP 所在的传输层报文。

3.2 三次握手

首先我们先看下TCP 报文的报文格式:

TCP报文格式
TCP报文格式

下面把TCP 报文的各个部分做了详细说明,分析网络问题不用全看,把加重的部分关注一下就可以了。好学的玩家可以把所有的都看了,不用记,有个概念就可以了。

  1. 源端口号和目的端口号:各占2个字节(16位),分别写入源端口和目的端口;

  2. 序号:4字节(32位),TCP连接中字节流每个字节都按顺序编号,这个序号用于标识这个报文段。

例如:一段报文序号seq 是201,而报文数据长度为100,下一个报文段的数据序号应该为301(201+100)。

  1. 确认号 :4字节(32位),期望收到对方下一个报文的序号。这个确认号是和序号seq 有点关系的,不要和ACK(状态标志位)混淆了。

  2. 首部长度:4位,表示报文数据距离报文起始位置的长度。 保留:保留今后可以会用到。

  3. 数据报状态标志位(非常重要),分为以下6种,二进制1 位表示一种(1代表开启 0 关闭)

URG:URG=1 代表报文有紧急数据

ACK:ACK = 1,确认位,TCP中连接建立后,所有报文的ACK 位置都为1;

PSH: 发送端和接收端都有缓冲区(发送端:写缓冲区 接收端:读缓冲区) 对于发送端:带PSH=1,报文会立即从缓冲区报文推送给服务端 对于服务端:服务端立即将读缓冲区内容推给进程。

RST:RST=1,代表连接出现严重错误,TCP连接的一方将连接重置了,必须释放连接,重新建立连接;

SYN:同步SYN,在连接建立时用来同步序号。三次握手时会用到,当SYN=1,ACK =0,表明是发起方请求建立连接,服务方同意建立连接,响应报文SYN=1,ACK =1,前者表明同步连接,后者是确认报文。

FIN:用来释放连接。当FIN =1,表明此报文的发送方的数据已经发送完毕,并且要求释放。

  1. 窗口:占2字节,通常用于告知对方自己的能够接受的数据量大小。窗口本质就是一个缓冲区buffer,该字段的值用于告知对方自己剩余的可用缓冲区大小。

  2. 校验和:奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。由发送端计算和存储,并由接收端进行验证。

  3. 紧急指针:只有当 URG 标志置 1 时紧急指针才有效。紧急指针是一个正的偏移量,和顺序号字段中的值相加表示紧急数据最后一个字节的序号。

  4. 选项:可选的。最常见的可选字段是最长报文大小,又称MSS(Maxinum Segment Size), 每个连接方通常在通信的第一个报文段(连接建立的SYN标志位为1的数据报文)设置这个选项,表示本端能接受的最大报文段的长度。因为长度不一定是32的整数倍,因此要加额外的0作为填充。

  5. 数据部分:可选的。连接建立和终止时,报文段只有TCP首部。

我们先回顾一下以前计算机网络课堂上学过的TCP传输的三次握手流程:

TCP连接三次握手
TCP连接三次握手
三次握手
三次握手

三次握手的具体过程如下:

  1. 服务端进程启动,准备接收客户端进程的连接请求,此时接收方进入LISTEN(监听)模式;

  2. 三次握手第一步:客户端向服务端发出连接请求报文,这时报文首部SYN 标志位为1,同时设置一个初始序列号seq = x(随机数); 做完这步动作,发送方进入SYN_SENT (同步已发送状态) 。

    名称解释: SYN:同步标志位 seq:包序列编号(每个包都有一个序列号)

    第一次握手客户端发送的报文称为同步请求报文,希望与服务端建立同步连接,SYN报文不携带数据。

  3. 三次握手第二步:服务端收到来自客户端的连接请求报文后,需要确认收货,响应报文中ACK(确认标志位)设置为1,将确认号ack 设置为第一步的请求序列号seq 加1(ack =x+1),另外自己也回客户端一个SYN包(可以建立同步连接),即SYN + ACK包,包序列号seq = y,服务端进入SYN_RCVD(同步收到)状态。

    名词解释:ACK:确认状态位(这里ACK=1),这个一定和ack(32位确认序号,这里ack=x+1)区分开,可以看下面的TCP 报文结构体图,ACK是包的状态标志,ack是确认序号。

  4. 三次握手第三步:客户端收到来自服务端的 SYN + ACK 包,会发送一个ACK 确认包,ACK =1,seq = x+1( 第二步的ack),ack = y+1(第二步的seq+1)。

玩家们如果觉得看安琪拉写的有收获,欢迎关注公众号【安琪拉的博客】,来找安琪拉草丛互动!

3.3 四次挥手

四次挥手的状态图如下所示:

四次挥手
四次挥手

四次挥手wireshark 包信息如下,可以对照着上图看,

Wireshark四次挥手
Wireshark四次挥手

四次挥手的具体过程如下:

  1. 客户端发送FIN 释放连接报文,表示结束连接,报文seq = u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN_WAIT1(终止等待1)状态。
  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE_WAIT(关闭等待)状态。TCP接收方通知上层的应用进程,客户端向服务器方向的发送通道关闭了,这时候处于半关闭状态,即客户端已经没有数据要发送了(已经发了FIN结束信号),但是服务器若发送数据,客户端依然要接受。这个状态要持续一段时间,也就是整个CLOSE_WAIT状态持续的时间。
  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN_WAIT2(终止等待2)状态,等待服务器发送连接释放报文(在服务端Close_Wiat期间还可以接受服务器发送的最后的数据)。
  4. 服务端发送完最后的数据,向客户端发送FIN 连接释放报文,ACK =1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,ack 和回复ACK报文一致,ack = u+1, 此时,服务器就进入了LAST_ACK(最后确认)状态,等待客户端的确认。
  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME_WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2 个MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。

4.异常定位:Wireshark 分析定位Broken Pipe 异常原因

4.1 网络包分析

上面我们已经看了正常的四次挥手的流程和截图,下面我们来看下线上遇到的 Broken Pipe异常,再次看一眼异常栈,如下图:

网络异常栈
网络异常栈

我们可以看Wireshark 的包信息如下图,

在这里插入图片描述
在这里插入图片描述

可以看到,我们来看一下流程,第一步是区分服务端和客户端:

  • 服务端:IP后缀为132 ,端口20004是,
  • 客户端:IP后缀为156,端口为4528。

前三行是三次握手,没有问题,但是最后的四次挥手这里有问题(可以对照着四次挥手的图看):

  • No.189 行:服务端发起FIN 报文希望关闭连接,服务端进入FIN_WAIT1 状态;

  • No.190 行:客户端响应服务端的FIN 报文发送ACK 报文,进入CLOSE_WAIT 状态;

  • No.191 ~ No.197:服务端接收到客户端的ACK 进入FIN_WAIT2 状态,此时服务端是不接收数据传输的,但是我们可以看到Wireshark 191 ~ 196 行客户端还在发送数据报文,正常应该是客户端发送FIN 报文关闭连接,让服务端进入TIME_WAIT 状态,但是客户端没有发送FIN报文,而是向已经准备关闭的连接通道中发送了数据报文,因为服务端不认你客户端的数据,所以发送了RST 信号报文来重置连接。

下面是正常的四次挥手和异常的四次挥手对照图:

对比
对比
4.2 问题定位
  1. 供应商接口查看状态

    让HTTP 请求的被调用方(供应商)查看当前网络状态,Linux 命令如下所示:

    netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}'
    

    结果如下:

    image-20200522214047234
    image-20200522214047234

    可以对照着四次挥手流程图看状态:

    ESTABLISHED: 处于连接建立状态的连接数

    FIN_WAIT1: 处于连接关闭FIN_WAIT1 状态的连接数

    FIN_WAIT2: 处于连接关闭FIN_WAIT2 状态的连接数

    TIME_WAIT:处于 TIME_WAIT 状态的连接数

    可以看到TIME_WAIT 状态很多,这个是正常的,只要记住,正常四次挥手流程中,主动关闭的一方会经过TIME_WAIT 状态,被动关闭一方会经过 CLOSE_WAIT 状态,这二个状态(TIME_WAIT & CLOSE_WAIT)需要做个区分,如果 CLOSE_WAIT 状态过多可能会有问题,这个我会在扩展阅读详细说,继续分析异常。

  2. 确认连接方式

    通过上面Wireshark 异常包可以知道是服务端进入FIN_WAIT2 状态后,客户端继续发送数据包,导致服务端RST 连接。这里有二个问题:

    1. 为什么服务端主动发起FIN 关闭连接呢?
    2. 为什么客户端在接收到服务端的FIN 并回复ACK 报文之后,为什么没有发送FIN 关闭连接报文呢?

    不卖关子了,讲了这么多吊足了玩家们的胃口,真实原因都不好意思讲了,其实是因为供应商不支持长连接,但是我们为了资源复用,降低HTTP 连接创造销毁的开销,使用了连接池,连接池的连接是复用的,是长连接,所以才会出现服务端第二次发送数据时不进行三次握手,而是直接传输数据报文的情况。

5. 代码修复:调整客户端代码

5.1 HttpClient 长连接改为短连接
  1. 服务端那边配置的Nginx 是短连接方式,客户端代码使用长连接,客户端HTTP 请求的代码(删除了部分代码),请求结束之后,连接不进行关闭,连接池复用连接。如下:

    长连接主要代码
    长连接主要代码

    如果要支持长连接,服务端Nginx 配置需要做些修改,如下:

    http{
      keepalive_timeout 60s;  #连接保持的超时时间
      
      upstream servers {
      keepalive 10;
     }
      
      server {
         listener 20004 so_keepalive=on #这个是支持长连接的配置
     }
    }
    
  2. 代码问题修复

    客户端连接方式从长连接改成短连接,设置连接不复用就可以了,如下:

    image-20200525203700284
    image-20200525203700284
  3. 番外

    从现象上看因为长连接的连接复用,导致了 Broken Pipe 异常,但是从实际业务角度,业务测试成功率没有明显变化。因为连接池设置了异常重试策略,如果发生Broken Pipe 异常,会再从连接池获取连接重试,重试的成功率非常高,因为大多数时候是新建的连接来重试。这点从wireshark 网络包中可以看出。

    因为每一笔业务都有流水号,因为可以使用Wireshark 过滤器过滤网络包的数据内容,如下所示:

    过滤器输入业务日志中的请求的流水号,过滤器过滤包的命令如下:

    tcp.segment_data contains "TCP包中业务流水号"
    
    image-20200525205104904
    image-20200525205104904

    如上图所示,可以看到有二笔TCP 数据包中数据包含有这个业务流水号,这个就是因为第一次TCP 失败后重试造成的,可以看到一笔是从4528端口发出,一笔是4542端口发出的,我们可以把这二笔单独拉出来看一下,使用过滤器的命令如下,4542端口失败后新建一个连接(端口为4528)发送数据:

    tcp.port == 4542 or tcp.port == 4528
    

    过滤后的数据包如下图所示:

    wireshark过滤数据包
    wireshark过滤数据包

6. 扩展阅读:其他网络知识及面试题

6.1 为什么需要三次握手来建立TCP 连接?

答: 第一次握手客户端发送报文给服务端,收到服务端的应答表明客户端发送数据的能力ok;第二次握手服务端发送数据给客户端表示服务端接收数据能力ok(我正常收到你的数据了,告诉你一声);第三次客户端发报文给服务端表明服务端的发送数据的能力也ok,客户端接收数据能力也ok(我能正常收到你的数据,代表你发的数据没问题,我的接收能力也没问题,所以告知你一声)。所以要验证客户端和服务端发送&接收数据的能力都ok至少需要三次握手才能达到。举个实际的例子,比如你投递简历,相当于第一次握手,HR回复你简历已收到,相当于第二次握手,你回复HR已收到批准通知了,这相当于三次握手,HR可以给你安排面试了。因为每一次握手都有消息丢失的风险,所以需要往返至少三次才能保证连接的建立。

6.2 TIME_WAIT 和 CLOSE_WAIT 有什么区别?

答:这二个状态是四次挥手中的状态,TIME_WAIT 是主动关闭的一方发出 FIN 包会经过的状态,CLOSE_WAIT 是被动关闭连接的一端会经过的状态。TIME_WAIT 经过2个MSL(最大报文段生存时间)才能到CLOSE状态,CLOSE_WAIT 如果不发送FIN 报文会一直处在CLOSE_WAIT 状态。所以一般在看机器连接状态,几千个TIME_WAIT 一般是正常的(过2MSL自动关闭),处于CLOSE_WAIT 状态的连接很多,证明有问题。

6.3 处于CLOSE_WAIT 状态的连接很多,怎么办?

答:处于CLOSE_WAIT 状态的连接很多,证明有问题,有几种可能:

  • 代码问题:短连接模式,忘记 close 连接,就不会发出 FIN 包,导致连接处于 CLOSE_WAIT状态;或者程序在close 连接之前陷入死循环或者执行时间过长;

  • backlog太大:backlog太大这里指的accept 的连接队列设置的太大,这个参数是在服务端创建ServerSocket作为参数传入的,默认为50,支持自定义,设置的太少容易出现连接reset或者拒绝,太大如果服务端处理连接不及时会放到accept队列等待太长时间。accept队列以及socket 连接建立流程如下图:

    image-20200527234141069
    image-20200527234141069

上图所示,这里有两个队列:syns queue(半连接队列);accept queue(全连接队列)。我们很多时候排查网络问题时也需要考虑到accept queue,很多场景需要对accept queue 大小做些调整。

6.4 如何判断是否需要调整 accept queue(全连接队列)大小?

答:例如我们机器并发量很高,accept queue(全连接队列) 可能会出现不够用的情况,会出现类似connection reset 和 connection timeout 异常,这个取决于机器上 tcp_abort_on_overflow 的设置,不同值服务端不同处理机制:

tcp_abort_on_overflow为0:连接建立过程中三次握手第三步时,发生全连接队列满了,server扔掉client 发过来的ack,那么client 会重新发送ack,直到超时,所以客户端会出现连接超时(connection timeout );

tcp_abort_on_overflow为1:遇到全连接队列满了,server会发一个reset包给client,表示废掉这个这个连接,这个握手过程无效,客户端会看到很多connection reset by peer的错误;

查看服务器处理accept queue 队列满时的处理机制:

image-20200527235838846
image-20200527235838846

那怎么判断服务端有没有发生accept queue(全连接队列)满呢? 在linux 可以采用如下指令:

netstat -s | grep "listen"

每隔几秒打印一次,如下图所示:

image-20200527235626558
image-20200527235626558

如果overflowed 代表队列溢出次数一直在增加,是accept 队列大小不够或者队列中连接等待处理时间太长导致的。

另外通过ss 命令可以看具体端口的队列占用情况,如下所示:

image-20200528000349200
image-20200528000349200

Send-Q 这列代表第四列端口上全连接队列的最大值,Recv-Q 代表当前队列有多少个连接,如果长期是满的,代表可以适当增加全连接队列大小了。

6.5 如何修改accept queue(全连接队列)大小呢?

答:accept queue(全连接队列)取决于 min(backlog, somaxconn) ,backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数。

对于Java后端程序,在创建Socket 时是可以传入参数设置backlog 大小的,很多人可能没关注过这个这个参数,看到很多程序直接使用的默认构造函数创建Socket 的。

服务端Socket构造函数 参考自JDK 1.8 文档

如上图所示,在创建Socket 时,可以通过传入backlog 设置全连接队列大小。如果不传这个参数,默认值是50,Tomcat 默认短连接,这个值默认是100, Nginx默认是511。

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

答:虽然按道理,挥手过程中的四个报文都发送完毕,主动关闭连接的一方应当是可以直接进入CLOSE状态了,但是必须考虑到网络环境是十分复杂且不可靠的,有可以四次挥手最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文,假设在Client发送出最后的ACK回复,但该ACK 如果丢失,Server没有收到ACK,Server会重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了第四次挥手的ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。这个2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送加上一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

6.7 如果三次握手连接已经建立,但是客户端突然出现故障了怎么办?

答:首先,从协议设计角度来讲,客户端如果出现故障,服务端肯定不能一直死等客户端,所以考虑这种情况的存在,TCP 协议中服务端有个计时器,每次收到客户端的响应报文都会重置这个计时器,服务端有个超时时间,通常是2个小时,2个小时没收到客户端的数据,服务端会每隔75秒发送探测报文段,连续10次探测报文没响应,认为客户端出现问题,服务器会关闭这个连接。一般程序设计者不会依赖这个机制,2个小时实在太长,框架里面都会自己做连接的检查,无效连接的关闭,例如连接池的连接驱逐策略,Apollo等框架的连接保活等。

写在最后的话

安琪拉很早就想写网络这个主题了,一方面是感觉网络这块内容确实非常庞大,另一方面觉得自己还没完全研究透彻,不敢写,这篇文章也只是从我们项目中遇到的一个网络问题引申出来的,还只是沧海一粟,要想把TCP/IP 底层的实现机制讲明白不是一二篇文章能做到的,如果未来工作有需要大家也感兴趣,我会把TCP/IP 这块内容继续深入挖掘,尽量用浅显易懂的文字表述出来,也可能用面试对话的方式,玩家们关注【安琪拉的博客】公众号,一起探索后端技术,还原技术背后的本质。

这又是一篇万字长文,希望玩家们能耐着性子看完,实在嫌长可以先收藏评论再慢慢读。我文章更新的很慢,但是每写一篇文章,都会希望在一篇文章中能把技术都聊清楚,聊透,所以目前也一共才写10篇文章,希望大家都能从文章中有所收获吧!

我是安琪拉,一个在互联网摸爬滚打的草丛技术流!