跳转至

Lecture 9. Unix 消息队列与远程过程调用(22 页)

远程过程调用 RPC

RPC 是啥

客户端与服务器通过 TCP 等协议建立好套接字连接后,客户端通常会希望服务器传来一些数据,或者希望服务器执行某些程序。

但是使用套接字来让服务器执行某个程序,对于程序员来说,虽然很灵活(底层),但是实现的难度还是比较大的。

RPC(远程过程调用)是一种在套接字等连接的基础上,以类似于调用本地函数的方法调用远程函数的一种通信抽象。

在 C 这种语言里面,这叫做 RPC;在 Java 之类的面向对象语言里面,叫做 RPI(Invocation)。

RPC 服务会定义一个接口,可以 自动生成存根。存根当中隐藏着 RPC 的实现代码。

RPC 流程

通过客户端提供的 存根(stub),RPC 系统隐藏通信细节。通常,对于每个单独的远程过程,都有一个存根。当客户端调用远程过程的时候,RPC 系统就调用适当的存根,并传递参数。

这个存根可以定位服务器的端口(对应某个特定的服务),并封装参数,将其打包,以便通过网络传输。

然后存根通过消息传递,向服务器发送一个信息。服务器的类似存根收到信息后,就调用相应的过程。如有必要,可以把返回值用同样的方式传回客户端。

关于端口

如何知道要跟服务器的哪个端口建立连接?

客户端可以先跟一个目录(或者说,字典)建立通信,从他那里询问得到相应的端口号 X,然后再去跟服务器的 X 端口连接。服务器也可以时常跟字典通知说,端口和对应服务的变化。

缺陷

像 Java 之类的语言,Object 存储的实际上只是地址。那你如果要把这个 Object 传给服务器,就会有问题——你传地址给服务器,服务器上的这个地址并不是你想传的那个东西。

还有一种缺陷。比如客户端和服务器的架构不一样(machine structure),那就有可能服务器和客户端相互读不懂对方说的话。所以有必要搞一种第三方的数据格式来表示信息。

还有为了防止丢包之类的,可能需要时不时的往存储设备里面暂存请求信息:

消息队列 Message Queue

消息队列是一种 异步 的通信机制(也就是说发消息的和收消息的不需要同时处理)。顺便一提,消息队列是一个优先队列,里面存储的消息是按照优先级排序的。

操作系统会帮助进程维护消息队列,直到销毁。消息队列以 /somename 的名字表示。要想通过某个消息队列进行通信,就必须同时具有对消息队列以及响应 API 的操作权限。

管道 VS 消息队列

  • 管道里面是字节流,消息队列里面可以是用户自定义的结构
  • 管道里面所有人写入的数据都堆在一起,分不清啥是谁写的,但是消息队列可以区分发送者是谁
  • 管道是 FIFO 的,消息队列是一个优先队列
  • 不能指明管道里面数据的状态,但是消息队列里面的最大消息长度啥的都可以指定

C 语言实现消息队列

  • mq_open():创建或打开一个消息队列,返回消息队列的 描述符(descriptor),跟文件描述符类似的结构
  • mq_send():向一个消息队列里面写入数据
  • mq_receive():从一个消息队列里面读取数据
  • mq_close():关闭这个消息队列
  • mq_unlink():必须在没有别的进程打开的时候使用,销毁消息队列
  • mq_getattr():可以获取消息队列的多个属性,比如状态、最大消息数量、最大消息长度、现有消息数量等

顺带一提,要想使用消息队列的这些 API 就必须在编译的时候指定 -lrt 选项。

// 创建或打开消息队列
strcpy(qname, "/test_queue");
// 如果消息队列咱不存在,就要创建,有一点麻烦
modeflags = O_RDWR | OCREAT;
permissions = 0600;  // 八进制,只允许 owner 读写
attr.mq_flags = 0;
attr.mq_maxmsg = 10;
attr.mq_msgsize() = 512;
mqd_t mq;
mq = mq_open(qname, modeflags, permissions, &attr);
// 如果消息队列已经存在,那就直接打开就好了
modeflags = O_RDWR;
mq = mq_open(qname, modeflags);
// blabla
mq_close(mq);
mq_unlink(qname);  // 必须没被其他进程占用才能执行成功
// 写入数据
char buffer[max_size];
int priority = 0;
memset(buffer, 0, max_size);
strcpy(buffer, "Hello There");
mq_send(mq, buffer, max_size, priority);
// 读取数据
char buffer[max_size];
max_size
int priority;
int numRead = mq_receive mq, buffer, max_size, &priority);

Unix 进程间通信总结

  • 管道:快,基于内存,面向流,相关的进程使用
  • 命名管道:用文件的方式存储管道在磁盘上,不相关的进程也可以使用
  • 套接字:底层的网络通信,一般基于 TCP/IP,不相关的进程也可以使用
  • 远程过程调用:就是对客户端-服务器编程的简化
  • 消息队列:异步,存储整条信息,是优先队列