admin 管理员组

文章数量: 887006

RPC初窥

1什么是RPC

 RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易----以上从百度百科拷贝过来。
 RPC这个概念,很早就听说过大名,但是之前对RPC进程通信原理还是一直不了解,主要是实际工作中也较少用这个。最近在接触并使用集团自己开发的HSF框架时,才知道原来这东西已经在集团内被做成很多服务间通信的标准框架了。
 HSF这么高大上的东西具体实现不太清楚,本文只讲讲其基础之一RPC的知识。当然,这个只是本人初次窥和学习RPC,未免有很多地方认识不全,还需以后进一步了解。

2 Linux下开发实例详解

 刚开始有很多地方不明白,先用一个网上经常用的例子开始,然后用解答例子中的疑问的方式来进一步认识。实例主要目的是获取服务器端的时间信息。

2.1 rpcgen

 rpcgen的作用是把RPC说明文件编译成C语言源程序;
 time.x内容如下:

program TIMEPROG {version PRINTIMEVERS {string PRINTIME(string) = 1;} = 1;
} = 0x20000001;

先不管里面的内容代表什么,执行rpcgen time.x;
执行完后会产生如下文件:time_svc.c time.h time_clnt.c timeclient.c timeserver.c

2.2 客户端代码

#include <stdio.h>
#include "time.h" /* time.h generated by rpcgen */
main(int argc, char **argv)
{CLIENT *clnt;char *result;char *server;char *message;if (argc != 3) {fprintf(stderr, "usage: %s host message\n", argv[0]);exit(1);}server = argv[1];message = argv[2];clnt = clnt_create(server, TIMEPROG, PRINTIMEVERS, "TCP");if (clnt == (CLIENT *)NULL) {clnt_pcreateerror(server);exit(1);}result =*printime_1(&message,clnt);if (result== (char *)NULL) {clnt_perror(clnt, server);exit(1);}if(strcmp(result,"Error") == 0) {fprintf(stderr, "%s: could not get the time\n",argv[0]);exit(1);}printf("From the Time Server ...%s\n",result);clnt_destroy( clnt );exit(0);
}

2.3 服务器端代码

#include <stdio.h>
#include <rpc/rpc.h>           /* always needed */
#include "time.h"                  /* time.h will be generated by rpcgen */
#include <time.h>/* Remote version of "printime" */
char ** printime_1_svc(char **msg,struct svc_req *req)
{static char * result; /* must be static! */static char tmp_char[100];time_t rawtime;printf("input:%s\n", *msg);                      //used for debugging time(&rawtime);sprintf(tmp_char,"Current time is :%s",ctime(&rawtime));result =tmp_char;return (&result);
}

2.4 测试

运行服务器:sudo ./timeserver
运行客户端:./timeclient 127.0.0.1 lsdf

3 RPC原理

3.1 流程图

3.2原理初窥

3.2.1 疑问

 经过上面的几个步骤,这里已经搭建了一个非常简易的RPC框架。客户端发送的调用请求被服务器端收到、处理并返回了。
 到了这儿,心里窃喜。但是心里还是很纳闷,因为没明白他们到底是怎么通信的,数据到底是怎么流通的。对客户端和服务器端是怎么识别的过程不明白。
 这个时候我做了一个测试:我同时启动了两个服务程序(两个都是相同的,上文中的timeserver),然后再启动客户端。诡异的事情发生了(现在看来一点也不诡异):每次总是后启动的那个RPC服务收到了客户端请求。
客户端:

先启动的服务器:

后启动的服务器:

 为啥与客户端通信的是后启动的服务程序,而不是先启动的?网上查找了一下,没太找的好的结果。这个时候想到了神器:stevens的unix网络编程。第二版的第十六章(16.3)里面我找到了答案,书里提到了一个东西:portmap。
 portmap即端口映射器。在linux系统里面portmap是一个系统的守护进程(新一点的系统有可能没有这个进程,换成了另外一个:rpcbind)。这个守护进程维护了一份程序到端口的映射表,该守护进程默认会绑定到111端口(细心地同学可以查看自己的服务器,应该大部分的服务器上的111端口都是被使用的)。
 此时执行命令:rpcinfo –p。结果如下:

 该命令打印出的就是portmap里面的映射表信息。这里看到最下面有两个端口:806和808。Proto分别是udp和tcp。Vers表示版本,这里都是1。
 最前面的program 是536870913,这里还不明白这个program代表的什么。那么看下我们的RPC说明书文件time.x,源码如下:

program TIMEPROG {version PRINTIMEVERS {string PRINTIME(string) = 1;} = 1;
} = 0x20000001;

 这里的program=0x20000001。0x20000001转成10进制,刚好536870913。
 到这里,我们似乎感觉到,这个rpcinfo里面的信息,和我们RPC说明书文件里面的定义信息有某种联系。那么具体的联系是什么呢?

3.2.2 解惑

 我们先看time.h文件,这个头文件里面会把time.x里面定义的数据定义进来,如下:

