网络编程——11. 进程间通信

11.1 进程间通信的基本概念

进程间通信意味着两个不同进程间可以交换数据,为了完成这一点,操作系统中应提供两个进程可以同时访问的内存空间。

对进程间通信的基本理解

只要有两个进程可以同时访问的内存空间,就可以通过此空间交换数据。
但是,进程具有完全独立的内存结构。就连通过fork函数创建的子进程也不会与父进程共享内存空间。此时,进程间通信只能通过其他特殊方法完成。

通过管道实现进程间通信

网络编程——11. 进程间通信_第1张图片

为了完成进程间通信,需要创建管道

管道并非属于进程的资源,而是和套接字一样,属于操作系统(也就不是fork函数的复制对象),所以,两个进程通过操作系统提供的内存空间进行通信
网络编程——11. 进程间通信_第2张图片
父进程调用该函数时将创建管道,同时获取对应于出入口的文件描述符,此时父进程可以读写同一管道。

但父进程的目的是与子进程进行数据交换,因此需要将入口或出口中的一个文件描述符利用fork函数传递给子进程

fds[0]:通过管道接收数据时使用的文件描述符,即管道出口
fds[1]:通过管道传输数据时使用的文件描述符,即管道入口

#include 
#include 

#define BUF_SIZE 30

int main(int argc, char *argv[]) {
    int fds[2];
    char str[] = "Who are you?";
    char buf[BUF_SIZE];
    pid_t pid;
 
    // 调用pipe函数创建管道,fds数组中保存用于IO的文件描述符
    pipe(fds);
    // 调用fork函数
    // 子进程将同时拥有通过pipe函数调用获取的2个文件描述符
    // 注意:复制的并非管道,而是用于管道IO的文件描述符
    // 至此,父子进程同时拥有IO文件描述符
    pid = fork();
    if(pid == 0) {
    	// 子进程通过write向管道传递字符串
    	// 传输数据,使用fds[1]
    	write(fds[1], str, sizeof(str));
    } else {
    	// 父进程通过read从管道接收字符串
    	// 接收数据,使用fds[0]
    	read(fds[0], buf, BUF_SIZE);
    	puts(buf);
    }
    return 0;
}

上述示例的重点是:父子进程都可以访问管道的IO路径,但子进程仅用输入路径,父进程仅用输出路径
网络编程——11. 进程间通信_第3张图片

通过管道进行进程间双向通信

1)通过一个管道进行双向通信
#include 
#include 

#define BUF_SIZE 30

int main(int argc, char *argv[]) {
    int fds[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for your message";
    char buf[BUF_SIZE];
    pid_t pid;
 
    pipe(fds);
    pid = fork();
    if(pid == 0) {
    	// 子进程通过write传输数据
    	write(fds[1], str1, sizeof(str1));
    	sleep(2);
    	// 子进程通过read接收数据
    	read(fds[0], buf, BUF_SIZE);
    	printf("child proc output: %s \n", buf);
    } else {
    	// 父进程通过read接收子进程通过write传输的数据
    	read(fds[0], buf, BUF_SIZE);
    	printf("parent proc output: %s \n", buf);
    	// 父进程通过write传输数据,这些数据被子进程用read接收
    	write(fds[1], str2, sizeof(str2));
    	sleep(3);
    }
    return 0;
}

运行结果
在这里插入图片描述
模型如图所示
网络编程——11. 进程间通信_第4张图片
如果注释掉子进程运行区域中的sleep(2),会发生错误,原因是:
向管道传递数据时,先读的进程会把数据取走。即数据进入管道后称为无主数据。

就是说,注释掉sleep以后,子进程调用read函数先读取数据,即使该进程将数据传到了管道。即子进程将读回自己向管道发送的数据。结果,父进程调用read函数后将无限期等待数据进入管道。
在这里插入图片描述

2)通过2个管道进行双向通信

由于1个管道很难完成双向通信任务,因此需要创建两个管道,各自负责不同的数据流动即可
模型如图所示
网络编程——11. 进程间通信_第5张图片

#include 
#include 

#define BUF_SIZE 30

