TCP/IP网络编程(8) 基于Linux的多进程服务器

1. Linux下的多进程服务器

1.1 进程的概念及应用

并发服务器实现的模型和方法:

  • 多进程服务器   (通过创建多个进程提供服务)
  • 多路复用服务器   (通过捆绑并统一管理IO对象提供服务)
  • 多线程服务器  (通过创建多个线程提供服务)

多进程技术是一种实现并发服务器的手段,在网络通信所占的时间中,数据通信时间比CPU运算时间的占比更大,向多个服务端同时提供服务是一种有效利用CPU资源的方式。

进程定义:占用内存空间的正在运行的程序。例如在电脑上,同时打开文档编辑软件,聊天软件,以及MP3播放器,此时就是创建了三个进程,从操作系统的角度来看,进程是程序流的基本单位,若创建多个进程,操作系统将同时运行,有时一个进程运行的过程中也会产生多个进程。

注:拥有n个运算设备(运算器)的CPU称为n核CPU,核的个数与同时可运行的进程数量相同,若进程数超过核数,进程将分时使用CPU资源,但是由于CPU运算速度足够快,使得用户感觉到所有进程都是同时运行的。

1.2 进程ID

在创建进程的时候,所有进程都会从操作系统分配到对应得分ID,其值为大于2的整数,1是分配给操作系统启动后的首个进程(Linux系统启动后,创建的第一个进程就是init进程),可通过如下命令查看当前Linux下的所有进程:

ps au  
// 指定au参数可列出进程的所有详细信息

ps -ef | grep xxx

创建进程fork

#include <unistd.h>

pid_t fork(void);    // 成功时返回进程ID,失败时返回-1 

fork函数创建的是调用它的进程的副本,即它是复制正在运行的,调用fork函数的进程,此外,在fork()函数返回后,两个进程都将执行fork()函数后面的语句。但是因为在fork()的时候,是通过同一个进程,复制相同的内存空间,因此fork()之后的程序需要根据fork()函数的返回值加以区分:

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

注:这里的父进程指调用fork()函数的原进程,子进程是指父进程通过调用fork()函数复制出来的进程。

代码实例:

main.cpp

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

int gval = 10;

int main(int argc, char** argv)
{
    pid_t pid;

    int localVal = 20;

    gval++; localVal+=5;

    pid  = fork();     // # fork出一个新的进程

    if (pid == 0)      // 子进程
    {
        /* code */
        gval+=2;
        localVal+=2;

        printf("The child Process: %d, %d\n", gval, localVal);
    }
    else               // 父进程
    {
        gval-=2;
        localVal-=2;

        printf("The father Process: %d, %d\n", gval, localVal);
    }
    
    return 0;
}

输出结果:

 父进程在调用fork()函数的同事,复制出子进程,并且获取到fork函数的返回值,在复制前,父进程分别对局部变量和全局变量的值进行了修改,在这种状态下进行复制,子进程也将获取到修改后的值。复制完成后,根据fork()函数的返回值,区分父子进程。在父子进程中,修改变变量值不会相互影响,因为调用fork()函数进行复制之后,父子进程具有完全独立的内存结构,二者只是共享相同的代码而已。

1.3 僵尸进程 zombie

文件操作中,文件的打开和关闭同样重要。同样,进程的创建和销毁也同样重要,如果未成功销毁进程,他们将变成僵尸进程。进程在完成工作后(执行完main函数中的程序后)应该被销毁,但是有的进程会变成僵尸进程,占用系统中的重要资源,首先介绍僵尸进程产生的原因,和如何去销毁僵尸进程。

子进程运行结束时,向exit()函数传递的参数值或者return语句的返回值都会传递给操作系统,而操作系统在此时不会将这个值传递给给产生该子进程的父进程,也不会销毁子进程。此时处于这种状态下的进程就是僵尸进程,也就是说,正是操作系统自己,将子进程变成了僵尸进程。

