TCP连接的部分细节及边界情况分析

Posted taocr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP连接的部分细节及边界情况分析相关的知识,希望对你有一定的参考价值。


图中即一个简单的socket建立连接的过程, 可以看到服务器端需要执行4个系统调用来准备好接收客户端的socket连接,而客户端在建立socket完成后,即可调用connect去主动连接服务器。

关于5个系统调用的具体含义详情可以去看unp,这里不多赘述,不过在一开始学习socket的时候我就对此处抱有疑问(也是没有细心看书导致),这里对我的疑问做一个解答。

Q:关于服务器与客户端的流程比较,为什么服务器端在调用socket创建完成一个套接字后,还需要去调用bind()、listen()、accept()这些系统调用,而客户端却在调用socket完成后直接调用connect进行连接?
A:这里的不理解主要是因为没去了解这些系统调用背后到底做了什么,而只是从字面理解。

  • bind
    bind为建立的socket指明了端口及ip地址,当客户端没有经过bind直接connect连接时,我们通过调用netstat命令可以看到它的端口号是由内核分配的,这对于TCP客户端来说没有任何问题,但是对于服务器端来说,如果端口号随机分配,那么客户们如何知道服务器的端口号是多少,另外当服务器重启,无法保证端口号跟原来一样,于是我们需要分配一个众所周知的端口号,以形成一个约定。
    另外bind在指定端口的同时还指定了IP地址,这是为了限制连接的客户,使得服务器只对部分指定的IP地址客户开放。

  • listen
    当一个套接字被建立时,它被假设为一个主动套接字,即它是一个将会调用connect进行连接的套接字,而listen将这样一个主动套接字转换成一个被动套接字。
    另外listen会使得内核为转换而来的被动监听套接字维护两个队列:未完成连接队列、已完成连接队列。当一个客户执行connect后进行三次握手,第一个SYN分节到达服务器后,就会在 未完成连接队列中 中建立一项(即套接字),处于SYN_RCVD状态。当三次握手完成时,内核将之前建立的项转移到已完成连接队列中。

  • accept
    用于从已完成连接队列队头返回下一个已完成连接,如果已完成连接队列为空,那么进程被投入睡眠。结合之前listen的解释,这里的accept就很容易理解了,它的作用只是在已经建立的连接队列中找到最早的一个被建立的连接进行返回,这也说明实际上客户端connect只需要在服务器端listen完成后执行即可建立连接,不一定非要在accept调用后再执行connect。

Q:一个经典的问题,为什么TCP要三次握手,四次挥手?
A:分两部分解答

  • 为什么三次握手
    这是因为经过验证建立连接最少要发三个分组,那么为什么不是两次握手?
    我们假设两种情况:
    1、客户端A发送的SYN分组成功到达服务器端B,B应答并分配套接字,但是B的应答并没有到达A,于是A认为连接未建立,B则认为连接已建立。
    2、客户端A发送的SYN分组因为某种原因未到,于是客户端A超时重发,这次发送的SYN分组成功到达服务器端B,之后成功建立连接传输数据,数据传输完成后断开连接,这之后一开始发的SYN分组到达了,于是B以为A再次建立连接,并返回一个ACK分组,B又存在了与A的连接的套接字,但是A并没有传输数据。

  • 为什么四次挥手
    这跟TCP的实现有关,TCP是全双工模式,可以实现半关闭,就是说无论是客户端还是服务器端,可以关闭掉自己写的功能,但是仍然可以读对方发送给自己的数据。因此必须四次挥手,确认了两端都关闭了自己写的功能,没有数据会在此连接中进行传递了才能将连接关闭。

下面讨论TCP正常连接以及终止的情况,并附上TCP状态转换图,用以对照

正常TCP连接的具体情形

一般情况下,TCP传输连接都是由一方发起的,于是正常的流程应该是如下图所示

Q:上图中最后客户端存在着TIME_WAIT状态,那么为什么不是在FIN_WAIT_2后确认关闭后直接进入CLOSED状态呢?
A:主要是两个原因:1、能够让TCP全双工连接终止更可靠;2、让一些旧的分组数据在网络中消失

  • 能够让TCP全双工连接终止更可靠
    TCP协议四次挥手过程中,最后的ACK可以发现是由一开始主动关闭连接的一方发出的,那么如果这个ACK被丢失了呢?那么另一端会因没有收到ACK而不断地重发FIN,如果没有TIME_WAIT状态,A端由于关闭了套接字会返回一个RST,于是B端接收后,就会因为产生错误。所以主动关闭连接的一端必须要维持一定时间的TIME_WAIT状态,防止四次挥手任何一端出现数据丢失的情况

  • 让一些旧的分组数据在网络中消失
    TCP传输的数据不是一定能够达到目的地的,中途可能经过多个路由器,而因路由器异常而导致的TCP数据迷路时有发生,但是这些数据在路由器修复后会被送达至目的地。假设不存在TIME_WAIT状态,我们在关闭连接后,就可以立刻建立一个新的连接,但是前一个连接的数据因为路由器异常被修复的关系,此时到达了另一端,于是就会被新连接接收到,这是我们不希望发生的。
    因此将TIME_WAIT状态的时间定为2MSL,在这段时间中不能够创建新的TCP连接。对于在路由器中传输的数据,能够存在的时间只有这么多,超过如果仍未到达目的地,就会消失,也就避免了刚才的情况的发生。

accept出现的一些不同寻常的情况

在UNP上描述了两种状况,都会导致accept出现一些特殊情况,但是并不致命,只需要再次执行accept即可正常运行

