《TCP IP网络编程》第十章

这篇具有很好参考价值的文章主要介绍了《TCP IP网络编程》第十章。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

第 10 章 多进程服务器端

10.1 进程概念及应用

并发服务端的实现方法:

        通过改进服务端,使其同时向所有发起请求的客户端提供服务,以提高平均满意度。而且,网络程序中数据通信时间比 CPU 运算时间占比更大,因此,向多个客户端提供服务是一种有效的利用 CPU 的方式。接下来讨论同时向多个客户端提供服务的并发服务器端。下面列出的是具有代表性的并发服务端的实现模型和方法:

  • 多进程服务器:通过创建多个进程提供服务
  • 多路复用服务器:通过捆绑并统一管理 I/O 对象提供服务
  • 多线程服务器:通过生成与客户端等量的线程提供服务

第一种方法:多进程服务器

 理解进程:

        进程的定义如下:

占用内存空间的正在运行的程序。

        假如你下载了一个游戏到电脑上,此时的游戏不是进程,而是程序。只有当游戏被加载到主内存并进入运行状态,这是才可称为进程。

进程 ID:

        无论进程是如何创建的,所有的进程都会被操作系统分配一个 ID。此 ID 被称为「进程ID」,其值为大于 2 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1 。接下来输入以下命令来观察在 Linux 中运行的进程:

ps au

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

         通过上面的命令可查看当前运行的所有进程。需要注意的是,该命令同时列出了 PID(进程ID)。参数 a 和 u列出了所有进程的详细信息。

通过调用 fork 函数创建进程:

        创建进程的方式很多,此处只介绍用于创建多进程服务端的 fork 函数:

#include <unistd.h>
pid_t fork(void);
// 成功时返回进程ID,失败时返回 -1

        fork 函数将创建调用的进程副本。也就是说,并非根据完全不同的程序创建进程,而是复制正在运行的、调用 fork 函数的进程。另外,两个进程都执行 fork 函数调用后的语句(准确的说是在 fork 函数返回后)。但因为是通过同一个进程、复制相同的内存空间,之后的程序流要根据 fork 函数的返回值加以区分。即利用 fork 函数的如下特点区分程序执行流程。

  • 父进程:fork 函数返回子进程 ID
  • 子进程:fork 函数返回 0

        此处,「父进程」(Parent Process)指原进程,即调用 fork 函数的主体,而「子进程」(Child Process)是通过父进程调用 fork 函数复制出的进程。接下来是调用 fork 函数后的程序运行流程。如图所示:        

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

         从图中可以看出,父进程调用 fork 函数的同时复制出子进程,并分别得到 fork 函数的返回值。但复制前,父进程将全局变量 gval 增加到 11,将局部变量 lval 的值增加到 25,因此在这种状态下完成进程复制。复制完成后根据 fork 函数的返回类型区分父子进程。父进程的 lval 的值增加 1 ,但这不会影响子进程的 lval 值。同样子进程将 gval 的值增加 1 也不会影响到父进程的 gval 。因为 fork 函数调用后分成了完全不同的进程,只是二者共享同一段代码而已。接下来给出一个例子:

#include <stdio.h>
#include <unistd.h>
int gval = 10;
int main(int argc, char *argv[])
{
    pid_t pid;
    int lval = 20;
    gval++, lval += 5;
    pid = fork();
    if (pid == 0)
        gval += 2, lval += 2;
    else
        gval -= 2, lval -= 2;
    if (pid == 0)
        printf("Child Proc: [%d,%d] \n", gval, lval);
    else
        printf("Parent Proc: [%d,%d] \n", gval, lval);
    return 0;
}

运行结果:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

        可以看出,当执行了 fork 函数之后,此后就相当于有了两个程序在执行代码,对于父进程来说,fork 函数返回的是子进程的ID,对于子进程来说,fork 函数返回 0。所以这两个变量,父进程进行了 +2 操作 ,而子进程进行了 -2 操作。 

10.2 进程和僵尸进程

        文件操作中,关闭文件和打开文件同等重要。同样,进程销毁和进程创建也同等重要。如果未认真对待进程销毁,他们将变成僵尸进程。        