销毁子进程的方法:

将子进程exit()或者return的值传递个产生它的父进程,此时子进程会被销毁

而操作系统不会主动把子进程的返回值传给父进程,只有父进程主动发起请求的时候,操作系统才会传递该值,此时自己成才会被销毁。如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存该值,且让子进程一直处于僵尸状态,即父进程负责回收自己产生的子进程。

通过一个代码实例来演示僵尸进程的产生:

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

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

    if (pid == 0)            // 子进程
    {
        printf("This is child process: %d\n", pid);
    }
    else
    {
        printf("This is Father process: %d\n", pid);
        sleep(30);           // 父进程延时30s
    }

    if (pid == 0)
        printf("Child process finished\n");
    else
        printf("Father process finished\n");

    return 0;
}

运行结果:

结果分析:可以看到在在30S以内查看进程,发现子进程为僵尸进程,当主进程到达30s而退出之后,处于僵尸状态的子进程将同时被销毁。

1.3.1 利用wait函数销毁僵尸进程

为了销毁子进程,父进程需要主动请求获取子进程的返回值,可用wait方法发起请求

#include <sys/wait.h>

/*
    statloc: 包含子进程终止时传递回来的信息 (返回值,返回状态)
    需要通过宏进行分离
    子进程返回状态:WIFEXITED(statloc)
    子进程的返回值:WEXITSTATUS(statloc)
    返回值:成功时返回终止的子进程的ID,失败返回-1
*/
pid_t wait(int* statloc)

调用wait函数消灭僵尸进程的时候,如果没有已经终止的子进程,程序将阻塞直到有结束的子进程,因此在调用wait函数的时候需要谨慎。

代码示例:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.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(5);    
        }
        else
        {
            printf("Child pid %d\n", pid);
            
            // 调用wait函数结束子进程
            wait(&status);
            
            if (WIFEXITED(status))
            {
                printf("Child returned %d\n", WEXITSTATUS(status));
            }

            wait(&status);

            if (WIFEXITED(status))
            {
                printf("Child returned %d\n", WEXITSTATUS(status));
            }
            sleep(30);
        }
    }
    return 0;
}

运行结果:

可以看待在系统下运行的线程中不存在僵尸进程。

1.3.2 使用waitpid函数销毁僵尸进程

wait函数会造成阻塞问题,waitpid()函数也是一种销毁僵尸进程的方法,且能够避免发生阻塞。

#include <sys/wait.h>

/*
    pid: 等待终止的目标子进程ID
    statloc: 存储进程返回后的状态
    options: 传递头文件sys/wait.h中声明的常量WNOHANG, 即使没有终止的进程也不会进入阻塞状态,
             而实返回0并退出。
*/
pid_t waitpid(pid_t pid, int * statloc, int options);

代码示例:

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

