UNIX网络编程-TCP套接字编程(实战)
概述
TCP客户端/服务器程序示例是执行如下步骤的一个回射服务器:
- 客户端从标准输入读入一行文本,并写给服务器。
- 服务器从网络输入读入这行文本,并回射给客户端。
- 客户端从网络输入读入这行回射文本,并显示在标准输出上。
TCP服务器程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#define MAXLINE 4096
#define SERV_PORT 9877
#define LISTENQ 1024
#define SA struct sockaddr
// 从客户端读入数据,并把它们回射给客户端
void str_echo(int sockfd) {
ssize_t n;
char buf[MAXLINE];
again:
// 从套接字读入数据
// 套接字中接收缓冲区和发送缓冲区是分开的,因此读和写不会发生混淆
while ((n = read(sockfd, buf, MAXLINE)) > 0)
write(sockfd, buf, n); // 把套接字中的内容回射给客户端
// 如果n<0表示读取数据出错或到达文件末尾
// 如果errno等于EINTR,表示读取操作被信号中断
// 如果上述两个条件同时满足,则重新尝试读取数据
if (n < 0 && errno == EINTR)
goto again;
// 如果表示文件描述符到达文件末尾
else if (n < 0)
printf("str_echo: read error");
}
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
/* --------------------------------------------- */
//1) 创建一个TCP连接套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
printf("socket error");
return -1;
}
/* --------------------------------------------- */
//2) 把服务器对应端口绑定到套接字
bzero(&servaddr, sizeof(servaddr)); // 开辟内存
servaddr.sin_family = AF_INET; // 地址族
// 指定IP地址为INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任意网络接口上接受客户端连接
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if (bind(listenfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {
printf("bind error");
return -1;
}
/* --------------------------------------------- */
//3) 把套接字转换为监听套接字
// LISTENQ表示系统内核允许在这个监听描述符上排队的最大客户端连接数
if(listen(listenfd, LISTENQ) < 0) {
printf("listen error");
return -1;
}
/* --------------------------------------------- */
//4) 接受客户端连接,发送应答
for ( ; ; ) {
clilen = sizeof(cliaddr);
// connfd为已连接描述符,用于和客户端进行通信
connfd = accept(listenfd, (SA *) &cliaddr, &clilen);
if(connfd < 0) {
printf("accept error");
return -1;
}
if ((childpid = fork()) == 0) {
// 子进程关闭监听套接字
if (close(listenfd) == -1) {
printf("child close listenfd error");
return -1;
}
str_echo(connfd); // 子进程处理客户端请求
exit(0); // 清理描述符
}
/* --------------------------------------------- */
//5) 父进程关闭已连接套接字
if (close(connfd) == -1) {
printf("parent close connfd error");
return -1;
}
}
}
TCP客户端程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h> /* basic socket definitions */
#define MAXLINE 4096
#define SERV_PORT 9877
#define SA struct sockaddr
char *Fgets(char *ptr, int n, FILE *stream)
{
char *rptr;
// 当遇到文件结束符或错误时,fgets函数将返回一个空指针,于是客户端处理循环终止
if ( (rptr = fgets(ptr, n, stream)) == NULL && ferror(stream)) {
printf("fgets error");
return NULL;
}
return (rptr);
}
ssize_t readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = read(fd, &c, 1)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
if (n == 1)
return(0); /* EOF, no data read */
else
break; /* EOF, some data was read */
} else
return(-1); /* error */
}
*ptr = 0;
return(n);
}
/* end readline */
void str_cli(FILE *fp, int sockfd) {
char sendline[MAXLINE], recvline[MAXLINE];
// 从控制台读入一行文本
while (Fgets(sendline, MAXLINE, fp) != NULL) {
// 把该行文本发送给服务器
if (write(sockfd, sendline, strlen(sendline)) != strlen(sendline)) {
printf("writen error");
return;
}
// 从服务器读入回射行
if (readline(sockfd, recvline, MAXLINE) < 0){
printf("readline error");
return;
}
// 把它写到标准输出
if (fputs(recvline, stdout) == EOF) {
printf("fputs error");
return;
}
}
}
int main(int argc, char **argv)
{
int sockfd;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
if (argc != 2)
exit(1);
/* --------------------------------------------- */
//1) 创建一个TCP连接套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
printf("socket error");
return -1;
}
/* --------------------------------------------- */
//2) 指定服务器的IP地址和端口
bzero(&servaddr, sizeof(servaddr)); // 初始化内存
servaddr.sin_family = AF_INET; // 地址族
servaddr.sin_port = htons(SERV_PORT); // 时间获取服务器端口为13
// 注意:此处的IP和端口是服务器的IP和端口
// 把点分十进制的IP地址(如:206.168.112.96)转化为合适的格式
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
printf("inet_pton error for %s", argv[1]);
return -1;
}
/* --------------------------------------------- */
//3) 建立客户端(sockfd)与服务器(servaddr)的连接,TCP连接
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {
printf("connect error");
return -1;
}
// 完成剩余部分的客户端处理工作
str_cli(stdin, sockfd);
/* --------------------------------------------- */
//5) 终止程序运行,关闭该进程打开的所有描述符和TCP套接字
exit(0);
}
正常启动
1)启动TCP服务器程序
gcc -o tcpserv tcpserv.c
gcc -o tcpcli tcpcli.c
./tcpserv &
服务器启动后,它调用socked、bind、listen和accept,并阻塞于accept调用。
2)启动TCP客户端程序
./tcpcli 127.0.0.1
// 输入字符串
kaikaixinxinxuebiancheng
启动客户端程序并指定服务器主机的IP地址。客户端调用socket和connect,后者引起TCP三次握手过程。当三次握手完成后,客户端中的connect和服务器中的accept均返回,连接于是被建立。
接着发生步骤如下:
- 客户端调用str_cli函数,该函数将阻塞于fgets调用,因为我们还未曾键入过一行文本。
- 当服务器中的accept返回时,服务器调用fork,再由子进程调用str_echo。该函数调用readline,readline调用read,而read在等待客户端送入一行文本期间阻塞。
- 服务器父进程再次调用accept并阻塞,等待下一个客户端连接。
连接建立后,不论在客户端中输入什么,都会回射到它的标准输出中。
接着在终端输入EOF字符(Ctrl+D)以终止客户端。
此时如果立刻执行netstat命令,则将看到如下结果:
// 服务器本地端口为9877,客户端本地端口为42758
netstat -a | grep 9877
当前连接的客户端(它的本地端口号为42758)进入了TIME_WAIT状态,而监听服务器仍在等待另一个客户端连接。
正常终止
正常终止客户端与服务器步骤:
1)当键入EOF字符时,fgets返回一个空指针,于是str_cli函数返回。
2)当str_cli返回到客户端的main函数时,main通过调用exit终止。
3)进程终止处理的部分工作是关闭所有打开的描述符,因此客户端打开的套接字由内核关闭。这导致客户端TCP发送一个FIN给服务器,服务器则以ACK响应,这就是TCP连接终止序列的前半部分。至此,服务器套接字处于CLOSE_WAIT状态,客户端套接字则处于FIN_WAIT_2状态。
4)当服务器TCP接收FIN时,服务器子进程阻塞于read调用,于是read返回0,这导致str_echo函数返回服务器子进程的main函数。
5)服务器子进程通过调用exit来终止。
6)服务器子进程中打开的所有描述符(包括已连接套接字)随之关闭。子进程关闭已连接套接字时会引发TCP连接终止序列的最后两个分节:一个从服务器到客户端的FIN和一个从客户端到服务器的ACK。至此,连接完全终止,客户端套接字进入TIME_WAIT状态(允许老的重复分节在网络中消逝)。
7)进程终止处理的另一部分内容是:在服务器进程终止时,给父进程发送一个SIGCHLD信号,这一点在上述程序示例中发生了,但是没有在代码中捕获该信号,而信号的默认行为是被忽略。既然父进程未加处理,子进程于是进入僵死状态(僵尸进程)。可以通过ps命令进行验证:
// 查看当前终端编号
tty
// 查看子进程状态
ps -t /dev/pts/0 -o pid,ppid,tty,stat,args,wchan
查看结果:
子进程状态表现为Z(表示僵死)。针对僵死进程(僵尸进程),必须清理。
POSIX信号处理
信号(signal)就是告知某个进程发生了某个事件的通知,有时也称为软件中断。信号通常是异步发生的,也就是说进程预先不知道信号的准确发生时刻。
注意:
1)信号可以由一个进程发给另一个进程(或自身)。
2)信号可以由内核发给某个进程。
上一小节提到的SIGCHLD信号就是由内核在任何一个进程终止时发给它的父进程的一个信号。
每个信号都有一个与之关联的处置,也称为行为。
SIGCHLD信号处理
思考:为什么必须要处理僵死进程?
答:因为僵死进程占用内核空间,最终可能导致耗尽进程资源。所以,无论何时针对fork出来的子进程都得使用wait函数处理它们,以防止它们变为僵死进程。
TCP服务器程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAXLINE 4096
#define SERV_PORT 9877
#define LISTENQ 1024
#define SA struct sockaddr
typedef void Sigfunc(int); /* for signal handlers */
// SIGCHLD信号处理函数,防止子进程变为僵死进程
void sig_chld(int signo)
{
pid_t pid;
int stat;
// 等待子进程结束,并获取子进程的PID和退出状态
pid = wait(&stat);
// 在此处调用诸如printf这样的标准I/O是不合适的,此处只是作为查看子进程何时终止的诊断手段
printf("child %d terminated\n", pid);
return;
}
Sigfunc *signal(int signo, Sigfunc *func)
{
// 定义信号动作
struct sigaction act, oact;
act.sa_handler = func; // 设置信号处理函数
sigemptyset(&act.sa_mask); // 清空信号掩码集
act.sa_flags = 0; // 设置信号处理方式为默认
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif
} else {
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
/* end signal */
// 捕捉指定信号并采取行动
Sigfunc *Signal(int signo, Sigfunc *func) /* for our signal() function */
{
Sigfunc *sigfunc;
if ( (sigfunc = signal(signo, func)) == SIG_ERR) {
printf("signal error");
}
return(sigfunc);
}
// 从客户端读入数据,并把它们回射给客户端
void str_echo(int sockfd) {
ssize_t n;
char buf[MAXLINE];
again:
// 从套接字读入数据
// 套接字中接收缓冲区和发送缓冲区是分开的,因此读和写不会发生混淆
while ((n = read(sockfd, buf, MAXLINE)) > 0)
write(sockfd, buf, n); // 把套接字中的内容回射给客户端
// 如果n<0表示读取数据出错或到达文件末尾
// 如果errno等于EINTR,表示读取操作被信号中断
// 如果上述两个条件同时满足,则重新尝试读取数据
if (n < 0 && errno == EINTR)
goto again;
// 如果表示文件描述符到达文件末尾
else if (n < 0)
printf("str_echo: read error");
}
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
/* --------------------------------------------- */
//1) 创建一个TCP连接套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
printf("socket error");
return -1;
}
/* --------------------------------------------- */
//2) 把服务器对应端口绑定到套接字
bzero(&servaddr, sizeof(servaddr)); // 开辟内存
servaddr.sin_family = AF_INET; // 地址族
// 指定IP地址为INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任意网络接口上接受客户端连接
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if (bind(listenfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {
printf("bind error");
return -1;
}
/* --------------------------------------------- */
//3) 把套接字转换为监听套接字
// LISTENQ表示系统内核允许在这个监听描述符上排队的最大客户端连接数
if(listen(listenfd, LISTENQ) < 0) {
printf("listen error");
return -1;
}
// 捕捉指定信号并采取行动
Signal(SIGCHLD, sig_chld); /* must call waitpid() */
/* --------------------------------------------- */
//4) 接受客户端连接,发送应答
for ( ; ; ) {
clilen = sizeof(cliaddr);
// connfd为已连接描述符,用于和客户端进行通信
connfd = accept(listenfd, (SA *) &cliaddr, &clilen);
if(connfd < 0) {
if (errno == EINTR) {
continue; // 重启被中断的accept
} else {
printf("accept error");
return -1;
}
}
if ((childpid = fork()) == 0) {
// 子进程关闭监听套接字
if (close(listenfd) == -1) {
printf("child close listenfd error");
return -1;
}
str_echo(connfd); // 子进程处理客户端请求
exit(0); // 清理描述符
}
/* --------------------------------------------- */
//5) 父进程关闭已连接套接字
if (close(connfd) == -1) {
printf("parent close connfd error");
return -1;
}
}
}
注意:如果connect函数返回EINTR,则不能重启,否则将立即返回一个错误。当connect被一个捕获的信号中断而且不自动重启时,必须调用select来等待连接完成。
TCP客户端程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h> /* basic socket definitions */
#define MAXLINE 4096
#define SERV_PORT 9877
#define SA struct sockaddr
char *Fgets(char *ptr, int n, FILE *stream)
{
char *rptr;
// 当遇到文件结束符或错误时,fgets函数将返回一个空指针,于是客户端处理循环终止
if ( (rptr = fgets(ptr, n, stream)) == NULL && ferror(stream)) {
printf("fgets error");
return NULL;
}
return (rptr);
}
ssize_t readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = read(fd, &c, 1)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
if (n == 1)
return(0); /* EOF, no data read */
else
break; /* EOF, some data was read */
} else
return(-1); /* error */
}
*ptr = 0;
return(n);
}
/* end readline */
void str_cli(FILE *fp, int sockfd) {
char sendline[MAXLINE], recvline[MAXLINE];
// 从控制台读入一行文本
while (Fgets(sendline, MAXLINE, fp) != NULL) {
// 把该行文本发送给服务器
if (write(sockfd, sendline, strlen(sendline)) != strlen(sendline)) {
printf("writen error");
return;
}
// 从服务器读入回射行
if (readline(sockfd, recvline, MAXLINE) < 0){
printf("readline error");
return;
}
// 把它写到标准输出
if (fputs(recvline, stdout) == EOF) {
printf("fputs error");
return;
}
}
}
int main(int argc, char **argv)
{
int sockfd;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
if (argc != 2)
exit(1);
/* --------------------------------------------- */
//1) 创建一个TCP连接套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
printf("socket error");
return -1;
}
/* --------------------------------------------- */
//2) 指定服务器的IP地址和端口
bzero(&servaddr, sizeof(servaddr)); // 初始化内存
servaddr.sin_family = AF_INET; // 地址族
servaddr.sin_port = htons(SERV_PORT); // 时间获取服务器端口为13
// 注意:此处的IP和端口是服务器的IP和端口
// 把点分十进制的IP地址(如:206.168.112.96)转化为合适的格式
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
printf("inet_pton error for %s", argv[1]);
return -1;
}
/* --------------------------------------------- */
//3) 建立客户端(sockfd)与服务器(servaddr)的连接,TCP连接
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {
printf("connect error");
return -1;
}
// 完成剩余部分的客户端处理工作
str_cli(stdin, sockfd);
/* --------------------------------------------- */
//5) 终止程序运行,关闭该进程打开的所有描述符和TCP套接字
exit(0);
}
执行流程
// 启动服务器程序
./tcpserv02 &
// 启动客户端程序
./tcpserv02 127.0.0.1
hi there
hi there
^D 键入EOF字符
child 16942 terminated 信号处理函数中的printf输出
accept error:Interrupted system call main函数终止执行
具体各步骤如下:
1)键入EOF字符终止客户端。客户端发送一个FIN给服务器,服务器响应一个ACK。
2)收到客户端的FIN导致服务器TCP递送一个EOF给子进程阻塞中的readline,从而子进程终止。
3)当SIGCHLD信号递交时,父进程阻塞与accept调用。sig_chld函数(信号处理函数)执行,其wait调用渠道子进程的PID和终止状态,随后是printf调用,最后返回。
4)既然该信号是在父进程阻塞于慢系统调用(accept)时由父进程捕获的,内核就会使accept返回一个EINTR错误(被中断的系统调用)。父进程不处理该错误,于是父进程中止,无法接受新的连接。
wait和waitpid函数
问1:什么是孤儿进程?什么是僵尸进程?二者分别会带来什么危害?
答:
1)孤儿进程:如果父进程在子进程结束前退出,那么子进程就会成为孤儿进程。在这种情况下,父进程没有机会调用wait或waitpid函数。每当出现一个孤儿进程的时候,内核就把孤儿进程交给init进程管理。即init进程会代替该孤儿进程的父进程回收孤儿进程的资源,因此孤儿进程并不会有什么危害。
2)僵尸进程:如果子进程结束时,父进程未调用wait或waitpid函数回收其资源,那么子进程就会称为僵尸进程。如果释放僵尸进程的相关资源,其进程号就会被一致占用,但是系统所能使用的进程号是有限的,如果产生大量的僵尸进程,最终将会因为没有可用的进程号而导致系统不能产生新的进程,所以应该避免僵尸进程的产生。
问2:为什么父进程需要在fork之前调用wait或waitpid函数等待子进程退出?
答:父进程使用fork函数创建子进程是为了处理多个客户端连接。fork会创建一个与父进程几乎完全相同的子进程,包括内存空间、文件描述符等。这样做的好处是父进程可以继续监听新的连接请求,而子进程可以专注于处理已接受的连接。因此,父进程调用wait或waitpid函数主要是为了防止出现僵尸进程。
wait和waitpid函数:
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
返回:若成功则返回已终止的进程ID,若出错则返回0或-1
函数wait和waitpid均返回两个值:已终止的进程ID号,以及通过statloc指针返回的子进程终止状态(一个整数)。
可以调用三个宏来检查终止状态,并辨别子进程是正常终止、由某个信号杀死还是仅仅由作业控制停止而已。另有些宏用于接着获取子进程的推出状态、杀死子进程的信号值或停止子进程的作业控制号值。
如果调用wait的进程没有已终止的子进程,不过有一个或多个子进程仍在执行,那么wait将阻塞到有子进程第一个终止为止。
wait和waitpid的区别
客户端程序
TCP客户端程序修改后:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h> /* basic socket definitions */
#define MAXLINE 4096
#define SERV_PORT 9877
#define SA struct sockaddr
char *Fgets(char *ptr, int n, FILE *stream)
{
char *rptr;
// 当遇到文件结束符或错误时,fgets函数将返回一个空指针,于是客户端处理循环终止
if ( (rptr = fgets(ptr, n, stream)) == NULL && ferror(stream)) {
printf("fgets error");
return NULL;
}
return (rptr);
}
ssize_t readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = read(fd, &c, 1)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
if (n == 1)
return(0); /* EOF, no data read */
else
break; /* EOF, some data was read */
} else
return(-1); /* error */
}
*ptr = 0;
return(n);
}
/* end readline */
void str_cli(FILE *fp, int sockfd) {
char sendline[MAXLINE], recvline[MAXLINE];
// 从控制台读入一行文本
while (Fgets(sendline, MAXLINE, fp) != NULL) {
// 把该行文本发送给服务器
if (write(sockfd, sendline, strlen(sendline)) != strlen(sendline)) {
printf("writen error");
return;
}
// 从服务器读入回射行
if (readline(sockfd, recvline, MAXLINE) < 0){
printf("readline error");
return;
}
// 把它写到标准输出
if (fputs(recvline, stdout) == EOF) {
printf("fputs error");
return;
}
}
}
int main(int argc, char **argv)
{
int sockfd[5];
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
if (argc != 2)
exit(1);
for (int i = 0; i < 5; i++) {
/* --------------------------------------------- */
//1) 创建一个TCP连接套接字
sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
printf("socket error");
return -1;
}
/* --------------------------------------------- */
//2) 指定服务器的IP地址和端口
bzero(&servaddr, sizeof(servaddr)); // 初始化内存
servaddr.sin_family = AF_INET; // 地址族
servaddr.sin_port = htons(SERV_PORT); // 时间获取服务器端口为13
// 注意:此处的IP和端口是服务器的IP和端口
// 把点分十进制的IP地址(如:206.168.112.96)转化为合适的格式
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
printf("inet_pton error for %s", argv[1]);
return -1;
}
/* --------------------------------------------- */
//3) 建立客户端(sockfd)与服务器(servaddr)的连接,TCP连接
if (connect(sockfd[i], (SA *) &servaddr, sizeof(servaddr)) < 0) {
printf("connect error");
return -1;
}
}
// 完成剩余部分的客户端处理工作
str_cli(stdin, sockfd[0]);
/* --------------------------------------------- */
//5) 终止程序运行,关闭该进程打开的所有描述符和TCP套接字
exit(0);
}
客户端建立5个与服务器的连接,随后在调用str_cli函数时仅用第一个连接(sockfd[0])。建立多个连接的目的是从并发服务器上派生多个子进程,如下图所示:
当客户端终止时,所有打开的文件描述符由内核自动关闭(无需调用close,仅调用exit),且所有5个连接基本在同一时刻终止。这就引发了5个FIN,每个连接一个,它们反过来使服务器的5个子进程基本在同一时刻终止。这又导致差不多在同一时刻有5个SIGCHLD信号递交给父进程,如图所示:
注意:如上所述,由于调用了exit函数,5个连接几乎同时产生SIGCHLD信号,即多个SIGCHLD信号同时递交给服务器。
测试结果
./tcpserv & 启动服务器程序
./tcpcli 127.0.0.1 启动客户端程序
hello
hello
^D 键入EOF字符
child 31591 terminated 服务器输出
从执行结果可以看出,只有一个printf输出而并非5个,即信号处理函数只处理了一个SIGCHLD信号,剩下四个子进程变为僵尸进程。
问1:为什么只处理了一个SIGCHLD信号?
答:建立一个信号处理函数并在其中调用wait并不足以防止出现僵尸进程。因为所有5个信号都在信号处理函数执行之前产生,而信号处理函数只执行一次,因为Unix信号一般不排队。更严重的是,本问题是不确定的。因为本实验是在同一个主机上,信号处理函数执行1次,留下4个僵尸进程。但是如果客户端程序和服务端程序不在同一个主机上,那么信号处理函数一般执行2次:一次是第一个产生的信号引起的,由于另外4个信号在信号处理函数第一次执行时发生,因此该处理函数仅仅再被调用一次,从而留下3个僵尸进程。不过有的时候,依赖于FIN到达服务器主机的时机,信号处理函数可能会执行3次甚至4次。
问2:如何让信号处理函数调用多次,以防止出现僵尸进程?
答:调用waitpid而不是wait函数。当在一个循环内调用waitpid,以获取所有已终止子进程的状态时,必须指定WNOHANG选项,它告知waitpid在有尚未终止的子进程在运行时不要阻塞。不能在循环内调用wait,因为没有办法防止wait在正运行的子进程尚有未终止时阻塞。
服务端程序
修改后的服务端程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAXLINE 4096
#define SERV_PORT 9877
#define LISTENQ 1024
#define SA struct sockaddr
typedef void Sigfunc(int); /* for signal handlers */
// SIGCHLD信号处理函数,防止子进程变为僵死进程
void sig_chld(int signo)
{
pid_t pid;
int stat;
// 等待子进程结束,并获取子进程的PID和退出状态
while (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
// 在此处调用诸如printf这样的标准I/O是不合适的,此处只是作为查看子进程何时终止的诊断手段
printf("child %d terminated\n", pid);
}
return;
}
Sigfunc *signal(int signo, Sigfunc *func)
{
// 定义信号动作
struct sigaction act, oact;
act.sa_handler = func; // 设置信号处理函数
sigemptyset(&act.sa_mask); // 清空信号掩码集
act.sa_flags = 0; // 设置信号处理方式为默认
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif
} else {
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
/* end signal */
// 捕捉指定信号并采取行动
Sigfunc *Signal(int signo, Sigfunc *func) /* for our signal() function */
{
Sigfunc *sigfunc;
if ( (sigfunc = signal(signo, func)) == SIG_ERR) {
printf("signal error");
}
return(sigfunc);
}
// 从客户端读入数据,并把它们回射给客户端
void str_echo(int sockfd) {
ssize_t n;
char buf[MAXLINE];
again:
// 从套接字读入数据
// 套接字中接收缓冲区和发送缓冲区是分开的,因此读和写不会发生混淆
while ((n = read(sockfd, buf, MAXLINE)) > 0)
write(sockfd, buf, n); // 把套接字中的内容回射给客户端
// 如果n<0表示读取数据出错或到达文件末尾
// 如果errno等于EINTR,表示读取操作被信号中断
// 如果上述两个条件同时满足,则重新尝试读取数据
if (n < 0 && errno == EINTR)
goto again;
// 如果表示文件描述符到达文件末尾
else if (n < 0)
printf("str_echo: read error");
}
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
/* --------------------------------------------- */
//1) 创建一个TCP连接套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
printf("socket error");
return -1;
}
/* --------------------------------------------- */
//2) 把服务器对应端口绑定到套接字
bzero(&servaddr, sizeof(servaddr)); // 开辟内存
servaddr.sin_family = AF_INET; // 地址族
// 指定IP地址为INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任意网络接口上接受客户端连接
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if (bind(listenfd, (SA *) &servaddr, sizeof(servaddr)) < 0) {
printf("bind error");
return -1;
}
/* --------------------------------------------- */
//3) 把套接字转换为监听套接字
// LISTENQ表示系统内核允许在这个监听描述符上排队的最大客户端连接数
if(listen(listenfd, LISTENQ) < 0) {
printf("listen error");
return -1;
}
// 捕捉指定信号并采取行动
Signal(SIGCHLD, sig_chld); /* must call waitpid() */
/* --------------------------------------------- */
//4) 接受客户端连接,发送应答
for ( ; ; ) {
clilen = sizeof(cliaddr);
// connfd为已连接描述符,用于和客户端进行通信
connfd = accept(listenfd, (SA *) &cliaddr, &clilen);
if(connfd < 0) {
if (errno == EINTR) {
continue; // 重启被中断的accept
} else {
printf("accept error");
return -1;
}
}
if ((childpid = fork()) == 0) {
// 子进程关闭监听套接字
if (close(listenfd) == -1) {
printf("child close listenfd error");
return -1;
}
str_echo(connfd); // 子进程处理客户端请求
exit(0); // 清理描述符
}
/* --------------------------------------------- */
//5) 父进程关闭已连接套接字
if (close(connfd) == -1) {
printf("parent close connfd error");
return -1;
}
}
}
小结
问:SIGCHLD信号是怎么产生的,有什么作用?
答:SIGCHLD 信号是由操作系统产生的,当一个子进程结束(无论是正常退出还是被终止)时,操作系统都会向父进程发送这个信号。这个信号的目的是通知父进程子进程的状态已经改变,父进程可以采取相应的行动,比如回收子进程使用的资源。
注意:父进程调用wait函数时会阻塞整个父进程的执行,直到某一个或几个子进程结束,才会结束阻塞。上述服务器程序是通过异步调用wait函数,所以看上去不是那么直观,非异步调用wait如下:
for ( ; ; ) {
clilen = sizeof(cliaddr);
// connfd为已连接描述符,用于和客户端进行通信
connfd = accept(listenfd, (SA *) &cliaddr, &clilen);
if(connfd < 0) {
if (errno == EINTR) {
continue; // 重启被中断的accept
} else {
printf("accept error");
return -1;
}
}
if ((childpid = fork()) == 0) {
// 子进程关闭监听套接字
if (close(listenfd) == -1) {
printf("child close listenfd error");
return -1;
}
str_echo(connfd); // 子进程处理客户端请求
exit(0); // 清理描述符
}
// 等待子进程结束并回收子进程资源
int status;
wait(&status);
/* --------------------------------------------- */
//5) 父进程关闭已连接套接字
if (close(connfd) == -1) {
printf("parent close connfd error");
return -1;
}
}
UNIX网络编程总结:
1)当fork子进程时,必须捕获SIGCHLD信号。
2)当捕获信号时,父进程必须处理被中断的系统调用,如accept函数。
3)SIGCHLD的信号处理函数必须正确书写,并使用waitpid函数以免留下僵尸进程。
如果需要代码包,请在评论区留言!!!
如果需要代码包,请在评论区留言!!!
如果需要代码包,请在评论区留言!!!