TCP/IP网络编程(9) 进程间通信
1. IPC基本概念
进程间通信(Inter process communication, IPC)以为这两个不同进程间可以交换数据,为了完成这一点,操作系统中应该提供两个进程可以同时访问的内存空间。因此,只要有两个进程可以同时访问的内存空间,就可以通过此空间来交换数据。但是,进程具有完全独立的内存结构,即使通过fork()函数创建的子进程,也不会与父进程共享内存,因此进程间通信需要通过其他的特殊方法完成。
1.1 利用管道实现进程间通信
为完成进程之间的通信,需要创建管道,管道不属于进程的资源(即不是fork复制的对象),而是和套接字一样,属于操作系统资源,因此通过管道首实现IPC的原理是,两个进程通过操作系统提供的内存进行通信。
  

创建管道的方式:
#include <unistd.h>
/*
   成功时返回0
   失败时返回-1
   参数:
   fields[0]  通过管道接收数据时使用的文件描述符
   fields[1]  通过管道传输数据时使用的文件描述符
*/
int pipe(int fields[2]);
父进程调用pipe函数时将创建管道,同时获取对应于管道出入口的文件描述符,此时父进程可以读写同一管道,在调用fork创建子进程之后,子进程也将同时拥有管道出入口的文件描述符:
#include <stdio.h>
#include <unistd.h>
#define BUFF_SIZE 30
int main(int argc, char** argv)
{
    int fds[2];     // 用于存储管道出入口的文件描述符
    char message[] = "Hello Process\n";
    char buffer[BUFF_SIZE];
    if (pipe(fds) == -1)
    {
        printf("Falied to create pipe.\n");
        return -1;
    }
    pid_t pid = fork();      // 创建进行
    if (pid == 0)
    { 
        // 子进程   写入管道
        write(fds[1], message, sizeof(message));
    }
    else
    { 
        // 父进程   读取管道
        read(fds[0], buffer, BUFF_SIZE);
        fputs(buffer, stdout);
    }
    return 0;
}进程间通信的示意图图下所示:

1.2 通过管道实现进程间双向通信
方案1:通过一个管道实现两个进程间的双向通信

代码实例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define BUFF_SIZE 30
int main(int argc, char** argv)
{
    int fds[2];     // 用于存储管道出入口的文件描述符
    char message1[] = "Hello Process\n";
    char message2[] = "I am vac!\n";
    char buffer[BUFF_SIZE];
    memset(buffer, 0, BUFF_SIZE);
    if (pipe(fds) == -1)
    {
        printf("Falied to create pipe.\n");
        return -1;
    }
    pid_t pid = fork();      // 创建进行
    if (pid == 0)
    { 
        // 子进程   写入管道
        write(fds[1], message1, sizeof(message1));
        sleep(2);
        read(fds[0], buffer, BUFF_SIZE);
        printf("Child proc received data: %s\n", buffer);
        memset(buffer, 0, BUFF_SIZE);
    }
    else
    { 
        // 父进程   读取管道
        read(fds[0], buffer, BUFF_SIZE);
        printf("Parent proc received data: %s\n", buffer);
        write(fds[1], message2, sizeof(message2));
        sleep(3);
        memset(buffer, 0, BUFF_SIZE);
    }
    return 0;
}运行结果:
如果将子进程中的sleep方法注释掉,再运行代码:
    if (pid == 0)
    { 
        // 子进程   写入管道
        write(fds[1], message1, sizeof(message1));
        //sleep(2);
        read(fds[0], buffer, BUFF_SIZE);
        printf("Child proc received data: %s\n", buffer);
        memset(buffer, 0, BUFF_SIZE);
    }结果如下所示:

此时父进程无法再收到数据,因为子进程在写入数据后,未经过延时,立即又读取数据,因此管道中写入的数据被子进程提前取走。按照管道数据的读取机制,数据在写入管道之后,称为无主数据,哪个进程先通过read函数读取数据,哪个进程就先得到数据。结果导致父进程通过read函数再也读取不到数据,而将无限期等待管道数据。
注:利用一个管道进行进程间数据双向通信,程序设计者需要提前预测并控制运行流程,程序实现难度很大。
方案2:通过两个管道实现进程间相互通信
通过创建两个管道,各自在进程间负责不同的数据流,即可方便的实现进程间双向通信,采用两个管道即可避免程序运行流程的预测或控制。
 
代码实例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define BUFF_SIZE 30
int main(int argc, char** argv)
{
    int fds1[2];     // 子进程向父进程写数据
    int fds2[2];     // 父进程向子进程写数据
    char message1[] = "Hello Process\n";
    char message2[] = "I am vac!\n";
    char buffer[BUFF_SIZE];
    memset(buffer, 0, BUFF_SIZE);
    if (pipe(fds1) == -1)
    {
        printf("Falied to create pipe.\n");
        return -1;
    }
    if (pipe(fds2) == -1)
    {
        printf("Falied to create pipe.\n");
        return -1;
    }
    pid_t pid = fork();      // 创建进行
    if (pid == 0)
    { 
        // 子进程   写入管道
        write(fds1[1], message1, sizeof(message1));
        read(fds2[0], buffer, BUFF_SIZE);
        printf("Child proc received data: %s\n", buffer);
        memset(buffer, 0, BUFF_SIZE);
    }
    else
    { 
        // 父进程   读取管道
        sleep(1);     // 可以延时更短,稍微等待一下
        read(fds1[0], buffer, BUFF_SIZE);
        printf("Parent proc received data: %s\n", buffer);
        write(fds2[1], message2, sizeof(message2));
        memset(buffer, 0, BUFF_SIZE);
    }
    return 0;
}运行结果:

