之所以去接触它,是因为实验室的项目中要求可信组和加密组可以进行通信。两者都在用户层,加密组向可信组发送请求,可信组返回响应。比如加密小组发送请求密钥信息,可信组返回所请求的密钥。由于传输的信息相对来说比较大,而且这两个程序逻辑上并不相关,所以在几种通信方式中我们选择了命名管道。
命名管道其实是FIFO文件。在linux中终端下创建一个管道,用mkfifo命令:mkfifo fifoname;或者mknod命令:mknod fifoname p;最后的p表明该文件是个管道文件。
再说下访问FIFO文件。首先利用mkfifo命令创建一个管道文件:mkfifo /tmp/tmpfifo。然后读取其内容:cat /tmp/tmpfifo。可以发现这条命令没有结束,这是因为管道正在等待数据的写入。打开另一个终端,向管道写数据:echo “test” > /tmp/tmpfifo;此时两条命令都正常结束。读取端将打印出从管道中读取到的”test”。当然也可以将一端命令放在后台执行,比如读取时可以用:cat /tmp/tmpfifo & ;(有关这方面的信息,可以查下几条命令:管理作业jobs,将任务放入后台bg,将程序放入前台fg,结束任务kill)。 上面的例子说明了,管道的一方(读取端或者写入端)在未得道另一方时,是一直被阻塞的,当然要想不被阻塞可以通过一些参数设定。在程序中,我们用open函数来打开管道。open函数的第一个参数即为管道文件路径,第二个参数为标志位。标志位有O_RDONLY,O_WRONLY,O_NONBLOCK,前两个参数可以和O_NONBLOCK配合使用,不过会改变open调用的处理方式。它们的作用从名字可以猜出来,O_RDONLY表明作为读取端,O_WRONLY表明作为写入端,O_NONBLOCK表示读取时不阻塞,也就是说这端立即检查管道中有没有数据,不论有没有都会立即返回,不会在没有数据时被阻塞。
现在来看如何利用FIFO实现进程间服务器/客户端的通信模式。首先,假设一个很简单的需求:客户端发送一串字符串,服务器将字符串翻转后返送给客户端。因为可能有不同的客户端同时发出请求,我们在回送管道名字中加入客户端进程的pid来标识。
首先,我们设计客户端和服务器端通信数据的结构和一些都会用到的系统头文件common.h。
#include "unistd.h"#include "stdlib.h"#include "stdio.h"#include "string.h"#include "fcntl.h"#include "limits.h"#include <sys/types.h> #include <sys/stat.h>#include "ctype.h"#define Server_FIFO_Name "serv_fifo"#define Client_FIFO_Name "client_%d_fifo" #define BufSize 200 struct data_to_pass{ pid_t client_pid; //保存客户端进程的PID。 char str[BufSize];};
再来看服务器端。服务器端首先要建立管道以便其他进程向其发送数据,然后再打开回送管道,向客户端返回数据,然后再回到开始等待客户端向其请求数据。
#include "common.h"//字符串翻转函数void reverse(char str[]){ int len = strlen (str); char tmpc; char *h,*t; h = str; t = str + (len-1); while(h < t){ //注意这里不能写成(h != t) tmpc = *h; *h = *t; *t = tmpc; h++; t--; } } int main(){ int server_fifo_fd,client_fifo_fd; struct data_to_pass my_data; int read_res; char client_fifo[256]; char *char_ptr; //创建server管道。 mkfifo(Server_FIFO_Name,0777);Again: //偷懒了,用goto去实现循环监听请求. //打开服务器端口,等待读取。此时如果客户端还未写入数据服务器端会被阻塞。 server_fifo_fd = open(Server_FIFO_Name , O_RDONLY); if( -1 == server_fifo_fd ){ fprintf( stderr , "Server fifo failure\n" ); exit(EXIT_FAILURE); } //从管道中读取数据。 read_res = read ( server_fifo_fd , &my_data , sizeof(my_data)); if(read_res > 0){ //将字符串翻转. reverse ( my_data.str ); //将客户端的pid号加入回送管道文件名中. sprintf ( client_fifo, Client_FIFO_Name , my_data.client_pid); //打开回送管道。 client_fifo_fd = open ( client_fifo , O_WRONLY ); if( -1 != client_fifo_fd ){ //向管道中写入返回的数据. write ( client_fifo_fd , &my_data, sizeof(my_data)); close ( client_fifo_fd ); } } close(server_fifo_fd); goto Again; unlink(Server_FIFO_Name); exit(EXIT_SUCCESS);}
最后就是客户端,模式其实差不多。先打开服务器管道,发送数据,然后创建回送管道,等待服务器返回数据。
#include "common.h" int main(){ int server_fifo_fd,client_fifo_fd; struct data_to_pass mydata; int times_to_send; char client_fifo[256]; int rst; //打开服务器管道,开始写入. 采用NON_BLOCK方式,所以需要服务器先打开。 server_fifo_fd = open(Server_FIFO_Name,O_WRONLY | NON_BLOCK); if(server_fifo_fd == -1){ fprintf(stderr , "Sorry ,No Server\n"); exit(EXIT_FAILURE); } //获得当前进程PID存入结构中。 mydata.client_pid = getpid(); sprintf ( client_fifo,Client_FIFO_Name ,mydata.client_pid); if(mkfifo(client_fifo ,0777) == -1){ fprintf(stderr , "Sorry ,can't make %s \n",client_fifo); exit(EXIT_FAILURE); } //填写发送的数据包. sprintf(mydata.str,"%s","12345"); printf("%d sent %s,",mydata.client_pid , mydata.str); //向管道中写入数据:发送. write(server_fifo_fd , &mydata , sizeof(mydata)); //打开回送管道,等待接受从服务器返回的数据。 client_fifo_fd = open(client_fifo,O_RDONLY); if(client_fifo_fd != -1){ rst = read( client_fifo_fd , &mydata , sizeof(mydata)); if( rst > 0){ printf("received : %s\n",mydata.str); } close(client_fifo_fd); } close(server_fifo_fd); unlink(client_fifo); exit(EXIT_SUCCESS);}
至此,一个虽然简单但是比较完整的客户端服务器模式的进程间通信过程完成了。下面是在我机器上的运行效果。
boluor@boluor-laptop:~/programs/pipe/testpipe$ ./server &
[1] 30259
boluor@boluor-laptop:~/programs/pipe/testpipe$ ./client
30301 sent 12345,received : 54321
boluor@boluor-laptop:~/programs/pipe/testpipe$ jobs
[1]+ Running ./server &
boluor@boluor-laptop:~/programs/pipe/testpipe$ kill %1
至于和socket的比较,首先命名管道是面向连接的,而socket是无连接的;其次,他们都可以实现任意两个进程间的通信;再次,命名管道只是单向传输,要想实现双向必须另建立管道模拟实现。
联系客服