int main(int argc, char *argv[]) {
    int fds1[2], fds2[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for your message";
    char buf[BUF_SIZE];
    pid_t pid;
 
    pipe(fds1);
    pipe(fds2);
    pid = fork();
    if(pid == 0) {
    	// 子进程通过数组fds1指向的管道向父进程传输数据
    	write(fds1[1], str1, sizeof(str1));
    	// 子进程通过数组fds2指向的管道从父进程接收数据
    	read(fds2[0], buf, BUF_SIZE);
    	printf("child proc output: %s \n", buf);
    } else {
    	// 父进程通过read接收子进程通过write传输的数据
    	read(fds1[0], buf, BUF_SIZE);
    	printf("parent proc output: %s \n", buf);
    	// 父进程通过write传输数据,这些数据被子进程用read接收
    	write(fds2[1], str2, sizeof(str2));
    	// 没啥意义,只是为了延迟父进程终止
    	sleep(3);
    }
    return 0;
}


11.2运用进程间通信

在学了基于管道的进程间通信后,将其运用到网络代码中。

保存消息的回声服务器端

另行创建进程,从向客户端提供服务的进程读取字符串消息。当然,该过程中需要创建用于接收数据的管道。

#include   
#include   
#include   
#include   
#include   
#include 
#include 
#include   
  
#define BUF_SIZE 100  
void error_handling(char *message);  
void read_childproc(int sig);
  
int main(int argc, char *argv[]) {  
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr;  
    struct sockaddr_in clnt_adr;  
  	int fds[2];
	
    pid_t pid;
    struct sigaction act;
	socklen_t adr_sz;

    char buf[BUF_SIZE];  
    int str_len, state;   
      
    if(argc != 2) {  
        printf("Usage : %s \n", argv[0]);  
        exit(1);  
    }  

    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD, &act, 0);

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);  
    if(serv_sock == -1) {  
        error_handling("socket error");  
    }  
  
    memset(&serv_adr, 0, sizeof(serv_adr));  
    serv_adr.sin_family = AF_INET;  
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);  
    serv_adr.sin_port = htons(atoi(argv[1]));  
  
    if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) {  
        error_handling("bind() error");  
    }  
  
    if(listen(serv_sock, 5) == -1) {  
        error_handling("listen() error");  
    }  
  
	// 创建管道
	pipe(fds);
	// 负责保存文件的进程
	pid = fork();
	if(pid == 0) {	// 子进程运行区域
		FILE *fp = fopen("echomsg.txt", "wt");
		char msgbuf[BUF_SIZE];
		int i, len;

		for(i = 0; i < 10; ++i) {
			// 该区域从管道出口fds[0]读取数据并保存到文件中
			len = read(fds[0], msgbuf, BUF_SIZE);
			fwrite((void*)msgbuf, 1, len, fp);
		}
		// 上述服务器端并不终止运行,而是不断向客户端提供服务。
		// 因此,数据在文件中累计到一定程度即关闭文件
		fclose(fp);
		return 0;
	}


    while(1) { 
    	adr_sz = sizeof(clnt_adr);  
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);  //创建请求套接字
        if(clnt_sock == -1) {  
            continue;
        } else {  
            printf("new client connected...\n");  
        }  
		
		// accept调用之后。父子进程分别带有1个accept处生成的套接字(受理客户端连接请求时创建的)文件描述符
		// 通过fork函数创建的所有子进程将复制之前创建的管道的文件描述符
		pid = fork();

		if(pid == 0) {	// 子进程运行区域
			// 因为服务器套接字文件描述符同样也传递到子进程,所以要关闭服务器套接字
			close(serv_sock);
			while((str_len = read(clnt_sock,buf,BUF_SIZE)) != 0) {
		   		write(clnt_sock,buf,str_len);
				
				write(fds[1], buf, str_len);
			}
	
			close(clnt_sock);
			puts("client disconnected...");
			return 0;
		} else {
			// 由于通过accept创建的套接字文件描述符已经复制给了子进程
			// 因此,服务器端需要销毁自己拥有的文件描述符
			close(clnt_sock);
		}
	}  
    close(serv_sock);  
  
    return 0;  
}  
  
void error_handling(char *message){  
    fputs(message, stderr);  
    fputs("\n", stderr);  
    exit(1);  
}

void read_childproc(int sig){
    pid_t pid;
    int status;
    pid = waitpid(-1, &status, WNOHANG);
    printf("removed proc id : %d \n", pid);
}   

你可能感兴趣的:(网络编程)