RPC(译)

来源:互联网 发布:层次数据 编辑:程序博客网 时间:2024/06/08 18:17

本文内容是关于Remote Procedure Calls的一篇overview。
原文见:https://www.cs.cf.ac.uk/Dave/C/node33.html
初次写博客译文,有些专业词汇可能翻译欠准确,若有不妥,望不吝赐教。

什么是RPC

RPC提供了一套强大的机制用来构建分布式的、基于客户端-服务器架构的应用。它对传统的本地过程调用进行扩展,这样被调用端不需要与调用段的过程存在于同一个地址空间。通过使用RPC,写分布式系统的程序员们可以避免网络接口的实现细节。RPC的传输独立性(transport independence)将应用程序与数据通信机制的物理和逻辑单元隔离开来,这样允许RPC使用各种各样的传输。

RPC 是如何运作的

一个RPC近似于一个函数调用,当一个调用一个RPC时,会先传递参数给远程的过程端(remote procedure),随后调用端等待其回应,下图显示了这个activity的flow。
client端发出一个过程调用,发出一个请求给server端随后阻塞自己进行等待,直到收到一个reply或者等待时间终止。
这里写图片描述
一个远程过程调用是一个独一无二的三元数组(程序号(program number),版本号(version number),过程号(procedure number))。每个程序可能有多个版本,每个版本可能有多个可以远程调用的过程。版本号使得我们可以同时允许使用不同版本的RPC协议。

RPC 应用程序开发

我们先考虑一个例子:
一个Client/Server在远程机器上的个人数据库中进行查找,假设我们我们不能通过个人的电脑来访问数据库(通过NFS,Network File System).
我们使用UNIX来运行一个远程的shell并执行命令。这种方法存在一些问题:

  • 命令可能很慢才执行
  • 你需要一个远程电脑的登陆帐号

使用RPC作为替换的方案就是说:

  • 在远程电脑上构造一个服务器可以对查询做出响应。
  • 通过调用一个query来查询信息,会比上面一种方案更快

那么怎么可以建立一个RPC应用呢,我们需要下面的步骤:

  • 明确客户端与服务器端通信的协议
  • 实现客户端的程序
  • 实现服务器端的程序

随后这两个程序会分别进行编译,通信协议通过生成stubs,这些stubs和rpc(以及别的库)也需要被链接进去。

协议的定义

最简单的定义和生成协议的方法是使用协议编译器,比如说rpcgen
你必须定义service procedures的名字,参数的类型以及返回类型。
协议编译器读取定义后会自动生成客户端和服务端的stubs。
rpcgen 使用它自己的语言,看上去很像是预处理器指令。
编译一个RPCL文件你只需要键入:

rpcgen rpcprog.x

这可能会自动生成4个文件:

  • rpcprog_clnt.c – 客户端stub
  • rpcprog_svc.c – 服务端stub
  • rpcprog_xdr.c – 如果必要XDR(external data representation)的过滤器
  • rpcprog.h – 任意一个XDR过滤器的头文件

实现client 和server端应用

现在来写客户端和服务器端的代码,它们必须通过协议中定义好的过程和数据类型进行通信。
服务器端必须首先注册一个过程,这个过程可能会被client端调用,随后服务器端会接受这个调用并返回需要的数据给客户端。
客户端通过远程调用,传输需要的数据给服务器端,并接收服务器端的返回值。
在应用层有不同的层级可以用来开发RPC的应用,我们首先简单讨论一下部分,随后再扩展到更为常用的部分。

编译和运行程序

我们先考虑整体的编译模型以运行一个RPC的应用,makefiles对于简化编译RPC应用来说是有用的,但是我们仍然需要先完整了解这个编译模型,随后再组装一个简易的makefile的版本。

假设客户端的程序叫做rpcprog.c,服务器端的程序叫做 rpcsvc.c,协议在rpcprog.x 中已经得到定义,rpcgen用来产生stub和filter文件:
rpcprog_clnt.c
rpcprog_svc.c
rpcprog_xdr.c
rpcprog.h
客户端和服务器端的程序必须包括头文件rpcprog.h

随后你需要:

  • 编译客户端代码