僵尸(Zombie)进程:

        进程的工作完成后(执行完 main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作「僵尸进程」,这也是给系统带来负担的原因之一。

僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源

产生僵尸进程的原因:

        为了防止僵尸进程产生,先解释产生僵尸进程的原因。利用如下两个示例展示调用 fork 函数产生子进程的终止方式。

  • 传递参数并调用 exit() 函数
  • main 函数中执行 return 语句并返回值

        向 exit 函数传递的参数值和 main 函数的 return 语句返回的值都会传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。也就是说将子进程变成僵尸进程的正是操作系统。既然如此,僵尸进程何时被销毁呢?

应该向创建子进程的父进程传递子进程的 exit 参数值或 return 语句的返回值。

如何向父进程传递这些值呢?操作系统不会主动把这些值传递给父进程。只有父进程主动发起请求(函数调用)的时候,操作系统才会传递该值。换言之,如果父进程未主动要求获得子进程结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态。接下来的示例是创建僵尸进程:        

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

int main(int argc, char *argv[])
{
    pid_t pid = fork(); // 创建一个新的子进程

    if (pid == 0) // 子进程执行代码
    {
        puts("Hi, I am a child Process"); // 输出一条消息:"Hi, I am a child Process"
    }
    else // 父进程执行代码
    {
        printf("Child Process ID: %d \n", pid); // 输出子进程的进程ID(PID)
        sleep(30); // 父进程睡眠30秒
    }

    // 以下代码父子进程共同执行

    if (pid == 0) // 子进程将执行这部分代码
        puts("End child process"); // 输出一条消息:"End child process"
    else // 父进程将执行这部分代码
        puts("End parent process"); // 输出一条消息:"End parent process"

    return 0;
}

运行结果:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

         通过 ps au 命令可以看出,子进程仍然存在,并没有被销毁,僵尸进程在这里显示为 Z+。30秒后,红框里面的两个进程会同时被销毁。

销毁僵尸进程 1:利用 wait 函数

为了销毁子进程,父进程应该主动请求获取子进程的返回值。下面是发起请求的具体方法。一共有两种:

#include <sys/wait.h>
pid_t wait(int *statloc);
/*
成功时返回终止的子进程 ID ,失败时返回 -1
*/

        调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit 函数的参数返回值,main 函数的 return 返回值)将保存到该函数的参数所指的内存空间。但函数参数指向的单元中还包含其他信息,因此需要用下列宏进行分离:

  • WIFEXITED 子进程正常终止时返回「真」
  • WEXITSTATUS 返回子进程时的返回值

也就是说,向 wait 函数传递变量 status 的地址时,调用 wait 函数后应编写如下代码:

if (WIFEXITED(status))
{
    puts("Normal termination");
    printf("Child pass num: %d", WEXITSTATUS(status));
}

        如下示例不会让子进程变成僵尸进程:

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

int main(int argc, char *argv[])
{
    int status;
    pid_t pid = fork(); // 创建第一个子进程

    if (pid == 0) // 子进程执行代码
    {
        return 3; // 子进程返回值为3,表示子进程正常终止
    }
    else // 父进程执行代码
    {
        printf("Child PID: %d \n", pid); // 输出第一个子进程的进程ID(PID)
        pid = fork(); // 在父进程中创建第二个子进程

        if (pid == 0) // 第二个子进程执行代码
        {
            exit(7); // 第二个子进程以退出码7正常终止
        }
        else // 父进程执行代码
        {
            printf("Child PID: %d \n", pid); // 输出第二个子进程的进程ID(PID)

            wait(&status); // 等待第一个子进程终止并处理其退出状态
            if (WIFEXITED(status)) // 验证第一个子进程是否正常终止
                printf("Child send one: %d \n", WEXITSTATUS(status)); // 输出第一个子进程的返回值

            wait(&status); // 等待第二个子进程终止并处理其退出状态
            if (WIFEXITED(status)) // 验证第二个子进程是否正常终止
                printf("Child send two: %d \n", WEXITSTATUS(status)); // 输出第二个子进程的返回值

            sleep(30); // 父进程睡眠30秒
        }
    }

    return 0;
}

