第十六章 RPC一 实现

RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.
最初由 Sun 公司提出 。

参考

其不需要了解底层网络技术协议的原因在于:
我们只编写服务端需要被调用的函数声明,和定义,使用了一种IDL语言(还是别的哈?),没有main函数。
使用rpcgen工具自动生成服务器端的完整代码,带有main

爽一爽

我们先通过一个例子来演示RPC:

编写RPC说明文件

*.x

该文件相当于一个啥啊。对,相当于CMakeLists.txt文件,执行cmake会根据这个文件申城会生成一系列的文件。
*.x文件,执行rpcgen也会生成对应的各种文件。

这是一种IDL语言

double_v.x文件

/*参数结构体*/
struct double_in
{
    long key;
};
/*返回值结构体*/
struct dpuble_out
{
    long value;
    pid_t pid;
};

/*调用程序*/
program DOUBLE_PROG
{
    /*版本号*/
    version DOUBLE_VERS1
    {
        /*调用程序*/
        double_out double_func(double_in)=1;/*这是个程序标号*/
    } = 1;/*这个1就是版本号*/
    version DOUBLE_VERS2
    {
        void double_func(double_in) = 1;
        void double_func2(double_in) = 2;
        
    } = 2;
    version DOUBLE_VERS3
    {
        double_out double_func(string) = 1;
    } =12;
}=0x12345678;

你没看错!!!这个古老的东西不支持//注释
后面都有个标号=1或者=2,这些东西的存在是因为:
我们客户端通过网络传给程序只是一些数字啊,之类的。所以如何通过这些数字,字符串去调用一个函数?可以使用反射,而sun RPC使用的是switch。具体看下面。

执行

rpcgen double_v.x

生成4个文件:double_v_clnt.cdouble_v_svc.cdouble_v_xdr.cdouble_v.h

rpc默认函数默认只能有一个参数,多个参数使用结构体。但是可以使用参数改变:
-N

《第十六章 RPC一 实现》 rpcgen

《第十六章 RPC一 实现》 文件关系

double_v_xdr.c文件

而在分布式系统中,不同远程机器上可能有不同的字节顺序,不同大小的整数,以及不同的浮点表示。想与异构系统通信,我们就需要想出一个“标准”来对所有数据类型进行编码,并可以作为参数传递。隐式类型,是指只传递值,而不传递变量的名称或类型。常见的例子是 ONC RPC 的 XDR 和 DCE RPC 的 NDR。显式类型,指需要传递每个字段的类型以及值。常见的例子是 ISO 标准 ASN.1 (Abstract Syntax Notation)、JSON (JavaScript Object Notation)、Google Protocol Buffers、以及各种基于 XML 的数据表示格式。
XDR估计应该是一种古老的格式了。
而这个文件中存储的就是我们定义的两个结构体:

/*
 * Please do not edit this file.
 * It was generated using rpcgen.
 */

#include "double_v.h"

bool_t
xdr_double_in (XDR *xdrs, double_in *objp)
{
    register int32_t *buf;

     if (!xdr_long (xdrs, &objp->key))
         return FALSE;
    return TRUE;
}

bool_t
xdr_dpuble_out (XDR *xdrs, dpuble_out *objp)
{
    register int32_t *buf;

     if (!xdr_long (xdrs, &objp->value))
         return FALSE;
     if (!xdr_pid_t (xdrs, &objp->pid))
         return FALSE;
    return TRUE;
}

同时还提示我们不要去修改这个文件。

double_v_svc.c文件

称为服务器存根
包含服务器端的 main函数

没错,这个文件就是服务器端最主要的文件了。
当消息到达服务器时,服务器上的操作系统将它传递给服务器存根(server stub)。服务器存根是客户存根在服务器端的等价物,也是一段代码,用来将通过网络输入的请求转换为本地过程调用。
就是这个文件客户端传入的参数转化为实际的函数调用。
可以使用反射,但是RCP这种古老的东西使用的是switch

/*
 * Please do not edit this file.
 * It was generated using rpcgen.
 */

#include "double_v.h"
#include <stdio.h>
#include <stdlib.h>
#include <rpc/pmap_clnt.h>
#include <string.h>
#include <memory.h>
#include <sys/socket.h>
#include <netinet/in.h>