int main(int argc, char** argv)
{
    int status;

    pid_t pid = fork();

    if (pid == 0)
    {
        // 子进程
        sleep(15);
        return 1;
    }
    else
    {
        while(waitpid(pid, &status, WNOHANG) == 0)
        {
            sleep(3);
            printf("The child process %d is running.\n", pid);
        }

        if (WIFEXITED(status))
        {
            printf("Child process exited with %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}

运行结果:

 1.4 信号处理

父进程创建子进程之后,子进程何时终止,父进程往往同子进程一样繁忙,因此不能只通过调用waitpid函数等待子进程结束。因此需要寻找其他的解决方案:
子进程终止的识别主体是操作系统,若在子进程结束的时候,操作系统能将结束的信号告诉忙于处理其他业务的主进程,则将大大提高程序的运行效率。此时父进程将暂时放下其他的业务,来专门处理子进程终止的相关事宜,在Linux系统下,可以借助信操作系统的信号机制实现此想法。

Linux中的信号是一种消息处理机制,不同的信号使用不同的值表示,代表不同的含义,虽然信号结构简单,不能携带很大的信息量,但是信号在系统中的优先级很高,在Linux系统下,很多常规的操作,都会产生响应的信号:

  • 键盘操作产生信号 :按下Ctrl+C,键盘输入一个硬件中断,产生一个信号,这个信号会杀死对应的某个进程
  • 通过shell命令产生信号:kill -9 pid ,终止某个进程
  • 函数调用产生信号:如进程中调用sleep()函数,进程收到相关信号,被迫挂起
  • 对硬件进程了非法访问产生了信号:程序访问内存错误,或者段错误,进程退出

利用信号机制也可以实现进程间通信,但是由于信号的结构简单,不能携带大量信息,且信号的优先级很高,它对应的信号处理函数是通过回调完成,会打乱程序原有的处理流程,因此不适合用信号处理进程间通信。

可通过kill -l 查看系统定义的信号列表:

Linux中能够产生信号的函数有很多:
(1)kill  发送指定的信号到指定的进程:

// 发送指定的信号到指定的进程
int kill(pid_t pid, int sig);

kill(getpid(), 9);    // 自己杀死自己

(2)raise  给当前进程发送指定的信号

// 给自己发送某一个信号
#include <signal.h>
int raise(int sig);	// 参数就是要给当前进程发送的信号

(3)abort  给当前进程发送一个固定信号 (SIGABRT)

// 这是一个中断函数, 调用这个函数, 发送一个固定信号 (SIGABRT), 杀死当前进程
#include <stdlib.h>
void abort(void);

(4)alarm  用于单次定时,定时完成向当前进程发出一个信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

(5)setitimer  用于周期定时,没触发一次定时器就会发出对应的信号

// 函数可实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号
#include <sys/time.h>

struct itimerval {
	struct timeval it_interval; /* 时间间隔 */
	struct timeval it_value;    /* 第一次触发定时器的时长 */
};


// 表示一个时间段: tv_sec + tv_usec
struct timeval {
	time_t      tv_sec;         /* 秒 */
	suseconds_t tv_usec;        /* 微妙 */
};

int setitimer(int which, const struct itimerval *new_value, 
              struct itimerval *old_value);


// new_value : 输入值,为定时器设置的参数
// old_value :  输出值,上一次为定时器设置的参数,如果不需要知到,传递NULL
// which     :  定时器的计数方式

/*
   which参数可选项:
   ITIMER_REAL: 自然计时法, 最常用, 发出的信号为SIGALRM, 一般使用这个宏值,自然计时法时间 = 用户区 + 内核 + 消耗的时间(从进程的用户区到内核区切换使用的总时间)
   ITIMER_VIRTUAL:只计算程序在用户区运行使用的时间,发射的信号为 SIGVTALRM
   ITIMER_PROF:只计算内核运行使用的时间, 发出的信号为SIGPROF
*/

信号与signal函数:

进程首先需要告诉操作系统,在它创建的子进程结束之后,请求帮他调用zombie_handler函数,为了完成这一过程,进程首先需要向操作系统注册一个信号才能实现调用这个函数。操作系统调用的这个函数称为信号注册函数。

#include <signal.h>

void (*signal(int signo, void(*func)(int)))(int);

函数名:signal

参数:int signo, void (*func)(int)

返回值:返回一个函数指针,这个函数指针指向的函数,具有一个int类型的参数,无返回值。

signal函数原型的理解:signal函数为带有两个参数的函数,一个参数为int类型的整数,另一个参数为函数指针,这个函数指针指向的函数原型没有返回值,具有一个int参数。signal函数执行完毕后,其返回值的也是一个函数指针,这个函数指针指向的是没有返回值,参数为int类型的函数。

在signal函数中可以注册的部分特殊情况和对应的常数值:

  • SIGALARM     已到了 通过调用alarm函数注册的时间
  • SIGINT            输入CTRL+C 程序中断
  • SIGCHLD        子进程终止

在信号注册好之后,当注册的情况发生时,操作系统将调用该信号对应的函数

代码实例:

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

// 定义信号处理函数,这种函数称为信号处理器 Handler
void timeOut(int signo)
{
    if (signo == SIGALRM)
    {
        // 到达alram注册的超时时间
        printf("Time out\n");
    }

    alarm(2);
}

// 定义信号处理函数,这种函数称为信号处理器 Handler
void keyControl(int signo)
{
    if (signo == SIGINT)
    {
        printf("CTRL+C pressed!\n");
    }
}

int main(int argc, char** argv)
{
    // 注册信号
    signal(SIGALRM, timeOut);
    signal(SIGINT, keyControl);
    alarm(2);

    for (size_t i = 0; i < 20; i++)
    {
        /* code */
        printf("Wait...\n");
        sleep(30);
    }
    

    return 0;
}

运行结果:

 发生信号时将会唤醒由于调用sleep而进入休眠状态的进程(即上述代码中,调用sleep之后,程序进入阻塞状态,当alarm(2)超时之后,会唤醒进程,直接的体现就是打印输出了"Wait")。调用函数的主体的确是操作系统,但是进程处于睡眠状态时无法调用函数,因此,产生信号的时候,为了调用信号处理函数,将唤醒由于调用sleep而处于阻塞状态的进程。而且,进程一旦被唤醒,就不会再进入休眠状态,即使还未到sleep中规定的睡眠时间。所以上述实例会很快运行结束。

利用sigaction函数进行信号处理

相比于signal函数,sigaction更加稳定,且具有通用性,因为signal在Unix系列的不同系统下,可能存在区别,但是sigaction函数完全相同。建议使用sigaction函数编写程序,以增强代码的可移植性。

#include <signal.h>

int sigaction(int signo, const struct sigaction* act, const struct sigaction* oldact);
  • signo          与signal函数相同,用于传递信号信息
  • act              对应于第一个参数的信号处理函数信息
  • oldact         通过此参数获取之前注册的信号处理函数的函数指针,若不需要则传递0

结构体sigaction定义如下:

struct sigaction
{
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
}
  • sa_handler用于保存信号处理函数的指针值
  • sa_mask 所有位均初始化0即可   (用于指定信号相关的选项和特性)
  • sa_flags所有位均初始化位0即可  (用于指定信号相关的选项和特性)

代码实例:

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

// 定义信号注册函数
void timeOut(int signo)
{
    if (signo == SIGALRM)
    {
        // 到达alram注册的超时时间
        printf("Time out\n");
    }

    alarm(2);
}


int main(int argc, char** argv)
{
    // 注册信号
    struct sigaction act;
    act.sa_handler = timeOut;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    sigaction(SIGALRM, &act, 0);

    alarm(2);

    for (size_t i = 0; i < 5; i++)
    {
        /* code */
        printf("Wait...\n");
        sleep(50);
    }
    

    return 0;
}

运行结果:

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

/*
   利用信号机制处理僵尸进程
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>

// 定义信号处理函数
void readChildProc(int signo)
{
    int status;

    pid_t pid = waitpid(-1, &status, WNOHANG);

    if (WIFEXITED(status))
    {
        printf("Terminated process %d\n", pid);
        printf("Process return %d at termination\n", WEXITSTATUS(status));
    }
}

int main(int argc, char** argv)
{
    struct sigaction sigact;
    sigact.sa_handler = readChildProc;
    sigemptyset(&sigact.sa_mask);
    sigact.sa_flags = 0;
    // 注册信号
    sigaction(SIGCHLD, &sigact, 0);

    pid_t pid = fork();     // 创建进程

    if (pid == 0)
    {
        // 子进程
        printf("I am child process.\n");
        sleep(10);
        return 5;
    }
    else
    {
        // 父进程
        printf("Create child process %d\n", pid);

        // 再创建一个子进程
        pid = fork();

        if (pid == 0)
        {
            // 子进程
            printf("I am child process.\n");
            sleep(20);
            return 3;
        }
        else
        {
            // 父进程
            printf("Create child process %d\n", pid);
            for (size_t i = 0; i < 5; i++)
            {
                /* code */
                printf("Wait.....\n");
                sleep(15);
            }
            
        }
    }

}

