管道通信的特点:
1. 单工通信---- 任何一个时刻只能发送方 向 接收方发送数据
2. 流式传输:
1> 先发送的数据先被接收,不能跳跃式接收 ----- 顺序发送顺序接收
2> 未被接收的数据仍然滞留在管道中,下一次可以继续接收后续
3> 已被接收的数据不会再出现管道中
3. 发送接收次序:
按被发送数据所占空间的地址值从小到大依次发送,接收到数据按次序依次存放在目标空间的从小到大的地址位置
Linux支持的管道有两种:
1. 匿名管道:
无名管道,只能用在具备亲缘关系的进程间命名管道:
2. 有名管道,
可以用在任意进程间,采用管道文件名给一个管道命名
1. 匿名管道
适用于具有亲缘关系的进程间
父进程向子进程发送数据的代码模板:
int arrfd[2] = {-1,-1};
pid_t pid;
pipe(arrfd);
pid = fork();
if(pid > 0){ //父进程才执行的代码
close(arrfd[0]);
//用arrfd[1]发送数据 ------ write(fd,...,...)
close(arrfd[1])//无需继续发送时,及时调用
}
else if(pid == 0){//子进程才执行的代码
close(arrfd[1]);
//用arrfd[0]接收数据 ----- read(fd,...,...)
close(arrfd[0])//无需继续接收时,及时调用
}
.......
子进程向父进程发送数据的代码模板:
int arrfd[2] = {-1,-1};
pid_t pid;
pipe(arrfd);
pid = fork();
if(pid > 0){ //父进程才执行的代码
close(arrfd[1]);
//用arrfd[0]接收数据 ----- read(fd,...,...)
close(arrfd[0])//无需继续接收时,及时调用
}
else if(pid == 0){ //子进程才执行的代码
close(arrfd[0]);
//用arrfd[1]发送数据 ------ write(fd,...,...)
close(arrfd[1])//无需继续发送时,及时调用
}
.......
管道读写数据的特点:
匿名、命名通用
管道中无数据可读时,read函数会阻塞
管道中数据已满时,write函数会阻塞
read函数返回0时,表示写端已关闭,不会有后续数据可接收
e.g.
#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc,char *argv[]){
pid_t pid;
int fds[2] = {-1,-1};
int ret = 0;
ret = pipe(fds);
if(ret){
printf("pipe error\n");
return 1;
}
pid = fork(); // fork
if(pid < 0){
printf("fork error\n");
return 2;
}
if(pid > 0){
close(fds[0]); // 关掉读
fds[0] = -1;
write(fds[1],"hello",6); // write
close(fds[1]); // 关掉写
fds[1] = -1;
}
else{ // 子进程
char buf[8] = "";
close(fds[1]); // 关掉写
fds[1] = -1;
read(fds[0],buf,8); // read
printf("In child process,buf = %s\n",buf);
close(fds[0]); // 关掉读
fds[0] = -1;
}
return 0;
}
输出:
2. 命名管道
匿名管道没有名字,pipe 函数直接获得两端描述符,然后进行接收发送
匿名管道只能借助于 fork 函数传递同一管道的描述符,因此只能用于具有亲缘关系的进程间
命名管道也称为有名管道
命名管道用管道文件名作为它的名字,因此创建命名管道时需要指定这个文件名,然后以两种不同的方式打开这个文件获得两端描述进行接收发送
只要两个进程操作的是同一个管道文件,即可通过该管道文件代表的管道进行通信,因此进程间没必要具有亲缘关系
创建函数:mkfifo
打开函数: open ------ 见系统IO
获得读端描述符:open(管道文件名,O_RDONLY);
获得写端描述符:open(管道文件名,O_WRONLY);
发送数据函数:write ---- 见系统IO
配合open(管道文件名,O_WRONLY)得到的描述符
接收数据函数:read ------ 见系统IO
配合 open (管道文件名,O_RDONLY) 得到的描述符
关闭函数:close ------ 见系统IO
使用命名管道的基本套路:
#define FIFO_NAME "/tmp/myfifo"
发送进程:
if(access(FIFO_NAME,F_OK)){//文件不存在,意味着命名管道未被创建
mkfifo(....);
}
? = open(FIFO_NAME,O_WRONLY);
调用write函数发送数据
close ----- 无需继续发送数据时及时调用
接收进程:
if(access(FIFO_NAME,F_OK)){//文件不存在,意味着命名管道未被创建
mkfifo(....);
}
? = open(FIFO_NAME,O_RDONLY);
调用read函数接收数据
close ----- 无需继续接收数据时及时调用
p.s.
write_process.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[]){
int wfd = -1;
if(argc < 2){
printf("The argument is too few\n");
return 1;
}
if(access(argv[1],F_OK)){ // 如果argv[1]存在,access为0
mkfifo(argv[1],0666); // 0666为使用权限
}
wfd = open(argv[1],O_WRONLY);
if(wfd < 0){
printf("w-open %s failed\n",argv[1]);
return 2;
}
write(wfd,"hello",6);
close(wfd);
wfd = -1;
return 0;
}
read_process.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[]){
int rfd = -1;
char buf[8] = "";
if(argc < 2){
printf("The argument is too few\n");
return 1;
}
if(access(argv[1],F_OK)){ // 如果argv[1]存在,access为0
mkfifo(argv[1],0666); // 0666为使用权限
}
rfd = open(argv[1],O_RDONLY);
if(rfd < 0){
printf("r-open %s failed\n",argv[1]);
return 2;
}
read(rfd,buf,6);
printf("buf:%s\n",buf);
close(rfd);
rfd = -1;
return 0;
}
p.s.
运行其中一个程序:发现程序堵塞 (堵塞在open处而不是read/write处)
创建新标签页,运行另一个程序:
程序成功输出
3. 通信协议的制定
任何通信形式,首先必须为参与通信的各方制定统一的规则,这种规则被称为通信协议(protocol)。
一份通信协议至少包括如下内容:
1. 交互的数据种类、作用
2. 每种数据的二进制位组成(统称为数据格式)
按协议组织好的一种通信数据,被称为PDU(Protocol Data Unit)
3. 区分数据种类的方式
4. 各种数据的使用次序
在设计每种数据的二进制位组成时,需要考虑如何避免混包现象:
1. 粘包现象
2. 拆包现象
通信数据的两种基本组成方案:
1. 文本形式-----即将数据组合成特定形式的字符串,并规定字符串中不同子串的作用
例如:“人名-年龄-工资”
2. 非文本形式----或称为二进制形式,即按字节或位指定数据中不同部分的作用
例如:20字节存放人名+4字节存放年龄+4字节存放工资
3. 由多个文本加非文本组合而成的混合形式-----即交互数据中某些部分组合成字符串,另一些部分以二进制形式存在
交互数据按数据长度是固定还是不固定,组成方案分为如下两种:
1. 定长:所有数据长度固定为多少字节
2. 变长:有的数据长度比较短,有的数据长度比较长
通信数据组成方案两种分类相互交叉就可能存在如下几种情况:
1. 定长文本:
发送方每次发送:1> 按数据格式在连续内存空间中组织好待发送的数据 2> 发送
接收方每次接收:1> 分配好用于存放接收数据的内存空间 2> 接收
2. 变长文本
一般采用两种方式来避免混包:
1> 以某个特殊字符或连续几个特殊字符的组合作为通信数据的结尾,例如:'\0'、'\n'、"\r\n"
2> 在字符串内容上做文章。 例如:“0023abcxxx???” 其中0023表示后续有效字符的长度
发送方每次发送:1> 按数据格式在连续内存空间中组织好待发送的数据 2> 发送
接收方每次接收:
方案1过程:
1)借助于一个临时文件,每次接收一个字节存放到临时文件,当接收到结尾标记时结束本轮接收
2)计算临时文件大小
3)动态分配空间
4)从临时文件中将数据读入动态空间里
方案2过程:
1) 接收固定长度的字节数
2) sscanf 将其扫描成整数得到后续需要动态分配空间的大小:整数 + 第1步的固定长度
3)动态分配空间
4)strcpy 第1步读到内容到动态空间
5)按第2步得到的整数去读后续数据
由于是文本形式,因此与协议数据相关的代码大部分都会涉及字符串处理
3. 定长非文本
发送方每次发送:1> 按数据格式在连续内存空间中组织好待发送的数据 2> 发送
接收方每次接收:1> 分配好用于存放接收数据的内存空间 2> 接收
4. 变长非文本
5. 定长混合
发送方每次发送:1> 按数据格式在连续内存空间中组织好待发送的数据 2> 发送
接收方每次接收:1> 分配好用于存放接收数据的内存空间 2> 接收
6. 变长混合
无论采用哪种都要考虑:1> 如何区分不同通信数据 2> 如何避免混包现象
对于变长非文本和变长混合多数项目采用变长结构体来组织协议数据
对于通信数据是变长非文本或变长混合形式的通信程序,开发过程:
1. 按协议数据格式设计变长结构体--按协议格式组合而成的一个完整数据包在行业里统称为PDU(Protocol Data Unit协议数据单元)
2. 为通信各方编写各种数据的创建函数 和 统一的销毁函数 以及发送、接收函数 ------ 统称为协议代码
3. 任何想要发送数据的任务每次发送:
1> 调用某一个创建函数,在连续的空间中按协议组织好待发送的数据
2> 调用发送函数
3> 调用销毁函数
4. 任何想要接收数据的任务每次接收:
1> 调用接收函数
2> 对接收下来的协议数据进行处理
3> 调用销毁函数
实际项目开发中,涉及通信的软件开发,往往需要程序员:
1. 研究别人提供的通信协议
2. 自行设计新的通信协议
二者必选其一
服务端进程:或称服务器进程,为其它进程提供通信服务
客户端进程:使用通信服务的进程
示例:编写如下程序:
客户端进程:接收用户输入的一个整数n,程序产生n个随机数,让用户选择对这n个随机数排序还是求均值,然后将对n个随机数的操作和n个随机数发送给服务端,接收服务端处理结果,并显示处理结果
服务端进程:接收客户端的操作请求和n个随机数,然后完成相应的操作,将操作结果发送回给客户端