Unix网络编程(四、非正常连接下的错误处理)

前言

前三篇都是说的在正常的三次握手下的处理。但是现实往往不会一直都能顺利连接,想要实现一个强大的服务端,必须考虑到各种可能发生的情况,并对其有处理策略。

accept返回前连接终止

当服务端接收到客户端的连接请求后,此时会发生三次握手,当三次握手完成之后,该连接已由tcp排队,就等着轮到它了由accept返回了。此时客户端发过来一个RST(复位),这个RST在三次握手完成之后,accept返回之前。此时accept会返回-1,并将errno置为ECONNABORTEN(软件引起的连接中断)。这种情况在比较繁忙的服务端进程中较为容易出现。
处理方式很简单,在accept的时候,发现errno为ECONNABORTEN,再次调用accept即可。

服务进程终止

这个服务进程是指的处理客户端请求的,由服务端主进程fork出来的那个子进程。
当客户服务端的连接建立好了之后,客户端是阻塞在fgets函数上等待用户输入的。此时客户端感知不到服务端已经终止了。
当服务端进程已经终止之后,此时客户端在向服务端发送数据分为两种情况

  1. 服务端进程刚刚关闭,处于半关闭状态,此时还能接收客户端发来的数据,只是不做回应。那么此时客户端会收到服务端返回的RST。read函数返回EOF
  2. 服务端进程已完全关闭,此时read函数会直接返回ECONNRESET

    书上是这么说的,但是根据tcpdump返回的数据来看,最后还是收到了服务端发送的RST

引发这个问题的原因是我们的客户端阻塞在两个源上,fgets和read。而一个好的客户端设计不应该单纯的阻塞在某个特定的输入上。这个将会在后面的网络模型select,poll,epoll中解决。

向接受了RST的套接字写数据

在某种场景下,客户端程序需要向服务端连续发送多次数据。服务端每次收到数据后都进行返回。举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <asm-generic/errno-base.h>

static const int MAXLINE = 4096;

int e_socket(int domain, int type, int protocol);

void my_cli(FILE *fd, int sockfd);

void my_cli(FILE *fd, int sockfd)
{
char sendline[MAXLINE] = {0};
char recvline[MAXLINE] = {0};
while (fgets(sendline, MAXLINE, fd) != NULL)
{
write(sockfd, sendline, 1);
sleep(1);
write(sockfd, sendline + 1, strlen(sendline) - 1);
int n;
if ((n = read(sockfd, recvline, MAXLINE)) == 0)
{
printf("service has exit %d", n);
}
fputs(recvline, stdout);
memset(recvline, 0, MAXLINE);
}
}

int e_socket(int domain, int type, int protocol)
{
int sockfd = socket(domain, type, protocol);
if (sockfd < 0)
{
exit(0);
}
return sockfd;
}

int main(int argc, const char *argv[])
{
int sockfd;
sockfd = e_socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(45678);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
connect(sockfd, (sockaddr *)&servaddr, sizeof(servaddr));
my_cli(stdin, sockfd);
return 0;
}

这这段客户端代码中,先发送数据的第一个字节,在等待1秒后,将剩下的部分发给服务端。等待1秒是为了有足够的时间将服务端关闭。
看一下这样一个场景

  1. fgets获取到用户输入abcdef
  2. 客户端将数据 a 发送给服务端
  3. 关闭服务端
  4. 服务端发送一个RST到客户端
  5. 客户端将剩下的数据 bcdef 发送到服务端

在这种场景下便会引发这个问题: 客户端在收到了服务端的RST后,继续向服务端发送数据。此时的write会产生一个SIGPIPE信号,产生这个信号的目的是告知客户端服务端已断开连接,不要再向服务端写数据了。该信号默认会结束当前进程,如果不对该信号进行处理,客户端的退出状态不会被记录,会导致查找原因的时候异常困难。
采取的措施要么就是将该信号设置为SIG_IGN 内核收到该信号后便不会进行任何处理。要么就自定义该信号的处理函数,可以通过打log等方式来方便查错。

服务端崩溃后重启

在现在的客户端程序中,如果我们不发送数据到服务端,客户端是无法知道服务器是否断开连接的。考虑这样一种情况
客户服务端建立好连接之后,客户端未主动向服务端发送数据。此时服务端进程崩溃,且在很短的时间重启,此时客户端再向服务端发送数据,由于服务端进程已经重启,他们之间建立的连接信息已经失效,那么客户端向那个已经失效了的服务端发送数据,将会收到一个RST。
这样的错误也是客户端需要处理的,在服务端崩溃后,客户端要能收到服务端的状态,从而及时的进行重连。
解决方法通常是客户端定时的发送一个数据包来检测服务端进程是否成活。也就是常用的心跳包。