运行结果:

 通过上述信号机制处理进程,可以避免创建的子进程变成僵尸进程。

1.5 基于多进程的并发服务器

对回声服务器的例子进行扩展,使其可以向多个客户端同时提供服务。每当有客户端请求服务的时候,回升服务器端都会创建一个子进程以提供服务,此时的服务器端运行主流程如下:

  1. 回声服务器端(父进程)通过调用accept函数受理连接请求
  2. 此时获取的套接字文件描述符创建并传递给子进程
  3. 子进程利用传递来的文件描述符为客户端提供服务

代码示例:
服务端:

/* 
    多进程服务器
    create_date: 2022-7-27
*/
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUFF_SIZE 30       // 缓冲区大小
#define PORT   13100       // 端口号

void error_handler(char* msg)
{
    printf("%s\n", msg);
    exit(1);
}

void read_child_proc(int signo)
{
    int status;
    pid_t pid = waitpid(-1, &status, 0);
    printf("Removed process %d\n", pid);
}

int main(int argc, char** argv)
{
    int serverSocket;
    int clientSocket;

    struct sockaddr_in serverAddr;
    struct sockaddr_in clientAddr;

    char buffer[BUFF_SIZE];

    socklen_t addrSize;

    // 注册信号处理函数
    struct sigaction sigact;
    sigact.sa_handler = read_child_proc;
    sigemptyset(&sigact.sa_mask);
    sigact.sa_flags = 0;
    sigaction(SIGCHLD, &sigact, 0);

    // 初始化服务端地址
    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddr.sin_port = htons(PORT);

    serverSocket = socket(PF_INET, SOCK_STREAM, 0);      // TCP socket

    // 为服务端socket绑定法地址
    if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
    {
        close(serverSocket);
        error_handler("Failed to bind server address");
    }

    // 开始监听客户端
    if (listen(serverSocket, 5) == -1)
    {
        close(serverSocket);
        error_handler("Failed to listen client");
    }
    
    while (true)
    {
        addrSize = sizeof(clientAddr);

        printf("Successfully init server and wait for connect......\n");

        clientSocket = accept(serverSocket, (sockaddr*)&clientAddr, &addrSize);

        if (clientSocket == -1)
            continue;
        
        printf("Receive connection from %s : %d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

        // 创建新的进程
        pid_t pid = fork();

        if (pid == -1)       // 创建进程失败
        {
            close(clientSocket);     
            continue;
        }     

        if (pid != 0)
        {
            printf("Created new process for client.\n");
            
            // 主进程中需要关闭客户端的socket,因为在创建进程的时候,这个socket会被复制到子进程中去  重要!
            close(clientSocket); 

            memset(&clientAddr, 0, sizeof(clientAddr));       
            
            continue;
        }
        else
        {
            // 在子进程创建的时候,父进程服务端的socket也会复制到子进程中去,而在子进程中,需要将其关闭  重要!
            close(serverSocket);

            int str_len = 0;

            memset(buffer, 0, BUFF_SIZE);

            while ((str_len = read(clientSocket, buffer, BUFF_SIZE)) != 0)
            {
                /* code */
                write(clientSocket, buffer, str_len);

                memset(buffer, 0, BUFF_SIZE);      // 清一下缓冲区
            }

            close(clientSocket);

            printf("Disconnect %s: %d client from server.\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

            return 0;
        }
    }

    close(serverSocket);
    
    return 0;
}

客户端:

/* 
    客户端
    create_date: 2022-7-29
*/

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

#define BUFF_SIZE  30
#define ADDRESS    "127.0.0.1"
#define PORT       13100


int main(int argc, char** argv)
{
    int socket;
    char buffer[BUFF_SIZE];

    struct sockaddr_in serverAddr;               // 服务端地址
    memset(&serverAddr, 0, sizeof(serverAddr));   
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(ADDRESS);
    serverAddr.sin_port = htons(PORT);

    memset(buffer, 0, BUFF_SIZE);

    socket = ::socket(PF_INET, SOCK_STREAM, 0);

    if (socket == -1)
    {
        printf("Failed to init socket.\n");
        return -1;
    }

    if (connect(socket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
    {
        printf("Failed to connect to server.\n");
        return -2;
    }

    printf("Successfully connect to the server.\n");

    while (true)
    {
        fputs("Input message(Type Q(q) to quit): ", stdout);

        fgets(buffer, BUFF_SIZE, stdin);

        if (strncmp(buffer, "Q\n", 2) == 0 || strncmp(buffer, "q\n", 2) == 0)
            break;
        
        int write_len = write(socket, buffer, strlen(buffer));

        int recv_len = 0;

        while (recv_len < write_len)
        {
            int recv_count = read(socket, &buffer[recv_len], BUFF_SIZE-1);

            recv_len += recv_count;

            // printf("Received %d bytes data from sever.\n");
        }

        printf("Receive data: %s", buffer);

        memset(buffer, 0, BUFF_SIZE);
    }

    close(socket);

    return 0;
    

    return 0;
}

运行结果:

服务端运行结果:

 客户端1运行结果:

 客户端2运行结果:

 查看运行的线程:

问题:

在上述多进程服务器代码中,在调用fork函数创建子进程的过程中,父进程将两个套接字(一个服务端套接字,一个客户端套接字) 的文件描述符复制给了子进程,在这一过程中,是仅仅复制了文件描述符吗?是否对套接字也进行了复制?

调用fork()函数的时候,会复制父进程的所有资源,同理文件客户端,服务端的描述符也属于父进程资源同样会被复制,但是不会复制套接字。因为套接字属于操作系统的资源,,而文件描述符属于父进程的资源(假设套接字被复制了,那么将会出现同一端口对应多个套接字的情况)。

调用fork函数复制文件描述符

 如上图所示,如果一个套接字存在两个文件描述符的时候,只有当两个文件描述符都关闭之后,才能销毁套接字。如上图中所示,即使子进程销毁了与客户端连接的套接字的文件描述符,也无法完全销毁套接字。服务端套接字也是同样如此。因此在调用fork函数之后,需要将无关的套接字进行关闭。

        if (pid != 0)
        {
            printf("Created new process for client.\n");
            
            // 主进程中需要关闭客户端的socket,因为在创建进程的时候,这个socket会被复制到子进程中去  重要!
            close(clientSocket); 

            memset(&clientAddr, 0, sizeof(clientAddr));       
            
            continue;
        }
        else
        {
            // 在子进程创建的时候,父进程服务端的socket也会复制到子进程中去,而在子进程中,需要将其关闭  重要!
            close(serverSocket);

            int str_len = 0;

        ....

1.6 分割TCP的IO程序

对上述的回声服务器程序进行改进,将客户端进行IO分割。在之前的客户端实现中,客户端首先向服务端发送数据,发送完成之后,无条件等待服务端回复(客户端中调用read),只有服务端回复之后,客户端才能进行下一次数据的发送。现在可利用多进程方法对客户端的程序进行改进,将接收与发送的逻辑放在两个不同的进程中进行。

设计方案:

  • 客户端父进程负责接收数据
  • 客户端子进程负责发送数据

这样设计之后,无论客户端是否从服务端接收到数据,都可以进行数据发送,IO分割之后,可以提高频繁交换数据的程序性能,可以提高同一时间数据的传输量。

客户端代码示例:

/* 
    客户端
    create_date: 2022-7-29
*/

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

#define BUFF_SIZE  30
#define ADDRESS    "127.0.0.1"
#define PORT       13100

void readRoutine(int sock, char* buf);
void writeRoutine(int sock, char* buf);

int main(int argc, char** argv)
{
    char buffer[BUFF_SIZE];

    struct sockaddr_in serverAddr;               // 服务端地址
    memset(&serverAddr, 0, sizeof(serverAddr));   
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(ADDRESS);
    serverAddr.sin_port = htons(PORT);

    memset(buffer, 0, BUFF_SIZE);

    int socket = ::socket(PF_INET, SOCK_STREAM, 0);

    if (socket == -1)
    {
        printf("Failed to init socket.\n");
        return -1;
    }

    if (connect(socket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
    {
        printf("Failed to connect to server.\n");
        return -2;
    }

    printf("Successfully connect to the server.\n");

    pid_t pid = fork();

    if (pid == 0)
    {
        // 子进程负责发送
        writeRoutine(socket, buffer);
    }
    else
    {
        // 父进程负责接收
        readRoutine(socket, buffer);
    }

    close(socket);

    return 0;
    
}


void readRoutine(int sock, char* buf)
{
    while (true)
    {
        memset(buf, 0, BUFF_SIZE);

        int str_len = read(sock, buf, BUFF_SIZE);

        if (str_len == 0)    // EOF
        {
            return;
        }

        printf("\nReceive data from server: %s\n", buf);
    }
    
}

void writeRoutine(int sock, char* buf)
{
    while (true)
    {
        fputs("Input message(Type Q(q) to quit): ", stdout);

        fgets(buf, BUFF_SIZE, stdin);

        if (strncmp(buf, "Q\n", 2) == 0 || strncmp(buf, "q\n", 2) == 0)
        {
            shutdown(sock, SHUT_WR);
            return;
        }
            
        write(sock, buf, strlen(buf));
    }
    
}

运行结果:

服务端:

 客户端1:

 客户端2:

 通过客户端数据结果可观察到,在提示输入之后,马上又会出现提示输入的语句,然后才出现服务端回复的内容,反映了在子进程中发送数据后,不用等待服务端回复,而又能马上发送数据,而父进程接收服务端回复数据会稍微慢于子进程发送数据,但是却不会对子进程的发送流程造成影响,适合客户端需要频繁发送数据的应用场景。

注:

在子进程中的发送流程中,有如下的代码:

        fputs("Input message(Type Q(q) to quit): ", stdout);

        fgets(buf, BUFF_SIZE, stdin);

        if (strncmp(buf, "Q\n", 2) == 0 || strncmp(buf, "q\n", 2) == 0)
        {
            shutdown(sock, SHUT_WR);
            return;
        }

在用户输入Q/q结束发送流程时,客户端子进程会通过shutdown关闭客户端socket的写功能,此时调用shutdown,相当于向服务端传输EOF。在客户端调用完shutdown之后,继续调用后面的代码,也就是main的close和return

    close(socket);

    return 0;

此时只是将子进程中的客户端进行一次关闭。

接着服务端接收到客户端发送来的的EOF,并将其返回给客户端(此时客户端的接收功能并未关闭,还能正常接收服务端的EOF),客户端在判断接收到服务端发送来的EOF之后,结束接收流程,也同样调用main中后续的代码,执行close和return,此时客户端的socket又被关闭了一次,至此,客户端中的socket被成功关闭,子进程和父进程都结束。(可查看并没有出现僵尸进程,因为在子进程结束后,父进程马上结束)。

另一种情况:

如果没有在客户端发送流程中调用shutdown,而是直接return,此时客户端子进程不会发送任何内容便关闭socket结束了。此时服务端未收到任何内容,也就不会向客户端回复内容,此时客户端父进程将一致处于等待接收的状态而无法结束,且出现了僵尸进程。

 ---------------------------------The end---------------------------------------

posted @ 2022-07-30 18:15  Alpha205  阅读(172)  评论(0编辑  收藏  举报