1、被中断的系统调用

accept函数属于慢系统调用(slow system call),即那些可能永远阻塞的系统调用,即调用可能永远都无法返回。适用于慢系统调用的基本规则时:当阻塞与某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。

不过有些系统调用不存在这里的问题,这跟具体内核有关,有些内核会自动重启一些被中断的系统调用,但是也有一些内核不会自动重启,甚至有些系统调用会重启而有些不会,这跟sigaction系统调用设定的对捕获某信号所执行的操作中,是否有选择SA_RESTART标志有关。

2、accept返回前连接终止

这里假设一种情况,不过这种情况我没有想出何种情况下会导致其发生。
服务器调用socket、bind、listen后过了一会再调用accept,而客户端则在服务器端调用listen、accept之间的时间内调用了connect,并且在成功返回后调用了设置了SO_LINGER套接字的close对套接字进行关闭(这里不去讨论SO_LINGER如何设置,以及含义,只是说明一种需要注意的情况),但是发送了一个RST给服务器端,这种情况下服务器端调用accept会产生一个错误,POSIX规定返回的errno值必须为ECONNABORTED(“software caused connection abort”,软件引起的连接中止),对于这样的错误服务器只需要忽略并再次调用accept即可。

SO_LINGER套接字选项指定close函数对面向连接的协议(TCP、SCTP等,不是UDP)如何操作。
默认操作为close立即返回,但是如果有数据残留在套接字发送缓冲区中,系统将试着把这些数据发送给对端;
但是SO_LINGER让我们可以改变这个默认设置

RST被发送场景
1. connect一个不存在的端口;
2. 向一个已经关掉的连接send数据;
3. 向一个已经崩溃的对端发送数据;
4. close(sockfd)的时候,直接丢弃接收缓冲区中未读取的数据,并给对方发送一个RST
5. a重启,收到b的保活探针,a发送RST

服务器进程终止

Q:对于服务器进程崩溃的情况,会出现什么状况呢?

A:测试的时候可以通过将服务器进程kill掉来进行模拟,当服务器进程被终止时,会关闭其打开的所有文件描述符,此时就会向客户端发送一个FIN,客户端则响应一个ACK,于是完成了TCP连接终止“四次挥手”的前半部分。
但是此种情况下,由于TCP连接为全双工模式,因此只实现了半关闭,服务器的FIN无法通知客户端其已经终止,客户端仍可以向服务器写入数据。
当客户端向服务器写入数据时,由于服务器端的套接字进程已经终止,于是以RST响应,此时如果对套接字进行读的操作,RST就会导致读的操作返回0。

那么如果继续写呢?

SIGPIPE信号

对于接着写的情况就符合了一个规则

当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号。该信号的默认行为是终止进程,因此进程必须捕获它以免不情愿地被终止。

因此如果继续往一个关闭的套接字中写入数据,就会导致本端进程也被终止,除非将SIGPIPE信号捕获进行忽略或其他操作。

这里的情况也适用于客户端进程的崩溃,如果服务器端不停地往客户端发送数据,但是却没有忽略掉SIGPIPE信号,那么就会导致如果客户端进程崩溃后,服务器端进程因为两次向不存在的套接字写入数据,接收到SIGPIPE信号,导致服务器端进程也被终止。

服务器主机崩溃

Q:如果服务器主机直接崩溃,甚至来不及去发送FIN信号进行四次挥手时会发生什么事情?

A:由于服务器主机崩溃,其无法在已有的网络连接上发出FIN分组,此时客户端无从知晓服务器端崩溃,只有当客户端主动给服务器端发送数据时,才会发现。
此时客户端发送数据后,希望能够从服务器端处接收一个ACK,但是由于服务器端崩溃,客户端发出的数据包无法到达,只能在一段时间后超时重传,直到最后客户端放弃,于是返回一个错误,从这个错误中可以判断出主机是否崩溃。
ETIMEDOUT对应服务器主机崩溃,对客户的数据分解没有响应的情况;而EHOSTUNREACH或者ENETUNREACH表示某个中间路由器判定服务器主机已不可达。

服务器主机崩溃后重启

Q:正如之前所有服务器主机突然崩溃后,客户端在没有主动向服务端发送数据的情况下是无法知晓这件事的,假设在客户端向服务器发送数据之前或者发送的过程中服务器主机重新启动,那么会发生什么状况?

A:当服务器主机崩溃后重新启动,由于内存中的数据全部丢失,于是TCP也丢失了崩溃前的所有连接信息,因此服务器TCP对于来自客户的数据分组响应一个RST(复位),之后当客户端收到RST时,使得客户端的socket套接字丢弃掉与服务器端进行重新连接

服务器主机关机

对于服务器主机关机的情况,具体流程如下:
(1)init进程给所有进程发送SIGTERM信号,并等待一段时间,即留给所有运行的进程一段时间来进行清除和终止
(2)这段时间过了后,init进程发送SIGKILL信号,终止所有进程
(3)一个服务器子进程终止时,所有打开的描述符被关闭,于是发生服务器子进程的半关闭,之后服务器子进程成功终止

以上是关于TCP连接的部分细节及边界情况分析的主要内容,如果未能解决你的问题,请参考以下文章

TCP粘包,拆包及解决方法

TCP连接及状态分析

TCP连接及状态分析

TCP连接及状态分析

Netty 中 TCP 消息边界问题及按行分割消息

TCP 的三次握手,四次挥手和重要的细节—干货满满,建议细读