UNIX网络编程-TCP套接字编程(实战)

news2024/12/23 6:34:28

概述


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 */
            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均返回,连接于是被建立。

接着发生步骤如下:

  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 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函数以免留下僵尸进程。

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2241860.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

『VUE』27. 透传属性与inheritAttrs(详细图文注释)

目录 什么是透传属性&#xff08;Forwarding Attributes&#xff09;使用条件唯一根节点禁用透传属性继承总结 欢迎关注 『VUE』 专栏&#xff0c;持续更新中 欢迎关注 『VUE』 专栏&#xff0c;持续更新中 什么是透传属性&#xff08;Forwarding Attributes&#xff09; 在 V…

408模拟卷较难题(无分类)

模拟卷特别是大题还是很有难度的&#xff0c;而且有些题有错&#xff0c;还是先把真题吃透&#xff0c;后面没时间的话就不整理了。 一棵树转化为二叉树&#xff0c;那么这棵二叉树一定为右子树为空的树 计算不同种形态&#xff0c;即计算6个结点的二叉树有几种形态&#xff0c…

【JavaScript】LeetCode:96-100

文章目录 96 单词拆分97 最长递增子序列98 乘积最大子数组99 分割等和子集100 最长有效括号 96 单词拆分 动态规划完全背包&#xff1a;背包-字符串s&#xff0c;物品-wordDict中的单词&#xff0c;可使用多次。问题转换&#xff1a;s能否被wordDict中的单词组成。dp[i]&#x…

安全见闻1-5

涵盖了编程语言、软件程序类型、操作系统、网络通讯、硬件设备、web前后端、脚本语言、病毒种类、服务器程序、人工智能等基本知识&#xff0c;有助于全面了解计算机科学和网络技术的各个方面。 安全见闻1 1.编程语言简要概述 C语言&#xff1a;面向过程&#xff0c;适用于系统…

相亲小程序(源码+文档+部署+讲解)

最近我在挖掘一些优秀的开源项目时&#xff0c;无意间发现了一个相当给力的系统——相亲小程序管理系统。这个系统不仅功能实用&#xff0c;而且代码结构清晰&#xff0c;易于二次开发。作为一名技术爱好者&#xff0c;我觉得有必要把这个好东西推荐给我的读者们。接下来&#…

RabbitMQ介绍和快速上手案例

文章目录 1.引入1.1同步和异步1.2消息队列的作用1.3rabbitMQ介绍 2.安装教程2.1更新软件包2.2安装erlang2.3查看这个erlang版本2.4安装rabbitMQ2.5安装管理页面2.6浏览器测试2.7添加管理员用户 3.rabbitMQ工作流程4.核心概念介绍4.1信道和连接4.2virtual host4.3quene队列 5.We…

aws(学习笔记第十二课) 使用AWS的RDS-MySQL

aws(学习笔记第十二课) 使用AWS的RDS 学习内容&#xff1a; AWS的RDS-MySQL 1. 使用AWS的RDS 什么是RDS RDS就是Relation Database Service的缩写&#xff0c;是AWS提供的托管关系型数据库系统。让用户能够在 AWS Cloud 云中更轻松地设置、操作和扩展关系数据库。 数据库和we…

跳房子(弱化版)

题目描述 跳房子&#xff0c;也叫跳飞机&#xff0c;是一种世界性的儿童游戏&#xff0c;也是中国民间传统的体育游戏之一。 跳房子的游戏规则如下&#xff1a; 在地面上确定一个起点&#xff0c;然后在起点右侧画 n 个格子&#xff0c;这些格子都在同一条直线上。每个格子内…

A029-基于Spring Boot的物流管理系统的设计与实现

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600…

Spring系统框架

Spring Framework系统架构 1.Spring核心概念 代码书写现状 耦合度偏高 解决方案 使用对象时&#xff0c;在程序中不要主动使用new产生对象&#xff0c;转换为外部提供对象 IOC(Inversion of Control)控制反转 对象的创建控制权由程序移到外部&#xff0c;这种思想称为控制…

