RPC程序一般是由一个stand-alone程序改写过来的。根据要移动到远程的函数,编写.x文件,通过rpcgen工具生成模板函数,将要改写的函数的实现代码写入模板中,编译即可。下面是详细的过程描述。
写出stand-alone程序,也就是要在本机运行的程序,编译通过,确认程序没有错误。这里举一个简单的例子,简单的加法运算器。我们最终的目标是将rsum函数改为由远程服务器运行。stand-alone程序代码如下:
#include <stdio.h> #include <stdlib.h>
int rsum(int, int);
int main (void) { int a = 0; int b = 0; int sum = 0;
printf("Input one number:/n"); scanf("%d", &a); printf("Input another number:/n"); scanf("%d", &b);
sum = rsum(a, b);
printf("The sum is: %d/n", sum);
return 0;
}
int rsum(int a, int b) { return (a+b); }
|
程序很简单,运行结果如下:
[root@vv 1test]# ./a.out Input one number: 5 Input another number: 6 The sum is: 11
|
下面我们将进行.x文件的编写。.x文件使用IDL语言。具体IDL语言的规范,可以参考下面的说明,更加系统和细致请参考相关手册和文档。
在我们程序中要改写的函数声明如下:
int rsum(int, int);
|
这个函数由两个int型参数和一个int型的返回值。由于RPC创建函数时强烈建议只使用一个参数,如果没有办法避免,则可以使用结构体进行封装。这里要注意在IDL中结构体的定义格式。具体的IDL,rsum.x如下:
struct rsum_param_st { int a; int b; }; typedef struct rsum_param_st rsum_param_st;
program RSUMPROG{ version RSUMPROG1{ int RSUM_FUNC(rsum_param_st) = 1; /* 第一个1,远程函数号 */ }=1; /* 第二个1,远程程序版本号 */ }=0x33333333; /* 远程程序号 */
|
在这篇文档中,我们将0x33333333称之为远程程序号。这个号码用来唯一的指明远程主机上向外提供的远程程序。第二个1,是程序的版本号。同一个主机上可以有同一个远程程序的多个版本,比如NFS。在同一个远程程序中,我们使用版本号来标示不同版本。在指定了版本后,远程程序中的函数是作为一个个单独的procedure来向外提供的,所以我们需要指明不同procedure的一个号码,这个号码就是上面写的第一个1。
下面我们使用rpcgen -C rsum.x生成ANSI C标准的代码,可以使用-k参数生成K&R C标准的代码。该命令可以生成四个文件,列表如下:
[root@vv 1test]# ls rsum_clnt.c rsum.h rsum_svc.c rsum.x rsum_xdr.c
|
其中rsum_clnt.c中定义了客户端可以使用的一个本地函数,该函数的命名一般由之前.x文件中定义的远程函数命名决定,一般是小写加上版本号。
int * rsum_func_1(rsum_param_st *argp, CLIENT *clnt) { static int clnt_res;
memset((char *)&clnt_res, 0, sizeof(clnt_res)); if (clnt_call (clnt, RSUM_FUNC, (xdrproc_t) xdr_rsum_param_st, (caddr_t) argp, (xdrproc_t) xdr_int, (caddr_t) &clnt_res, TIMEOUT) != RPC_SUCCESS) { return (NULL); } return (&clnt_res); }
|
函数中clnt是clnt_create函数返回值。clnt_create函数通过IP或者主机名加上远程程序号,版本号等信息生成的表明远程过程的句柄。通过该句柄,我们对远程procedure进行调用。
下面我们用rpcgen来生成一份服务器端的模板。命令的形式如下:
[root@vv 1test]# rpcgen -C -Ss rsum.x >server.c [root@vv 1test]# cat server.c /* * This is sample code generated by rpcgen. * These are only templates and you can use them * as a guideline for developing your own functions. */
#include "rsum.h"
int * rsum_func_1_svc(rsum_param_st *argp, struct svc_req *rqstp) { static int result; /* * insert server code here */
return &result; }
|
将实现的代码添加到注释的地方。
/* * insert server code here */ result = argp->a + argp->b;
|
服务器程序代码已经基本编写完毕,下面我们将注意力集中到客户端。后面的操作对象是之前的stand-alone源文件。
首先我们需要将程序改写成可以从命令行接受参数的形式,由用户指定服务器。所以我们需要一下代码。
#include <stdio.h> #include <stdlib.h>
int rsum(int, int);
int main (int argc, char **argv) { int a = 0; int b = 0; int *sum; extern char *optarg; extern int optind;
char *server = "localhost"; /* default */ int err = 0; while ((c = getopt(argc, argv, "h:")) != -1) switch (c) { case 'h': server = optarg; break; case '?': err = 1; break; }
if (err || (optind < argc)) { fprintf(stderr, "usage: %s [-h hostname]/n", argv[0]); exit(1); }
|
上面标出的代码是Linux getopt库函数相关的语句,用来接受并检测参数。
下面我们需要通过调用clnt_creat获得远程程序的句柄。在此之前需要在源文件头部添加两个额外的头文件<rpc/rpc.h>和"date.h"。
...... CLIENT *cl = NULL; ......
cl = clnt_create(server, RSUMPROG, RSUMPROG1, "UDP"); ...... |
在获取了远程过程的句柄后,我们就可以调用远程procedure为我们服务了。添加代码如下:
...... rsum_param_st param ; ......
printf("Input one number:/n"); scanf("%d", ¶m.a); printf("Input another number:/n"); scanf("%d", ¶m.b);
sum = rsum_func_1(param, cl);
|
这样我们的客户端程序就修改完毕了。完整的代码如下:
#include <stdio.h> #include <stdlib.h> #include <rpc/rpc.h> #include "rsum.h"
extern char *optarg; extern int optind;
int main (int argc, char **argv) { static rsum_param_st param; CLIENT *cl = NULL; int *sum = NULL; char *server = "localhost"; /* default */ int err = 0; char c;
while ((c = getopt(argc, argv, "h:")) != -1) switch (c) { case 'h': server = optarg; break; case '?': err = 1; break; }
/* exit if error or extra arguments */ if (err || (optind < argc)) { fprintf(stderr, "usage: %s [-h hostname]/n", argv[0]); exit(1); }
printf("Input one number:/n"); scanf("%d", &(param.a)); printf("Input another number:/n"); scanf("%d", &(param.b));
cl = clnt_create(server, RSUMPROG, RSUMPROG1, "UDP");
sum = rsum_func_1(¶m, cl);
printf("Answer from %s. The sum is: %d/n", server, *sum);
return 0;
}
|
下面进行编译,首先是客户端
[root@vv 1test]# cc -o client client2.c rsum_clnt.c rsum_xdr.c -lnsl
|
然后是服务器端编译
[root@vv 1test]# cc -o server -DRPC_SVC_FG server.c rsum_svc.c rsum_xdr.c -lnsl
|
完成之后,现在看一看执行结果吧。
[root@vv 1test]# ./server & [root@vv 1test]# ./client Input one number: 4 Input another number: 5 Answer from localhost. The sum is: 9
|
到这里,基本就是一个简单的RPC程序编写的情况了。
下面我们分析下报文交换的过程并对RPC的数据帧进行简单的分析。
首先是交互流程:
图 1 交互过程
第一个包是由客户端向服务器端发送的一个GETPORT请求,这个请求的作用是根据客户端提供的程序号和版本号等信息想服务器索取下面将要进行的RPC请求的端口。我看来看一下具体的帧内容。
图 2 第一个帧内容
XID:有call-reply关系的两个帧的这个域相同,用来表示一个交换。
Message Type:表示消息的类型。这里有两种类型,一种是CALL,另一种是REPLY。
RPC Version:目前是2
Program:指明远程程序号,GETPORT为100000。下面是主要程序号的分配规则:
0x0 - 0x1fffffff Defined by Sun 0x20000000 - 0x3fffffff Defined by user 0x40000000 - 0x5fffffff Transient 0x60000000 - 0x7fffffff Reserved 0x80000000 - 0x9fffffff Reserved 0xa0000000 - 0xbfffffff Reserved 0xc0000000 - 0xdfffffff Reserved 0xe0000000 - 0xffffffff Reserved
|
Program Version:指明了远程程序的版本号。
Procedure:表明远程调用的过程或者函数号。
我们要注意的一个点是Program unknown这个点,里面有个串数字858993459,我们把它转成十六进制就可以看出来,它等于0x33333333,这个号码就是我们当初写程序时指定的程序号码。
在Reply报文中,我们可以得到向0x33333333程序进行请求调用的端口号802。同时XID相同也得到了验证。
图 3 第一个Reply
对后面两次交互,虽然wireshark没有识别我们私有的RPC调用但可以通过RPC帧来的到一些信息。
图 4 后续报文
图 5 后续Reply
结束。
如果有额外的文档需要,可以通过发送邮件到wnlo.zw@gmail.com进行相关文档的获取。来信表明:RPC资料。