2. 进程间通信的应用
保存消息的回声服务器: 在向客户端提供服务的同时,服务端能够将接收到的数据同步保存到文件中。
客户端代码可复用《TCP/IP网络编程(8) 基于Linux的多进程服务器》中的客户端代码
/* 
    客户端
    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
        {
            printf("Receive EOF\n");
            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));
    }
    
}服务端示例代码代码示例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <iostream>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/wait.h>
void readChildProcess(int signo)
{
    int status;
    pid_t pid = waitpid(-1, &status, WNOHANG);   // 非阻塞
    if (WIFEXITED(status))
    {
        printf("Removed child process %d\n", WEXITSTATUS(status));
    }
}
#define BUFF_SIZE 30
#define PORT  13100
int main(int argc, char** argv)
{
    int serverSocket;
    int clientSocket;
    struct sockaddr_in servAddr;
    struct sockaddr_in clientAddr;
    char buffer[BUFF_SIZE];
    memset(buffer, 0, BUFF_SIZE);
    socklen_t addrSize; 
    int fds[2];     // 管道的文件描述符
    struct sigaction sigact;
    sigact.sa_handler = readChildProcess;
    sigemptyset(&sigact.sa_mask);
    sigact.sa_flags = 0;
    sigaction(SIGCHLD, &sigact, 0);      // 注册信号
    serverSocket = socket(PF_INET, SOCK_STREAM, 0);
    if (serverSocket == -1)
    {
        printf("Failed to init server socket.\n");
        return -1;
    }
    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAddr.sin_port = htons(PORT);
    if (bind(serverSocket, (struct sockaddr*)&servAddr, sizeof(servAddr)) == -1)
    {
        printf("Failed to bind the server socket.\n");
        return -1;
    }
    if (listen(serverSocket, 5) == -1)
    {
        printf("Failed to listen client.\n");
        return -1;
    }
    // 创建管道
    pipe(fds);
    // 创建写文件进程
    pid_t pid = fork();
    if (pid == 0)
    {
        // 写文件子进程
        FILE* fp = fopen("data.txt", "a");
        char msg[100];
        while (true)
        {
            /* code */
            usleep(1000*40);
            memset(msg, 0, 100);
            int len = read(fds[0], msg, 100);
            // 解析读取到的内容
            if (len <= 5)
            {
                break;
            }
            char magicChar1 = msg[0];
            char magicChar2 = msg[1];
            if (magicChar1 != 'M' || magicChar2 != 'F')
            {
                break;
            }
            if (msg[2] == 1)
            {
                break;
            }
            ushort dataLen = msg[3] | (msg[4] << 8);
            if (dataLen <= 0)
            {
                continue;
            }
            // 将解析到的内容写入文件进行保存
            fwrite(&msg[5], dataLen, 1, fp);
        }
        fclose(fp);
        return 0;
    }
    else
    {
        // 父进程
        while (true)
        {
            printf("Waiting connect from client...\n");
            addrSize = sizeof(clientAddr);
            
            clientSocket = accept(serverSocket, (sockaddr*)&clientAddr, &addrSize);
            if (clientSocket == -1)
                continue;
            printf("Accept new connection from %s:%d.\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
            pid_t processId = fork();
            if (processId == 0)
            {
                // 子进程
                close(serverSocket);
                // 接受消息
                int recvLen = 0;
                // 构造报文
                char message[100];
                usleep(1000*100);
            
                while ((recvLen = read(clientSocket, buffer, BUFF_SIZE)) != 0)
                {
                    /* code */
                    write(clientSocket, buffer, recvLen);
                    // 向管道写数据
                    memset(message, 0, 100);
                    message[0] = 'M';
                    message[1] = 'F';
                    message[2] = 0;
                    message[3] = recvLen & 0xFF;
                    message[4] = (recvLen >> 8) & 0xFF;
                    memcpy(&message[5], buffer, recvLen);       // 注意:memcpy里面的指针不要转换成void*
                    
                    
                    write(fds[1], message, recvLen + 5);
                     
                    memset(buffer, 0, BUFF_SIZE);
                } 
                close(clientSocket);
                
                memset(message, 0, 100);
                // 发送结束标志
                message[0] = 'M';
                message[1] = 'F';
                message[2] = 1;
                message[3] = 0;
                message[4] = 0;
                write(fds[1], (void*)message, 5);
                
                printf("Disconnecting from server...\n");
                sleep(2);
                return 0;
                
            }            
            else if (processId == -1)
            {
                close(clientSocket);
                continue;
            }
            else
            {
                // 主进程
                printf("Create new process %d for client.\n", processId);
                close(clientSocket);
                memset(&clientAddr, 0, sizeof(clientAddr));
                continue;
            }
        }
        
    }
    close(serverSocket);
    return 0;
}
/*
服务器进程和文件进程之间设计通信格式
0    M          // 报文头
1    F          // 报文头
2    0/1        // 报文类型  0数据  / 1 结束
3    len        // 数据长度  低位
4    len        // 数据长度  高位
5    x          // 数据           
6    x          // 数据
7    x          // 数据
.               // .
.
.
*/客户端运行结果:

服务端运行结果:

通过管道进行进程间通信,将服务端收到的消息发送给写文件进程,将收到的消息保存至文件:

-----------------------------------------//end//---------------------------------------------
 
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号