#ifndef SIG_PF
#define SIG_PF void(*)(int)
#endif

/*省略 double_prog_1 部分
*/

static void
double_prog_2(struct svc_req *rqstp, register SVCXPRT *transp)
{
    union {
        double_in double_func_2_arg;
        double_in double_func2_2_arg;
    } argument;
    char *result;
    xdrproc_t _xdr_argument, _xdr_result;
    char *(*local)(char *, struct svc_req *);

    switch (rqstp->rq_proc) {
    case NULLPROC:
        (void) svc_sendreply (transp, (xdrproc_t) xdr_void, (char *)NULL);
        return;

    case double_func:
        _xdr_argument = (xdrproc_t) xdr_double_in;
        _xdr_result = (xdrproc_t) xdr_void;
        local = (char *(*)(char *, struct svc_req *)) double_func_2_svc;
        break;

    case double_func2:
        _xdr_argument = (xdrproc_t) xdr_double_in;
        _xdr_result = (xdrproc_t) xdr_void;
        local = (char *(*)(char *, struct svc_req *)) double_func2_2_svc;
        break;

    default:
        svcerr_noproc (transp);
        return;
    }
    memset ((char *)&argument, 0, sizeof (argument));
    if (!svc_getargs (transp, (xdrproc_t) _xdr_argument, (caddr_t) &argument)) {
        svcerr_decode (transp);
        return;
    }
    result = (*local)((char *)&argument, rqstp);
    if (result != NULL && !svc_sendreply(transp, (xdrproc_t) _xdr_result, result)) {
        svcerr_systemerr (transp);
    }
    if (!svc_freeargs (transp, (xdrproc_t) _xdr_argument, (caddr_t) &argument)) {
        fprintf (stderr, "%s", "unable to free arguments");
        exit (1);
    }
    return;
}
/*省略 double_prog_12 部分
*/

int
main (int argc, char **argv)
{
    register SVCXPRT *transp;

    pmap_unset (DOUBLE_PROG, DOUBLE_VERS1);
    pmap_unset (DOUBLE_PROG, DOUBLE_VERS2);
    pmap_unset (DOUBLE_PROG, DOUBLE_VERS3);

    transp = svcudp_create(RPC_ANYSOCK);
    if (transp == NULL) {
        fprintf (stderr, "%s", "cannot create udp service.");
        exit(1);
    }
    if (!svc_register(transp, DOUBLE_PROG, DOUBLE_VERS1, double_prog_1, IPPROTO_UDP)) {
        fprintf (stderr, "%s", "unable to register (DOUBLE_PROG, DOUBLE_VERS1, udp).");
        exit(1);
    }
    if (!svc_register(transp, DOUBLE_PROG, DOUBLE_VERS2, double_prog_2, IPPROTO_UDP)) {
        fprintf (stderr, "%s", "unable to register (DOUBLE_PROG, DOUBLE_VERS2, udp).");
        exit(1);
    }
    if (!svc_register(transp, DOUBLE_PROG, DOUBLE_VERS3, double_prog_12, IPPROTO_UDP)) {
        fprintf (stderr, "%s", "unable to register (DOUBLE_PROG, DOUBLE_VERS3, udp).");
        exit(1);
    }

    transp = svctcp_create(RPC_ANYSOCK, 0, 0);
    if (transp == NULL) {
        fprintf (stderr, "%s", "cannot create tcp service.");
        exit(1);
    }
    if (!svc_register(transp, DOUBLE_PROG, DOUBLE_VERS1, double_prog_1, IPPROTO_TCP)) {
        fprintf (stderr, "%s", "unable to register (DOUBLE_PROG, DOUBLE_VERS1, tcp).");
        exit(1);
    }
    if (!svc_register(transp, DOUBLE_PROG, DOUBLE_VERS2, double_prog_2, IPPROTO_TCP)) {
        fprintf (stderr, "%s", "unable to register (DOUBLE_PROG, DOUBLE_VERS2, tcp).");
        exit(1);
    }
    if (!svc_register(transp, DOUBLE_PROG, DOUBLE_VERS3, double_prog_12, IPPROTO_TCP)) {
        fprintf (stderr, "%s", "unable to register (DOUBLE_PROG, DOUBLE_VERS3, tcp).");
        exit(1);
    }

    svc_run ();
    fprintf (stderr, "%s", "svc_run returned");
    exit (1);
    /* NOTREACHED */
}