运行结果:《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

        此时,系统中并没有上述 PID 对应的进程,这是因为调用了 wait 函数,完全销毁了该子进程。另外两个子进程返回时返回的 3 和 7 传递到了父进程。

        这就是通过 wait 函数消灭僵尸进程的方法,值得注意的是:调用 wait 函数时,如果没有已经终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此要谨慎调用该函数。

销毁僵尸进程 2:使用 waitpid 函数

        wait 函数会引起程序阻塞,还可以考虑调用 waitpid 函数。这是防止僵尸进程的第二种方法,也是防止阻塞的方法:

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
/*
成功时返回终止的子进程ID 或 0 ,失败时返回 -1
pid: 等待终止的目标子进程的ID,若传 -1,则与 wait 函数相同,可以等待任意子进程终止
statloc: 与 wait 函数的 statloc 参数具有相同含义
options: 传递头文件 sys/wait.h 声明的常量 WNOHANG ,即使没有终止的子进程也不会进入阻塞状态,而是返回 0 退出函数。
*/

 waitpid 的使用示例:

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

int main(int argc, char *argv[])
{
    int status;
    pid_t pid = fork(); // 创建子进程

    if (pid == 0) // 子进程执行代码
    {
        sleep(15); // 用 sleep 推迟子进程的执行,让其睡眠15秒
        return 24; // 子进程返回值为24,表示子进程正常终止
    }
    else // 父进程执行代码
    {
        // 使用 waitpid 传递参数 WNOHANG,这样在没有终止的子进程时,waitpid 立即返回,不会阻塞
        // 循环等待子进程终止
        while (!waitpid(-1, &status, WNOHANG))
        {
            sleep(3); // 父进程睡眠3秒
            puts("sleep 3 sec.");
        }

        if (WIFEXITED(status)) // 验证子进程是否正常终止
            printf("Child send %d \n", WEXITSTATUS(status)); // 输出子进程的返回值
    }

    return 0;
}

运行结果:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

        可以看出来,在 while 循环中正好执行了 5 次。这也证明了 waitpid 函数并没有阻塞 。

10.3 信号处理

        我们已经知道了进程的创建及销毁的办法,但是还有一个问题没有解决:子进程究竟何时终止?调用 waitpid 函数后要无休止的等待吗?

向操作系统求助:

        子进程终止的识别主题是操作系统,因此,若操作系统能把子进程结束的信息告诉正忙于工作的父进程,将有助于构建更高效的程序。

        为了实现上述的功能,引入信号处理机制(Signal Handing)。此处「信号」是在特定事件发生时由操作系统向进程发送的消息。另外,为了响应该消息,执行与消息相关的自定义操作的过程被称为「处理」或「信号处理」。

信号与 signal 函数:

        下面进程和操作系统的对话可以帮助理解信号处理。

进程:操作系统,如果我之前创建的子进程终止,就帮我调用 zombie_handler 函数。

操作系统:好的,如果你的子进程终止,我就帮你调用 zombie_handler 函数,你先把函数要执行的语句写好。        

        上述的对话,相当于「注册信号」的过程。即进程发现自己的子进程结束时,请求操作系统调用的特定函数。该请求可以通过如下函数调用完成: 

#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
/*
为了在产生信号时调用,返回之前注册的函数指针
函数名: signal
参数:int signo,void(*func)(int)
返回类型:参数类型为int型,返回 void 型函数指针
*/

        调用上述函数时,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针)。发生第一个参数代表的情况时,调用第二个参数所指的函数。下面给出可以在 signal 函数中注册的部分特殊情况和对应的函数。

  • SIGALRM:已到通过调用 alarm 函数注册时间
  • SIGINT:输入 ctrl+c
  • SIGCHLD:子进程终止

        接下来编写调用 signal 函数的语句完成如下请求:子进程终止则调用 mychild 函数。

        此时 mychild 函数的参数应为 int ,返回值类型应为 void 。只有这样才能成为 signal 函数的第二个参数。另外,常数 SIGCHLD 定义了子进程终止的情况,应成为 signal 函数的第一个参数。也就是说,signal 函数调用语句如下:

signal(SIGCHLD , mychild);