cc -c rpcprog.c
  • 编译客户端的stub(存根)
cc -c rpcprog_clnt.c
  • 编译XDR filter:
cc -c rpcprog_xdr.c
  • 构造client可执行文件:
cc -o rpcprog prcprog.o rpcprog_clnt.o rpcprog_xdr.c
  • 编译服务器端过程
cc -c prcsvc.c
  • 编译服务器端stub
cc -c rpcprog_svc.c
  • 构造服务器端可执行文件
cc -o rpcsvc rpcsvc.o rpcprog_svc.o rpcprog_xdr.c

接口例行程序的概览(Interface Rountines)

RPC提供了不同层次的接口,按照控制程度和复杂性逐渐增加。
最简单的接口就是远程调用,在远程机器上运行例行程序,仅仅用transport的类型来加以区分。事实上这类rountine是最为常用的。

简单层的例行程序

  • rpc_reg()
    在指定类型的所有transports上注册一个过程作为一个RPC程序
  • rpc_call()
    在定义的远程主机上远程调用定义的过程
  • rpc_broadcast()
    在所有的指定类型的transports上广播一条调用信息,标砖的接口例行程序被划分为顶层(top level)、中间层(intermediate level)、专家层(expert level)、底层(bottom level)。这些接口层给了开发者对通信的参数有了更多的控制,比如说使用的transport,在报告出错和重传前需要等待多少时间等。

Top Level Rountines

在顶层interface仍然是简单的,但是程序必须在远程调用前创建一个client的handle,以及在接收远程调用前创建一个server的handle。如果你想要你的应用在所有的transport上都能运行,那么使用这一层的rountine。

  • clnt_create()
    普通的客户端的创建,告知clnt_create() server在哪里,使用类型的transport
  • clnt_create_times()
    和clnt_create()类似,但是允许程序员定义在创建过程中最大的尝试次数。
  • svc_create()
    创建服务器端的handles,针对特定的type的所有transport,告诉svc_create()要使用哪一个dispatch函数。
  • clnt_call()
    客户端调用一个过程向服务器端发出请求。

Intermediate Level Rountines

中间层的接口允许你控制更多细节,实现起来更加负责,但是可能运行起来更高效。这一层允许你指定使用的transport。

  • clnt_tp_create()
    为指定的transport创建一个client 的handle。
  • clnt_tp_create_timed()
    允许用户指定最多允许的创建次数
  • clnt_call()
    客户端调用一个过程向服务器端发出请求

Expert Level Routines

提供一个更大的rountines集合,可以用它们来定义与transport相关的参数

  • clnt_tli_create()
    创建一个客户端的handle用于指定的transport
  • svc_tli_create()
    创建一个服务器端的handle用于指定的transport
  • rpcb_set()
    调用一个rpcbind 建立一个RPC服务与网络地址的map
  • rpcb_unset()
    删除由rpcb_set()建立的映射
  • rpcb_getaddr()
    调用rpcbind来获得指定RPC服务的transport地址
  • svc_reg()
    将相关的程序号和版本号的pair与定义的dispatch rountine连接起来
  • svc_unreg()
    删除由svc_reg()建立的连接
  • clnt_call()
    客户端调用一个过程来向服务器端发出请求.

Bottom Level Routines

bottom level 包含对transport的完全的控制。

  • clnt_dg_create()
    使用连接无关的transport,创建一个RPC client的handle
  • svc_dg_create()
    使用连接无关的transport,创建一个RPC server的handle
  • clnt_vc_create()
    面向连接的transport,创建一个RPC client 的handle
  • svc_vc_create()
    面向连接的transport,创建一个RPC server的handle
  • clnt_call()
    客户端调用一个procedure来向服务器发出请求

Programmers RPC interface

如何使用RPC来写一个网络应用程序。

简单的接口

一个简单的接口很容易使用因为不需要调用别的RPC rountines。它同时也限制了对潜在的通信机制的控制。程序这一层的研发可以很快,它直接由rpcgen支持。对于大多数的应用程序,rpcgen和它的应用是很高效的。一些RPC服务不能作为C的函数,但是可以作为RPC的程序。简化的接口库例程为我们提供了高效的RPC 设施。

