使用rpcgen构建分布式程序的一个简单例子
一. RPC概念
1.1 介绍
在中间件的实现中,引入了远程过程调用RPC(Remote Procedure Call)的概念。同时,许多分布式系统是基于进程间的显式消息交换的,然而消息的发送和接收过程无法隐藏通信的存在,而通信的隐藏对于在分布式系统中实现访问透明性是极为重要的。因此这个问题在很长一段时间内都没有找到合适的解决办法,后来Birrel和Nelson在1984年的一篇论文中引入了一套与传统方法截然不同的通信处理手段。他们认为应该允许程序调用位于其它机器上的进程。当机器A上的进程调用B上的进程时,A上的调用进程被挂起,而B上的被调用进程开始执行。调用方可以通过使用参数将信息传送给被调用方,然后可以通过传回的结果得到信息。编程人员看不到任何消息传递过程。这种方法就称为远程过程调用RPC。目前,RPC作为一种广泛使用的技术,已成为许多分布式系统的基础。
1.2 构建分布式程序的两种模式
在设计分布式应用时,程序员可以使用下列两种方法之一:
面向通信的设计
由通信协议开始。设计报文格式和语法,指明对每个传入报文将如何反应以及如何产生每个外发报文,以此来设计客户和服务器各构件。
面向应用的设计
由应用开始。设计常规的应用程序来解决问题。构建并测试可在单台机器上运行的常规程序的工作版本。将这个程序划分成两个或多个程序片,加入通信协议以允许每片程序在单独的计算机上执行。
远程过程调用模型使用面向应用的方法,它强调的是所要解决的问题而不是所需要的通信。利用远程过程调用,程序员首先设计一个解决问题的常规程序,接着将其划分成若赶干片,这些程序片运行在两台或更多的计算机上。程序员可遵循良好的设计原则,以便使代码模块化并且可维护。
在理想的情况下,远程过程调用提供的不只是抽象上的概念。它允许程序员在将一个程序划分成若干片之前,先构建,编译和测试一个解决该问题的常规程序的版本,以便确保能够正确解决问题。不但如此,因为RPC以方法调用为边界划分程序,所以将程序划分为本地部分和远程部分并不会引起程序结构的很大变化。实际上,将某些过程从一个程序转移到远程机器上时,有可能不需要改变。
1.3 常规过程调用的概念性模型
如下图所示,为常规的程序调用。
1.4 远程过程调用模型
远程过程调用模型使用了和常规程序一样的过程抽象,但是它允许一个过程的边界跨越两台计算机。如下图所示。
1.5 常规过程调用的执行和返回
程序从一个主程序开始执行,并一直继续下去,直到遇到一个过程调用。这个调用使程序的执行转入到某个指定的代码处继续执行。常规过程调用的执行流程如下图所示:
1.6 分布式系统的过程模型
在分布式系统中,其中的某个过程有可能在另外的机器上,因此,其调用过程模型如下图如示:
1.7 客户-服务器和RPC之间的对比
远程过程调用允许程序员以一种他所熟悉的环境来思考客户和服务器的交互,如同常规的过程调用,远程过程调用把控制权传递给被调用的进程。也像常规过程调用一样,在调用进程中,系统把调用过程的执行挂起。而只允许被调用过程执行。
当远程程序发出响应时,这对应于在常规过程调用中执行return。控制权返回给调用者,被调用过程停止执行。嵌套的过程调用的想法也可应用到远程过程调用。远程过程调用也许要调用另一个远程过程。如上图所示。
二. 分布式程序的生成原理
RPC的实现包括一个工具,它自动地生成实现分布式程序所需要的大多数代码。这个工具叫做rpcgen,它读取一个规约文件作为输入,生成C的源文件作为输出。规范文件包含常量,全局数据类型,全局数据以及远程过程(包括过程参数和结果类型)的声明。rpcgen产生的代码包含了实现客户和服务器程序所需要的大部分源代码。具体地说,rpcgen包括参数整理,发送RPC报文,把传入调用分派到正确的过程,发送应答,在参数和结构的外部表示和本地数据表示之间进行转换。rpcgen的输出与应用程序和程序员编写的少数文件相结合后,便产生出了完整的客户和服务器程序。
rpcgen读取输入文件,该文件包含有对远程过程的说明。它产生四个输出文件,每个文件都包含有源代码,如果输入的文件(即规约文件)具有名字q.x, 则输出的文件如下所示:
q.h:常量和类型的声明
q_xdr.c XDR过程调用
q_clnt.c 客户端的通信接口
q_svc.c 服务器端的通信接口
三. 分布式程序的生成步骤
在这里列举出一个例子。这个例子可以很好的解释出rpcgen是如何工作的。
3.1 查找字典
在这个例子中,考虑实现一个简单的数据库功能的程序。该数据库提供四个基本的操作:初始化,插入一个新的条目,删除一个条目,查找一个条目。假设每个条目都是一个单词。因此该数据库的功能就可以看作是一个字典。应用程序插入一组单词,接着使用数据库来检查新单词,以便知道这个单词是否在该字典中。
3.2 构建程序的八个步骤
(1)构建解决该问题的常规应用程序
要构建这个字典应用例子的分布式版本,第一步要求程序员构造解决该问题的常规程序。文件dict.c包含了该常规程序:
/* dict.c */
#include
#include
#include
#include
#define MAXWORD 50 /* 每个单词的长度限制*/
#define DICTSIZ 100 /* 字典一共可以存放多少个单词 */
char dict[DICTSIZ][MAXWORD+1];
int nwords = 0;
/*
* nextin(): 读入命令与单词
*/
int nextin(char *cmd, char *word)
{
int i, j;
char buf[100] = {0};
if (fgets(buf, sizeof(buf)-1, stdin) == NULL)
return -1;
for (i = 0; i < 100; i++)
if (buf[i] != ' ')
break;
if (i == 100)
return -1;
*cmd = buf[i];
while (1)
{
if (i == 100)
return -1;
i++;
if (buf[i] == ' ')
continue;
else if (buf[i] == '\n')
return 0;
else
break;
}
j = 0;
while (buf[i] != '\n')
{
*word++ = buf[i];
i++;
j++;
}
return j;
}
/*
* initw(): 初始化这个字典
*/
int initw()
{
nwords = 0;
return 1;
}
/*
* insertw(const char *word): 向这个字典中加入单词
*/
int insertw(const char *word)
{
strcpy(dict[nwords], word);
nwords++;
return nwords;
}
/*
* deletew(const char *word): 从这个字典中删除单词
*/
int deletew(const char *word)
{
int i;
for (i = 0; i < nwords; i++)
{
if (strcmp(word, dict[i]) == 0)
{
nwords--;
strcpy(dict[i], dict[nwords]);
return 1;
}
}
return 0;
}
/*
* lookupw(const char *word): 在字典中查找某个单词
*/
int lookupw(const char *word)
{
int i;
for (i = 0; i < nwords; i++)
{
if (strcmp(word, dict[i]) == 0)
return 1;
}
return 0;
}
int main(int argc, char **argv)
{
char word[MAXWORD+1];
char cmd;
int wrdlen;
while (1)
{
wrdlen = nextin(&cmd, word);
if (wrdlen < 0)
exit(0);
word[wrdlen] = '\0';
switch(cmd)
{
case 'I':
initw();
printf("Dictionary initialized to empty.\n");
break;
case 'i':
insertw(word);
printf("%s inserted.\n", word);
break;
case 'd':
if (deletew(word))
printf("%s deleted.\n", word);
else
printf("%s not found.\n", word);
break;
case 'l':
if (lookupw(word))
printf("%s is found.\n", word);
else
printf("%s is not found.\n", word);
break;
case 'q':
printf("program quits.\n");
exit(0);
default:
printf("command %c invalid.\n", cmd);
break;
}
}
return 0;
}
该dict.c程序使用了一个二维数组来存储单词。全局变量nwords记录了任何时刻字典中单词的数量。主程序包含一个循环,在每次循环中读取并处理输入文件中的一行。
该程序就是普通的常规程序,编译并运行:
gcc dict.c –o dict
./dict
可以如下来实验:
I
i hello
i world
l hello
d hello
l hello
q
(2)将程序划分为两个部分
常规程序一旦构建完成并经过测试,就可以将它划分成本地构件和远程构件了。下图为这个程序的过程调用情况:
在考虑哪个过程可以转移到远程机器上时,程序员必须考虑每个过程所需要的资源。例如nextin在每次被调用时要读取下一个输入行,并对它进行分析。因为它需要访问程序的标准输入,所以nextin必须放在主程序的机器中。简单来说就是执行I/O或者访问文件描述符的过程不能轻易地转移到远程机器中。
同时还要考虑每个过程所要访问的数据所处的位置。例如,lookupw需要访问全部单词数据库,如果执行lookupw的机器不同于字典所处的机器,对lookupw的RPC调用就必须将整个字典作为参数来传递。将巨大的数据结构作为参数传递给远程过程的效率非常低。一般来说执行过程的机器应当与放置过程所要访问数据的机器是同一台,将巨大的数据结构传递给远程过程的效率是很低的。
对于本应用来说,应当把过程insertw,deletew,initw,lookupw和字典本身放在同一台机器中。下图说明了这种划分方式:
相当于将dict.c 划分成了两个文件dict1.c和dict2.c。这里的两个程序只是一个示例,不需要写出来,但是后面将要编写与这两个程序相似的另外两个程序。
文件dict1.c包含主程序和过程nextin:
/* dict1.c */
#include
#include
#include
#include
#define MAXWORD 50
char dict[DICTSIZ][MAXWORD+1];
int nwords = 0;
int nextin(char *cmd, char *word)
{
int i, j;
char buf[100] = {0};
if (fgets(buf, sizeof(buf)-1, stdin) == NULL)
return -1;
for (i = 0; i < 100; i++)
if (buf[i] != ' ')
break;
if (i == 100)
return -1;
*cmd = buf[i];
while (1)
{
if (i == 100)
return -1;
i++;
if (buf[i] == ' ')
continue;
else if (buf[i] == '\n')
return 0;
else
break;
}
j = 0;
while (buf[i] != '\n')
{
*word++ = buf[i];
i++;
j++;
}
return j;
}
int main(int argc, char **argv)
{
char word[MAXWORD+1];
char cmd;
int wrdlen;
while (1)
{
wrdlen = nextin(&cmd, word);
if (wrdlen < 0)
exit(0);
word[wrdlen] = '\0';
switch(cmd)
{
case 'I':
initw();
printf("Dictionary initialized to empty.\n");
break;
case 'i':
insertw(word);
printf("%s inserted.\n", word);
break;
case 'd':
if (deletew(word))
printf("%s deleted.\n", word);
else
printf("%s not found.\n", word);
break;
case 'l':
if (lookupw(word))
printf("%s is found.\n", word);
else
printf("%s is not found.\n", word);
break;
case 'q':
printf("program quits.\n");
exit(0);
default:
printf("command %c invalid.\n", cmd);
break;
}
}
return 0;
}
文件dict2.c包含了来自最初的应用程序的一些函数。它们将成为远程程序的一部分。另外,还包含对各个函数要共享的全局数据的声明。
/* dict2.c */
#include
#define MAXWORD 50
#define DICTSIZ 100
char dict[DICTSIZ][MAXWORD+1];
int nwords = 0;
int initw()
{
nwords = 0;
return 1;
}
int insertw(char *word)
{
strcpy(dict[nwords], word);
nwords++;
return nwords;
}
int deletew(char *word)
{
int i;
for (i = 0; i < nwords; i++)
{
if (strcmp(word, dict[i]) == 0)
{
nwords--;
strcpy(dict[i], dict[nwords]);
return 1;
}
}
return 0;
}
int lookupw(char *word)
{
int i;
for (i = 0; i < nwords; i++)
if (strcmp(word, dict[i]) == 0)
return 1;
return 0;
}
注意,对符号常量MAXWORD的定义在两个构件中都出现了,因为它们都要声明用于存储字的变量,然而,只有在文件dict2.c中才含有用于存储字典的数据结构的声明,因为只有远程过程才包含字典的数据结构。
此时
gcc –c dict1.c
gcc –c dict2.c
编译一下检查是否有语法错误。
(3)创建rpcgen规约
程序员一旦为某个分布式程序选择了一种结构,就可以准备rpc规约了。从本质上说,rpcgen规约文件包含了对远程程序的声明以及它所使用的数据结构。
该规约文件包含常量,类型定义,以及对客户和服务器程序的声明。即:
声明在客户或服务器中所使用的常量
声明所使用的数据类型
声明远程程序,每个程序中所包含的过程以及它们的参数类型
所有这些声明必须用RPC编程语言来给出。对于该例子的规约如下,rdict.x:
/* rdict.x */
const MAXWORD = 50;
const DICTSIZ = 100;
struct example
{
int exfield1;
char exfield2;
};
program RDICTPROG /* 远程程序的名称 */
{
version RDICTVERS /* 版本 */
{
int INITW(void) = 1; /* 第一个函数 */
int INSERTW(string) = 2; /* 第二个函数 */
int DELETEW(string) = 3; /* 第三个函数 */
int LOOKUPW(string)= 4; /* 第四个函数 */
} = 1; /* 该程序的版本号 */
} = 0x30090949; /* 远程程序标识, 必须唯一 */
(4)运行rpcgen
在完成了规约后,程序员运行rpcgen来检查语法错误,并且生成四个代码文件:rdict.h rdict_clnt.c rdict_svc.c和rdict_xdr.c。
输入命令:rpcgen rdict.x
rdict.h为rpcgen产生的.h文件,即头文件。
rdict_xdr.c为rpcgen产生的XDR转换文件,XDR即External Data Representation。是数据传输的一个规范。
rdict_clnt.c为rpcgen产生的客户端的代码。
rdict_svc.c为rpcgen产生的服务器端的代码。
这些文件一旦生成,就可以被编译成目标代码的形式。
gcc –c rdict_clnt.c
gcc –c rdict_svc.c
gcc –c rdict_xdr.c
到了这一步,需要程序员再编写接口过程。程序员需要编写四个程序,分别为客户端程序与客户端的接口程序,服务器程序与服务器端的接口程序。如下图所示,图中阴影部分为需要程序员自己编写的代码:
(5)编写接口过程
rpcgen产生的文件并没有构成完整的程序,它还要求程序员必须编写客户端和服务器端的接口。
客户端接口rdict_cif.c程序如下所示:
/* rdict_cif.c */
#include
#include
#define RPC_CLNT
#include"rdict.h"
extern CLIENT *handle;
static int *ret;
int initw()
{
ret = initw_1(0, handle);
return ret == NULL ? 0 : *ret;
}
int insertw(char *word)
{
char **arg;
arg = &word;
ret = insertw_1(arg, handle);
return ret == NULL ? 0 : *ret;
}
int deletew(char *word)
{
char **arg;
arg = &word;
ret = deletew_1(arg, handle);
return ret == NULL ? 0 : *ret;
}
int lookupw(char *word)
{
char **arg;
arg = &word;
ret = lookupw_1(arg, handle);
return ret == NULL ? 0 : *ret;
}
服务器端接口例程rdict_sif.c如下:
/* rdict_sif.c */
#include
#define RPC_SVC
#include"rdict.h"
static int retcode;
int initw(void), insertw(char *), deletew(char *), lookupw(char *);
int *insertw_1_svc(char **w, struct svc_req* rqstp)
{
retcode = insertw(*(char**)w);
return &retcode;
}
int *initw_1_svc(void* w, struct svc_req* rqstp)
{
retcode = initw();
return &retcode;
}
int *deletew_1_svc(char **w, struct svc_req* rqstp)
{
retcode = deletew(*(char**)w);
return &retcode;
}
int *lookupw_1_svc(char **w, struct svc_req *rqstp)
{
retcode = lookupw(*(char**)w);
return &retcode;
}
(6)编译和链接客户程序
首先
gcc –c rdict_cif.c编译客户端的接口程序。
再gcc –c rdict_xdr.c编译XDR程序。
而客户端程序还需要声明并初始化一个句柄,RPC通信例程用该句柄和服务器通信。
客户端程序rdict.c如下:
/* rdict.c */
#include
#include
#include
#include
#include"rdict.h"
#define MAXWORD 50
#define RMACHINE "127.0.0.1" /* 服务器端ip address */
CLIENT *handle;
int nextin(char *cmd, char *word)
{
int i, j;
char buf[100] = {0};
if (fgets(buf, sizeof(buf)-1, stdin) == NULL)
return -1;
for (i = 0; i < 100; i++)
if (buf[i] != ' ')
break;
if (i == 100)
return -1;
*cmd = buf[i];
while (1)
{
if (i == 100)
return -1;
i++;
if (buf[i] == ' ')
continue;
else if (buf[i] == '\n')
return 0;
else
break;
}
j = 0;
while (buf[i] != '\n')
{
*word++ = buf[i];
i++;
j++;
}
return j;
}
int main(int argc, char **argv)
{
char word[MAXWORD+1];
char cmd;
int wrdlen;
handle = clnt_create(RMACHINE, RDICTPROG, RDICTVERS, "tcp");
if (handle == NULL)
{
printf("cound not contact remote program.\n");
exit(1);
}
while (1)
{
wrdlen = nextin(&cmd, word);
if (wrdlen < 0)
{
exit(0);
}
word[wrdlen] = '\0';
switch(cmd)
{
case 'I':
initw();
printf("Dictionary initialized to empty.\n");
break;
case 'i':
insertw(word);
printf("%s inserted.\n", word);
break;
case 'd':
if (deletew(word))
printf("%s deleted.\n", word);
else
printf("%s not found.\n", word);
break;
case 'l':
if (lookupw(word))
printf("%s is found.\n", word);
else
printf("%s is not found.\n", word);
break;
case 'q':
printf("program quits.\n");
exit(0);
default:
printf("command %c invalid.\n", cmd);
break;
}
}
return 0;
}
之后再进行编译gcc –c rdict.c
最后将所有目标代码链接起来
gcc –o rdict rdict.o rdict_clnt.o rdict_xdr.o rdict_cif.o
(7)编译和链接服务器程序
和客户端程序一样,也需要编写服务器端的程序。
远程过程的版本在文件rdict_srp.c中:
/* rdict_srp.c */
#include
#include
#include"rdict.h"
char dict[DICTSIZ][MAXWORD+1];
int nwords = 0;
int initw()
{
nwords = 0;
return 1;
}
int insertw(char *word)
{
strcpy(dict[nwords], word);
nwords++;
return nwords;
}
int deletew(char *word)
{
int i;
for (i = 0; i < nwords; i++)
{
if (strcmp(word, dict[i]) == 0)
{
nwords--;
strcpy(dict[i], dict[nwords]);
return 1;
}
}
return 0;
}
int lookupw(char *word)
{
int i;
for (i = 0; i < nwords; i++)
if (strcmp(word, dict[i]) == 0)
return 1;
return 0;
}
远程过程如下编译:
gcc –c rdict_srp.c
gcc –c rdict_svc.c
最后将所有目标程序链接起来
gcc –o rdictd rdict_svc.o rdict_xdr.o rdict_sif.o rdict_srp.o
(8)启动服务器和执行客户程序
最后就启动运行这个分布式的版本,也可以将客户端程序放到另外一台机器上面编译运行(我将这个程序的客户端与服务器端在同一机器与不同机器上面都试过, 都没有问题)。不过在rdict.c中需要写明服务器的IP地址。
./rdictd
./rdict