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 */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均返回,连接于是被建立。
接着发生步骤如下:
- 客户端调用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 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客户端/服务器程序示例是执行如下步骤的一个回射服务器: 客户端从标准输入读入一行文本,并写给服务器。服务器从网络输入读入这行文本,并回射给客户端。客户端从网络输入读入这行回射文本,并显示在标准输出上。 TCP服务器…...
python编写一个自动清理三个月以前的邮件脚本
以下是一个使用 Python 编写的自动清理三个月以前的邮件的脚本。这个脚本适用于连接支持 IMAP 协议的邮箱服务,例如 Gmail。请注意,在执行此操作时,您需要提供电子邮件账号和应用程序专用密码(建议不要使用普通密码,并…...
C++组合复用中,委托的含义与作用
委托(Delegation)的含义与作用 委托是一种软件设计技术,它允许一个对象在处理某个请求时,将请求的处理责任转移给另一个对象。委托的核心思想是通过组合(composition)而不是继承(inheritance&a…...
自制C++游戏头文件:C++自己的游戏头文件!!!(后续会更新)
引言 在这个数字时代,计算机游戏已经成为人们生活中不可或缺的一部分。它们不仅为我们带来了无尽的乐趣,还激发了我们的创造力和解决问题的能力。今天,我们将深入探讨一个特别的头文件——CPPgame.h,它包含了多个结构体和函数&am…...
java 读取 有时需要sc.nextLine();读取换行符 有时不需要sc.nextLine();读取换行符 详解
在 Java 中,使用 Scanner 类读取输入时,换行符的处理行为取决于所用的读取方法。不同方法的工作原理会影响是否需要额外调用 sc.nextLine() 来清理缓冲区中的换行符。 核心问题 根本原因:Scanner 是基于输入流工作的,而换行符&am…...
Redis知识分享(三)
目录 前言 七、事务管理 7.1事务中的异常处理 八、订阅发布 8.1概述 8.2.Redis针对发布订阅相关指令 九、主从复制 9.1主从复制概述 9.2.主从复制的用处 9.3主从复制实现原理 9.3.1.psync指令 9.3.2.复制偏移量 9.3.3复制积压缓冲区&节点ID 前言 今天…...
python安装包报错
多次安装均报错 ERROR: Could not find a version that satisfies the requirement win10toast ERROR: No matching distribution found for win10toast 然后还提示 WARNING: Retrying (Retry(total4, connectNone, readNone, redirectNone, statusNone)) after connectio…...
Linux性能优化之火焰图简介
Linux 火焰图(Flame Graph)是一种可视化工具,用于分析程序性能问题,尤其是 CPU 使用情况。它展示了程序中函数调用的层次结构和各个调用栈占用的时间比例。 以下是详细介绍,包括火焰图的工作原理、生成步骤和实际使用中…...
Unity类银河战士恶魔城学习总结(P129 Craft UI 合成面板UI)
【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili 教程源地址:https://www.udemy.com/course/2d-rpg-alexdev/ 本章节实现了合成面板的UI设置 UI_CraftWindow.cs 字段作用: UI 组件: itemName / itemDescription / icon&#…...
linux基础笔试练习题笔记(2)
在Linux系统上,下面那个命令不可以用来查看文件内容() A.cat B.ls C.less D.more 答案解析: cat命令用用于一次性显示文件的所有内容,一般文件内容较多时一般会使用more或less命令。 more:分页显示文件内容…...
Android OpenGL ES详解——glTexImage2D方法
glTexImage2D是OpenGL中的一个重要函数,其作用是为2D纹理分配显存并上传数据。以下是关于glTexImage2D作用的详细解释: 一、函数原型 在OpenGL ES 2.0中,glTexImage2D的函数原型如下: GL_APICALL void GL_APIENTRY glTexImage2…...
Redisson 中开启看门狗(watchdog)机制
在分布式系统中,分布式锁是一种常用的技术手段,用于确保在多个节点同时访问共享资源时的一致性和正确性。Redisson 是一个强大的 Java 分布式框架,它提供了丰富的分布式数据结构和服务,其中开启看门狗(watchdog&#x…...
【JSOO】设计模式
单例模式工厂模式状态模式观察者模式桥接模式 设计模式(是一种通过经验中总结出来的经过反复验证能够解决一类通用问题的可以反复重用的就可称它为模式,否则只能称为功能模块);模式:把解决问题的方法抽取出来ÿ…...
本草纲目数字化:Spring Boot在中药实验管理中的应用
1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及,互联网成为人们查找信息的重要场所,二十一世纪是信息的时代,所以信息的管理显得特别重要。因此,使用计算机来管理中药实验管理系统的相关信息成为必然。开发…...
java八股-jvm入门-程序计数器,堆,元空间,虚拟机栈,本地方法栈,类加载器,双亲委派,类加载执行过程
文章目录 PC Register堆虚拟机栈方法区(Metaspace元空间双亲委派机制类加载器 类装载的执行过程 PC Register 程序计数器(Program Counter Register)是 Java 虚拟机(JVM)中的一个组件,它在 JVM 的内存模型中扮演着非常…...
重构Action-cli前端脚手架
一、概述 最近一年,为了满足公司业务开发,解决重复搭建项目繁琐过程,自己开发了一个前端脚手架,并发布到npm。随着时间的推移,发现之前的版本存在很多问题,有些功能做不到位,而且代码也不是很规…...
华为USG5500防火墙配置NAT
实验要求: 1.按照拓扑图部署网络环境,使用USG5500防火墙,将防火墙接口加入相应的区域,添加区域访问规则使内网trust区域可以访问DMZ区域的web服务器和untrust区域的web服务器。 2.在防火墙上配置easy-ip,使trust区域…...
【大数据学习 | HBASE高级】hive操作hbase
一般在查询hbase的数据的时候我们可以直接使用hbase的命令行或者是api进行查询就行了,但是在日常的计算过程中我们一般都不是为了查询,都是在查询的基础上进行二次计算,所以使用hbase的命令是没有办法进行数据计算的,并且对于hbas…...
集群聊天服务器(9)一对一聊天功能
目录 一对一聊天离线消息服务器异常处理 一对一聊天 先新添一个消息码 在业务层增加该业务 没有绑定事件处理器的话消息会派发不出去 聊天其实是服务器做一个中转 现在同时登录两个账号 收到了聊天信息 再回复一下 离线消息 声明中提供接口和方法 张三对离线的李…...
《FreeRTOS列表和列表项篇》
FreeRTOS列表和列表项 1. 什么是列表和列表项?1.1 列表list1.2 列表项list item 2. 列表和列表项的初始化2.1 列表的初始化2.2 列表项的初始化 3. 列表项的插入4. 列表项末尾插入5. 列表项的删除6. 列表的遍历 列表和列表项是FreeRTOS的一个数据结构,是F…...
C++:哈希拓展-位图
目录 一.问题导入 二.什么是位图? 2.1如何确定目标数在哪个比特位? 2.2如何存放高低位 2.3位图模拟代码实现 2.3.1如何标记一个数 2.3.2如何重置标记 2.3.3如何检查一个数是否被标记 整体代码实现 标准库的Bitset 库中的bitset的缺陷 简单应用 一.问题导入 这道…...
【数据结构与算法】查找
文章目录 一.查找二.线性结构的查找2.1顺序查找2.2折半查找2.3分块查找 三.树型结构的查找3.1二叉排序树1.定义2.二叉排序树的常见操作3.性能分析 3.2平衡二叉树1.定义2.平衡二叉树的常见操作3.性能分析 3.3B树1.定义2.B树的相关操作 3.4B树1.定义2.B树与B树的比较 四.散列表1.…...
从零开始学习 sg200x 多核开发之 milkv-duo256 编译运行 sophpi
sophpi 是 算能官方针对 sg200x 系列的 SDK 仓库 https://github.com/sophgo/sophpi ,支持 cv180x、cv81x、sg200x 系列的芯片。 SG2002 简介 SG2002 是面向边缘智能监控 IP 摄像机、智能猫眼门锁、可视门铃、居家智能等多项产品领域而推出的高性能、低功耗芯片&a…...
LLM - 使用 LLaMA-Factory 微调大模型 Qwen2-VL SFT(LoRA) 图像数据集 教程 (2)
欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://spike.blog.csdn.net/article/details/143725947 免责声明:本文来源于个人知识与公开资料,仅用于学术交流,欢迎讨论,不支持转载。 LLaMA-…...
基于STM32设计的大棚育苗管理系统(4G+华为云IOT)_265
文章目录 一、前言1.1 项目介绍【1】项目开发背景【2】设计实现的功能【3】项目硬件模块组成【4】设计意义【5】国内外研究现状【6】摘要1.2 设计思路1.3 系统功能总结1.4 开发工具的选择【1】设备端开发【2】上位机开发1.5 参考文献1.6 系统框架图1.7 系统原理图1.8 实物图1.9…...
深入浅出《钉钉AI》产品体验报告
1. 引言 随着人工智能技术的迅猛发展,企业协同办公领域迎来了新的变革。钉钉作为阿里巴巴集团旗下的企业级通讯与协同办公平台,推出了钉钉AI助理,旨在提高工作效率,优化用户体验。本报告将对钉钉AI助理进行全面的产品体验分析&am…...
2020年计挑赛往届真题(C++)
因为17号要开赛了,甚至是用云端编辑器,debuff拉满,只能临时抱佛脚了 各个选择题的选择项我就不标出来了,默认ABCD排,手打太麻烦了 目录 单选题: 1.阅读以下语句:double m0;for(int i3;i>0;i--)m1/i;…...
ES6进阶知识二
一、promise方法的案例 Promise对象通过new Promise()语法创建,它接受一个函数作为参数,该函数接受两个参数:resolve和reject。resolve表示异步操作成功,reject表示异步操作失败。 案例:异步加载图片 const loadIma…...
大语言模型通用能力排行榜(2024年10月8日更新)
数据来源SuperCLUE 榜单数据为通用能力排行榜 排名 模型名称 机构 总分 理科 文科 Hard 使用方式 发布日期 - o1-preview OpenAI 75.85 86.07 76.6 64.89 API 2024年11月8日 - Claude 3.5 Sonnet(20241022) Anthropic 70.88 82.4…...
第六节、Docker 方式部署指南 github 上项目 mkdocs-material
一、简介 MkDocs 可以同时编译多个 markdown 文件,形成书籍一样的文件。有多种主题供你选择,很适合项目使用。 MkDocs 是快速,简单和华丽的静态网站生成器,可以构建项目文档。文档源文件在 Markdown 编写,使用单个 YAML 配置文件配置。 MkDocs—markdown项目文档工具,…...
简单个人网站制作流程/微营销平台系统
在网上看到一些关于NetBeans 6.0以上版本中设置中文的Java_Docs API 自己设置了多次,总是没办法成功,后面在看到关于Eclipse中设置中文API时需要将原代码的路径删除后才有用,就自己试了一下,果然,不将NetBeans中的源的…...
wordpress 缩放大小 设置/网络优化工程师前景如何
来源:网络密码管理器 NordPass 公布了一份 2019 年最常用的 200 个密码的列表,其中排名前三的为:12345、123456、123456789,看来大家对数字还是比较喜欢的,123456 这个密码应该大部分都有设置过。这家公司总共收集了 5…...
wordpress太强大/百度搜索引擎收录入口
文章来源:M78安全团队首发于浙江大学cc98论坛和个人博客本文目的是【希望学弟学妹们千万不要去拼多多】,本人从一个底层员工的视角,记录一下拼多多这家公司三年以来的整体变化,本文不涉及技术机密,只讨论行政制度、文化…...
做网站起什么名字比较好/seo排名系统源码
英文原文:14 lessons after five years of professional programming 1. 当性能遇到问题时,如果能在应用层进行计算和处理,那就把它从数据库层拿出来。排序和分组就是典型的例子。在应用层做性能提升总是要比在数据库层容易的多。就像对于 My…...
做设计有哪些免费网站/百度人工服务热线24小时
如此前预告,今天上午,华为消费者BG软件部总裁王成录正式发布了鸿蒙OS 2.0手机开发者Beta版本,支持运行安卓应用,P40、Mate 30系列可申请公测。王成录表示,今年已有美的、九阳、老板电器、海雀科技搭载鸿蒙OS࿰…...
网站公司说我们做的网站服务器不够用/企拓客软件多少钱
为什么要用HDFS 一台服务器存不下数据,需要多台服务器。多台服务器上的数据需要一个系统来组织和管理。 tip:HDFS、NTFS是不同的文件管理系统 HDFS的定义 HDFS是一个分布式的文件系统,通过目录树来定位文件 HDS的使用场景 一次写入&…...