下面这个程序展示了例程rusers, 它在库librpcsvc中,包括了rusers.c文件,这个程序显示有多少个用户当前正在远程host上。

调用:

#include <rpc/rpc.h> #include <rpcsvc/rusers.h>#include <stdio.h>/** a program that calls the* rusers() service*/main(int argc,char **argv){int num;if (argc != 2) {   fprintf(stderr, "usage: %s hostname\n",   argv[0]);   exit(1);   }if ((num = rnusers(argv[1])) < 0) {   fprintf(stderr, "error: rusers\n");   exit(1);  }fprintf(stderr, "%d users on %s\n", num, argv[1] );exit(0);}

编译的方法

cc program.c -lrpcsvc -lnsl

客户端

客户端只有一个函数,也就是rpc_call(),它有9个参数:

int rpc_call (char *host /* Name of server host */,    u_long prognum /* Server program number */,    u_long versnum /* Server version number */,    xdrproc_t inproc /* XDR filter to encode arg */,    char *in /* Pointer to argument */,    xdr_proc_t outproc /* Filter to decode result */,    char *out /* Address to store result */,    char *nettype /* For transport selection */);

这个函数调用由prognum,versnum以及host端的procnum定义的过程,传递到远程的过程的参数通过in参数设定,它是一个指向参数的指针,inproc是XDR filter,对参数进行编码,out参数是一个地址,这里存放远程过程的结果,outproc是一个XDR filter,它对结果进行解码,存储到该地址。
客户端会对rpc_call()进行block直到收到服务器端的回应,如果服务器端接受了客户端的请求,它将返回一个RPC_SUCCESS,它的值为0。如果这个远程调用不成功,它就返回非零值。这个值可以转化为clnt_stat, 这些枚举值都在<rpc/rpc.h>中定义,通过clnt_sperrno()函数解读。这个函数返回一个指向RPC错误信息的指针。
在这个例子中,对/etc/netconfig中列举的所有“可见”的transports进行了尝试。调整尝试的次数要求使用低层的RPC库。我们可以通过把它们放在结构体内来处理多个参数和结果的情况。
使用简单interface的例子是这样的:

#include <stdio.h>#include <utmp.h> #include <rpc/rpc.h>#include <rpcsvc/rusers.h>/* a program that calls the RUSERSPROG* RPC program*/main(int argc, char **argv){   unsigned long nusers;   enum clnt_stat cs;   if (argc != 2) {     fprintf(stderr, "usage: rusers hostname\n");     exit(1);    }   if( cs = rpc_call(argv[1], RUSERSPROG,          RUSERSVERS, RUSERSPROC_NUM, xdr_void,          (char *)0, xdr_u_long, (char *)&nusers,          "visible") != RPC_SUCCESS ) {              clnt_perrno(cs);              exit(1);            }   fprintf(stderr, "%d users on %s\n", nusers, argv[1] );   exit(0);

由于数据类型在不同的机器上可能会有不同的表示,rpc_call()同时需要RPC参数的类型以及指针(对result也是一样)。对于RUSERSPROC_NUM而言,返回值是一个unsigned long,所以第一个返回参数是xdr_u_long(它是一个unsigned long),第二个返回参数是&nusers(它也指向一个unsigned long),由于RUSERSPROC_NUM没有参数,XDR 编码函数是xdr_void(),它的参数是NULL。

服务器端

在服务器端使用简单接口是非常简单直接的。它只是仅仅调用rcp_reg()来注册将要被调用的过程,随后调用svc_run(),它是RPC库远程过程的dispatcher(调度器)。
rpc_reg() 具有以下的函数原型:

rpc_reg(u_long prognum /* Server program number */,        u_long versnum /* Server version number */,        u_long procnum /* server procedure number */,        char *procname /* Name of remote function */,        xdrproc_t inproc /* Filter to encode arg */,        xdrproc_t outproc /* Filter to decode result */,        char *nettype /* For transport selection */);

