TCP/IP网络编程(9) 进程间通信

1. IPC基本概念

进程间通信(Inter process communication, IPC)以为这两个不同进程间可以交换数据,为了完成这一点,操作系统中应该提供两个进程可以同时访问的内存空间。因此,只要有两个进程可以同时访问的内存空间,就可以通过此空间来交换数据。但是,进程具有完全独立的内存结构,即使通过fork()函数创建的子进程,也不会与父进程共享内存,因此进程间通信需要通过其他的特殊方法完成。

1.1 利用管道实现进程间通信

为完成进程之间的通信,需要创建管道,管道不属于进程的资源(即不是fork复制的对象),而是和套接字一样,属于操作系统资源,因此通过管道首实现IPC的原理是,两个进程通过操作系统提供的内存进行通信。
 

TCP/IP网络编程(9) 进程间通信

创建管道的方式:

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

进程间通信的示意图图下所示:

TCP/IP网络编程(9) 进程间通信

1.2 通过管道实现进程间双向通信

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

TCP/IP网络编程(9) 进程间通信

代码实例:

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

运行结果:
TCP/IP网络编程(9) 进程间通信

 如果将子进程中的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);
    }

结果如下所示:                            

TCP/IP网络编程(9) 进程间通信

 此时父进程无法再收到数据,因为子进程在写入数据后,未经过延时,立即又读取数据,因此管道中写入的数据被子进程提前取走。按照管道数据的读取机制,数据在写入管道之后,称为无主数据,哪个进程先通过read函数读取数据,哪个进程就先得到数据。结果导致父进程通过read函数再也读取不到数据,而将无限期等待管道数据。

注:利用一个管道进行进程间数据双向通信,程序设计者需要提前预测并控制运行流程,程序实现难度很大。

方案2:通过两个管道实现进程间相互通信

通过创建两个管道,各自在进程间负责不同的数据流,即可方便的实现进程间双向通信,采用两个管道即可避免程序运行流程的预测或控制。

 TCP/IP网络编程(9) 进程间通信

 代码实例:

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

运行结果:

TCP/IP网络编程(9) 进程间通信

 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          // 数据
.               // .
.
.

*/

客户端运行结果:

TCP/IP网络编程(9) 进程间通信

 服务端运行结果:

TCP/IP网络编程(9) 进程间通信

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

TCP/IP网络编程(9) 进程间通信

 -----------------------------------------//end//---------------------------------------------

原文链接: https://www.cnblogs.com/ncepubye/p/17012736.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    TCP/IP网络编程(9) 进程间通信

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/396386

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月6日 上午11:17
下一篇 2023年4月6日 上午11:17

相关推荐