目录
第1关:socket之本地通信
任务描述
相关知识
创建 socket 流程
命名 socket
其他接口
编程要求
答案:
第2关:命名管道
任务描述
相关知识
命名管道的定义
命名管道的创建
命名管道的访问
命名管道的访问
编程要求
答案:
第3关:消息队列
任务描述
相关知识
消息队列的基本概念
消息队列的创建
发送消息
接收消息
控制函数
编程要求
答案 :
第1关:socket之本地通信
任务描述
试想这样的场景:
你有事情要告知同一个科室的同事 A ,但是你没有同事 A 的微信,你需要找同一个科室的同事 B 获取同事 A 的微信,然后给同事 A 发消息。
整个事件的过程如下:
你向同事 B 询问同事 A 的微信号;
同事 B 得到同事 A 的允许后将同事 A 的微信号告诉你;
你添加同事 A 为微信好友;
同事 A 确认并通过好友认证。
以本地 socket 通信代替整个事件的过程:创建 socket->链接socket->向对端发送消息->接收对端发过来的消息。
本关任务:
建立一个命名 socket,指定地址为本地 socket 文件;
连接 socket 成功后,发送消息到服务端;
接收从服务端来的消息。
相关知识
在 Linux 中,套接字 socket 通信是非常常见的通信方式。通信过程中采用三次握手,四次挥手的通信流程。
通常套接字 socket 通信是运用在网络上两台主机上,通信双方采用网络地址和端口号作为通信地址来进行通信。服务端绑定此地址,然后开始监听此地址;客户端连接此地址即可。
在本地通信中,socket 进程通信与网络通信使用的是统一套接口,只是地址结构与某些参数不同。Socket 通信在另外的实训中已有介绍,对此不太熟悉的学员可以自行查阅下。
创建 socket 流程
创建 socket 采用系统调用 socket ,原型如下:
#include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> int socket(int domain, int type, int protocol);
参数详解如下:
domain:参数指定协议族,对于本地套接字来说,其值须被置为 AF_UNIX ;
type:参数指定套接字类型,可被设置为 SOCK_STREAM (流式套接字)或 SOCK_DGRAM (数据报式套接字);对于本地套接字来说, SOCK_STREAM 是一个有顺序的、可靠的双向字节流,相当于在本地进程之间建立起一条数据通道; SOCK_DGRAM 相当于单纯的发送消息,在进程通信过程中,理论上可能会有信息丢失、复制或者不按先后次序到达的情况,但由于其在本地通信,不通过外界网络,这些情况出现的概率很小。
protocol :参数指定具体协议,protocol 字段应被设置为 0 。
返回值为生成的套接字描述符。
命名 socket
本地套接字的通信双方均需要具有本地地址,其中本地地址需要明确指定,指定方法是使用
struct sockaddr_un
类型的变量。#include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> struct sockaddr_un {sa_family_t sun_family; /* AF_UNIX */char sun_path[UNIX_PATH_MAX]; /* 路径名 */ };
socket 根据此命名创建一个同名的 socket 文件,客户端连接的时候通过读取该 socket 文件连接到 socket 服务端。
应用示例如下:
//name the server socket server_addr.sun_family = AF_UNIX; strcpy(server_addr.sun_path,"/tmp/UNIX.domain"); server_len = sizeof(struct sockaddr_un); client_len = server_len;
其他接口
其他接口,比如服务端的
listen
、accept
,客户端的connect
等均没有区别。
编程要求
在本实训中,后台存在一个用于与客户端之间通信的服务端,学员需要实现的代码位于客户端。
本关的编程任务是完成客户端的代码编写,补全右侧代码片段
localsocket_test
中Begin
至End
中间的代码,具体要求如下:首先建立一个命名 socket ,指定地址为./socket_test;
连接 socket 成功后,发送消息(发送内容为 localsocket_test 中的参数 buffer)到服务端;
接收从服务端来的消息。关闭 socket ;
如果接收成功返回 0,接受失败返回 -1。
答案:
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h>int localsocket_test(const char *buffer, char *recv_buff, int length) {/*********begin***********/struct sockaddr_un server_addr;//创建socketint sockfd=socket(AF_UNIX,SOCK_STREAM,0);//设置服务器地址server_addr.sun_family=AF_UNIX;strncpy(server_addr.sun_path,"./socket_test",sizeof(server_addr.sun_path)-1);//连接socketconnect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));//发送消息到服务端write(sockfd,buffer,strlen(buffer));//send(sockfd,buffer,strlen(buffer),0);也可//接收从服务端发来的消息read(sockfd,recv_buff,length-1);//recv(sockfd,recv_buff,length-1,0);也可//关闭socketclose(sockfd);return 0;/**********end************/ }
第2关:命名管道
任务描述
试想这样的场景:
你想请工程部来建筑一个别墅,但是你没有工程部的联系方式,恰好你的好友 Mike 与工程部非常熟,你打电话给你的好友,希望可以与工程部经理进行三方通话并传递别墅需求。
整个事件的过程如下:
你给好友打电话告知你的别墅需求;
好友让你等下,他把工程部经理加进来进行三方通话;
好友把工程部经理加入三方通话;
你把别墅需求直接告知工程部经理;
谈话结束,你挂断电话;
工程部经理也挂断电话。
以命名管道代替整个事件的过程,创建命名管道->打开命名管道->等待对端打开命名管道->发送消息->关闭命名管道。
本关任务:
检测命名管道文件是否存在,如若不存在则创建道;
以阻塞的方式打开命名管道;
通过命名管道发送消息;
关闭命名管道。
相关知识
在 Linux 中,命名管道是一种比较常见的进程间通信方式。相对于其他类型管道来说,命名管道弥补了通信的双方进程需要有共同祖先的弊端,这给我们在不相关的的进程之间交换数据带来了可能性。
命名管道的定义
命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为和其他管道类似。
由于 Linux 中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,使用也非常方便,同时我们也可以像平常的文件名一样在命令中使用。
命名管道的创建
创建命名管道的函数原型如下:
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *filename, mode_t mode);
参数详解如下:
filename
:命名管道文件的路径,类似"/tmp/my. fifo ";
mode
:命名管道文件的模式,表明执行此文件的权限,类似"0666"。
返回值:
若成功则返回 0,否则返回 -1,错误原因存于 errno 中。
命名管道的访问
mkfifo 函数只是创建一个 FIFO文件 ,要使用命名管道首先要将其打开,与打开其他文件一样, FIFO文件 也可以使用 open 调用来打开。
open 函数的相关知识这里就不做赘述了,但是这里要强调四点:
不能以 O_RDWR 模式打开 FIFO文件 进行读写操作,而其行为也未明确定义,因为如果以读/写方式打开一个管道,进程就会读回自己的输出,同时我们通常使用 FIFO 只是为了单向的数据传递;
传递给 open 调用的是 FIFO 的路径名,而不是正常的文件。
当使用 O_NONBLOCK 时,打开 FIFO文件 来读取的操作会立刻返回,但是若还没有其他进程打开 FIFO文件 来读取,则写入的操作会返回 ENXIO 错误代码;
当没有使用 O_NONBLOCK 时,打开 FIFO 来读取的操作会等到其他进程打开 FIFO文件 来写入才正常返回。同样地,打开 FIFO文件 来写入的操作会等到其他进程打开 FIFO 文件 来读取后才正常返回。
核心代码如下:
参数详解如下:
filename
:命名管道文件的路径,类似"/tmp/my. fifo ";
mode
:命名管道文件的模式,表明执行此文件的权限,类似"0666"。返回值:
若成功则返回 0,否则返回 -1,错误原因存于 errno 中。
命名管道的访问
mkfifo 函数只是创建一个 FIFO文件 ,要使用命名管道首先要将其打开,与打开其他文件一样, FIFO文件 也可以使用 open 调用来打开。
open 函数的相关知识这里就不做赘述了,但是这里要强调四点:
不能以 O_RDWR 模式打开 FIFO文件 进行读写操作,而其行为也未明确定义,因为如果以读/写方式打开一个管道,进程就会读回自己的输出,同时我们通常使用 FIFO 只是为了单向的数据传递;
传递给 open 调用的是 FIFO 的路径名,而不是正常的文件。
当使用 O_NONBLOCK 时,打开 FIFO文件 来读取的操作会立刻返回,但是若还没有其他进程打开 FIFO文件 来读取,则写入的操作会返回 ENXIO 错误代码;
当没有使用 O_NONBLOCK 时,打开 FIFO 来读取的操作会等到其他进程打开 FIFO文件 来写入才正常返回。同样地,打开 FIFO文件 来写入的操作会等到其他进程打开 FIFO 文件 来读取后才正常返回。
核心代码如下:
/*客户端*/ ...... int res = mkfifo(fifo_name, 0777); char buffer[1024] = {0}; strncpy(buffer, "this is client.", strlen("this is client.")); if (res == 0) {int pipe_fd = open(fifo_name, O_WRONLY); int bytes_write= write(pipe_fd, buffer, bytes_read); if(0 < bytes_write) { printf("send message successful. "); } ...... }
/*服务端*/ ...... int res = mkfifo(fifo_name, 0777); char buffer[1024] = {0}; if (res == 0) {int pipe_fd = open(fifo_name, O_RDONLY); int bytes_read = read(pipe_fd, buffer, PIPE_BUF); if(0 < bytes_read) { printf("receive: %s ", buffer); } ...... }
测试步骤:
分别运行
./server
和./client
。运行结果:
send message successful.receive : this is client.
编程要求
在本实训中,存在一个服务端用于与客户端之间的通信。
本关的编程任务是完成客户端的代码编写,补全右侧代码片段
namepipe_commu
中Begin
至End
中间的代码,具体要求如下:检测命名管道文件
./my_fifo
是否存在,如若不存在则创建命名管道文件;以可读、阻塞的方式打开命名管道;
通过命名管道发送消息(namepipe_commu 的参数 buffer );
关闭命名管道。
答案:
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <limits.h> #include <string.h>void namepipe_commu(const char *buffer) {/***********begin***********/const char *fifo_path="./my_fifo";//检测命名管道文件./my_fifo是否存在int fd=open(fifo_path,O_WRONLY);//以可读、阻塞的方式打开命名管道if (write(fd,buffer,strlen(buffer))==-1){//通过命名管道发送消息(namepipe_commu的参数buffer)close(fd);return;}/***********end***********/ }
第3关:消息队列
任务描述
试想这样的场景:
你辅助你学弟的专业学习,你把相关的书籍不定期的放在图书馆的 1-2-58 书架上,学弟每次学完一本后会去拿之后的书籍。
整个事件的过程如下:
你打开书架;
每隔一段时间,你放上一本书;
学弟打开书架;
每学完一本书后,学弟再取走一本书。
以消息队列代替整个事件的过程,创建消息队列>发送消息->对方取走消息->删除消息队列。
本关任务:
创建消息队列;
每隔 1s 发送一则消息,共发送三条,分别为"C", "Linux", "Makefile";
最后发送一条"End"消息;
等待 10s ,删除消息队列。
相关知识
在 Linux 中,每种通讯方式应用的主流场景不同,在异步处理或者不需要很高的实时性的场景下,比较频繁的是消息队列。
消息队列的基本概念
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。
消息队列中的每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。
消息队列与命名管道一样,每个数据块都有一个最大长度的限制。在 Linux 系统中,使用宏 MSGMAX 和 MSGMNB 来限制一条消息的最大长度和一个队列的最大长度。
消息队列是链表队列,它通过内核提供一个
struct msqid_ds *msgque[MSGMNI]
向量维护内核的一个消息队列列表,因此 Linux 系统支持的最大消息队列数由 msgque 数组大小来决定,每一个 msqid_ds 表示一个消息队列,并通过msqid_ds.msg_first
、msg_last 维护一个先进先出的msg
链表队列。当发送一个消息到该消息队列时,把发送的消息构造成一个 msg 结构对象,并添加到
msqid_ds.msg_first
、msg_last 维护的链表队列;当接收消息的时候也是从**msg
链表队列尾部查找到一个 msg_type 匹配的 msg 节点,从链表队列中删除该 msg 节点,并修改** msqid_ds 结构对象的数据。消息队列的创建
创建命名管道的函数原型如下:
参数详解如下:
key
:消息队列关联的键,比如 1024;
msgflg
:消息队列的建立标志和存取权限;使用 IPC_CREAT 时,如果内核中没有此队列,则创建队列;当 IPC_EXCL 和 IPC_CREAT 一起使用时,如果队列已经存在,则失败。返回值:
成功执行时,返回消息队列标识值;失败返回 -1。
发送消息
#include <sys/msg.h> int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
参数详解如下:
msgid
:msgget 函数返回的消息队列标识符;
msg_ptr
:一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针 msg_ptr 所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型;struct my_message { long int message_type; /* The data you wish to transfer*/ };
msg_sz
:msg_ptr 指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,不包括长整型消息类型成员变量的长度;
msgflg
:用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情,一般设为 0。
返回值:如果调用成功,消息数据的一分副本将被放到消息队列中,并返回 0,失败时返回 -1。
接收消息
参数详解如下: msgid,msg_ptr,msg_st 和 msgflg 的作用和函数 msgsnd 的一样。#include <sys/msg.h> int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
msgtype
:实现一种简单的接收优先级。如果 msgtype 为 0,就获取队列中的第一个消息。如果它的值大于 0,将获取具有相同消息类型的第一个信息。如果它小于 0,就获取类型等于或小于 msgtype 的绝对值的第一个消息;返回值:
失败时返回 -1。
控制函数
#include <sys/msg.h> int msgctl(int msgid, int command, struct msgid_ds *buf);
参数详解如下:
msgid 的作用和函数 msgsnd 的一样。
command :将要采取的动作,一般使用 IPC_RMID ,用于删除消息队列;
buf :用于删除消息队列时,一般设为 NULL 。
返回值:
成功时返回 0,失败时返回 -1。
编程时使用的核心代码如下:
/*客户端*/ #include <sys/msg.h> struct msg_st {long int msg_type;char text[BUFSIZ]; }; int main(int argc, char *argv[]) {...... struct msg_st data; ...... //创建消息队列 int msgid = msgget((key_t)1234, 0666 | IPC_CREAT); ....... data.msg_type = 6;//指定消息类型 strncpy(data.text, "this is client.", strlen("this is client.")); msgsnd(msgid, (void *)&data, MAX_TEXT, 0); printf("send message successful. "); ...... }
/*服务端*/ #include <sys/msg.h> struct msg_st {long int msg_type;char text[BUFSIZ]; }; int main(int argc, char *argv[]) {...... struct msg_st data; ...... //创建消息队列 int msgid = msgget((key_t)1234, 0666 | IPC_CREAT); ...... msgtype = 6;//指定接收消息的类型 memset(&data, 0, sizeof(struct msg_st)); msgrcv(msgid, (void *)&data, BUFSIZ, msgtype, 0); printf("receive: %s ", data.text); ...... //删除消息队列 msgctl(msgid, IPC_RMID, NULL); }
测试步骤:
分别运行
./server
和./client
。运行结果:
send message successful.receive : this is client.
编程要求
在本实训中,存在一个服务端用于与客户端之间的通信。
本关的编程任务是完成发送方的代码编写,补全右侧代码片段
mq_commu
中Begin
至End
中间的代码,具体要求如下:创建 key 为 0x1234 的消息队列;
每隔 1s 发送一则消息,共发送三条,分别为"C", "Linux", "Makefile",消息类型均为 66 ;
最后发送一条"End"消息;
等待 10s ,删除消息队列。
答案 :
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <unistd.h> #include <string.h> #include <stdlib.h>struct msgbuf {long mytype;char bookname[100]; };void mq_commu (void) {/*********Begin*********///创建key为0x1234key_t key=0x1234;//创建消息队列int msgid=msgget(key,IPC_CREAT|0666);struct msgbuf message;message.mytype=66;//发送三条消息strcpy(message.bookname,"C");msgsnd(msgid,&message,sizeof(struct msgbuf)-sizeof(long),0);sleep(1);//等待一秒strcpy(message.bookname,"Linux"); msgsnd(msgid,&message,sizeof(struct msgbuf)-sizeof(long),0);sleep(1);//等待一秒strcpy(message.bookname,"Makefile");msgsnd(msgid, &message,sizeof(struct msgbuf)-sizeof(long),0);sleep(1);//等待一秒//最后发送一条"End"消息strcpy(message.bookname,"End");msgsnd(msgid,&message,sizeof(struct msgbuf)-sizeof(long),0);/**********End**********/ }