接下来编写 signal 函数的调用语句,分别完成如下两个请求:

  1. 已到通过 alarm 函数注册时间,请调用 timeout 函数
  2. 输入 ctrl+c 时调用 keycontrol 函数

代表这 2 种情况的常数分别为 SIGALRM 和 SIGINT ,因此按如下方式调用 signal 函数:

signal(SIGALRM , timeout);
signal(SIGINT , keycontrol);

        以上就是信号注册过程。注册好信号之后,发生注册信号时(注册的情况发生时),操作系统将调用该信号对应的函数。先介绍 alarm 函数:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 返回0或以秒为单位的距 SIGALRM 信号发生所剩时间

        如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生 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); //为了每隔 2 秒重复产生 SIGALRM 信号,在信号处理器中调用 alarm 函数
}
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); //预约 2 秒后发生 SIGALRM 信号

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

        运行结果:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

        第一次结果是没有任何输入的运行结果 。 第二次是连续键入3次 ctrl+c 的结果。发生信号时将唤醒由于调用 sleep 函数而进入阻塞状态的进程。

        调用函数的主体的确是操作系统,但是进程处于睡眠状态时无法调用函数,因此,产生信号时,为了调用信号处理器,将唤醒由于调用 sleep 函数而进入阻塞状态的进程。而且,进程一旦被唤醒,就不会再进入睡眠状态。即使还未到 sleep 中规定的时间也是如此。所以上述示例运行不到 10 秒后就会结束,连续输入 CTRL+C 可能连一秒都不到。

利用 sigaction 函数进行信号处理:

        前面所学的内容可以防止僵尸进程,还有一个函数,叫做 sigaction 函数,他类似于 signal 函数,而且可以完全代替后者,也更稳定。之所以稳定,是因为:signal 函数在 Unix 系列的不同操作系统可能存在区别,但 sigaction 函数完全相同。

        实际上现在很少用 signal 函数编写程序,他只是为了保持对旧程序的兼容,下面介绍 sigaction 函数,只讲解可以替换 signal 函数的功能:

#include <signal.h>

int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
/*
成功时返回 0 ,失败时返回 -1
act: 对于第一个参数的信号处理函数(信号处理器)信息。
oldact: 通过此参数获取之前注册的信号处理函数指针,若不需要则传递 0
*/

        声明并初始化 sigaction 结构体变量以调用上述函数,该结构体定义如下: 

struct sigaction
{
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
};

        此结构体的成员 sa_handler 保存信号处理的函数指针值(地址值)。sa_mask 和 sa_flags 的所有位初始化 0 即可。这 2 个成员用于指定信号相关的选项和特性,而我们的目的主要是防止产生僵尸进程,故省略。

        下面的示例是关于 sigaction 函数的使用方法:

#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);   //将 sa_mask 成员的所有位初始化成0
    act.sa_flags = 0;            //sa_flags 同样初始化成 0
    sigaction(SIGALRM, &act, 0); //注册 SIGALRM 信号的处理器。

    alarm(2); //2 秒后发生 SIGALRM 信号

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

        运行结果:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

         可以发现,结果和之前用 signal 函数的结果没有什么区别。

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

        下面利用子进程终止时产生 SIGCHLD 信号这一点,来用信号处理来消灭僵尸进程。看以下代码:

#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);             //子进程的 pid
        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;
}

        运行结果:《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

        代码中创建了两个子进程,父进程在创建子进程后会输出子进程的信息,然后进入等待状态,睡眠5秒,循环输出"wait"。而子进程在输出自身信息后,睡眠10秒后返回不同的返回值。父进程在循环等待期间,若有子进程终止,则信号处理函数 read_childproc 会被调用,输出子进程的PID和返回值。 

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

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

        之前的回声服务器每次只能同事向 1 个客户端提供服务。因此,需要扩展回声服务器,使其可以同时向多个客户端提供服务。下图是基于多进程的回声服务器的模型:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

        从图中可以看出,每当有客户端请求时(连接请求),回声服务器都创建子进程以提供服务。如果请求的客户端有 5 个,则将创建 5 个子进程来提供服务,为了完成这些任务,需要经过如下过程:

  • 第一阶段:回声服务器端(父进程)通过调用 accept 函数受理连接请求
  • 第二阶段:此时获取的套接字文件描述符创建并传递给子进程
  • 第三阶段:进程利用传递来的文件描述符提供服务

