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

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 */
            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.kler.cn/a/399450.html

相关文章:

  • Oracle OCP认证考试考点详解082系列19
  • VuePress v2 快速搭建属于自己的个人博客网站
  • 如何让手机ip变成动态
  • 【操作系统实验课】Makefile与编译
  • 【Apache Paimon】-- 1 -- Apache Paimon 是什么?
  • Python作业05
  • Amazon Web Services (AWS)
  • Linux第四讲:Git gdb
  • 数学建模问题攻略指南
  • XXL-JOB相关面试题
  • 【第四课】rust声明式宏理解与实战
  • FFmpeg 4.3 音视频-多路H265监控录放C++开发十三.2:avpacket中包含多个 NALU如何解析头部分析
  • 算法——有序数组的平方(leetcode977)
  • 力扣第 55 题 跳跃游戏
  • 大语言模型通用能力排行榜(2024年11月8日更新)
  • 项目技术栈-解决方案-注册中心
  • JavaSE常用API-日期(计算两个日期时间差-高考倒计时)
  • Android 删除设置的WLAN偏好选项菜单,即设置不可见
  • 【PHP】ThinkPHP基础
  • [NSSCTF Round#16 Basic]了解过PHP特性吗 详细题解
  • web前端开发网页--css样式的使用
  • Prometheus面试内容整理-场景应用和故障排查
  • Flutter开发之flutter_local_notifications
  • 2024年了,TCP分析工具有哪些?
  • 力扣-Hot100-链表其一【算法学习day.34】
  • websocket身份验证