TCP/IP网络编程_第10章多进程服务器端(下)

信号与 signal 函数

下列进程和操作系统间的对话是帮助大家理解信号处理而编写的, 其中包含了所有信号处理相关内容.
在这里插入图片描述
上述对话中进程所讲相当于"注册信号" 过程, 即进程发现自己的子进程结束时, 请求操作系统调用特定函数. 该请求通过如下函数调用完成 (因此称此函数为信号注册函数).
在这里插入图片描述
上述函数的返回值类型为函数指针, 因此函数声明有些繁琐. 若各位不太熟悉返回值类型为函数指针的声明, 希望加强学习(不懂函数指针将无法理解后续内容). 现在为了便于讲解, 我将上述函数声明整理如下
在这里插入图片描述
调用上述函数时, 第一个参数为特殊情况信息, 第二个参数为特殊情况下将要调用的函数的地址值(指针). 发生第一个参数代表的情况时, 调用第二个参数所指的的函数. 下面给出可以在signal 函数中注册的部分特殊情况对应的常数.
在这里插入图片描述
接下来编写调用 signal 函数的语句完成如下请求:
在这里插入图片描述
此时 mychild 函数的参数应为int, 返回值类型应为void. 只有这样才能成为signal 函数的第二个参数. 另外, 常数 SIGCHLD 定义了子进程终止的情况, 应成为 signal 函数的第一参数. 也就是说, signal 函数调用语句如下.
在这里插入图片描述
接下来编写 signal 函数的调用语句, 分别完成如下2个请求.
在这里插入图片描述
代表这2种情况的常数分别为 SIGALRM 和 SIGINT, 因此按如下方式调用 signal 函数.
在这里插入图片描述
以上就是信号注册过程. 注册好信号后, 发生注册信号时(注册的情况发生时), 操作系统将调用该信号对应的函数. 下面通过示例验证, 先介绍 alarm 函数.
在这里插入图片描述
如果调用该函数的同时向它传递一个正整参数, 相应时间后(以秒为单位)将产生 SIGALRM 信号. 若向该函数传递0, 则之前对 SIGALRM 信号的预约将取消. 如果通过该函数预约信号后未指定该信号对应处理函数, 则(通过调用signal 函数)终止进程, 不做任何处理.
希望引起注意.
接下来给出信号处理相关示例, 希望各位通过该示例彻底掌握之前的内容.

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig)
{
    if (sig == SIGALRM)
    {
        puts("Time out!");
    }

    alarm(2);
}

void keycontrol(int sig)
{
    if (sig == SIGINT)
    {
        puts("CTRL+C pressed");
    }
}

int main(int argc, char *argv[])
{
    int i;
    signal(SIGALRM, timeout);
    signal(SIGINT, keycontrol);
    alarm(2);

    for (i=0; i<3; i++)
    {
        puts("wait...");
        sleep(100);
    }    
    
    return 0;
}

运行结果:
在这里插入图片描述
上述是没有任何输入时的运行结果. 下面在运行过程中输入 CTRL+C. 可以看到输出 "CTRL+C pressed " 字符串. 有一点必须说明:
在这里插入图片描述
调用函数的主体的确是操作系统, 但进程处于睡眠状态时无法调用函数. 因此, 产生信号时, 为了调用信号处理器, 将唤醒由于调用sleep 函数而进入阻塞状态的进程. 而且, 进程一旦被唤醒, 就不会再进入睡眠状态. 即使还未到sleep 函数中规定时间也是如此. 所以, 上述示例运行不到10秒就会结束, 连续输入 CTRL+C 则有可能1秒都不到.

利用 sigaction 函数进行信号处理

前面所学的内容主以用来防止僵尸进程生成的代码. 但我还想介绍 sigaction 函数, 它类似于 signal 函数, 而且完全可以代替后者, 也更稳定. 之所以稳定, 是因为如下原因:
在这里插入图片描述
实际上现在很少使用 signal 函数编写程序, 它只是为了保持对旧程序的兼容. 下面介绍sigaction 函数, 但只讲解可替代 signal 函数的功能, 因为全面介绍给各位带来不必要的负担.
在这里插入图片描述
声明并初始化 sigaction 结构体变量以上调用上述函数, 该结构体定义如下.
在这里插入图片描述
此结构体的 sa_handler 成员保存处理函数的指针值(地址值). sa_mask 和 sa_flags 的所有位均初始化为0即可. 这2个成员用于指定信号相关的选项和特性, 而我们的目的主要是防止产生僵尸进程, 故省略. 理解这些参数所需参考书将在后面给出.

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig)
{
    if (sig == SIGALRM)
    {
        puts("Time out!");
    }
    alarm(2);
}