实现并发服务器:

        

#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("Usgae : %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);         //注册信号处理器,把成功的返回值给 state
    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) //分配IP地址和端口号
        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, BUFSIZ)) != 0)
                write(clnt_sock, buf, str_len);

            close(clnt_sock);
            puts("client disconnected...");
            return 0;
        }
        else
            close(clnt_sock); //通过 accept 函数创建的套接字文件描述符已经复制给子进程,因为服务器端要销毁自己拥有的
    }
    close(serv_sock);

    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
void read_childproc(int sig)
{
    pid_t pid;
    int status;
    pid = waitpid(-1, &status, WNOHANG);
    printf("removed proc id: %d \n", pid);
}

 运行结果:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

        此时的服务端支持同时给多个客户端进行服务,每有一个客户端连接服务端,就会多开一个子进程,所以可以同时提供服务。 

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

        示例中给出了通过 fork 函数复制文件描述符的过程。父进程将 2 个套接字(一个是服务端套接字另一个是客户端套接字)文件描述符复制给了子进程

        调用 fork 函数时复制父进程的所有资源,但是套接字不是归进程所有的,而是归操作系统所有,只是进程拥有代表相应套接字的文件描述符。

        复制套接字后,同一端口将对应多个套接字。

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

         如上图所示,1 个套接字存在 2 个文件描述符时,只有 2 个文件描述符都终止(销毁)后,才能销毁套接字。如果维持图中的状态,即使子进程销毁了与客户端连接的套接字文件描述符,也无法销毁套接字(服务器套接字同样如此)。因此调用 fork 函数后,要将无关紧要的套接字文件描述符关掉,如下图所示:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

10.5 分割 TCP 的 I/O 程序

分割 I/O 的优点:

        我们已经实现的回声客户端的数据回声方式如下:向服务器传输数据,并等待服务器端回复。无条件等待,直到接收完服务器端的回声数据后,才能传输下一批数据。     

        传输数据后要等待服务器端返回的数据,因为程序代码中重复调用了 read 和 write 函数。只能这么写的原因之一是,程序在 1 个进程中运行,现在可以创建多个进程,因此可以分割数据收发过程。默认分割过程如下图所示:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

        从图中可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,这样,无论客户端是否从服务器端接收完数据都可以进程传输。

分割 I/O 程序的另外一个好处是,可以提高频繁交换数据的程序性能,如下图所示:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程

        下面是回声客户端的 I/O 分割的代码实现: 

#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 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); //向服务器端传递 EOF,因为fork函数复制了文件描述度,所以通过1次close调用不够
            return;
        }
        write(sock, buf, strlen(buf));
    }
}

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

运行结果:

《TCP IP网络编程》第十章,《TCPIP网络编程》,网络,tcp/ip,服务器,网络编程


习题: 

1、请说明进程变为僵尸进程的过程以及预防措施。

        进程变为僵尸进程的过程:

  1. 创建子进程:当一个进程创建子进程后,子进程会拷贝父进程的资源和状态,包括代码、数据、打开的文件等。

  2. 子进程终止:子进程执行完任务后,会调用 exit() 或者从 main 函数中返回一个值来终止。此时,子进程的终止状态和退出码会被保存在内核的进程表项中,等待父进程回收。

  3. 父进程未及时回收子进程:父进程可能因为各种原因,无法及时处理子进程的终止状态,比如父进程忙于其他任务、死锁、阻塞在某个操作上等。在这种情况下,子进程的进程表项仍然保留在系统进程表中,成为僵尸进程。尽管子进程已经终止,但内核仍然保存它的状态信息,以便父进程在合适的时候获取。

        预防措施:通过 wait 和 waitpid 函数加上信号函数写代码来预防。文章来源地址https://www.toymoban.com/news/detail-605428.html