该文件的代码有这些,很多但重复的也有很多。
其中很多的宏,比如下面这个

《第十六章 RPC一 实现》 double_func2

其对应的值就是我们在
double_v.x文件中,等于号后面的数值。

还有下面这个

《第十六章 RPC一 实现》 DOUBLE_PROG

所以,这就是等于号后面的那些标识的作用。
如果version DOUBLE_VERS1这一个,同名了,那么就会有多个define对应不同的值,是不正确的。

分析一下代码:

《第十六章 RPC一 实现》 选取要执行的函数

从上面的代码我们可以看出,是使用的
switch来选取要执行的函数,将函数赋值给了
local,同时将传入参数进行了转化。

《第十六章 RPC一 实现》 执行函数

上面的代码就是具体的执行了函数。

首先double_prog_2是服务器端选取函数并执行函数的函数。
他的命名是和double_v.xversion DOUBLE_VERS2的命名一致的。这个函数是生成的不需要我们去修改

《第十六章 RPC一 实现》 函数命名

而选出出来的函数:double_func2_2_svc是和version DOUBLE_VERS2中的程序名一直的。这个函数是我们需要编写的。使用rpcgen -Ss -o double_v_server.c double_v.x ,该函数就定义在double_v_server.c中,但是需要我们去手动编写。
后面讲。

然后我们看一下main函数:

《第十六章 RPC一 实现》 注册端口

这部分代码使用来注册端口的,根据
tcp还是
udp来选择执行的代码。

后面在执行流程中再细细的将。

《第十六章 RPC一 实现》 执行函数

这个函数应该就是要执行循环等待的函数了。
。。没找到这个函数的实现。

double_v_clnt.c文件

客户端凭证

该文件定义了我们需要调用的那些远程调用函数。并且封装参数,发送给客户端

/*
 * Please do not edit this file.
 * It was generated using rpcgen.
 */

#include <memory.h> /* for memset */
#include "double_v.h"

/* Default timeout can be changed using clnt_control() */
static struct timeval TIMEOUT = { 25, 0 };

/*省略了其他几个函数的定义,应为模样都差不多。
*/
void *
double_func2_2(double_in *argp, CLIENT *clnt)
{
    static char clnt_res;

    memset((char *)&clnt_res, 0, sizeof(clnt_res));
    if (clnt_call (clnt, double_func2,
        (xdrproc_t) xdr_double_in, (caddr_t) argp,
        (xdrproc_t) xdr_void, (caddr_t) &clnt_res,
        TIMEOUT) != RPC_SUCCESS) {
        return (NULL);
    }
    return ((void *)&clnt_res);
}

分析一下代码

《第十六章 RPC一 实现》 i封装信息

这个函数将我们的参数进行转换。最终都转换为下面这个:

《第十六章 RPC一 实现》 转换

然后clnt_call应该是将信息封装,发送给客户端,并阻塞,等待返回值吧。

但是这里:static char clnt_res;,该函数不是一个线程安全函数。
我们可以在rpcgen -M double_v.x的线程安全版本,这样子生成的客户端凭证代码为:

《第十六章 RPC一 实现》 线程安全版本

使用rpcgen -Sc -o double_v_client.c double_v.x可以生成一个简单的客户端的样例(没啥用的感觉),就是让你爽一爽。

double_v.h文件

头文件

就是个头文件,使用的时候引用他就行。
在这里面可以看到,我们定义的那些标号=1

XDR 数据格式

external data representation外部数据表示

存在的情况如下:

  1. C/C++下类型的字节大小不同
  2. 大端小端的差异

XDR

是一种用于描述数据类型的语言,同时也用语编码数据的规则。
XDR使用隐式类型指定,也就是发送者和接受者都知道数据的类型和字节序。
以大端传送。

一系列的类型的对应

《第十六章 RPC一 实现》 数据类型

    原文作者:Myth52125
    原文地址: https://www.jianshu.com/p/0a8ee12d85cc
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