int main(int argc, char *argv[])
{
    int i;
    struct sigaction act;
    act.sa_handler = timeout;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGALRM, &act, 0);

    alarm(2);

    for (i=0; i<3; i++)
    {
        puts("wait...");
        sleep(100);
    }

    return 0;
}

运行结果:
在这里插入图片描述
这就是信号处理相关理论, 以此为基础讨论消灭僵尸进程的方法.

利用信号处理技术消灭僵尸进程

我相信各位也可以独立编写消灭僵尸进程的示例. 子进程终止时将产生 SIGCHLD 信号, 知道这一点就很容易完成, 接下来利用 sigaction 函数编写示例.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void read_childproc(int sig)
{
    int status;
    pid_t id = waitpid(-1, &status, WNOHANG);
    if (WIFEXITED(status))
    {
        printf("Removed proc id: %d \n", id);
        printf("Child send: %d \n", WEXITSTATUS(status));
    }
}

int main(int argc, char *argv[])
{
    pid_t pid;
    struct sigaction act;
    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, 0);

    pid = fork();
    if (pid == 0) /* 子进程执行区域 */
    {
        puts("Hi! I'm child process");
        sleep(10);
        return 12;
    }
    else /* 父进程执行区域 */
    {
        printf("Child proc id: %d \n", pid);
        pid = fork();
        if (pid == 0) /* 另一子进程执行的区域 */
        {
            puts("Hi! I'm child process");
            sleep(10);
            exit(24);
        }
        else
        {
            int i;
            printf("Child proc id: %d \n", pid);
            for (i=0; i<5; i++)
            {
                puts("wait...");
                sleep(5);
            }
        }
    }
    
    return 0;
}

运行结果:
在这里插入图片描述
可以看出, 子进程并未变成僵尸进程, 而是正常终止了. 接下来利用进程相关知识编写服务器端.

10.4 基于多任务的并发服务器

我们已做好了利用 fork 函数编写并发服务器的准备, 现在可以开始编写像样的服务器端了.

基于进程的并发服务器模型

之前的回声服务器端每次只能向一个客户端提供服务. 因此, 我们将扩展回声服务器端, 使其可以同时向多个客户端提供服务. 图10-2给出了基于多进程的并发回声服务器端的实现模型.
在这里插入图片描述
从图10-2可以看出, 每当有客户端请求服务(连接请求)时, 回声服务器端都创建子进程以提供服务, 请求服务的客户端若有5个, 则将创建5个子进程提供服务. 为了完成这些任务, 需要经过如下过程, 这是于之前的回声服务器端的区别所在.
在这里插入图片描述
此处容易引起困惑的是子进程传递传递套接字文件描述的方法. 但各位读完代码后会发现, 这其实没什么大不了的, 因为子进程会复制父进程拥有的所有资源. 实际上根本不用另外经过传递文件描述符的过程.

实现并发服务器

虽然我已经给出了所有的理论说明, 但大家也许还没想出具体的实现方法, 这就是有必要理解具体代码. 下面给出并发服务器端的实现代码. 当然, 程序是基于多进程实现的, 可以结合第4章的回声客户端运行.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;

    pid_t pid;
    struct sigaction act;
    socklen_t adr_sz;
    int str_len, state;
    char buf[BUF_SIZE];
    if (argc != 2)
    {
        printf("Usage: %s <port> \n", argv[0]);
        exit(1);
    }

    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD, &act, 0);
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_handling("bind() error");
    }
    
    if (listen(serv_sock, 5) == -1)
    {
        error_handling("listen() error");
    }

    while(1)
    {
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
        if (clnt_sock == -1)
        {
            continue;
        }
        else
        {
            puts("new client connected...");
        }

        pid = fork();
        if (pid == -1)
        {
            close(clnt_sock);
            continue;
        }
        if (pid == 0) /* 子进程运行区域 */
        {
            close(serv_sock);
            while((str_len=read(clnt_sock, buf, BUF_SIZE)) != 0)
            {
                write(clnt_sock, buf, str_len);
            }

            close(clnt_sock);
            puts("clitne disconnected...");
            return 0;
        }
        else
        {
            close(clnt_sock);
        }
    }

    close(serv_sock);
    return 0;
}