svc_run()根据RPC调用的信息触发响应的服务器端服务。rpc_reg()中的调度器负责解析远程调用的参数,并将结果进行编码,这会使用过程在注册时绑定的XDRfilter。
一些关于服务程序的notes:

  • 大多数的RPC应用使用的命名规范是在函数名后添加一个_1,序列_n添加到过程名后表明是服务的第n个版本。

  • 参数和结果是传地址,这对于所有远程调用的函数而言都是成立的。如果你传递NULL作为函数的结果,表明什么回复都没有给客户端,也就是假设没有回复

  • 结果必须存储在静态数据区,因为当实际的过程结束时还会读取它的值。RPC库函数会读取结果值构造一个返回结果并将其传递给客户端。
  • 只允许一个参数,如果有多个参数,就要用一个结构体将它们wrapped起来,然后做为一个整体传递。
  • 过程注册针对transport的特定类型,如果类型参数是(char*)NULL, 那么过程注册针对NETPATH中定义的所有的tranports。

有时你也可以尝试实现比rpcgen更快和简洁的代码。rpcgen 处理的是一般的代码生成的情况,下面就是一个手动编写的routine。它注册了单个过程,随后进入svc_run() 来为请求服务。

#include <stdio.h> #include <rpc/rpc.h>#include <rpcsvc/rusers.h>void *rusers();main(){  if(rpc_reg(RUSERSPROG, RUSERSVERS,        RUSERSPROC_NUM, rusers,        xdr_void, xdr_u_long,        "visible") == -1) {           fprintf(stderr, "Couldn't Register\n");            exit(1);          }   svc_run(); /* Never returns */  fprintf(stderr, "Error: svc_run returned!\n");  exit(1);}

rpc_reg() 可以根据不同的程序、版本和过程的注册的需要调用多次。

传递任意的数据类型

通过远程过程传递和接受的数据类型可以是任意内置的类型或者是程序员自己定义的类型。RPC会处理任意的类型而不管机器的字节顺序湖综合结构体layout的习惯。做法就是总是将他们转化为标准的传递格式,称为(XDR,external data reprensentation),在传递它们之前会完成这一转化。这个过程也称为序列化,它对应的反向过程称为反序列化。rpc_call()和rpc_reg()的translator参数可以定义一个XDR原有的过程,比如说xdr_u_long(),也可以是程序员提供的一个例程,它拥有一个完整的参数结构。参数的处理必须仅仅包含两个参数:
一个指向结果的指针 和 一个指向XDR handle的指针。

可以使用下面的XDR 原有rountines:

xdr_int() xdr_netobj() xdr_u_long() xdr_enum()xdr_long() xdr_float() xdr_u_int() xdr_bool()xdr_short() xdr_double() xdr_u_short() xdr_wrapstring()xdr_char() xdr_quadruple() xdr_u_char() xdr_void()

非原型的xdr_string(),它可以带有超过两个参数,通过xdr_wrapstring()调用。
下面举一个程序员提供的rountine的例子,结构体:

struct simple {   int a;   short b;  } simple;

包含了过程调用的所有参数。xdr_simple() 对参数结构体进行翻译:

#include <rpc/rpc.h>#include "simple.h"bool_t xdr_simple(XDR *xdrsp, struct simple *simplep){   if (!xdr_int(xdrsp, &simplep->a))      return (FALSE);   if (!xdr_short(xdrsp, &simplep->b))      return (FALSE);   return (TRUE);}

一个等价的rountine可以通过rpcgen自动产生。
一个XDR例程如果成功将返回非0值,否则返回0.
对于更为复杂的数据结构,可以使用XDR 预制的routines:

xdr_array() xdr_bytes() xdr_reference()xdr_vector() xdr_union() xdr_pointer()xdr_string() xdr_opaque()

举个例子,为了传递一个变长的整数数组,将它们打包到一个结构体内,包含这个数组和它的长度:

struct varintarr {int *data;int arrlnth;} arr;

接下来使用xdr_array()进行转换:

bool_t xdr_varintarr(XDR *xdrsp, struct varintarr *arrp){    return(xdr_array(xdrsp, (caddr_t)&arrp->data,              (u_int *)&arrp->arrlnth, MAXLEN, sizeof(int), xdr_int));}

它的参数是XDR句柄,以及一个指向array的指针和一个指向array长度的指针,最大的array size,以及每一个array元素的大小,以及一个指针指向一个XDR例程来对每一个数组元素进行转换。
如果数组的大小一开始是知道的,使用xdr_vector() 来代替会更加高效:

