一、概述
构建一个应用程序时,应该在以下两者之间作出选择:
- 构建一个庞大的单一程序,完成全部工作。
- 把整个应用程序散布到彼此通信的多个进程中。
选择2,需做如下选择:
- 假设所有进程运行在同一台主机上。
- 假设某些进程会运行在其他主机上。
不同部分之间需要网络通信的应用程序大多数是使用显式网络编程方式编写的,也就是如UNPvl中讲述的那样直接调用套接字API或 XTI API 。使用套接字API时,客户调用socket、connect、read和write , 服务器则调用socket、bind、listen、 accept、read和write 。熟悉的大多数应用程序(Web浏览器、Web服务器、Telne客户、 Telnet服务器等程序)就是以这种方式编写的。
编写分布式应用程序的另一种方式是使用隐式网络编程远程过程调用(RPC)提供了这样的一个工具。使用早已熟悉的过程调用来编写应用程序,但是调用进程(客户)和含有被调用过程的进程(服务器)可在不同的主机上执行。
客户和服务器运行在不同的主机上而且过程调用中涉及网络I/O, 这样的事实对于程序员基本上是透明的。事实上衡量 一 个RPC 软件包的测度之一就是它能使作为底层支撑的网络I/O对程序员的透明度有多大。
二、函数clnt_create
此函数运行成功返回一个客户句柄。
#include<rpc/rpc.h>
CLIENT *clnt_create(const char *host,unsigned long program,unsigned long versnum,const char *protocol);
三、RPC例子
例:客户以一个长整数调用服务器过程,服务器返回该值的平方。
文件square.x(.x 结尾的文件称为RPC说明书文件,定义了服务器过程以及这些过程的参数和结果)内容:
struct square_in {
long arg1;
};
struct square_out {
long res1;
};
program SQUARE_PROG {
version SQUARE_VERS {
square_out SQUAREPROC(square_in) = 1;
} = 1;
} = 0x31230000;
使用随Sun RPC软件包提供的程序来编写这个说明书文件,程序是rpc_gen 。
#include "square.h"
int main(int argc, char **argv)
{
CLIENT *cl;
square_in in;
square_out *outp;
if (argc != 3)
err_quit("usage: client <hostname> <integer-value>");
cl = clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp");
in.arg1 = atol(argv[2]);
if ( (outp = squareproc_1(&in, cl)) == NULL)
err_quit("%s", clnt_sperror(cl, argv[1]));
printf("result: %ld\n", outp->res1);
exit(0);
}
square.x说明文件中,称远程为SQUAREPROC ,客户程序中称squareproc_1 。约定:.x 文件中的名字转换成小写字母形式,添上一个底划线后跟版本号。
服务器程序:只编写过程,服务器程序的main函数由rpc_gen 程序自动生成。
#include "square.h"
square_out * squareproc_1_svc(square_in *inp, struct svc_req *rqstp)
{
static square_out out;
out.res1 = inp->arg1 * inp->arg1;
return(&out);
}
此客户-服务器程序没有使用任何显示的网络编程方式。涉及的套接字以及网络I/O的所有细节都由RPC运行时系统来处理。这就是RPC目的:不需要显示的网络编程知识就允许编写分布式应用程序。
构建客户程序可执行文件的步骤:
rpcgen 的-C 表示在square.h头文件在生成ANSI C原型。rpcgen 还产生一个称为客户程序存根的源文件square_clnt.c 和一个名为square_xdr.c 的用来处理XDR数据转换文件。libunpipc.a 是函数库,-lnsl 选项指定存放网络支撑函数的系统函数库。
构建服务器程序可执行文件的步骤:
文件square_svc.c 中含有服务器程序main 函数,构建客户程序生成的含有XDR函数的square_xdr.o 文件在服务器程序的构建也需要。
下图显示构建客户-服务器程序所需的文件和步骤。阴影方框必须编写的文件。短划线指出来需要C伪指令#include square.h 的那些文件。
下图汇总了一次远程过程调用中通常发生的步骤。
-
服务器启动,向所在主机上的端口映射器注册自身。然后客户启动调用clent_create ,与服务器主机上的端口映射器联系,找到服务器的临时端口。clent_create 函数还建立一个与服务器的TCP连接。 -
客户调用一个称为客户程序存根的本地过程(过程名squareproc_1,含有客户程序存根(相要调用真正的服务器过程)的文件由rpcgen产生,名为square_clnt.c)。存根目的在于把有待传递给远程过程的参数打包,转换成某种标准格式,然后构造一个或多个网络消息(集结)。客户程序的各个例程和存根通常调用RPC运行函数库中的函数。 -
这些网络消息由客户程序存根发送给远程系统,需要一次陷入本地内核的系统调用(write或sendto)。 -
这些网络消息传送到远程系统。运用网络协议TCP或UDP。 -
一个服务器程序存根过程一直在远程系统上等待客户的请求。它从这些网络消息中解散出参数。 -
服务器程序存根执行一个本地过程调用以激活真正的服务器函数(squareoc_1_svc过程),传递给该函数的参数是它从来自客户的网络消息中解散出来的。 -
当服务器过程完成时,它向服务器程序存根返回其返回值。 -
服务器程序存根在必要时对返回值进行转换,然后把它们集结到一个或多个网络消息中,以 便发送回客户。 -
这些消息通过网络传送回客户。 -
客户程序存根从本地内核中读出这些网络消息(例如read或recvfrom)。 -
对返回值进行可能的转换后,客户程序存根最终返回客户函数。这一步看起来像是一个普通的过程返回客户。
四、多线程化
下列程序Sun RPC默认提供一个迭代服务器。
服务器程序:输出所在线程的线程ID,睡眠5秒钟,再输出自己的线程ID然后返回。
#include "square.h"
square_out * squareproc_1_svc(square_in *inp, struct svc_req *rqstp)
{
static square_out out;
printf("thread %ld started, arg = %ld\n",
pr_thread_id(NULL), inp->arg1);
sleep(5);
out.res1 = inp->arg1 * inp->arg1;
printf("thread %ld done\n", pr_thread_id(NULL));
return(&out);
}
客户端程序:
#include "square.h"
int main(int argc, char **argv)
{
CLIENT *cl;
square_in in;
square_out *outp;
if (argc != 3)
err_quit("usage: client <hostname> <integer-value>");
cl = clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp");
in.arg1 = atol(argv[2]);
if ( (outp = squareproc_1(&in, cl)) == NULL)
err_quit("%s", clnt_sperror(cl, argv[1]));
printf("result: %ld\n", outp->res1);
exit(0);
}
启动服务器,运行客户程序三次: 每个客户输出各自的结果时彼此间有5秒钟的等待发生。服务器输出: 发现各个客户请求是迭代地处理的:处理第一个客户请求后,接着处理第二个客户的请求处理完毕,最后处理第三个客户的请求直到处理完毕。单个线程处理所有的客户请求:默认情况下服务器并不多线程化。
Sun RPC提供了多线程化的服务器的,通过向rpcgen 指定一个-M 命令行选项启用。这使由rpcgen 产生的服务器代码变得线程安全。另一个选项-A 是让服务器根据新客户请求的需要自动创建线程。运行rpcgen时,同时使用这两个选项。 例:
square.x文件:版本号从1改为2,服务器过程的参数结构的声明都不变。
struct square_in {
long arg1;
};
struct square_out {
long res1;
};
program SQUARE_PROG {
version SQUARE_VERS {
square_out SQUAREPROC(square_in) = 1;
} = 2;
} = 0x31230000;
客户端程序:
#include "square.h"
int main(int argc, char **argv)
{
CLIENT *cl;
square_in in;
square_out out;
if (argc != 3)
err_quit("usage: client <hostname> <integer-value>");
cl = clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, "tcp");
in.arg1 = atol(argv[2]);
if (squareproc_2(&in, &out, cl) != RPC_SUCCESS)
err_quit("%s", clnt_sperror(cl, argv[1]));
printf("result: %ld\n", out.res1);
exit(0);
}
服务器程序:输出自己所在线程的ID,睡眠5秒钟,输出另一个消息,然后返回。
#include "square.h"
bool_t squareproc_2_svc(square_in *inp, square_out *outp, struct svc_req *rqstp)
{
printf("thread %ld started, arg = %ld\n",
pr_thread_id(NULL), inp->arg1);
sleep(5);
outp->res1 = inp->arg1 * inp->arg1;
printf("thread %ld done\n", pr_thread_id(NULL));
return(TRUE);
}
int square_prog_2_freeresult(SVCXPRT *transp, xdrproc_t xdr_result,
caddr_t result)
{
xdr_free(xdr_result, result);
return(1);
}
同时运行客户程序的三个副本:
三个结果是一个紧接一个地输出的,服务器使用三个线程,同时运行的:
五、服务器捆绑
RPC服务器总是先捆绑一个临时端口,再向端口映射器注册自己的临时端口。当一个客户启动时,必须首先跟服务器主机上的端口映射器联系,询问服务器的临时端口号,然后跟这个临时端口上的服务器联系。rpcinfo -p 查看端口映射器注册的所有的RPC程序。
六、inetd和RPC服务器
默认情况下,由rpcgen创建的服务器可由inetd超级服务器激活。
七、认证
默认情况下,RPC请求中没有标识客户的信息,服务器回答客户的请求时并不关心客户是谁,这称为空认证或AUTH_NONE 。下一个认证级称为Unix认证或AUTH_SYS 。客户必须告诉RPC运行时系统随每个请求携带其身份信息。
八、超时和重传
Sun RPC使用了两个超时值:
-
总超时: 一个客户等待其服务器的应答的总时间量。TCP和UDP都使用该值。 -
重试超时: 只用于UDP,是一个客户在等待其服务器的应答期间每次重传请求的间隔时间。
创建客户句柄后,调用clnt_control查询或设置影响该句柄的选项。
#include<rpc/rpc.h>
bool_t clnt_control(CLIENT *cl,unsigned int request,char *ptr);
square.x文件:
struct square_in {
long arg1;
};
struct square_out {
long res1;
};
program SQUARE_PROG {
version SQUARE_VERS {
square_out SQUAREPROC(square_in) = 1;
} = 1;
} = 0x31230000;
客户端程序:查询并输出两个RPC超时值。
#include "square.h"
int main(int argc, char **argv)
{
CLIENT *cl;
square_in in;
square_out *outp;
struct timeval tv;
if (argc != 4)
err_quit("usage: client <hostname> <integer-value> <protocol>");
cl = clnt_create(argv[1], SQUARE_PROG, SQUARE_VERS, argv[3]);
clnt_control(cl, CLGET_TIMEOUT, (char *) &tv);
printf("timeout = %ld sec, %ld usec\n", tv.tv_sec, tv.tv_usec);
if (clnt_control(cl, CLGET_RETRY_TIMEOUT, (char *) &tv) == TRUE)
printf("retry timeout = %ld sec, %ld usec\n", tv.tv_sec, tv.tv_usec);
in.arg1 = atol(argv[2]);
if ( (outp = squareproc_1(&in, cl)) == NULL)
err_quit("%s", clnt_sperror(cl, argv[1]));
printf("result: %ld\n", outp->res1);
exit(0);
}
服务器程序:
#include "square.h"
square_out * squareproc_1_svc(square_in *inp, struct svc_req *rqstp)
{
static square_out out;
printf("thread %ld started, arg = %ld\n",
pr_thread_id(NULL), inp->arg1);
sleep(10000);
out.res1 = inp->arg1 * inp->arg1;
printf("thread %ld done\n", pr_thread_id(NULL));
return(&out);
}
九、TCP连接管理
一个客户的TCP连接或者通过调用clnt_destroy 显示的终止,或者由客户进程的终止隐式地终止。
#icnlude<rpc/rpc.h>
void clnt_destroy(CLIENT *cl);
十、事务ID
超时和重传策略的另一部分是使用事务ID 即 XID 来标识客户话求和服务器应答的。当一个客户发出一个RPC调用时,RPC运行时系统给这个调用赋 一 个32位整数XID ,该值伴随RPC消息发送。服务器必须伴随其应答返回这个XID。RPC运行时系统重传一 个请求时XID并不改变。使用XID的目的有两个。
-
客户验证应答的XlD等于早先随请求发送的XlD, 否则的话客户忽略这个应答。如果使用的是TCP协议,那么客户收到XID不正确的应答的机会非常罕见,然而如果使用的是UDP协议,而且存在重传请求的可能,网络也易于丢失分组,那么接收到XID不正确的应答是绝对可能的。 -
服务器允许维护一个存放已发送应答的高速缓存,而用于确定一个请求是否为一个重复请求的条目之一是XID。
十一、服务器重复请求高速缓存
使RPC运行时系统维护一个重复请求高速缓存,服务器必须调用此函数,一旦启用这个高速缓存,没有办法关闭。
#include<rpc/rpc.h>
int svc_dg_enavlecache(SVCXPRT *xprt,unsigned long size);
启用高速缓存后,服务器便为它所发送的全部应答维护一个FIFO高速缓存。每个应答是由如下信息唯一标识的:
- 程序号;
- 版本号;
- 过程号;
- XID;
- 客户地址;
每当服务器中的RPC运行时系统接收到一个客户请求时首先会搜索重复请求高速缓存,看其中是否已有该请求的一个应答。若有,这个高速缓存的应答就返回给客户,而不再调用相应的服务器过程。
重复请求高速缓存的目的:当接收到每个服务器过程的多个重复请求时,避免多次调用该服务器过程,因为该过程也许表示等势的。在网络中接收到重复请求的可能原因是应答丢失或者客户重传请求超前于应答的接收。重复请求只适用于UDP这样的数据报协议,使用TCP协议时应用绝对看不到重复的请求,请求的重复问题由TCP处理的。
十二、客户或服务器的过早终止
1.服务器的过早终止
当服务器线程终止时,与客户的TCP连接并未关闭,仍然在服务器进程中保持打开。因此,服务器主机没有给客户发送FIN,客户仅仅超时。在客户请求已发送给服务器,并且服务器主机的TCP已确认该请求后,若服务器主机崩溃,会出现同样情况。
2.客户的过早终止
当一个RPC客户在其使用TCP的某个RPC过程调用仍在进展期间终止时,客户主机的TCP将向服务器主机的TCP发送一个FIN。服务器的RPC运行时系统能否检测到这个条件,从而可能向服务器过程发出通知。
在客户端发生的情况是我们预期的,但在服务器端没有发生任何特殊之事。服务器过程结束其6秒钟的睡眠后返回。tcpdump 查看发生的情况:
-
当客户终止时,客户主机的TCP向服务器的TCP发送一个FIN。服务器主机的TCP对它作了确认。TCP称这个过程为半关闭。 -
客户和服务器启动约6秒钟时,服务器发送其应答,该应答由服务器主机的TCP发送给看客户。客户主机的TCP响应以一个RST分节,因为客户进程已经终止。服务器下一次在这个连接上读或写时将认识到这个现状。
使用UDP的RPC客户和服务器永远不知道对方是否过早终止。当接收不到响应时,它们可能超时,不过无法分辨错误类型:进程过早终止、对方主机崩溃、网络不可达,等等。
使用TCP的客户和服务器检测出对方所存在问题的机会要大得多,因为对方进程的过早终止自动导致对方主机的TCP关闭其所在端的连接。但是如果对方是一个线程化的服务器,这一点就不起作用,因为对方线程的终止并不会关闭其所在端的连接。另外这一点也无助于检测对方主机的崩溃,因为发生这种情况时,对方主机的TCP并没关闭它的打开着的连接。为处理所有这些情形,超时机制仍然是必需的。
十三、XDR:外部数据表示
Sun RPC使用XDR即外部数据表示标准来描述和编码数据。XDR既是一种描述数据的语言,又是一组用于编码数据的规则。XDR使用隐式类型指定公式,意味着发送者和接收者都得知道数据的类型和字节序:例如32位整数值后跟一个单精度浮点数值,再跟一个字符串。 所有数据类型的XDR表示都需要4的倍数的字节数,这些字节总是以大端字节序传送的。带符号整数值使用二进制补码记法存放,浮点数值则使用 IEEE 格 式 存 放 。可变长度字段总是在其末端含有最多 3个字节的填 充 ,这 样下 一 个条目总是落 在 某 个4字 节 的边界。例如 一 个5字 节 的A SC II字 符 串将 作 为 12个字 节 来传送 :
- 一个4字节的整数计数,其值为5;
- 5字 节 的字 符 串本 身 ;
- 3个字节的值为0的填充。
十四、例子:链表处理
使用XDR进行编码和解码含有可变数目元素的链表,下列使用名-值对链表。
opt.x 文件:
struct mylist {
string name<>;
long value;
mylist *next;
};
struct args {
mylist *list;
};
rpcgen 根据opt.x 生成的.h 文件:
struct mylist {
char *name;
long value;
struct mylist *next;
};
typedef struct mylist mylist;
struct args {
mylist *list;
};
typedef struct args args;
#include "opt2.h"
int main(int argc, char **argv)
{
int i;
XDR xhandle;
long *lptr;
args out;
char *buff;
mylist nameval[4];
size_t size;
out.list = &nameval[2];
nameval[2].name = "name1";
nameval[2].value = 0x1111;
nameval[2].next = &nameval[1];
nameval[1].name = "namee2";
nameval[1].value = 0x2222;
nameval[1].next = &nameval[0];
nameval[0].name = "nameee3";
nameval[0].value = 0x3333;
nameval[0].next = NULL;
buff = malloc(BUFFSIZE);
xdrmem_create(&xhandle, buff, BUFFSIZE, XDR_ENCODE);
if (xdr_args(&xhandle, &out) != TRUE)
err_quit("xdr_args error");
size = xdr_getpos(&xhandle);
lptr = (long *) buff;
for (i = 0; i < size; i += 4)
printf("%8lx\n", (long) ntohl(*lptr++));
exit(0);
}
十五、RPC分组格式
下图显示了封装在一个TCP分节中的一个RPC请求格式:
- 既然TCP是一个字节流,不提供消息边界,因此应用程序必须提供界定各个消息的某种方法。
- Sun RPC定义了既可作为请求也可作为应答的记录,每个记录由一个或多个片段构成。
- 每个片段以一个4字节值开头:其中最高位是晕终片段的标志,低序31位是计数。如果录终片段标志位为0,那么构成当前记录的还有别的片段。
如果所用的是UDP而不是TCP,那么紧跟在UDP首部之后的第一个字段是XID,如下图所示。 下图展示了RPC应答的各种可能:
下图展示了一个成功的RPC应答格式,不过这次封装在一个UDP数据报中。
|