TCP/IP网络编程——多进程服务器端

第 10 章 多进程服务器端

本章代码在多进程服务器端中可以找到

10.1 进程概念及应用

10.1.1 并发服务端的实现方法

下面列出的是具有代表性的并发服务端的实现模型和方法

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

先是第一种方法多进程服务器

10.1.2 理解进程

进程的定义如下

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

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

10.1.3 进程 ID

在说进程创建方法之前先要简要说明进程 ID。无论进程是如何创建的所有的进程都会被操作系统分配一个 ID。此 ID 被称为进程ID其值为大于 2 的整数。1 要分配给操作系统启动后的用于协助操作系统首个进程因此用户无法得到 ID 值为 1 。接下来观察在 Linux 中运行的进程。

ps au

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

10.1.4 通过调用 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 函数后的程序运行流程。如图所示

程序实现
fork.c

#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;
}

运行结果

在这里插入图片描述

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

10.2 进程和僵尸进程

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

10.2.1 僵尸Zombie进程

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

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

10.2.2 产生僵尸进程的原因

为了防止僵尸进程产生先解释产生僵尸进程的原因。利用如下两个示例展示调用 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");
    else
    {
        printf("Child Process ID: %d \n", pid);
        sleep(30);
    }

    if (pid == 0)
        puts("End child process");
    else
        puts("End parent process");
    return 0;
}

运行结果

在这里插入图片描述

因为暂停了 30 秒所以在这个时间内可以验证一下子进程是否为僵尸进程。

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

利用 ./zombie &可以使程序在后台运行不用打开新的命令行窗口。

在这里插入图片描述

10.2.3 销毁僵尸进程 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));
}

wait.c程序

#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;
    else
    {
        printf("Child PID: %d \n", pid);
        pid = fork();
        if (pid == 0)
            exit(7);
        else
        {
            printf("Child PID: %d \n", 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);
        }
    }

    return 0;
}

运行结果

在这里插入图片描述

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

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

10.2.4 销毁僵尸进程 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.c 程序

#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 推迟子进程的执行
        return 24;
    }
    else
    {
        //调用waitpid 传递参数 WNOHANG 这样之前有没有终止的子进程则返回0
        while (!waitpid(-1, &status, WNOHANG))
        {
            sleep(3);
            puts("sleep 3 sec.");
        }
        if (WIFEXITED(status))
            printf("Child send %d \n", WEXITSTATUS(status));
    }
    return 0;
}

运行结果

在这里插入图片描述

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

10.3 信号处理

我们已经知道了进程的创建及销毁的办法但是还有一个问题没有解决。

子进程究竟何时终止调用 waitpid 函数后要无休止的等待吗

10.3.1 向操作系统求助

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

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

10.3.2 信号与 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 函数终止进程不做任何处理。

signal.c 程序

#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 函数而进入阻塞状态的进程。而且进程一旦被唤醒就不会再进入睡眠状态。即使还未到 sleep 中规定的时间也是如此。所以上述示例运行不到 10 秒后就会结束连续输入 CTRL+C 可能连一秒都不到。

简言之就是本来系统要睡眠100秒但是到了 alarm(2) 规定的两秒之后就会唤醒睡眠的进程进程被唤醒了就不会再进入睡眠状态了所以就不用等待100秒。如果把 timeout() 函数中的 alarm(2) 注释掉就会先输出wait...然后再输出Time out! (这时已经跳过了第一次的 sleep(100) 秒),然后就真的会睡眠100秒因为没有再发出 alarm(2) 的信号。

10.3.3 利用 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 函数的使用方法

sigaction.c 程序

#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;
}

运行结果

在这里插入图片描述

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

下面利用子进程终止时产生 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;
}

运行结果

在这里插入图片描述

结果中的每一个空行代表间隔了5 秒程序是先创建了两个子进程然后子进程 10 秒之后会返回值第一个 wait 由于子进程在执行所以直接被唤醒然后这两个子进程正在睡 10 秒所以 5 秒之后第二个 wait 开始执行又过了 5 秒两个子进程同时被唤醒。所以剩下的 wait 也被唤醒。

所以在本程序的过程中当子进程终止时候会向系统发送一个信号然后调用我们提前写好的处理函数在处理函数中使用 waitpid 来处理僵尸进程获取子进程返回值。

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

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

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

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

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

10.4.2 实现并发服务器

下面是基于多进程实现的并发的回声服务器的服务端

echo_mpserv.c 程序

#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);
}

运行结果

在这里插入图片描述

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

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

在代码里面可能不明白为什么子进程要关闭服务端套接字父进程为什么要关闭客户端套接字

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

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

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

10.5 分割 TCP 的 I/O 程序

10.5.1 分割 I/O 的优点

我们已经实现的回声客户端的数据回声方式如下

向服务器传输数据并等待服务器端回复。无条件等待直到接收完服务器端的回声数据后才能传输下一批数据。

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MHXHEErQ-1675686240435)(null)]

根据上图显示可以看出在网络不好的情况下明显提升速度。

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

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

echo_mpclient.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 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);
}
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: 服务器