Unix网络编程(二、回显服务器代码分析和优化)

代码分析

代码如下:

回显服务端

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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

static const int MAXLINE = 4096;
static const int MAXLISTEN = 1024;
static const int PORT = 45678;

int e_socket(int domain, int type, int protocol);
int e_bind(int sockfd, sockaddr_in* myaddr, socklen_t addrlen);
int e_listen(int sockfd, int backlog);
int e_accept(int sockfd, sockaddr_in* myaddr, socklen_t *addrlen);
void my_echo(int connectfd);

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

int e_bind(int sockfd, sockaddr_in* myaddr, socklen_t addrlen){
int ret = bind(sockfd, (sockaddr*)myaddr, addrlen);
if (ret != 0){
close(sockfd);
exit(0);
}
return ret;
}

int e_listen(int sockfd, int backlog){
int ret = listen(sockfd, backlog);
if (ret != 0) {
close(sockfd);
exit(0);
}
return ret;
}

int e_accept(int sockfd, sockaddr_in* myaddr, socklen_t *addrlen){
int ret = accept(sockfd, (sockaddr*)myaddr, addrlen);
if (ret == -1)
{
printf("%d", errno);
}
return ret;
}

void my_echo(int connectfd){
ssize_t n;
char buf[MAXLINE] = {0};
while ((n = read(connectfd, buf, MAXLINE)) > 0) {
write(connectfd, buf, n);
memset(buf, 0, MAXLINE);
if (n < 0 && errno == EINTR)
continue;
}
}

int main(int argc, const char * argv[]) {

int listen_sock = e_socket(AF_INET, SOCK_STREAM, 0);

sockaddr_in servaddr, clientaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);

e_bind(listen_sock, &servaddr, sizeof(servaddr));
e_listen(listen_sock, MAXLISTEN);

socklen_t clientlen = 0;
int clientfd = -1;
while (true) {
clientlen = sizeof(clientaddr);
clientfd = e_accept(listen_sock, &clientaddr, &clientlen);
if (clientfd == -1){
continue;
}

if (fork() == 0) {
close(listen_sock);
my_echo(clientfd);
exit(0);
}
close(clientfd);
}
return 0;
}

回显客户端

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
#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, strlen(sendline));

read(sockfd, recvline, MAXLINE);
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 = 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;
}

首先运行service, 此时服务端会出去监听状态。

然后运行client,此时客户端调用 connect 与服务端进行三次握手,建立连接之后客户端的状态会变为ESTABLISHED态。服务端accept某来自某个客户端的连接之后,会fork一个子进程,在这个子进程中去处理客户端发送过来的数据,而本身则继续去监听有没有其他连接到来。
所以此时客户端和服务器的状态分别是

  1. 客户端 ESTABLISHED
  2. 服务端 LISTEN
  3. 服务端fork的子进程 ESTABLISHED

用ps命令查看一下进程的关系。

图上可以看到 105137 的进程为服务端进程,此时正处于accept状态。
105140为105137的子进程,也就是服务端fork出来的子进程,此时处于wait状态,它此时正阻塞在read函数上
105139为客户端进程,此时阻塞在fgets上等待用户输入

现在来讨论几种情况

正常终止


在客户端随便输入几行数据,服务端都成功的将数据回传给客户端,客户端将服务端传回来的数据显示出来。然后,在末尾键入EOF结束符(CTRL+D),结束客户端。

此时手速一定要快,在终端输入命令 netstat -a | grep 45678 观察服务端和客户端的状态。

客户端在收到EOF之后, my_cli函数中的fgets返回NULL,循环终止。因为我们没有主动调用close关闭socket,在进程结束之后内核帮我们主动回收打开的文件描述符(也就是将内核中维护的文件描述符的饮用计数减1),当这个文件描述符的引用计数为0时,内核将其close。调用close时,会向服务端发送一份FIN包,此时客户端的连接状态也就变成了TIME_WAIT

说完了客户端之后再来说服务端,服务端进程一直处于listen状态毋庸置疑,重点要说的是服务端fork出的子进程。

这个子进程一直执行的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
close(listen_sock);
my_echo(clientfd);
exit(0);

void my_echo(int connectfd){
ssize_t n;
char buf[MAXLINE] = {0};
while ((n = read(connectfd, buf, MAXLINE)) > 0) {
write(connectfd, buf, n);
memset(buf, 0, MAXLINE);
if (n < 0 && errno == EINTR)
continue;
}

子进程在收到客户端发送过来的FIN包后,进入四次挥手状态,当他们断开连接之后,read函数返回-1,循环结束,此时子进程调用exit结束进程。这个子进程虽然结束了,但其并没有彻底的消亡。在它的pcb中还存在着一份数据要交给他的父进程。这就是Linux中著名的僵尸进程。关于僵尸进程更深入的内容,可以参考我的博客。此时我们观察一下进程的状态来验证之前所说

很明显可以看到,105140号进程变为了 Z状态

当不断的有客户端连接和退出的时候,僵尸进程就会慢慢的占满内核空间,导致服务端再也无法创建进程,这个问题是必须要被解决的。

信号

信号的概念就是告知某个进程发生了某件事情,它的本质是软件中断。比如在子进程终止的时候,子进程会给父进程发送SIGCHLD信号,父进程只需要在收到该信号的时候wait子进程即可解决僵尸进程的问题。

信号通常是异步发生的。它可以由一个进程发给另一个进程,也可以由内核发给进程。接下来讲讲使用信号的基本函数。

sigaction

1
2
3
4
5
6
7
8
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);