int intarr[SIZE];bool_t xdr_intarr(XDR *xdrsp, int intarr[]){   return (xdr_vector(xdrsp, intarr, SIZE, sizeof(int), xdr_int));}

在序列化时XDR 批量转化为4字节,每个character将占据32bit,xdr_bytes() 对character进行打包,它有4个参数,如同上文提到的xdr_array().
以Null结束的字符串通过xdr_string()进行转化,它和xdr_bytes()一样没有长度参数。在序列化时它通过strlen()获得字符串长度,在反序列化时它又重新构造一个以null结尾的string。
xdr_finalexample()[脚注1]调用内置的函数xdr_string()和xdr_reference()。 它对指针进行转化以传递字符串,并且把刚才的结构体simple一起放入另一个结构体中。一个使用xdr_reference() 的例子如下:

struct finalexample {    char *string;    struct simple *simplep;   } finalexample;bool_t xdr_finalexample(XDR *xdrsp, struct finalexample *finalp){  if (!xdr_string(xdrsp, &finalp->string, MAXSTRLEN))        return (FALSE);   if (!xdr_reference( xdrsp, &finalp->simplep, sizeof(struct simple), xdr_simple))       return (FALSE);    return (TRUE);}

注意xdr_simple()这里可以用来替代xdr_reference()。

实现High Level的RPC应用

现在我们引入更多的函数来看看如何实现high level的RPC例程。我们首先用一个案例来入门。
我们将实现一些远程的文件夹读取的工具包。
我们首先考虑我们要如何写一个本地的directory reader。
假设程序是由两个文件构成:

  • lls.c : 主要程序,它会调用局部模块read_dir.c
/* * ls.c: local directory listing main - before RPC */#include <stdio.h>#include <strings.h>#include "rls.h"main (int argc, char **argv){        char    dir[DIR_SIZE];        /* call the local procedure */        strcpy(dir, argv[1]);   /* char dir[DIR_SIZE] is coming and going... */        read_dir(dir);        /* spew-out the results and bail out of here! */        printf("%s\n", dir);        exit(0);}
  • read_dir.c : 包含了局部例程read_dir()
/* note - RPC compliant procedure calls take one input and   return one output. Everything is passed by pointer.  Return   values should point to static data, as it might have to    survive some while. */#include <stdio.h>#include <sys/types.h>#include <sys/dir.h>     /* use <xpg2include/sys/dirent.h> (SunOS4.1) or        <sys/dirent.h> for X/Open Portability Guide, issue 2 conformance */#include "rls.h"read_dir(char    *dir)   /* char dir[DIR_SIZE] */{        DIR * dirp;        struct direct *d;              printf("beginning ");        /* open directory */        dirp = opendir(dir);        if (dirp == NULL)                return(NULL);        /* stuff filenames into dir buffer */        dir[0] = NULL;        while (d = readdir(dirp))                sprintf(dir, "%s%s\n", dir, d->d_name);        /* return the result */          printf("returning ");        closedir(dirp);        return((int)dir);  /* this is the only new line from Example 4-3 */} 
  • 头文件rls.h 仅仅包含如下(至少对目前来说是):
#define DIR_SIZE 8192

显然,我们目前仅仅需要在文件之间共享这个大小信息,随后我们会实现RPC的版本会需要增加更多内容。

这个局部运行的程序通过下面的命令就可以编译:

cc lls.c read_dir.c -o lls

现在我们需要修改这个程序使其能应用在网络上:
允许我们可以通过网络访问远程服务器上的文件夹。
我们需要下面的步骤:

  • 我们需要修改read_dir.c,使其能在服务器上运行。
    • 我们必须注册server以及运行在server上例程read_dir()
  • 客户端的lls.c必须远程调用这个例程。
  • 我们必须定义好客户端和服务器程序的通信协议 。

定义协议

