当前位置: 首页 > news >正文

UNIX网络编程-TCP套接字编程

概述


TCP客户端/服务器程序示例是执行如下步骤的一个回射服务器:

  1. 客户端从标准输入读入一行文本,并写给服务器。
  2. 服务器从网络输入读入这行文本,并回射给客户端。
  3. 客户端从网络输入读入这行回射文本,并显示在标准输出上。

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 */elsebreak;      /* EOF, some data was read */} elsereturn(-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均返回,连接于是被建立。

接着发生步骤如下:

  1. 客户端调用str_cli函数,该函数将阻塞于fgets调用,因为我们还未曾键入过一行文本。
  2. 当服务器中的accept返回时,服务器调用fork,再由子进程调用str_echo。该函数调用readline,readline调用read,而read在等待客户端送入一行文本期间阻塞。
  3. 服务器父进程再次调用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 sockaddrtypedef 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_INTERRUPTact.sa_flags |= SA_INTERRUPT;   /* SunOS 4.x */
#endif} else {
#ifdef  SA_RESTARTact.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 */elsebreak;      /* EOF, some data was read */} elsereturn(-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 */elsebreak;      /* EOF, some data was read */} elsereturn(-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 sockaddrtypedef 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_INTERRUPTact.sa_flags |= SA_INTERRUPT;   /* SunOS 4.x */
#endif} else {
#ifdef  SA_RESTARTact.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函数以免留下僵尸进程。

如果需要代码包,请在评论区留言!!! 

如果需要代码包,请在评论区留言!!! 

如果需要代码包,请在评论区留言!!! 

相关文章:

UNIX网络编程-TCP套接字编程

概述 TCP客户端/服务器程序示例是执行如下步骤的一个回射服务器&#xff1a; 客户端从标准输入读入一行文本&#xff0c;并写给服务器。服务器从网络输入读入这行文本&#xff0c;并回射给客户端。客户端从网络输入读入这行回射文本&#xff0c;并显示在标准输出上。 TCP服务器…...

美团代付微信小程序 read.php 任意文件读取漏洞复现

0x01 产品描述&#xff1a; ‌ 美团代付微信小程序‌是美团点评旗下的一款基于微信小程序技术开发的应用程序功能&#xff0c;它允许用户方便快捷地请求他人为自己支付订单费用。通过微信小程序&#xff0c;用户可以轻松实现代付操作&#xff0c;无需跳转到其他应用或网页…...

centos7 node升级到node18

使用jenkins发布vue3项目提示node18安装失败 错误日志&#xff1a; /var/lib/jenkins/tools/jenkins.plugins.nodejs.tools.NodeJSInstallation/Node18/bin/node: /lib64/libm.so.6: version GLIBC_2.27 not found (required by /var/lib/jenkins/tools/jenkins.plugins.node…...

使用Matlab建立随机森林

综述 除了神经网络模型以外&#xff0c;树模型及基于树的集成学习模型是较为常用的效果较好的预测模型。我们以下构建一个随机森林模型。 随机森林是一种集成学习方法&#xff0c;通过构建多个决策树并结合其预测结果来提高模型的准确性和稳定性。在MATLAB中&#xff0c;可以…...

【江苏-无锡】《无锡市市级政务信息化建设及运行维护项目支出预算标准(试行)》 -省市费用标准解读系列10

2023年3月28日&#xff0c;无锡市政府投资评审中心出台《无锡市市级政务信息化建设及运行维护项目支出预算标准&#xff08;试行&#xff09;》&#xff0c;此标准是为进一步提高财政评审效率&#xff0c;规范评审行为&#xff0c;保证评审质量&#xff0c;节约政府投资&#x…...

2024-11-12 问AI: [AI面试题] 您将如何设计一个人工智能系统来预测电信公司的客户流失?

文心一言 设计一个人工智能系统来预测电信公司的客户流失&#xff0c;是一个复杂但至关重要的任务&#xff0c;它涉及到多个步骤和关键要素。以下是一个详细的设计方案&#xff1a; 一、明确需求和目标 在设计智能系统之前&#xff0c;需要明确系统的需求和目标&#xff0c;…...

【数字静态时序分析】复杂时钟树的时序约束SDC写法

以上图为例&#xff0c;SoC芯片上往往存在几种不同的时钟源&#xff0c;有pll时钟、环振时钟、外部的晶振时钟&#xff0c;在SoC不同的模块或者不同的运行阶段使用的时钟也往往不同&#xff0c;所以在使用的时候&#xff0c;相同的模块会出现选择不同的时钟源的情况。上图的情形…...

springboot苍穹外卖实战:五、公共字段自动填充(aop切面实现)+新增菜品功能+oss

公共字段自动填充 不足 比起瑞吉外卖中的用自定义元数据类型mybatisplus的实现&#xff0c;这里使用的是aop切面实现&#xff0c;会麻烦许多&#xff0c;建议升级为mp。 定义好数据库操作类型 sky-common中已经定义好&#xff0c;OperationType。 自定义注解 AutoFill co…...

Go 语言中,golang结合 PostgreSQL 、MySQL驱动 开启数据库事务

Go 语言中&#xff0c;golang结合 PostgreSQL 、MySQL驱动 开启数据库事务 PostgreSQL代码说明&#xff1a; MySQL代码说明&#xff1a; PostgreSQL 在 Go 语言中&#xff0c;使用 database/sql 包结合 PostgreSQL 驱动&#xff08;如 github.com/lib/pq&#xff09;可以方便地…...

Git核心概念

目录 版本控制 什么是版本控制 为什么要版本控制 本地版本控制系统 集中化的版本控制系统 分布式版本控制系统 认识Git Git简史 Git与其他版本管理系统的主要区别 Git的三种状态 Git使用快速入门 获取Git仓库 记录每次更新到仓库 一个好的 Git 提交消息如下&#…...

网络安全技术在能源领域的应用

摘要 随着信息技术的飞速发展&#xff0c;能源领域逐渐实现了数字化、网络化和智能化。然而&#xff0c;这也使得能源系统面临着前所未有的网络安全威胁。本文从技术的角度出发&#xff0c;探讨了网络安全技术在能源领域的应用&#xff0c;分析了能源现状面临的网络安全威胁&a…...

这些场景不适合用Selenium自动化!看看你踩过哪些坑?

Selenium是自动化测试中的一大主力工具&#xff0c;其强大的网页UI自动化能力&#xff0c;让测试人员可以轻松模拟用户操作并验证系统行为。然而&#xff0c;Selenium并非万能&#xff0c;尤其是在某些特定场景下&#xff0c;可能并不适合用来自动化测试。本文将介绍Selenium不…...

PHP反序列化靶场(php-SER-libs-main 第一部分)

此次靶场为utools-php-unserialize-main。适合有一定基础的师傅&#xff0c;内容是比较全面的&#xff0c;含有我们的大部分ctf中PHP反序列化的题型。 level1&#xff1a; <?php highlight_file(__FILE__); class a{var $act;function action(){eval($this->act);} } …...

基于大数据爬虫+Python+SpringBoot+Hive的网络电视剧收视率分析与可视化平台系统(源码+论文+PPT+部署文档教程等)

博主介绍&#xff1a;CSDN毕设辅导第一人、全网粉丝50W,csdn特邀作者、博客专家、腾讯云社区合作讲师、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术范围…...

DHCP与FTP

DHCP dhcp&#xff1a;动态主机配置的协议&#xff0c;应用在大型的局域网环境中 服务端和客户端 服务端&#xff1a;提供IP地址&#xff0c;某种特定功能的提供者 客户端&#xff1a;请求IP地址&#xff0c;请求对应的功能的使用者 服务端的端口号&#xff1a;67 客户端的端…...

云渲染与云电脑,应用场景与技术特点全对比

很多朋友问&#xff0c;你们家一会宣传云渲染&#xff0c;一会宣传云电脑的&#xff0c;我到底用哪个&#xff1f;今天&#xff0c;渲染101云渲染和川翔云电脑就来对比下两者的区别&#xff01; 渲染101&川翔云电脑&#xff0c;都是我们的产品&#xff0c;邀请码6666 一、…...

RockPI 4A单板Linux 4.4内核下的RK3399 GPIO功能解析

RockPI 4A单板Linux 4.4内核下的RK3399 GPIO功能解析 摘要&#xff1a;本文将基于RockPI 4A单板&#xff0c;介绍Linux 4.4内核下RK3399 GPIO&#xff08;通用输入输出&#xff09;功能的使用方法。通过详细的代码解析和示例&#xff0c;帮助读者理解如何在Linux内核中使用GPI…...

【Vue】Vue3.0(二十三)Vue3.0中$attrs 的概念和使用场景

文章目录 一、$attrs的概念和使用场景概念使用场景 二、代码解释Father.vueChild.vueGrandChild.vue 三、另一个$attrs使用的例子 一、$attrs的概念和使用场景 概念 在Vue 3.0中&#xff0c;$attrs是一个组件实例属性&#xff0c;它包含了父组件传递给子组件的所有非props属性…...

RHEL/CENTOS 7 ORACLE 19C-RAC安装(纯命令版)

一 首先需要安装两个CENTOS 7虚拟机(此处省略)。 由于我们是要安装ORCLE-RAC双节点集群所以至少每个CENTOS虚拟机上需要两块网卡&#xff0c;并且两块网卡都是HOST-ONLY具体步骤请看视频一《为虚拟机添加网卡》 这里大家需要注意的是&#xff0c;我们需要绑定两台机器的IP一共…...

CCSK:面试云计算岗的高频问题

在竞争激烈的云计算岗位求职市场中&#xff0c;拥有 CCSK云计算安全知识认证无疑能为你增添强大的竞争力。而深入了解云计算面试中的高频问题并熟练掌握答案&#xff0c;更是迈向成功的关键一步。 一、AWS 相关问题 AWS 是重要考点&#xff0c;常被问到其关键特性&#xff0c…...

python版若依框架开发:前端开发规范

python版若依框架开发 从0起步,扬帆起航。 python版若依部署代码生成指南,迅速落地CURD!项目结构解析前端开发规范文章目录 python版若依框架开发新增 view新增 api新增组件新增样式引⼊依赖新增 view 在 @/views文件下 创建对应的文件夹,一般性一个路由对应⼀个文件, 该…...

【JJ斗地主-注册安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞 …...

HA: Wordy靶场

HA: Wordy 来自 <HA: Wordy ~ VulnHub> 1&#xff0c;将两台虚拟机网络连接都改为NAT模式 2&#xff0c;攻击机上做namp局域网扫描发现靶机 nmap -sn 192.168.23.0/24 那么攻击机IP为192.168.23.128&#xff0c;靶场IP192.168.23.130 3&#xff0c;对靶机进行端口服务探…...

最新Spring Security实战教程(十七)企业级安全方案设计 - 多因素认证(MFA)实现

&#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Micro麦可乐的博客 &#x1f425;《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程&#xff0c;入门到实战 &#x1f33a;《RabbitMQ》…...

YOLO在QT中的完整训练、验证与部署方案

以下是YOLO在QT中的完整训练、验证与部署方案&#xff1a; 训练方案 准备数据集&#xff1a; 收集数据&#xff1a;收集与目标检测任务相关的图像数据集&#xff0c;可以是公开数据集如COCO、Pascal VOC&#xff0c;也可以是自定义数据集。标注数据&#xff1a;使用标注工具如…...

【高效开发工具系列】Blackmagic Disk Speed Test for Mac:专业硬盘测速工具

博客目录 一、Blackmagic Disk Speed Test 概述二、软件核心功能解析三、v3.3 版本的新特性与改进四、实际应用场景分析五、使用技巧与最佳实践六、与其他工具的比较及优势 一、Blackmagic Disk Speed Test 概述 Blackmagic Disk Speed Test 是 Mac 平台上广受专业人士青睐的一…...

字符串字典序最大后缀问题详解

字符串字典序最大后缀问题详解 一、问题定义与背景1.1 问题描述1.2 实际应用场景 二、暴力解法及其局限性2.1 暴力解法思路2.2 代码示例2.3 局限性分析 三、双指针算法&#xff1a;高效解决方案3.1 算法核心思想3.2 算法步骤3.3 代码实现3.4 与暴力解法对比 四、复杂度分析4.1 …...

【SSM】SpringBoot笔记2:整合Junit、MyBatis

前言&#xff1a; 文章是系列学习笔记第9篇。基于黑马程序员课程完成&#xff0c;是笔者的学习笔记与心得总结&#xff0c;供自己和他人参考。笔记大部分是对黑马视频的归纳&#xff0c;少部分自己的理解&#xff0c;微量ai解释的内容&#xff08;ai部分会标出&#xff09;。 …...

1.3 古典概型和几何概型

文章目录 古典概型模型(等可能模型)几何概型 古典概型模型(等可能模型) 两个条件&#xff1a; 1) 有限个样本点 2) 等可能性 例题&#xff1a; 设有n个人&#xff0c;每个人都等可能地被分配到N个房间中的任一间(n≤N)&#xff0c; 求下列事件的概率: (1)某指定的n间房…...

如何通过外网访问内网?哪个方案比较好用?跨网远程连接网络知识早知道

广域网&#xff08;英语&#xff1a;Wide Area Network&#xff0c;缩写为 WAN&#xff09;&#xff0c;又称外网、公网。是连接不同地区局域网或城域网计算机通信的远程网。 局域网(LAN)&#xff0c;全称为Local Area Network&#xff0c;指在较小的地理区域内&#xff0c;通…...