socket 通信中可能发生哪些异常
socket 是网络编程的基础,业务开发中常用的 HttpClinet、RPC 框架等底层网络通信基础都离不开 socket,在使用这些框架的时候,可能会遇到 connect refused、connect reset by peer 等异常情况,这些异常的含义很容易理解。
但是当发生这些异常的时候,socket 的一些行为细节,更近一步的,TCP 协议中的发生了什么,可能就不是那么直观清晰了。
所以我专门针对 socket 编程中可能出现的异常进行了调研,针对不同的异常情况,给出了 C 语言 socket 编程代码示例,并分析了发生对应情况时 TCP 协议层的具体动作。
拒绝连接异常
拒绝连接异常就是服务端拒绝了客户端的连接,在如下这个代码示例中,当我们试图连接到一个本地未监听的端口时,就会收到这样的一个异常信息。
根据 ip 地址和 端口,建立 TCP 连接:
1 | int tcp_client(const char *address, int port) { |
不启动服务端的情况下,尝试建立连接,终端输出信息如下:
1 | connect failed : Connection refused (61) |
可以看到当发生 Connection refused 异常时,socket 层是由 connect 函数返回了对应的错误码,那么 TCP 协议层发生了什么呢?
我们知道 TCP 建立连接时需要进行 3 次握手,客户端首先发送 sync 报文段,如果报文段到达了目标主机,但是发现目标端口上没有正在监听的服务,则会发出 RST 报文段作为回复,拒绝此次连接。客户端收到 RST 回答后,通过 connect 函数报告这一错误。
产生 RST 的情况由如下 3 种:
- sync 报文段到达,但目标端口没有正在监听的服务(如上所述)
- TCP 想取消一个已有连接
- TCP 接收到一个根本不存在的连接上的报文段
连接超时异常
还是上一小节的代码示例,如果在建立连接时,指定了一个无法连接的 ip 地址,比如和本机不在同一个局域网的私有 ip 地址,终端会输出如下错误信息:
1 | connect failed : Operation timed out (60) |
connect 函数默认情况下是阻塞调用,直到连接建立成功或者报告错误。如果客户端发出的 SYN 包没有任何响应,最终会返回 TIMEOUT 错误。
Linux 下默认的超时时间是 75s,实际应用中这个超时时间是不能接受的,所以在实际的网络通信中我们通常会IO多路复用的方式来建立连接,在调用 select 或者 poll 或者 epoll 函数时指定超时时间。
发生这种情况代表目标主机不可达,可能是 ip 地址写错,或者是网络出现故障。但有时我们会收到明确的 destination unreachable 错误,表示客户端和服务器端路由不通,这是因为客户端收到了路由器或者防火墙报告的 icmp 差错报文。
目标不可达
ICMP 类型 3 表示“Destination Unreachable”(目标不可达),而类型 3 中的不同代码用于指示导致目标不可达的具体原因,常见的代码有如下几种:
- Code 0: Net Unreachable(网络不可达)
- Code 1: Host Unreachable(主机不可达)
- Code 2: Protocol Unreachable(协议不可达)
- Code 3: Port Unreachable(端口不可达)
所以,当端口不可达时,客户端除了收到 RST 回答,还可能收到 ICMP 差错报文,这可能会因不同的网络设备、操作系统或防火墙设置而有所不同。
连接断开异常
上述两个异常是建立连接时可能遇到的,那么当连接建立成功后,因为某种原因连接断开了,会发生什么呢?
socket 中通常只有两种方式来感知 TCP 链路的异常中断,一种是以 read 为核心的读操作,一种是以 write 为核心的写操作。下面我们看下不同场景下通过读写操作来感知 TCP 连接的异常。
对端有 FIN 包发出
read 直接感知 FIN 包
在正常的连接断开过程中,对端会发送 FIN 报文段,客户端通过 read 接口感知到后,会做出相应的处理,比如关闭连接,释放连接资源。
服务端程序如下:
1 | int tcp_server(int port) { |
客户端程序如下:
1 | int main(int argc, char** argv) { |
分别启动服务端和客户端后,kill 掉服务端进程,客户端终端输出如下:
1 | peer connection closed |
因为阻塞的 read 操作读取到了 FIN 包后返回值为 0,客户端程序感知到 FIN 包后,执行正常退出逻辑。
通过 write 产生 RST,read 调用感知 RST
客户端程序如下:
1 | int main(int argc, char **argv) { |
分别启动服务端和客户端后,kill 掉服务端进程,然后在客户端终端输入“Hello World”,回车后会看到如下错误信息:
1 | read failed: Connection reset by peer (54) |
客户端程序启动后,会阻塞在从标准输入读取数据的 fgets 方法上,因为无法感知到连接已经断开。
当从标准输入获取数据后,会将数据通过 socket 发送给服务端,因为服务端进程已经不存在,所以会返回一个 RST 包,接下来客户端的 read 调用感知到 RST,会返回异常信息,表明连接已断开。
注意
以上实验结果是在 MacOS 14.2.1 上进行的,在 Linux 系统上,这里的 read 调用可能读取到服务端程序关闭时发出的 FIN 包,从而正常关闭,而不是返回 RST 错误。说明不同系统内核中的的 TCP 协议实现可能略有不同。
向一个以关闭连接连续写,导致 SIGPIPE
客户端程序如下:
1 | static void sig_pipe(int signo) { |
分别启动服务端和客户端后,kill 掉服务端进程,然后在客户端终端连续输入数据,第二次回车后会看到如下错误信息:
1 | SIGPIPE(13) |
和前一个例子类似,当向一个已经关闭的连接发送数据时,会得到 RST 回复。如果客户端再次向这个连接发送数据,则会收到一个 SIGPIPE 信号。如果不捕捉这个信号,应用程序会在毫无征兆的情况下直接退出。
因为我们在程序中捕获了 SIGPIPE 信号,所以可以看到两行输出,第一行是对 SIGPIPE 信号的处理逻辑打印的通知信息,第二行是 write 调用的返回的错误信息。
注意:
在某些 Linux 版本中,第二次 write 操作不会收到 SIGPIPE 信号,而是返回 RST 错误信息。
对端无 FIN 包
网络中断导致对端没有 FIN 包
当网络中断时,如果网络中其他设备,比如路由器发出了 ICMP 差错报文,说明网络或者主机不可达,这是 read 或者 write 调用会返回 unreachable 错误。
如果没有 ICMP 报文,TCP 并不能及时感知到异常信息。如果此时程序阻塞在 read 方法上,则将无法回复运行,所以通常我们通过设置超时时间来避免这个问题,具体方式下一节分析读取超时异常时会介绍。
如果此时程序首先调用 write 发送了数据,然后阻塞在 read 调用上,系统的 TCP 协议栈会不断尝试将发送缓冲区的数据发送出去,大概在重传 12 次、合计时间约为 9 分钟之后,协议栈会标识该连接异常,这时,阻塞的 read 调用会返回一条 TIMEOUT 错误信息。如果程序继续 wirite 数据,则会收到一个 SIGPIPE 信号。
系统崩溃导致对端没有 FIN 包
这种情况和网络中断的情况是类似的,在没有 ICMP 报文的情况下,只能通过 read 或者 write 感知异常。
不同的地方在于,如果系统崩溃之后重新启动,重传的数据到达重启后的系统后,因为系统中没有该连接信息,会返回一个 RST 包。
如果客户端阻塞在 read 调用上,会立即返回 connect reset 错误;如果是 write 调用,也会立即失败,并收到一个 SIGPIPE 信号。
读取超时异常
前面提到过,服务端可能因为各种原因断开连接后,没有发出 FIN 包。如果是阻塞套接字,会一直阻塞在 read 调用上,没有办法感知套接字的异常。
所以我们通常会给套接字的 read 操作设置超时。可以说,读取超时异常是上层应用最常见的网络异常之一。
客户端代码如下:
1 | int main(int argc, char** argv) { |
分别启动服务端和客户端后,在客户端终端输入数据,1s 后会看到如下错误信息:
1 | read failed: Resource temporarily unavailable (35) |
因为客户端程序中,socket 超时时间设置为 1s,而服务端发送数据前 sleep 了 5s,所以这里 read 函数返回读取超时错误信息。
总结
本文中总结了 socket 编程可能遇到的几种常见异常场景,并结合 TCP 协议进行了分析,其中,当服务端连接意外中断时,客户端对连接异常信息的检测,可能因不同的条件而产生 Connection Reset、Broken Pipe 等不同的错误信息。读完本篇文章后,如果应用层遇到了类似的问题,我想可以有更加清晰的理解和排查思路。
另外,文章中涉及的代码已经上传到了 githug,地址如下:https://github.com/qinjianmin/socket-exception。