到了这里,关于《TCP IP网络编程》第十章的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《TCP/IP网络编程》阅读笔记--基于UDP的服务器端/客户端

    目录 1--TCP和UDP的主要区别 2--基于 UDP 的数据 I/O 函数 3--基于 UDP 的回声服务器端/客户端 4--UDP客户端Socket的地址分配 5--UDP存在数据边界 6--UDP已连接与未连接的设置 ① TCP 提供的是可靠数据传输服务,而 UDP 提供的是不可靠数据传输服务; ② UDP 在结构上比 TCP 更简洁,其不会

    2024年02月09日
    浏览(50)
  • Linux网络编程之TCP/IP实现高并发网络服务器设计指南

    目录 引言: 多进程服务器 例程分享: 多线程服务器  例程分享: I/O多路复用服务器 select 例程分享: poll 例程分享: epoll 例程分享: 总结建议         随着互联网的迅猛发展,服务器面临着越来越多的并发请求。如何设计一个能够高效处理大量并发请求的服务器成为

    2024年02月20日
    浏览(44)
  • 《TCP/IP网络编程》阅读笔记--基于Windows实现Hello Word服务器端和客户端

    目录 1--Hello Word服务器端 2--客户端 3--编译运行 3-1--编译服务器端 3-2--编译客户端 3-3--运行 运行结果:

    2024年02月10日
    浏览(44)
  • 《TCP IP网络编程》

            2023.6.28 正式开始学习网络编程。 每一章每一节的笔记都会记录在博客中以便复习。         网络编程又叫套接字编程。所谓网络编程,就是编写程序使两台连网的计算机相互交换数据。 为什么叫套接字编程? 我们平常将插头插入插座上就能从电网中获取电力,同

    2024年02月11日
    浏览(35)
  • TCP/IP网络编程(三)

    多播(Multicast)方式的数据传输是 基于 UDP 完成的 。因此 ,与 UDP 服务器端/客户端的实现方式非常接近。区别在于,UDP 数据传输以单一目标进行,而多播数据 同时传递到加入(注册)特定组的大量主机 。换言之, 采用多播方式时,可以同时向多个主机传递数据 。 14.1.1 多

    2024年02月03日
    浏览(37)
  • TCP/IP网络编程(二)

    本章将讨论如何优雅地断开相互连接的套接字。之前用的方法不够优雅是因为,我们是调用 close 或 closesocket 函数单方面断开连接的。 TCP中的断开连接过程比建立连接过程更重要,因为连接过程中一般不会出现大的变数,但断开过程有可能发生预想不到的情况,因此应准确掌

    2024年02月03日
    浏览(39)
  • TCP/IP网络编程(一)

    1.1.1 构建打电话套接字 以电话机打电话的方式来理解套接字。 **调用 socket 函数(安装电话机)时进行的对话:**有了电话机才能安装电话,于是就要准备一个电话机,下面函数相当于电话机的套接字。 **调用 bind 函数(分配电话号码)时进行的对话:**套接字同样如此。就想

    2024年02月03日
    浏览(36)
  • 网络编程——TCP/IP协议族(IP协议、TCP协议和UDP协议……)

    1、IP协议简介 IP协议又称 网际协议 特指为实现在一个相互连接的网络系统上从源地址到目的地传输数据包(互联网数据包)所提供必要功能的协议,是网络层中的协议。 2、特点 不可靠 :它不能保证IP数据包能成功地到达它的目的地,仅提供尽力而为的传输服务 无连接 :IP 并不

    2024年02月13日
    浏览(45)
  • 《TCP IP网络编程》第六章

    UDP 套接字的特点:         通过寄信来说明 UDP 的工作原理,这是讲解 UDP 时使用的传统示例,它与 UDP 的特点完全相同。寄信前应先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当 然,信件的特点使我们无法确认信件是否被收到。邮寄过程中也可能

    2024年02月16日
    浏览(39)
  • 《TCP IP网络编程》第一章

            2023.6.28 正式开始学习网络编程。 每一章每一节的笔记都会记录在博客中以便复习。         网络编程又叫套接字编程。所谓网络编程,就是编写程序使两台连网的计算机相互交换数据。 为什么叫套接字编程? 我们平常将插头插入插座上就能从电网中获取电力,同

    2024年02月11日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包