鸿蒙实战:页面跳转

文章目录 1. 实战概述2. 实现步骤2.1 创建项目2.2 准备图片素材2.3 编写首页代码2.4 创建第二个页面 3. 测试效果4. 实战总结 1. 实战概述 实战概述&#xff1a;本实战通过ArkUI框架&#xff0c;在鸿蒙系统上开发了一个简单的两页面应用。首页显示问候语和“下一页”按钮&…

文献解读-DNAscope: High accuracy small variant calling using machine learning

关键词&#xff1a;基准与方法研究&#xff1b;基因测序&#xff1b;变异检测&#xff1b; 文献简介 标题&#xff08;英文&#xff09;&#xff1a;DNAscope: High accuracy small variant calling using machine learning标题&#xff08;中文&#xff09;&#xff1a;DNAsc…

程序设计方法与实践-变治法

变换之美 变治法就是基于变换的思路&#xff0c;进而使原问题的求解变得简单的一种技术。 变治法一般有三种类型&#xff1a; 实例化简&#xff1a;将问题变换为同问题&#xff0c;但换成更为简单、更易求解的实例。改变表现&#xff1a;变化为同实例的不同形式&#xff0c;…

解决Anaconda出现CondaHTTPError: HTTP 000 CONNECTION FAILED for url

解决Anaconda出现CondaHTTPError: HTTP 000 CONNECTION FAILED for url 第一类情况 在anaconda创建新环境时&#xff0c;使用如下代码 conda create -n charts python3.7 错误原因&#xff1a; 默认镜像源访问速度过慢&#xff0c;会导致超时从而导致更新和下载失败。 解决方…

Spring Boot框架:电商系统的技术革新

4 系统设计 网上商城系统的设计方案比如功能框架的设计&#xff0c;比如数据库的设计的好坏也就决定了该系统在开发层面是否高效&#xff0c;以及在系统维护层面是否容易维护和升级&#xff0c;因为在系统实现阶段是需要考虑用户的所有需求&#xff0c;要是在设计阶段没有经过全…

wordpress下载站主题推荐riproV5 wordpress日主题

iPro主题全新V5版本&#xff0c;是一个优秀且功能强大、易于管理、现代化的WordPress虚拟资源商城主题。支持首页模块化布局和WP原生小工具模块化首页可拖拽设置&#xff0c;让您的网站设计体验更加舒适。同时支持了高级筛选、自带会员生态系统、超全支付接口等众多功能&#x…

微服务即时通讯系统的实现(客户端)----(1)

目录 1. 项目整体介绍1.1 项目概况1.2 界面预览和功能介绍1.3 技术重点和服务器架构 2. 项目环境搭建2.1 安装Qt62.3 安装vcpkg2.3 安装protobuf2.4 构建项目2.5 配置CMake属性 3. 项目核心数据结构的实现3.1 创建data.h存放核心的类3.2 工具函数的实现3.3 创建编译开关 4. 界面…

2024年11月15日

1.计算机网络 逻辑右移 做加减法 定点乘法 原码乘法运算 一位乘 计组 2.英语六级

算法定制LiteAIServer摄像机实时接入分析平台玩手机打电话检测算法:智能监控的新篇章

在现代社会&#xff0c;随着智能手机的普及&#xff0c;无论是在工作场所还是公共场所&#xff0c;玩手机或打电话的行为日益普遍。然而&#xff0c;在某些特定环境下&#xff0c;如工厂生产线、仓库、学校课堂等&#xff0c;这些行为可能会影响到工作效率、安全或教学秩序。为…

算法--解决二叉树遍历问题

第一 实现树的结构 class Node(): # 构造函数&#xff0c;初始化节点对象&#xff0c;包含数据和左右子节点 def __init__(self, dataNone): self.data data # 节点存储的数据 self.left None # 左子节点&#xff0c;默认为None self.rig…