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//---------------------------------------------

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值