struct sigaction{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flag;
void (*sa_sigaction)(int,siginfo_t*,void*);
};

先看一下结构体 sigaction
sa_handler 为信号处理函数
sa_mask 表示在当前信号处理函数被调用时,需要被阻塞的一组信号。
sa_flag 一个选项,有两个值比较常用

  1. SA_NODEFER:信号处理函数正在进行时,不堵塞对于信号处理函数自身信号功能
  2. SA_RESETHAND:当用户注册的信号处理函数被执行过一次后,该信号的处理函数被设为系统默认的处理函数。

sigaction 函数简单的理解就是在某个信号上绑定一个函数,当该信号到来时,就会执行我们自定义的处理函数中去。

处理僵尸进程

接下来就使用sigaction来捕获子进程的SIGCHLD信号,防止子进程变成僵尸进程。

信号处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef void signfun(int);
void sig_child(int signo);
signfun* sigaction_child(int signo, signfun *func);


void sig_child(int signo)
{
int stat, pid;
pid = wait(&stat);
}

signfun* sigaction_child(int signo, signfun *func)
{
struct sigaction act, oldact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;

if (sigaction(signo, &act, &oldact) < 0) {
return SIG_ERR;
}
return oldact.sa_handler;
}

完整代码如下

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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

static const int MAXLINE = 4096;
static const int MAXLISTEN = 1024;
static const int PORT = 45678;

typedef void signfun(int);

int e_socket(int domain, int type, int protocol);
int e_bind(int sockfd, sockaddr_in* myaddr, socklen_t addrlen);
int e_listen(int sockfd, int backlog);
int e_accept(int sockfd, sockaddr_in* myaddr, socklen_t *addrlen);
void my_echo(int connectfd);
void sig_child(int signo);
signfun* sigaction_child(int signo, signfun *func);

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

int e_bind(int sockfd, sockaddr_in* myaddr, socklen_t addrlen){
int ret = bind(sockfd, (sockaddr*)myaddr, addrlen);
if (ret != 0){
close(sockfd);
exit(0);
}
return ret;
}

int e_listen(int sockfd, int backlog){
int ret = listen(sockfd, backlog);
if (ret != 0) {
close(sockfd);
exit(0);
}
return ret;
}

int e_accept(int sockfd, sockaddr_in* myaddr, socklen_t *addrlen){
int ret = accept(sockfd, (sockaddr*)myaddr, addrlen);
if (ret == -1)
{
printf("%d", errno);
}
return ret;
}

void my_echo(int connectfd){
ssize_t n;
char buf[MAXLINE] = {0};
while ((n = read(connectfd, buf, MAXLINE)) > 0) {
write(connectfd, buf, n);

memset(buf, 0, MAXLINE);
if (n < 0 && errno == EINTR)
continue;
}
}

void sig_child(int signo)
{
int stat, pid;
pid = wait(&stat);
}

signfun* sigaction_child(int signo, signfun *func)
{
struct sigaction act, oldact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;

if (sigaction(signo, &act, &oldact) < 0) {
return SIG_ERR;
}
return oldact.sa_handler;
}

int main(int argc, const char * argv[]) {

int listen_sock = e_socket(AF_INET, SOCK_STREAM, 0);

sockaddr_in servaddr, clientaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);

e_bind(listen_sock, &servaddr, sizeof(servaddr));
e_listen(listen_sock, MAXLISTEN);
sigaction_child(SIGCHLD, sig_child);
socklen_t clientlen = 0;
int clientfd = -1;
while (true) {
clientlen = sizeof(clientaddr);
clientfd = accept(listen_sock, (sockaddr*)&clientaddr, &clientlen);
if (clientfd < 0) {
if (errno == EINTR) {
continue;
}
exit(0);
}

if (fork() == 0) {
close(listen_sock);
my_echo(clientfd);
exit(0);
}
close(clientfd);
}
return 0;
}

重新编译后执行结果如下,已经没有了Z进程

被中断的系统调用

前面我们解决了fork子进程引发的僵尸进程问题。但是也引发了一些新的问题。我们再来理一下执行流程

服务端

  1. 服务端首先打开soccet,然后accept阻塞等待客户端的连接
  2. 当客户端的连接到来之后,服务端fork子进程,用这个子进程与客户端进行通信,而本身任然阻塞在accept上继续等待其他客户端的连接到来
  3. 当某个客户端退出之后,服务端收到了SIGCLID 信号,调用该信号对应的处理程序,也就是调用wait。

客户端

  1. 打开socket
  2. connect
  3. 通信
  4. 断开连接

问题非常明显,就发生在服务端的第三步上。服务端进程本身是阻塞在accept上的,而当它接收到子进程退出的SIGCLID信号之后,触发了一个新的系统调用,那么服务端此时就会从原本的accept系统调用中返回,去执行新触发的系统调用。
此时accept函数会返回-1,并将errno置为4(EINTR, 被中断的系统调用)。如果我们的父进程没有处理这个错误,就会终止执行,在这种情况下,我们的服务端进程要忽略这个错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (true) {
clientlen = sizeof(clientaddr);
if ((clientfd = accept(listen_sock, (sockaddr*)&clientaddr, &clientlen) < 0) {
if (errno == EINTR) {
continue;
}
else {
exit(0);
}
}

if (fork() == 0) {
close(listen_sock);
my_echo(clientfd);
exit(0);
}
close(clientfd);
}