我们可以使用简单的NULL结尾的字符串来传递和接收文件夹的名字和内容。更进一步,我们将爱那个这些参数嵌入到客户端和服务器端的代码里面。
因此,我们需要定义客户端和服务器端程序、版本和过程号。可以通过使用rpcgen自动完成或者依靠简化的接口层的预定义的宏。这里我们手动定义它们。
服务器端和客户端必须一开始就统一好将要使用的逻辑地址(物理地址是不重要的,因为对于应用开发者来说它们是被隐藏的)
程序号(Program number)通过标准的方式定义:

  • 0x00000000 - 0x1FFFFFFF: Defined by Sun
  • 0x20000000 - 0x3FFFFFFF: User Defined
  • 0x40000000 - 0x5FFFFFFF: Transient
  • 0x60000000 - 0xFFFFFFFF: Reserved

我们简单的选择一个用户定义的值(User Defined),版本号和过程号通过标准做法设定。
文件夹的缓冲区大小对于客户端和服务器端程序来说都是需要的,所以和local的版本一样,我们仍然那需要定义,定义为DIR_SIZE
所以我们现在的rls.h这个文件包含:

#define DIR_SIZE 8192#define DIRPROG ((u_long) 0x20000001)   /* server program (suite) number */#define DIRVERS ((u_long) 1)    /* program version number */#define READDIR ((u_long) 1)    /* procedure number for look-up */

共享数据

我们之前提到我们可以将数据简单那用strings来传递,我们需要定义一个XDR filter routine xdr_dir()用来传递数据。记不记得我们说过,编码和解码只有能使用一个参数,这也很简单,通过标准的xdr_string()我们很容易定义。
这个XDR 文件,rls_xrd.c 如下:

#include <rpc/rpc.h>#include "rls.h"bool_t xdr_dir(XDR *xdrs, char *objp){ return ( xdr_string(xdrs, &objp, DIR_SIZE) ); }

服务器端

我们可以使用原来的read_dir.c 文件,所有要做的事情就是注册一个过程,然后启动服务。
过程的注册依赖于registerrpc()函数。它的原型是:

registerrpc(u_long prognum /* Server program number */,        u_long versnum /* Server version number */,        u_long procnum /* server procedure number */,        char *procname /* Name of remote function */,        xdrproc_t inproc /* Filter to encode arg */,        xdrproc_t outproc /* Filter to decode result */);

参数和简单接口层的rpc_reg()函数近似。我们已经讨论过rls.h头文件和rls_xrd.c XDR文件进行参数定义。
svc_run()例程也已经讨论过了。
rls_svc.c整个文件如下:

#include <rpc/rpc.h>#include "rls.h"main(){        extern bool_t xdr_dir();        extern char * read_dir();        registerrpc(DIRPROG, DIRVERS, READDIR,                        read_dir, xdr_dir, xdr_dir);        svc_run();}

客户端

在客户端我们仅仅需要调用远程的过程,函数callrpc()起到这个作用。它的原型如下:

callrpc(char *host /* Name of server host */,    u_long prognum /* Server program number */,    u_long versnum /* Server version number */,    char *in /* Pointer to argument */,    xdrproc_t inproc /* XDR filter to encode arg */,    char *out /* Address to store result */    xdr_proc_t outproc /* Filter to decode result */);

我们调用了一个本地函数read_dir() ,它使用了callrpc()来调用远程过程(已经被注册为READDIR)。
整个rls.c文件如下:

/* * rls.c: remote directory listing client */#include <stdio.h>#include <strings.h>#include <rpc/rpc.h>#include "rls.h"main (argc, argv)int argc; char *argv[];{char    dir[DIR_SIZE];        /* call the remote procedure if registered */        strcpy(dir, argv[2]);        read_dir(argv[1], dir); /* read_dir(host, directory) */        /* spew-out the results and bail out of here! */        printf("%s\n", dir);        exit(0);}read_dir(host, dir)char   *dir, *host;{        extern bool_t xdr_dir();        enum clnt_stat clnt_stat;        clnt_stat = callrpc ( host, DIRPROG, DIRVERS, READDIR,                        xdr_dir, dir, xdr_dir, dir);        if (clnt_stat != 0) clnt_perrno (clnt_stat);}

脚注

  1. 翻译注:原文这里写的xdr_reference()结合上下文看应该写错了
1 0
原创粉丝点击