TCP通信的这些异常,你考虑了吗?

TCP是面向连接的,在传输数据之前要先和对端建立一个连接,建立连接的过程我们通常叫做3次握手。下面通过图片来给大家展示3次握手的过程

 
 

在数据传输完之后我们可能不再需要这个连接,那么就需要把连接断开。断开连接的过程被称之为4次挥手。

 
 

关于三次握手和4次挥手的过程,有不明白的可以移步http://www.2cto.com/net/201209/157577.html本篇文章主要讨论以下几种情况

01

试图与一个不存在的端口建立连接

服务器端口还没有监听,我们的客户端就调用connect,试图与其建立连接。这是会发生什么呢?没错,这符合触发发送RST分节的条件,目的为某端口的SYN分节到达,而端口没有监听,那么内核会立即响应一个RST,表示出错。客户端TCP收到这个RST之后则放弃这次连接的建立,并且返回给应用程序一个错误。正如上面所说的,建立连接的过程对应用程序来说是不可见的,这是操作系统帮我们来完成的,所以即使进程没有启动,也可以响应客户端。

02

试图与一个不存在的主机上面的某端口建立连接

这也是一种比较常见的情况,当某台服务器主机宕机了,而客户端并不知道,仍然尝试去与其建立连接。根据上面的经验,这次主机已经处于未启动状态,操作系统也帮不上忙了,那么也就是连RST也不能响应给客户端,此时服务器端是一种完全没有响应的状态。那么此时客户端的TCP会怎么办呢?据书上介绍,如果客户端TCP没有得到任何响应,那么等待6s之后再发一个SYN,若无响应则等待24s再发一个,若总共等待了75s后仍未收到响应就会返回ETIMEDOUT错误。这是TCP建立连接自己的一个保护机制,但是我们要等待75s才能知道这个连接无法建立,对于我们所有服务来说都太长了。更好的做法是在代码中给connect设置一个超时时间,使它变成我们可控的,让等待时间在毫秒级还是可以接收的。

03

Server进程被阻塞

由于某些情况,服务器端进程无法响应任何请求,比如所在主机的硬盘满了,导致进程处于完全阻塞,通常我们测试时会用gdb模拟这种情况。上面提到过,建立连接的过程对应用程序是不可见的,那么,这时连接可以正常建立。当然,客户端进程也可以通过这个连接给服务器端发送请求,服务器端TCP会应答ACK表示已经收到这个分节(这里的收到指的是数据已经在内核的缓冲区里准备好,由于进程被阻塞,无法将数据从内核的缓冲区复制到应用程序的缓冲区),但永远不会返回结果。

04

我们杀死server

这是线上最常见的操作,当一个模块上线时,OP同学总是会先把旧的进程杀死,然后再启动新的进程。那么在这个过程中TCP连接发生了什么呢。在进程正常退出时会自动调用close函数来关闭它所打开的文件描述符,这相当于服务器端来主动关闭连接——会发送一个FIN分节给客户端TCP;客户端要做的就是配合对端关闭连接,TCP会自动响应一个ACK,然后再由客户端应用程序调用close函数,也就是我们上面所描述的关闭连接的4次挥手过程。接下来,客户端还需要定时去重连,以便当服务器端进程重新启动好时客户端能够继续与之通信。

当然,我们要保证客户端随时都可以响应服务器端的断开连接请求,就必须不能让客户端进程再任何时刻阻塞在任何其他的输入上面。比如,书上给的例子是客户端进程会阻塞在标准输入上面,这时如果服务器端主动断开连接,显然客户端不能立刻响应,因为它还在识图从标准输入读一段文本……当然这在实际中很少遇到,如果有多输入源这种情况的话开通通常会用类似select功能的函数来处理,可以同时监控多个输入源是否准备就绪,可以避免上述所说的不能立即响应对端关闭连接的情况。

05

Server进程所在的主机关机

实际上这种情况不会带来什么更坏的后果。在系统关闭时,init进程会给所有进程发送SIGTERM信号,等待一段时间(5~20秒),然后再给所有仍在运行的进程发送SIGKILL信号。当服务器进程死掉时,会关闭所有文件描述符。带来的影响和上面杀死server相同。

06

Server进程所在的主机宕机

这是我们线上另一种比较常见的状况。即使宕机是一个小概率事件,线上几千台服务器动不动一两台挂掉也是常有的事。主机崩溃不会像关机那样会预先杀死上面的进程,而是突然性的。那么此时我们的客户端准备给服务器端发送一个请求,它由write写入内核,由TCP作为一个分节发出,随后客户阻塞于read的调用(等待接收结果)。对端TCP显然不会响应这个分节,因为主机已经挂掉,于是客户端TCP持续重传分节,试图从服务器上接收一个ACK,然而服务器始终不能应答,重传数次之后,大约4~10分钟才停止,之后返回一个ETIMEDOUT错误。

这样尽管最后还是知道对方不可达,但是很多时候我们希望比等待4~10分钟更快的知道这个结果。可以为read设置一个超时时间,就得到了一个较好的解决方法。但是这样还是需要等待一个超时时间,事实上TCP为我们提供了更好的方法,用SO_KEEPALIVE的套接字选项——相当于心跳包,每隔一段时间给对方发送一个心跳包,当对方没有响应时会一更短的时间间隔发送,一段时间后仍然无响应的话就断开这个连接。

07

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

在客户端发出请求前,服务器端主机经历了宕机——重启的过程。当客户端TCP把分节发送到服务器端所在的主机,服务器端所在主机的TCP丢失了崩溃前所有连接信息,即TCP收到了一个根本不存在连接上的分节,所以会响应一个RST分节。如果开发的代码足够健壮的话会试图重新建立连接,或者把这个请求转发给其他服务器。

总结

列出的这7种情况大部分是我们在平时工作中可以遇到的,也是我们作为测试工程师需要关注的,有些情况是TCP协议自动完成,有些情况是需要在代码层面完成。如果有问题可以一起讨论,一个人的思考总是片面的,大家一起思考,可以得到更深入、更成熟的见解。



作者:夜境
链接:https://www.jianshu.com/p/d759788ab83f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2020-04-08 17:05  繁星如雨  阅读(769)  评论(0编辑  收藏  举报