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

posted @   Alpha205  阅读(58)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示