void read_childproc(int sig)
{
    pid_t pid;
    int status;
    pid = waitpid(-1, &status, WNOHANG);
    printf("removed proc id: %d \n", pid);
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

运行结果:
服务器端:
在这里插入图片描述
客户端A:
在这里插入图片描述
客户端B:
在这里插入图片描述
启动服务器端后, 要创建多个客户端并建立连接. 可以验证服务器端同时向大多数客户端提供服务, 不, 一定要验证这一点.

通过 fork 函数复制文件描述符

实例 echo_mpserv.c 中给出了通过 fork 函数复制文件描述符的过程. 父进程将2个套接字(一个是服务器端套接字, 另一个是客户端连接的套接字) 文件描述符复制给子进程.
在这里插入图片描述
文件描述符的实际复制有多少有些难以理解. 调用 fork 函数时复制父进程的所有资源, 有些人可能认为也会同时复制套接字. 但套接字并非进程所有–从严格意义上说, 套接字属于操作系统–只是进程拥有代表相应套接字的文件描述符. 也不一定非要这样理解, 仅因为如下原因, 复制套接字也并不合理.
在这里插入图片描述
示例echo_mpserv.c 中的 fork函数调用过程如图10 - 3 所示. 调用 fork 函数后, 2个文件描述符指向同一套接字.
在这里插入图片描述
如图 10-3 所示, 1个套接字中存在2个文件描述符时, 只有2个文件描述符都终止(销毁)后, 才能销毁套机字. 如果维持图中的连接状态, 即使子进程销毁了与客户端连接的套接字文件描述符, 也无法完全销毁套接字(服务器端套接字同样如此). 因此, 调用 fork 函数后, 要求无关的套接字文件描述符关掉, 如图10-4所示.
在这里插入图片描述
为了将文件描述符整理成图10-4的形式, 实例echo_mpserv.c 的第60行和第69行 调用了close 函数.

10.5 分割 TCP 的 I/O 程序

各位应该已经理解 fork 函数相关的所有有用的内容. 下面以此为基础, 再讨论客户端中分割I/O程序 (Routine) 的方法. 内容非常简单, 大家不必有负担.

分割 I/O 程序的优点

我们已实现的回声客户端的数据回声方式如下:
在这里插入图片描述
传输数据后需要等待服务器端返回的数据, 因为程序代码中重复调用了 read 和 write 函数, 只能这么写的原因之一是, 程序在一个进程中运行. 但现在可以创建多个进程, 因此可以分割数据收发过程. 默认的分割模型如图10-5所示.

从图10-5可以看出, 客户端的父进程负责接收数据, 额外创建的子进程负责发送数据. 分割后, 不同进程分别负责输入和输出, 这样, 无论客户端是否从服务器接收完数据都可以进行传输.
在这里插入图片描述
选择这种实现方式的原因有很多, 但最重要的的一定是, 程序的实现更加简单. 也许有人质疑: 既然多产生1个进程, 怎么能简化程序实现呢? 其实, 按照这种实现方式, 父进程中需编写接收数据的代码, 子进程中需要发送的数据的代码, 所以会简化. 实际上, 在1个进程内同时实现收发逻辑需要考虑更多细节, 程序越复杂, 这种区别越明显, 它也是公认的优点.
在这里插入图片描述
分割I/O 程序的另一个好处, 可以提高频率交换数据的程序性能, 如图10-6所示.
在这里插入图片描述
如图10-6左测演示的是之前的的回声客户端数据交换方式, 右侧演示的是分割I/O后的客户端数据传递方式. 服务器端相同, 不同的是客户端区域. 分割I/O 后客户端发送的数据时不必考虑接收数据的情况, 因此可以连续发送数据, 由此提高同一时间内传输的数据量. 这种差异在网速较差时尤为明显.

回声客户端的I/O 程序分割

我们已经知道I/O 程序分割的意义, 接下来通过实际代码进行实现, 分割的对象是回声客户端. 下列回声客户端可以结合之前的回声服务器端 echo_mpserv.c 运行.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);

int main(int argc, char *argv[])
{
    int sock;
    pid_t pid;
    char buf[BUF_SIZE];
    struct sockaddr_in serv_adr;
    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_handling("connect() error");
    }
    
    pid = fork();
    if (pid == 0)
    {
        write_routine(sock, buf);
    }
    else
    {
        read_routine(sock, buf);
    }
    
    close(sock);
    return 0;
}


void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

void read_routine(int sock, char *buf)
{
    while(1)
    {
        int str_len = read(sock, buf, BUF_SIZE);
        if (str_len == 0)
        {
            return;
        }

        buf[str_len] = 0;
        printf("Message from server: %s", buf);
    }
}

void write_routine(int sock, char *buf)
{
    while(1)
    {
        fgets(buf, BUF_SIZE, stdin);
        if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
        {
            shutdown(sock, SHUT_WR);
            return;           
        }       
        write(sock, buf, strlen(buf));
    }
}

运行结果跟普通回声服务器/客户端相同, 故省略. 只是上述示例分割了I/O, 为了简化输出过程,不会输出如下字符串:
在这里插入图片描述
无论是否接收到消息, 每次通过键盘输入字符串时都会输出上述字符串, 可能造成输出混乱. 基于多任务的服务器端实现方法讲解到此结束.

结语:

你可以下面这个网站下载这本书<TCP/IP网络编程>
https://www.jiumodiary.com/

时间: 2020-06-03

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值