从TCP协议到TCP通信的各种异常现象和分析(下)

2,730 阅读7分钟

今天我们继续介绍关于TCP异常情况的内容。本篇文章接着上一篇文章,前面分析了在连接过程中的各种异常,本篇文章重点介绍的是在数据传输过程中的各种异常,以及出现异常后的TCP连接的情况。为了便于大家理解本文,这里我们将上一篇文章的前半部分内容拷贝到这里,这部分内容主要介绍协议的内容。

下图是网络通信中常见的架构,也就是CS架构。其中程序包括两部分,分别为客户端(Client)和服务端(Server)。当然,实际的环境还要复杂的多,在客户端和服务端之间可能有多种不同种类和数量的设备,这些设备都会增加网络通信的复杂性。自然,也会增加程序开发容错的复杂性。

图1 基本架构

TCP的基本流程

在分析异常情况之前,我们先回忆一下TCP协议的基本逻辑。在客户端和服务端能够收发数据之前首先必需建立连接。连接的建立在协议层面也是通过收发数据包完成,只不过在用户层面就是客户端调用了一个connect函数。连接的过程俗称“三次握手”,具体流程如图2所示。

图2 TCP的三次握手流程

TCP连接的断开也是比较复杂的,需要经过所谓的“四次挥手”的流程。其原因是因为TCP是双工通信,分别需要从客户端和服务端2侧断开连接。

图3 TCP的四次挥手
另外一个比较重要的内容是TCP协议的状态转换,理解了这个内容,我们才能清楚出现各种异常情况下数据包的内容。

图4 TCP状态转换图

本文只是简单回忆一下TCP的基本流程,详细的内容可以参考本号之前的文章《从TCP到Socket,彻底理解网络编程是怎么回事

异常情况分析

本文的分析假设连接已经建立,目前正在数据收发过程。这种情况下会出现各种异常,比如服务器宕机、进程crash或者进程被kill等等。下面我们分别介绍上述集中情况在TCP通信中的表现。

服务进程crash

服务进程crash恐怕是我们日常生成环境最长遇到的情况,没有之一吧。那么在这种情况下客户端软件是什么反应?客户端是否可以感知?

我们分别写客户端和服务端的程序,客户端不断的发送数据,服务端接收数据。异常的模拟很简单,我们可以在服务端制造一个指针访问异常。此时服务端的程序就会crash掉。然后我们观察客户端的表现。先上结果,客户端的表现如下图所示。

image

可以看到客户端被reset掉了。我们在结合通过wireshark抓获的此时的数据报文内容,可以看到是一个RST报文。

image

回忆一下什么情况下服务端会发送RST报文。这种场景跟我们前文介绍的服务端没有监听的情况是类似的。由于服务端程序crash了,此时在操作系统中的套接字数据结构已经被释放,因此在协议层收到数据包的时候无法找到对应的套接字进行处理,于是发送了一个RST报文。

手动杀死服务端应用

这也是线上比较常见的操作,当一个模块上线时,ops同学总是会先把旧的进程杀死,然后再启动新的进程。**那么在这个过程中TCP连接又会发生了什么呢?是否会像上一种情况一样被RST呢?**同样,我们先看一下结果,如下是客户端的情况。

image

从上面错误码来看是管道破裂,其实也就是连接被中断了。我们再看一下通过wireshark的抓包结果可以看出服务端发送了一个FIN报文,这个报文表示服务端发起了关闭的请求。而接下来的一个报文是客户端对该请求的确认。

image

所以,从上面客户端的错误码和报文情况我们可以知道,在kill进程时TCP协议是能够感知到的,并且发送的FIN报文。

我们再进一步的思考一下,**为什么kill进程会有FIN呢?这个与前面crash的差异在哪?**其实kill进程是通过shell想内核发送了SIGKILL或者SIGTERM,内核接收到该信号之后会进行相应的扫尾工作,因此可以看到服务端发送了FIN报文。

Server进程所在的主机关机

主机关机(这里指手动关机)的情况与进程被kill是类似的。这时因为在系统关闭时,init进程会给所有进程发送SIGTERM信号,等待一段时间(5~20秒),然后再给所有仍在运行的进程发送SIGKILL信号。当服务器进程死掉时,会关闭所有文件描述符。带来的影响和上面杀死server相同。

Server进程所在的主机宕机

这是我们线上另一种比较常见的状况。即使宕机是一个小概率事件,线上几千台服务器动不动一两台挂掉也是常有的事。这里挂掉其实包括2种情况,一种是内核panic,另外一种情况是出现了掉电。对于内核panic的情况不会像关机那样会预先杀死上面的进程,而是突然性的。那么此时我们的客户端准备给服务器端发送一个请求,它由write写入内核,由TCP作为一个报文发出,但因为主机已经挂掉,因此客户端无法收到ACK。于是客户端TCP持续重传分节,试图从服务器上接收一个ACK,然而服务器始终不能应答,重传数次之后,大约几分钟才停止,之后返回一个ETIMEDOUT错误。在这种情况下,如果我们调用的是同步发送接口,则在发送缓冲区慢的情况下会阻塞在这里,导致程序阻塞。

这个时间真的很长,对于某些应用这种长时间的卡顿是不能接受的。因此,需要一种手段处理这种情况,在套接字接口中可以通过SO_SNDTIMEO标记进行设置。但是有利也有弊,如果设置了该参数,可能会出现这的数据发送超时的情况,进而出现向服务端发送重复数据的情况,此时需要服务端做去重处理。

服务器进程所在的主机宕机后重启

在客户端发出请求前,服务器端主机经历了宕机—重启的过程。当客户端TCP把分节发送到服务器端所在的主机,服务器端所在主机的TCP丢失了崩溃前所有连接信息,即TCP收到了一个根本不存在连接上(也就是我们前文介绍的查找不到socket数据结构)的报文,所以会响应一个RST分节。

至此,关于TCP协议中各种异常情况介绍完了,详细了解这些内容后对后续线上问题的分析和解决会有很大的帮助。当然,也有可能还有其它本文没有介绍到的异常情况,也欢迎大家留言交流。