#define TIMEPROG 0x20000001
#define PRINTIMEVERS 1#if defined(__STDC__) || defined(__cplusplus)
#define PRINTIME 1
extern  char ** printime_1(char **, CLIENT *);
extern  char ** printime_1_svc(char **, struct svc_req *);
extern int timeprog_1_freeresult (SVCXPRT *, xdrproc_t, caddr_t);#else /* K&R C */
#define PRINTIME 1
extern  char ** printime_1();
extern  char ** printime_1_svc();
extern int timeprog_1_freeresult ();
#endif 

 而客户端和服务端的程序编译的时候,都会带上这个头文件,因此双方都会获取到程序号、版本、过程号等协议好的信息。
 再来看看time_svc.c文件中的main函数,里面调用了一个注册函数:svc_register(transp, TIMEPROG, PRINTIMEVERS, timeprog_1, IPPROTO_TCP)。这里的TIMEPROG, PRINTIMEVERS就是time.h文件里面的程序号和版本。然后用tcp和UDP协议都注册了一个。
svc_register函数调用的本质就是先绑定并监听一个端口。然后将我们客户端和服务器端协议好的程序号、版本、过程号等信息 以及服务端的实际服务端口写入到portmap的映射表中。
那么我们再看看,是如何根据portmap来连接客户端和服务器端的。
首先,通过上面的分析,服务器端把服务程序的信息注册到portmap里面。这个时候,服务端已经准备好了。
 其次,我们再来看客户端,timeclient.c文件里面,调用了clnt_create(server, TIMEPROG, PRINTIMEVERS, "TCP")函数。 TIMEPROG和PRINTIMEVERS也是time.h里面定义的,这里的协议采用了TCP。前面我们说过,portmap本身是一个守护进程(端口111)一直存在,clnt_create所做的工作就是向服务器的portmap发送一个RPC请求,根据传入的IP、程序号、版本号、协议类型四个参数,来获取真正RPC的服务端口。比如这里用的TCP,按上面的图中可以看出,服务端口为808。客户端获取到该端口后,采用普通的网络通信就可以和服务器交互了。
 到此时,我们终于弄明白了,RPC客户端是如何找服务端的服务程序的。再回过头来看看上面我遇到的问题:我启动两次timeserver服务程序,每次接收到客户端的始终是后启动的一个服务。此时这个答案已经非常明了了:两次启动的时候都会调用svc_register函数去更新portmap的映射表。由于两次我们启动的时候,IP、程序号、版本号、协议都是一致的,所以后启动的服务会把前一个启动的映射表信息给刷新掉。所以当客户端来向portmap询问RPC服务的端口时,返回的是后一个注册的服务的端口。

4 其他

4.1 webservice

 RPC本质还是进程间通信。只是RPC的框架可以让开发着忽略繁琐网络通信开发,更多的精力可以着眼于实际业务。想到这里,可能大家会蹦出一个跟我一样的想法,这不是正式web service所干的事吗?确实,RPC和webservice本身都是一种提供远程服务的方式。
RPC Webservice
传输层协议 tcp/udp tcp
应用层协议 自定义 http
 由于http协议现在已经成为了一种标准协议,标准的东西当然也是优劣并存的。优点是通用,webservice现在被使用的非常广泛,已经成为整个互联网的基础之一了。
当然RPC也有一些自己优势。既然应用协议可以自定义,那么就会有更大的灵活性。比如总有那么一群牛叉的团队,根据实际的需求,会自己写一套更为高效的RPC协议。其实集团的HSF在我看来,本身就是定义了一套RPC的协议(纯粹自己感觉哈)。 突然感觉webservice也就是一种特殊的RPC,htpp就是其RPC协议。

4.2 portmap

 前面提到,portmap这个东西在RPC通信过程中,起着非常重要的作用。有一个比喻很好:portmap就像媒婆,为客户端和服务端第一次认识牵线搭桥。一旦客户端和服务器端已经互相识别,那么portmap就会被踢到一边了(好形象。不禁感叹了一下媒婆这个职业)。
 当然,这里所说的portmap是一个地址映射的泛称,不仅仅是指linux下面的这个守护进程,而是指端口映射这个概念。或许这个端口映射的程序是工程师自己写的;甚至有可能都不单单是端口映射,比如RPC的服务是一个集群的时候,是不是也可以自己写一个虚拟的地址映射程序,所映射的也就有可能不单单是端口,而是地址+端口。

4.3序列化

 一般自己实现一套rpc的话,都会有一些序列化和反序列化的工作。特别是支持跨语言的话,这个就是更不可缺少的。目前常用protobuf、json 、hessian、thrift等等,具体这些的性能没测试过,不过网上的这种对比很多,看他们的评比的话,protobuf无论在性能还是压缩比上面都貌似是遥遥领先啊。这个目前还没亲测,有空自己测试下再下定论。
 在上面的例子中,写的比较简单,所以没有涉及到序列化,其实在time.x里面还可以定义更为复杂的输入、输出的通信结构,这个时候序列化和反序列化就很重要了。

 以上是自己最近一段时间在用HSF后,自己去学习的一点RPC知识,从最简单的实例出发,来解答我自己之前的一些疑问。当时实际的RPC框架实现的时候,有很多知识和设计,需要进一步去了解。上面的认识或许其中还有一些地方的理解不太对或者甚至是错的。还有很多的基本知识需要去了解,服务端的线程安全、认证、超时等。

本文标签: RPC初窥