第十一章 RPC远程过程调用
在顾客服务员模型中,进程之间的相互作用是由一个进程先向另一个进程发送一个报文请求服务,然后等待回答;服务进程接收一个请求,然后发送回答。这样一种交互作用很象通常意义的过程调用。但是在计算机网络系统中,这种调用可能在不同的机器上执行,因此称为远程过程调用(remote procedure call)。远程过程调用的基础是XDR协议。
11.1 XDR标准
11.1.1 数据结构传输的问题
在异构的网络系统中,在顾客进程和服务器进程之间可能需要传递一些复杂的数据结构,这些数据结构可能用于控制进程的行为或者返回进程处理的结果。在数据结构传输过程中可能存在的问题有:
1.网络字节序问题
不同类型的计算机系统对于数据的存储格式可能不同,例如对于一个整数int,PC机存储时低位字节在前,而高位字节在后;而Sun工作站存储时是低位字节在后,而高位字节在前。这将导致它们对相同整数的2进制序列理解不同。
2.浮点数的传递
浮点数的传递比整数更加困难,通常浮点数使用若干比特表示整数部分,其它比特表示小数部分。不同类型的浮点数float和double,它们使用的比特数不同,这使得在网络中传递它们有一定的困难。
对于浮点数的处理,用户可以将浮点数前后的两个部分分别看成两个整数,分别进行传递,也可以将浮点数看成字符串的形式传递。
3.指针的处理
在数据结构传递中,指针的传递是最困难的,因为指针的含义是本机上存放某个数据的地址,这个地址在远端的主机上没有意义。所以用户必须传递的是指针的内容而不是指针本身。例如,对于一个字符串指针,用户需要将字符串的内容包含在数据内容中,同时还需要包含字符串的长度信息。
11.1.2 XDR标准
数据类型的传输可以多种多样,用户可以使用自己定义的规则,满足应用程序的数据结构传递。但是如果要使网络程序能够很好地同其它网络程序互通,则需要遵循一个公共的标准。在数据传递过程中实际使用的标准是Sun microsystem设计的XDR标准。
11.1.2.1 XDR标准中包含的数据类型
Sunmicrosystem设计的XDR标准规定了在网络中传输数据如何表示成公共的形式,它已经成为大多数顾客服务员应用中的事实上的标准。在XDR标准中定义了表11-1中的数据类型。
表11-1 XDR标准中的是数据类型
数据类型 |
长度( bits) |
含义 |
int |
32 |
32比特的2进制符号整数 |
unsigned int |
32 |
32比特的2进制无符号整数 |
bool |
32 |
布尔值,用1或0表示 |
enum |
任意 |
枚举类型,值被定义成常数 |
hyper |
64 |
64比特的2进制符号整数 |
unsigned hyper |
64 |
64比特的2进制无符号整数 |
float |
32 |
单精度浮点数 |
double |
32 |
双精度浮点数 |
opaque |
任意 |
不对这样的字节序列进行转化 |
string |
任意 |
ASCII字符串 |
fixed array |
任意 |
任何其它数据类型的定长数组 |
counted array |
任意 |
数组中的类型有一个固定上界,但各个数组的上限大小不同 |
structure |
任意 |
数据的聚合,类似C语言中的结构 |
discriminated union |
任意 |
类似C语言中的union,可以在几种形式中选择一种数据类型 |
void |
0 |
如果数据项可选,它又没有给出具体数据,则使用这种类型 |
symbolic constant |
任意 |
一个符号常量及相关值 |
optional data |
任意 |
允许一个数据出现0次或1次 |
XDR标准中的数据类型和C语言中的数据类型非常相似,XDR允许有结构数组,结构中可以有多个字段,每个字段成员可以是一个数组、结构或者联合,它完全能够适应复杂数据结构的传递。
11.1.2.2 XDR实现的原理
XDR对各种数据类型规定了编码的方式。用户可以使用函数xdrmem_create在内存中创建一个XDR流来存放用户将要发送的数据结构。在初始化后的XDR流包含一个流的头部,函数xdrmem_create的使用方法如下:
#include
externvoid xdrmem_create ((XDR *xdrs, const caddr_t addr, u_int size, enum xdr_opxop));
其中,变量xdrs是创建XDR流的指针,变量addr是内存中用于存放XDR流空间的起始地址,变量size是这个空间的长度,变量xop是说明对函数xdrmem_create调用的操作,其定义如下:
enumxdr_op {
XDR_ENCODE=0,
XDR_DECODE=1,
XDR_FREE=2
};
如果变量xop取XDR_ENCODE,则创建一个用于发送的XDR流;如果变量xop取XDR_DECODE,则创建一个用于接收的XDR流;如果变量xop取XDR_FREE,则释放这个XDR流。
随后假如用户调用相应的函数来填写一个整数0x00000004,XDR流的结果如图11-1所示。
XDR在编码中并没有提供关于数据类型的信息。例如,当XDR对一个32比特的整数进行编码时,编码的结果仍然是32比特,所以,只有数据的接收者知道这32比特的数据类型时,数据的接收者才能正确地恢复这项数据。因此,用户必须配对地编写发送和接收函数,分别处理每个数据项。
11.1.2.3 XDR的转换函数库
1.转换函数库
在XDR库函数中提供了对各种数据类型进行编码和解码的函数。这些函数具体进行编码还是解码,不是由这些函数决定的,而是由函数中XDR流的性质决定。当XDR流被创建为编码流,则这些函数将对数据进行编码处理;当XDR流被创建为解码流,则这些函数对数据进行解码处理。
表11-2列出了XDR库函数中进行类型转化的函数。用户在调用这些函数之前,必须包含头文件rpc/xdr.h。
表11-2 XDR中类型转换函数
函数调用 |
说明 |
extern bool_t xdr_void((void)); |
xdr_void用于处理没有数据的空选项 |
extern bool_t xdr_short((XDR *xdrs, short *sp)); |
xdr_short用于处理short类型数据。xdrs是XDR流指针,sp是指向存放short类型数据空间的指针 |
extern bool_t xdr_u_short((XDR *xdrs, u_short *usp)); |
xdr_u_short用于处理u_short类型数据。xdrs是XDR流指针,usp是指向存放u_short类型数据空间的指针 |
extern bool_t xdr_int((XDR *xdrs, int *ip)); |
xdr_int用于处理int类型数据。xdrs是XDR流指针,ip是指向存放int类型数据空间的指针 |
extern bool_t xdr_u_int((XDR *xdrs, int *up)); |
xdr_u_int用于处理u_int类型数据。xdrs是XDR流指针,up是指向存放u_int类型数据空间的指针 |
extern bool_t xdr_long((XDR *xdrs, long *lp)); |
xdr_long用于处理long类型数据。xdrs是XDR流指针,lp是指向存放long类型数据空间的指针 |
extern bool_t xdr_u_long((XDR *xdrs,u_long *ulp)); |
xdr_u_long用于处理u_long类型数据 |
extern bool_t xdr_hyper((XDR *xdrs,u_quad_t *llp)); |
xdr_hyper用于处理hyper64比特类型数据。llp是指向hyper类型的指针 |
extern bool_t xdr_u_hyper((XDR *xdrs,u_quad_t *ullp)); |
xdr_u_hyper用于处理u_hyper64比特类型数据。ullp是指向u_hyper类型的指针 |
extern bool_t xdr_longlong_t((XDR *xdrs, quad_t *llp)); |
类似函数xdr_hyper |
extern bool_t xdr_u_longlong_t((XDR *xdrs, u_quad_t *llp)); |
类似函数xdr_u_hyper |
extern bool_t xdr_int8_t((XDR *xdrs, int8_t *ip)); |
用于处理8比特符号整数类型数据 |
extern bool_t xdr_uint8_t((XDR *xdrs, uint8_t *up)); |
用于处理8比特无符号整数类型数据 |
extern bool_t xdr_int16_t((XDR *xdrs, int16_t *ip)); |
用于处理16比特符号整数类型数据 |
extern bool_t xdr_uint16_t((XDR *xdrs, uint16_t *up)); |
用于处理16比特无符号整数类型数据 |
extern bool_t xdr_int32_t((XDR *xdrs, int32_t *ip)); |
用于处理32比特符号整数类型数据 |
extern bool_t xdr_uint32_t((XDR *xdrs, uint32_t *up)); |
用于处理32比特无符号整数类型数据 |
extern bool_t xdr_int64_t((XDR *xdrs, int64_t *ip)); |
用于处理64比特符号整数类型数据 |
extern bool_t xdr_uint64_t((XDR *xdrs, uint64_t *up)); |
用于处理64比特无符号整数类型数据 |
extern bool_t xdr_bool((XDR *xdrs,bool_t *bp)); |
用于处理bool_t类型数据 |
extern bool_t xdr_enum((XDR *xdrs,enum_t *ep)); |
用于处理枚举类型数据 |
extern bool_t xdr_array((XDR *xdrs,caddr_t *addrp, u_int *sizep, u_int maxsize, u_int elsize, xdrproc_t elproc)); |
用于处理数组类型数据 |
extern bool_t xdr_bytes((XDR *xdrs, char **cpp, u_int *sizep, u_int maxsize)); |
用于处理字节类型数据 |
extern bool_t xdr_opaque((XDR *xdrs, caddr_t cp, u_int cnt)); |
用于处理opaque类型数据 |
extern bool_t xdr_string((XDR *xdrs, char **cpp, u_int maxsize)); |
用于处理字符串类型数据 |
extern bool_t xdr_union((XDR *xdrs, enum *dscmp, char *unp, const struct xdr_discrim *choices, xdrproc dfault)); |
用于处理联合类型数据 |
extern bool_t xdr_char((XDR *xdrs,char *cp)); |
用于处理字符类型数据 |
extern bool_t xdr_u_char((XDR *xdrs,u_char *cp)); |
用于处理u_char类型数据 |
extern bool_t xdr_vector((XDR *xdrs,char *basep, u_int nelem, u_int elemsize, xdrproc_t xdr_elem)); |
用于处理vector类型数据 |
extern bool_t xdr_float((XDR *xdrs,float *fp)); |
用于处理float类型数据 |
extern bool_t xdr_double((XDR *xdrs,double *dp)); |
用于处理double类型数据 |
extern bool_t xdr_reference((XDR *xdrs,caddr_t *xpp, u_int size, xdrproc_t roc)); |
用于处理reference类型数据 |
extern bool_t xdr_pointer((XDR *xdrs,char **objpp, u_int obj_size, xdrproc_t xdr_obj)); |
用于处理pointer类型数据 |
extern bool_t xdr_wrapstring((XDR *xdrs,char **cpp)); |
用于处理wrapstring类型数据 |
extern u_long xdr_sizeof((xdrproc_t, void *)); |
获得数据类型的长度 |
上表中的函数都需要一个已经创建好的XDR流作为操作对象,并且都返回一个bool_t类型说明操作是否成功。关于这些函数的具体使用方法可以参见函数的帮助手册。
2.XDR工作方式
实际上XDR库函数中提供了2种XDR流的支持,即工作在内存的XDR流和工作在I/O的XDR流。
调用函数xdrmem_create,它可以用于在内存中创建一个XDR流,用户可以将需要传递的数据结构在这个XDR编码流中编码,而后使用系统调用将编码发送到套接口缓冲区中,当接收方收到这些数据后也在内存中创建XDR解码流,并从套接口中将数据复制到这个XDR流中,接着使用对应的函数进行解码,得到原来的数据结构。
图11-2说明了使用内存XDR的工作过程。
函数xdrstdio_create用于创建工作在I/O上的XDR流,函数的使用形式如下:
#include
externvoid xdrstdio_create((XDR *xdrs, FILE *file, enum xdr_op xop));
该函数中,xdrs是指向创建的XDR流的指针,file是用于输入/输出的文件流,xop是操作选项,它的取值同xdrmem_create函数中的xop类似。
I/OXDR流可以将编码/解码的结果使用系统提供的标准输出函数输出到文件流中,或者通过标准输入函数库,从文件流中读取编码/解码的结果。
图11-3说明了I/O XDR流的工作过程。
实际上,对于套接口描述字,可以使用fdopen把它转化成对应的I/O流形式,函数fdopen的使用方法如下:
#include
FILE*fdopen(int fd);
调用函数fdopen之后,应用程序每次调用一个XDR的转化函数,则转化函数将使用文件流指针所包含的描述符,自动完成一个带缓冲的write/read操作,将数据发送到套接口缓冲区,或者从套接口缓冲区中读取数据,这样,可以不需要进行显示的write/read系统调用,如图11-4所示。
3.面向记录的XDR
上面的两种XDR都是以流方式工作的,由于XDR和TCP都是流的抽象,所以XDR可以同TCP很好的结合,但是UDP提供的是面向记录的数据抽象,因此为了使XDR同UDP能够很好的结合,XDR库中还提供了面向记录的XDR抽象。
总之,XDR标准中定义了同C语言中类似的数据类型,并提供了相应的转化函数,XDR流包含内存XDR流、I/O XDR流和具有记录定义能力的XDR。XDR的转化函数所做的操作决定于XDR流本身的性质,当XDR流是编码流时,转化函数进行编码的工作;如果XDR流是解码流时,转化函数进行解码操作。XDR标准是RPC远程过程调用的基础。
11.2 远程过程调用(RPC)的原理
11.2.1 分布式数据处理方法
在计算机网络中,数据可以分布式地存放,数据的存放地点和数据的处理地点可能不在同一主机上。一种方法是将数据从数据的存放主机发送到对数据进行处理的主机上,而后在数据处理主机上进行数据处理。这种方法在需要传输的数据量较小时性能较好,但是如果数据量很大,这种方式将消耗大量的网络资源。并且可能由于网络的不可靠性,间接地影响应用程序的不可靠性。
另一种方法是前台的进程仅仅做一些同顾客相关的界面的处理,还有一些诸如输入数据的有效性检测等事务,而将有效的数据输入转化成请求,并将请求发送到数据处理主机上,数据处理主机对数据进行处理后将数据的处理结果发送回来。两种方法的工作过程如图11-5所示。
RPC基于后一种方法。它希望达到的目的是将网络通信的功能和应用的需求分离开。由于通信协议的复杂性,因此它需要设计报文格式,指明进程收到每个报文后应当如何处理。协议需要严格的协议认证,以确保通信协议的可靠性。
11.2.2 RPC系统组成及特点
在顾客服务员模型中,进程之间相互作用是由一个进程先向另一个进程发送一个报文请求服务,然后等待回答;服务进程接收一个请求,经过处理后发送回答。这样一种交互作用很象通常意义的过程调用。但是在计算机网络系统中,这种调用可能在不同的机器上执行,所以叫远程过程调用(remote procedure call)。
在进行远程过程调用时,调用过程暂停执行,将参数经由网络送至被调用者执行此过程调用,执行结束后将结果返回给调用者,调用者恢复执行。整个过程就象发生在本地一样。远程过程调用原语是在报文传递原语基础上产生的。把可靠的阻塞原语SEND和RECEICE结合起来成为SEND-GET,用于顾客进程向服务员进程发送一个请求,之后等待服务员进程的回答。GET-REQUEST用于服务员得到一个报文,报文告诉服务员要做的工作。当服务员完成该工作时,用原语SEND-REPLY发回一个回答报文。远程过程调用可以看作是由这些原语结合起来完成的过程,具有对用户更方便的句法。
远程过程调用是基于进程相互作用的顾客服务员模型之上的同步通信的一种形式。顾客为了得到一个服务员(Server)的某种服务(Service)的结果(result),可以使用下面的原语:
CALL(Server:…;Service:…; var result:…; var status:…; time-out:…);
其中status是调用执行情况,可能是成功(OK),或未执行(not-done),或者不能执行(absent)等等;time-out是一个参数,指定允许顾客可以等待的最长时间;status、result和参量均用数值传递。
远程过程调用与报文传递方式相比有许多优点:语义清楚、简单、容易使用;对通信来说也非常简单,有较高的效率;有通用性,象在单机上计算时,过程就象是算法两个部分之间进行通信的最重要的机构一样。
远程过程调用的程序由五部分组成:用户程序、用户代理程序、RPC通信软件包、服务员代理程序和服务员程序,见图11-6。
通信程序包分成两部分,分别位于顾客机和服务员机。用户进行远程过程调用时,只需进行通常的本地调用,引起用户代理过程运行,后者将目标过程的说明和参数装入一个或几个调用包内,交给通信程序发送给被调用服务员。被调用服务员上的通信程序收到调用包后交给服务员代理程序,后者把参数取出后进行通常的本地调用。服务员完成这一调用,把结果返回给服务员代理程序进行打包,再交给通信程序发送给顾客机的通信程序。这是顾客机上的通信程序正等待接收这一结果包,然后把它交给用户代理程序拆包,把结果交给用户。在此期间,顾客进程暂停运行,等待返回结果。通信程序负责重发、确认、包的路径选择和数据加密等等。如果不考虑多机和通信失效,调用就好象用户直接调用服务员中的过程一样。事实上,如果服务员和顾客程序都在一个机器上,去掉代理程序仍能正常工作。
RPC实际上拓宽了“调用”。用户可以使用某些定义好的信息格式来保存被调用过程需要的参数,并在信息格式中说明应当如何去找到被调用者(通常是某种标志)。然后通过网络将信息报文发送到被调用者所在的机器,调用者等待被调用者发回调用结果。
在被调用者的那台机器上,应当有一个分派器,这个分派器知道它所控制的所有的远程过程。当它收到报文后,通过标志可以知道调用者希望调用哪个远程过程。然后,它从报文中取出被调用者需要的参数,并将参数传递给被调用者。
当被调用者被调用后,它在它的环境中运行,并将运行的结果写在信息格式中,最后通过网络将信息返回。
11.2.3 实现RPC要解决的问题
实现RPC要解决的一个问题是如何在报文中表示不同机型的参数和结果。不同型号的机器,其浮点数和负数的表示以及字节次序可能不同;各机器语言可能用不同的句法和不同的语义表示相似的数据结构;各机器语言可能支持不同的数据结构和过程接口。使用RPC进行通信时,必须在各不同机器之间进行转换工作。其转换工作量可能很大。对于有很多型号机器的系统,常使用一种标准格式,每种机型中的表示都转换成标准格式。但如果双方都使用同一种表示,这种转换就有些浪费。如果使用另一种方法,发送者使用自己的内部格式,让接收者转换成自己的内部表示,那么每种机器必须对其它种机器的每一种进行转换,当机种很多时工作量很大。而且当新机种加入时,很多现有的软件都必须加以补充。
再一个问题是RPC透明性带来的服务员定位问题。例如在具有多个文件服务员的系统中,如果一个用户在某个服务员上创建一个文件,则通常希望以后的操作(例如写操作)都对该服务员上的那个文件进行。但使用RPC时,该用户只在其程序上设一个过程调用:
write(FileDestriptor,BufferAddress, ByteCount);
RPC设备试图对用户隐去所有的关于服务员定位的细节。而在这种情况下,这些细节却是十分重要的。
还有关于广播通信问题。很多应用场合,例如确定某个进程或某种服务的位置,需要广播通信和组通信,即向所有的或若干个目的地而不只是一个目的地发送查询报文,并等待回答。RPC本身并不提供这种机制,语义也是完全不同的。因为RPC是一种同步通信,发送者必须等待每个接收者全部接收完毕。这不仅要等待很长时间,而且发送者并不一定知道共有多少个接收者接收并处理完毕的时间。
还有可靠性语义问题。在计算机网络系统中,RPC并不能总是得到正确的回答,因为通信双方(顾客和服务员)都可能失效,通信线路也可能失效。顾客在向服务员发出一个RPC后,如果由于服务员或信道的故障未得到响应,一定时间后重发RPC;当服务员工作正常而只是顾客未收到回答时,服务员可能重复执行RPC操作;如果顾客在发出RPC后出现故障,而服务员继续执行这一RPC,则成为“孤儿”,顾客恢复后又可能重发这一RPC。从而服务员再次执行这一RPC。
在实际应用场合中,RPC的重复执行有时无害,有时是不允许的。例如顾客要求服务员读取某个文件,即使服务员在故障前后执行两次也没什么关系,这叫作重复等效(idempotent)的操作。但是如果一个用户要求从某个银行帐户向另一个帐户转移一笔钱的操作,则要求准确地执行一次,不能执行多次或者不执行。工厂自动生产过程中某些开关的控制要求恰好执行一次RPC,否则会出现故障。所以,发生RPC的多次执行的系统并不总是符合实际应用的需求。因此必须区别不同情况的RPC。RPC可能执行:
恰好一次(Exactly-Once):每次调用时只精确地执行一次,具有和本地过程调用相同的语义;
至少一次(At-Least-Once):用户代理重复发出RPC一直到服务员至少执行了一次;
最多一次(At-Most-Once):通信双方及信道无故障时恰好执行一次并返回一次结果;如果发现服务员崩溃,用户代理将放弃并返回一个错误码,不进行重发。在此情况下,顾客知道操作可能执行0次或一次,不会再多。进一步的恢复由顾客负责。
多次中最后一次语义(Last-of-Many-Call)。这种语义要求给一个调用的每次请求一个顺序号。顾客只接受最近一次请求的返回值。
等效语义(Idempotent)。这种语义可以使用有状态服务员,也可以使用无状态服务员。在有多个顾客请求远程过程调用的情况下,这种语义保证不会对结果构成不利影响。对于有状态服务员,服务员所保持的状态不会被多个顾客的请求所破坏。
为了使RPC具有“恰好执行一次”或“最多执行一次”的语义。可在顾客的请求报文中设置顺序号,它应与返回结果报文中的顺序号一致。所有重发的报文包含相同的顺序号。服务员拒绝执行具有相同顺序号的请求,或以前的顺序号的请求。为了解决“孤儿”问题,服务员应能检测“孤儿”的产生,并在接受新的RPC之前退回到初始状态(不执行那个RPC操作)。
11.3 RPC的实现
11.3.1 本地函数调用的过程
本地的函数调用,通常是将函数的传入参数压进函数栈,然后将函数的返回地址压入函数栈中,最后在函数栈为函数的局部变量分配空间,并将程序计数器(PC)指向被调用的函数的入口地址(即调用函数在正文段中的位置),接着开始调用函数。函数调用的栈结构如图11-7所示。
在函数调用结束后将函数的局部变量弹出栈,并用函数的返回地址设置PC,而后程序将控制权返回到调用者。
如果从较高的抽象层来考虑函数调用,就可以发现调用的关键步骤:
1. 将被调用函数需要的参数准备好,并通过某种方式使被调用函数可以访问到。在本地函数调用中,它通过函数栈来实现。
2. 必须包含函数的返回信息,在被调用函数结束后,可以将控制权重新交给调用者。在本地函数调用中,这一点通过指明函数的返回地址来实现。
3. 能够确定被调用者的位置,也就是说,调用者需要通过某种方式说明被调用者的位置。在本地函数调用中,这一点通过将PC指针指向被调用函数的入口地址来实现。
4. 必须为被调用函数创建它可以运行的环境,例如函数的需要访问的数据和变量,以及函数的代码等是函数可以访问到的。在本地函数调用中,这一点是这样实现的:被调用函数的局部变量在函数栈中分配,而其它需要的数据可以共享进程中数据(如果具有权限)。
经过上面的抽象,可以发现只要满足这4点要求,“调用”就可以实现。而这4点要求其实与具体实现无关。本地过程调用只是上面4点原则的一个具体实现。如果实现了一种新的方式,则一种新的调用就产生了。
因此,“调用”的概念可以进一步拓宽,本地调用只是“调用”的一种具体实现形式。用户只要准备好被调用者需要的参数,然后找到它,为被调用者创建好运行的环境,然后运行它,最后通过已经说明了的返回方式将控制权移交,将返回参数正确传递回来,这就完成了一个“调用”。至于数据应当以什么方式存放,数据应当怎么传送给被调用者,被调用者应当如何识别等,这些都是具体的实现细节的问题。
11.3.2 远程过程的标识
RPC的实现也贯穿前面介绍的4个调用的原则。RPC的具体实现就是对4个原则的实现。远程过程的标识实际上就是引导如何能够找到远程过程。通常将一组相关的远程调用编写在一个程序中,然后通过一个分派器来管理它们。所以,标志一个远程调用过程需要(程序号、过程号),程序号是这组远程调用过程所在程序的名称,过程号是具体的过程在程序中的索引号。
由于程序可能会不断地升级,所以还可以在标识中加入一个过程的版本号,这样做有两点好处:
1. 当远程过程的版本升级时,应用程序仅仅需要改变调用的远程过程的版本号,就可以使用新的版本来工作。应用程序的改变非常小。
2. 有的应用程序可能需要使用旧版本的远程过程调用来工作,如果在过程标识中加入版本号,则多个版本的远程过程可以同时并存。这样,旧版本的应用程序不会因为远程调用过程的版本升级而失效。
这样,远程调用过程的完整标识如下:
(程序号,远程调用过程的版本号,远程过程的序号)
在ONC RPC的标准中指出:在某个机器上执行的每个远程程序都必须分配一个唯一的32比特整数,调用者通过这个整数来标识这个远程程序。所谓程序名,就是这里所说的32比特整数。
为了确保不同的组织所定义的程序号不会冲突,ONC RPC将程序号的集合分成8组,供不同的组织使用。Sun公司管理其中的第一组标识,它允许任何人申请一个标准的RPC程序号。Sun在它所管理的标志中分配了一些标准的程序号,如表11-3所示。
表11-3 标准的程序号
程序名称 |
程序号 |
程序用途 |
portmap |
100000 |
端口映射器 |
nfs |
100003 |
网络文件系统 |
rusersd |
100002 |
远程用户 |
Ypserv |
100004 |
NIS网络信息系统 |
mountd |
100005 |
Mount, showmount |
etherstatd |
100010 |
以太网统计信息 |
远程过程的具体描述如下:
programRMESGPROG { /*nameof remote program*/
version RMESGVERS { /*versionof program*/
int INIT(void) = 1; /*first procedure in theprogram*/
int ADDMESG(string)=2; /*second procedure*/
int DELMESG(string)= 3;
string LOOKMESG(int)= 4;
} =2; /*程序的版本号*/
} = 0x30091000; /*程序唯一的程序号*/
11.3.3 端口的动态映射
由于一台机器上可能同时运行多个远程程序,所以每个远程程序将使用不同的传输层端口。而调用远程程序的应用并不知道它希望调用的远程程序具体使用哪个端口。
由于给远程程序的程序号是32比特整数,而传输层使用的端口是16比特整数,因此不能将一个远程调用唯一地映射到一个传输层端口上。即使能够做到这样的映射,那也将浪费大量的传输层端口资源。
某个远程程序运行的端口不是直接提供的,它将由操作系统根据当时传输层端口的使用情况来动态分配。而真正使用固定端口的是端口映射器。而在远程程序提供服务之前,它将向端口映射器注册自己,使端口映射器知道它是谁,它将使用什么端口,具体工作情况如图11-8所示。
所有应用程序希望调用远程程序时,都先向端口映射器发出请求,询问具体的远程程序使用的端口号。端口映射器维持一张表,表中保存本机运行的远程程序和该远程程序使用的端口号。当它收到请求后,将查表来获取端口信息,并返回给发出请求的应用程序。然后应用程序将向具体的远程程序发起调用请求,具体工作情况如图11-9所示。
11.3.4 RPC报文
RPC使用XDR语言来定义应用的报文,这样可以为应用提供更大的灵活性。
1. RPC报文的类型
使用XDR定义的RPC报文类型如下:
enum msg_type { /* RPC 报文的类型*/
CALL = 0;
REPLY = 1;
};
RPC报文的类型只有两种:RPCCALL和 RPC REPLY;分别用于远程调用和远程调用结果的返回。
2.RPC报文的格式
RPC报文格式使用XDR语言定义如下:
structrpc_msg {
unsignedint mesgid; /*使用mesgid来匹配CALL和REPLY报文*/
unionswitch(msg_type mesgt) {
caseCALL:
call_body cbody;
caseREPLY:
rply_bodyrbody;
}body;
};
上面的语句声明了一个RPC报文格式,报文中的mesgid用于将调用和调用的返回进行匹配,因为一个应用可能会调用多个远程过程,那么需要一个机制将调用报文和调用结果报文对应起来。报文根据mesgt的值来区分不同的报文体。如果mesgt的值为CALL,也就是调用RPC报文,则RPC报文使用CALL报文体;如果mesgt的值为REPLY,也就是返回RPC报文,则使用REPLY报文体。
3.CALL报文体
根据前面的原则,CALL报文体中的内容包含远程程序的程序号、远程过程在程序中的过程号、版本信息、调用过程需要的参数。RPC CALL报文体定义如下:
structcall_body {
unsignedint rpcvers; /*RPC的版本*/
unsignedint rprog; /*远程程序的程序号*/
unsignedint rprogvers; /*远程程序的版本号*/
unsignedint rproc; /*远程过程的过程号*/
opaque_authcred; /*鉴别信息*/
opaque_authverf; /*鉴别的确证*/
/*ARGS*/ /*过程的参数*/
};
RPC报文为了网络安全而增加了鉴别信息,一个RPC CALL报文的具体形式如图11-10所示。
11.3.5 RPC开发工具
由于ONC RPC协议规程非常复杂,所以虽然可以直接使用报文实现远程过程调用,但是这样做需要耗费大量的时间和精力。因此,系统提供了专门用于开发的工具。这些工具主要包括:
1. XDR库函数,用于将各种数据结构从本机的表示转化成XDR的标准表示方式,不需要程序员来实现比较复杂的转化工作。
2. RPC运行时库函数(runtime library),这些库函数将具体实现RPC报文的形成和发送以及其它细节。
顾客端实现:在顾客端向端口映射器发送请求,并从端口映射器中接收回答;形成CALL报文,并向真正的远程程序发送调用请求,接收来自服务器的调用结果。服务器端实现:在服务器提供服务前向端口映射器注册自己的实际端口;将一个调用分派到具体的调用程序中的一个调用过程。
实际上它们将实现同RPC相关的大部分工作,程序员使用这些工具可以快速地进行RPC的应用开发。
3. 一些程序的自动生成工具,它产生一个构件RPC分布式程序所需要的许多C文件,这些程序主要是屏蔽底层通信对应用的影响。
11.3.6 RPC设计的原则
由于RPC调用实际上是实现前面说明的4个原则,所以在RPC设计中,也应当将这4个原则进一步具体化成设计的原则:
1. 由于RPC通过网络将被调用过程需要的各种参数进行传递,因此虽然RPC可以传递十分复杂的数据结构,例如链表等,但是这将消耗大量的系统和网络资源。所以应当将RPC调用的接口尽量设计成需要传递的参数较少,且传递的数据结构较简单。例如,在传递多个参数时,可以将它们定义在一个结构中,然后传递结构,这样既有利于程序的可读性,也有利于提高程序的效率。
2. RPC并没有指定使用的传输层端口,并且TCP和UDP都可以使用。当用户使用UDP协议来构建程序时,需要注意UDP协议的不可靠性,对于远程过程的一次调用,将可能导致RPC过程被调用的次数不确定。
在使用UDP协议通信时,一个RPC请求报文可能丢失,可能会丢失后重传,所以当一个应用进程收到RPC的回应时,RPC可能被调用了一次或者多次。而当一个应用进程没有收到回应报文时,RPC可能被调用了0次或者多次。由于回应的报文也可能丢失,所以在使用UDP协议实现RPC时,用户应当充分考虑这些问题。
3. ONC RPC规定:在给定的时刻,一个远程程序中最多可以有一个远程过程被调用,也就是说,在一个给定的程序中,RPC将保证过程调用是互斥的,这一点对于应用端是十分重要的。
4. 由于远程过程是在远程的机器上运行,所以一定要保证过程中的语句是适合它所在的运行环境。例如printf语句,由于printf语句希望输入来自于同用户相关的终端,而不是RPC过程所运行的环境;还有对本地系统I/O和文件的访问语句,也不应当出现在远程过程中。
总之,RPC的思想来源于本地过程调用,两者都遵守相同的调用原则,只是实现的手段不同。在构造RPC应用时,首先象编写普通的应用那样进行需求分析,这使得用户不会将注意力过早地放在复杂的通信协议上;而后将应用划分为两部分,分别放在顾客端和提供RPC服务的一端。进行划分时,需要尽量的遵循上面的原则,包括在两端传递的参数应当尽量简单,RPC端必须能满足调用所需要的环境等等。
用户可以使用系统提供的RPC开发工具加快开发的速度,而直接使用RPC报文的方式来进行应用开发,效率将会很低。
RPC同时支持TCP和UDP协议。对于使用UDP协议的RPC应用,由于使用UDP协议的RPC是不可靠的,因此必须使顾客端的程序能够满足“至多被调用了一次”的语义。
11.4 SUN RPC
第一个RPC软件包是1985年发表的,它是SUN OPEN NETWORK COMPUTING (ONC) RPC,一般称它为SUNRPC。它最初是在SUN OS上实现的,现在也在Solaris操作系统上实现了。SUN RPC支持最多一次的调用语义和等效调用语义。除此之外,它还支持广播RPC和无响应(no-response or batching)RPC。无响应RPC不需要返回值,并常用于修改记录。SUN RPC对参数数目进行了限制,它只允许两个参数,一个是输入参数,一个是输出参数。但是C语言支持结构数据类型,可以将多个参数集合到一个数据结构中作为一个参数传递给远程过程。SUNRPC在三个层次上支持认证,最高层次的认证称为安全RPC并使用DES加密技术。
我们以计算一个数的平方为例来说明怎样实现一个RPC应用程序。在这个例子中,顾客给出一个整数作为参数调用服务员上的过程,服务员上的过程计算该数的平方,并将结果返回给顾客。实现这个远程过程调用应用的整个过程包括如下步骤。
(1)编写一个RPC说明文件square.x。RPC说明文件名的后缀为.x,它说明服务员能执行哪些过程,使用哪些参数。square.x文件如下:
1 struct square_in { /* input parameter */ 2 long arg1; 3 }; 4 struct square_out { /* output parameter */ 5 long res1; 6 }; 7 program SQUARE_PROG { 8 version SQUARE_VERS { 9 square_out SQUAREPROC(square_in)=1; /* 过程号=1 */ 10 }=1; /* 版本号=1 */ 11 }=0x31230000; |
此文件包含如下内容:
定义参数和返回值。文件的1—6行定义了两个结构,一个是参数,另一个是返回值。
定义程序、过程和版本。7—11行用于定义程序、过程和版本,RPC程序名为SQUARE_PROG,这个程序有一个版本SQUARE_VERS,该版本中只有一个过程。这个过程名为SQUAREPROC,它的参数的类型为struct square_in,它的返回值的类型为struct square_out。过程号被赋值为1,版本号也被赋值为1。程序号为32位,这里用8位16进制数表示。
这个文件经过rpcgen编译程序编译后可以生成4个文件:square.h、square_clnt.c、square_xdr.c、square_svc.c。
(2)编写一个顾客程序,该程序调用远程过程。该例子的顾客程序源文件为client.c,如下所示:
1 #include “unpipc.h” /*our header*/ 2 #include “square.h” /*generated by rpcgen*/ 3 int 4 main(int argc, char **argv) 5 { 6 CLIENT *cl; 7 square_in in; 8 square_out *outp; 9 if(argc!=3) 10 err_quit(“usage: client 11 cl=clnt_creat(argv[1],SQUARE_PROG,SQUARE_VERS, “tcp”); 12 in.arg1=atoll(argv[2]); 13 if((outp=squareproc_1(&in,cl))==NULL) 14 err_quit(“%s”,clnt_sperror(cl,argv[1])); 15 printf(“result:%ld\n”,outp->res1); 16 exit(0); 17 } |
此文件包含如下内容:
包含有rpcgen产生的头部文件。在本例中这个头部文件为square.h,如程序中的第2行。
说明一个顾客句柄变量。如程序中第6行的变量cl。
获得一个顾客句柄。调用函数clnt_create,如果调用成功,该函数返回一个顾客句柄,如程序中的第11行。同标准的I/O文件句柄类似,用户不必关心顾客句柄指针所指定的具体内容是什么,它由RPC运行时系统所保持,它是某种结构类型的信息。函数clnt_create的第一个参数是运行服务员程序的机器的名字或IP地址;第二个参数是服务员程序的程序号码;第三个参数是该程序的版本号,这两个参数是RPC说明文件中所定义的(本例在square.x中定义);第四个参数是所选择的协议,通常是TCP或UDP协议。
调用远程过程和打印结果。这一部分由程序的12行到15行完成。第一个是一个指向输入结构的指针,第二个参数是这个顾客的句柄。返回值是一个指向结果结构的指针。输入结构的存储空间由用户程序分配,而结果结构的存储空间是有RPC运行时系统分配的。在square.x说明文件中,把要调用的过程命名为SQUAREPROC,但是在顾客程序中则调用过程squareproc_1,对应的规则是将square.x中的过程名由大写改为小写,再加上一个下划线,后跟一个版本号数字。
(3)编写一个服务员程序。服务员的主程序由rpcgen自动生成,只需要编写服务过程程序。如下所示是过程程序文件server.c:
1 #include “unpipc.h” 2 #include “square.h” 3 square_out * 4 squareproc_1_svc(square_in *inp,struct svc_req *rqstp) 5 { 6 static square_out out; 7 oui.res1=inp->arg1*inp->arg1; 8 return(&out); 9 } |
此文件包含的头部文件同顾客程序文件包含的头部文件一样,此外还包含如下内容:
过程名及参数。服务员过程名是在版本号后再加上_svc,过程第一个参数是指向输入结构的一个指针,第二个参数也是一个指向结构的指针,这个结构由RPC运行时系统传递,它包含调用请求的相关信息。如程序中的3行和4行。
执行和返回。取出输入参数并计算它的平方。结果存放在一个结构中,结构的地址是过程的返回值。由于是从一个函数里返回一个变量的地址,所以该变量要说明为static。如程序中的6到8行。
(4)第四步,编译。有如下编译任务:
使用rpcgen对RPC说明文件进行编译。
Solaris% rpcgen –C square.x |
通过编译生成4个文件:square.h、square_clnt.c、square_xdr.c、square_svc.c。
用cc编译生成顾客程序client。
Solaris% cc –o client client.c square_clnt.c square_xdr.c libunpipc.a -lnsl |
用cc编译生成服务员程序server。
Solaris% cc –o server server.c square_svc.c square_xdr.c libunpipc.a -lnsl |
(5)第五步,执行程序。在服务员机上执行server程序,在顾客机上执行client程序。
当顾客程序和服务员程序在两个不同的系统上实现的话,一些文件如本例中的square.h和square_xdr.c需要共享或拷贝到不同的机器上,同顾客和服务员程序一起在不同的机器上编译。
图11-11描述了上述例子的实现过程,图中阴影部分是由用户编写的。
图11-11 建立SUN RPC顾客/服务员的过程
想详细了解SUN RPC请参见[STEVENS,1999],该书对SUN RPC有非常详细的介绍。
11.5 加密技术
在计算机网络系统中的保护和安全有以下三个方面:
1. 数据加密:这个问题已经成为世界上许多国家计算机安全工作的重点,主要研究对数据加密的算法、算法实现的效率等问题。
2. 计算机网络的安全保密:计算机网络的目的是资源共享,同时也是分布计算系统、网格系统的基础平台,但网络容易产生不安全和失密问题。系统可以采用多级安全控制,包括使用可信赖的计算机系统,主机将保密的数据完全隔离。一个非常方便并且行之有效的方法是采用加密技术,实现用户到用户之间的加密,确保数据在网络传输过程中(特别是通信系统中)不丢失和被窜改。
3. 访问控制:象多用户单机系统那样,规定什么人可以访问系统的什么数据,得到何种服务,并对用户进行身份鉴别,对他们的访问权限加以限制(授权)。保护的对象可以是数据、程序、计算机性能、存储空间、存储介质或外部设备,这种保护称为对这些对象的访问控制。由于广泛使用网络和数据库系统,机器不仅要识别用户或者其进程是否合法,而且需要双方互相鉴别身份,有时甚至需要多方的鉴别。口令文件本身也需要保密。现代网络系统常采用多种方法进行用户身份鉴别,除了口令外还可以使用磁卡、指纹、签名、确认用户机器位置等。更严格的鉴别系统不仅在用户登录时进行鉴别,而且在整个运行期间随时限制访问。一般来说,使用硬件和固件做的鉴别系统不易受到攻击,而且不易出错。
可以把上述三个问题归并成两个:一个是加密技术,包括对数据及通信系统的加密;另一个是访问控制。加密技术不仅用于保护通信,也用于保护鉴别报文和访问控制。
为了传输敏感信息,如军事或金融数据,系统必须能够保证保密性。但是,微波、卫星以及各式电缆上传输的信息都很容易被截取。实际上,任何系统都不可能完全防止未经授权的用户对传输介质进行非法访问。
在实际系统中,比较实际的保护信息的方法是对信息加以改变,使得只有经过授权的用户才能够理解它,未经授权的用户即使得到它,也不能理解它。这种保护信息的方法称为对信息的加密和解密。加密意味着发送者将信息从最初的格式改变为另一种格式,将最终不可阅读的消息通过网络发送出去。解密是加密的相反过程,它将消息变换回原来的格式。图11-12显示了加密和解密的基本过程。
图11-12加密和解密的过程
加密和解密的方法分为两种类型:传统方法和公开密钥方法。
11.5.1传统加密方法
11.5.1.1单密钥系统加密模型
传统的单密钥加密系统的加密模型如图11-13所示。A处待加密的明文x,它被一个以密钥k为参数的加密函数E变换成密文y=E(k,x),通过网络传送到B处,在B处,密文y被一个以密钥k为参数的解密函数D变换成明文x=D(k,y)。
图11-13单密钥系统加密模型
加密函数必须与解密函数配对使用,以便恢复原文。非法用户想要弄懂密文,必须知道密钥和解密函数,只知道其中一个是无法将密文转换为明文的。对于接收者B来说,由于它确信只有发送者A才知道如何将消息按它们之间约定的方法加密,非法用户冒充A发送来的消息经过解密变换后得到的是毫无意义的信息。因此,凡经解密后有明确意义的消息肯定是由A发来的。B在收到A发来的消息后,也用保密方式给A发送一个应答,表示收到。这样,A和B之间就实现了保密通信。
通常密钥是由简单的字符串组成,它选择很多可能加密方式中的一种。只要有必要,可以经常改变密钥。加密和解密函数指出一种加密和解密的方法。为了使保密有效,似乎应该尽可能把所有的加密和解密算法的全部细节保密,但是,使用一段时间之后,难免会泄密。为了安全起见,加密和解密算法应该经常更换,可是,能够经得起攻击的过硬算法并不是很容易寻找和设计的,经常更换加密和解密算法是不现实的,因此才使用密钥。加密和解密算法可以长时间使用,但是密钥应该经常更换。
传统的加密方法有两种:替换法和位置变换法。
11.5.1.2替换法
替换法在很久以前就开始使用了。在开始的时候,加密是通过使用被称为单字母替换的办法来实现的:在这种加密方法中,每个字符都被另一个字符所代替。这种加密方法称为恺撒密码,因为它最初被恺撒使用。
更安全的字符替换方法是多字母替换。这里,我们仍然使用一个字符替换另一个字符,但是在这种方法中,同样的原文字符是用不同的密文字符替换的;替换不仅取决于原文的字符,也取决于字符在文中的位置。例如,消息GOOD MORNING中的三个O可以被三个不同的密码字符所代替。
11.5.1.3位置交换法
在这种方法中,字符将保持它们在原文中的格式,但是它们的位置将被改变来创建密文。这种类型的加密用下面的方法非常有效地加以实现,将文本组织成一个二维表格,然后根据一个密钥将列重新组织,这个密钥指出了用哪一列替换哪一列。
例如下列明文:pleasegive me the books, when you come up next.
使用一个不重复出现字母的短语MEGABUCK作为密钥,将明文各自母对准此密钥各字母排列,形成8列,同时给每列一个编号,编号按对应的密钥字母在字母表中出现的先后顺序给出。
M |
E |
G |
A |
B |
U |
C |
K |
7 |
4 |
5 |
1 |
2 |
8 |
3 |
6 |
p |
l |
e |
a |
s |
e |
|
g |
i |
v |
e |
|
m |
e |
|
t |
h |
e |
|
b |
o |
o |
k |
s |
, |
|
w |
h |
e |
n |
|
y |
o |
u |
|
c |
o |
m |
e |
|
u |
p |
|
n |
e |
x |
t |
. |
然后从小号列到大列,每列从上到下,把明文字母排列即得密文:
a bhcnsmoeoe k etlveupee w gtsy .pih,oueeonmx
11.5.1.4DES加密
上面提到的方法都是很少使用的。现在的传统加密方法都是基于比特而不是字符的。在比特级别的技术中,数据首先是划分为比特块,然后通过替换、位置变换、交换、异或、循环移位等方法进行改变。
比特级别加密的一个例子是数据加密标准(DES)。DES是由IBM公司制定的,被美国政府接受成为非军事和非保密的加密标准。算法使用64比特的原文和56比特的密钥,原文经过19个不同而复杂的过程来产生一个64比特的密文。
图11-14显示了DES的流程图。第一个和最后两个步骤相对简单,而步骤2到步骤17非常复杂,每一个步骤都由位置替换、替换、交换、异或和循环移位组合的多个子步骤组成。它的复杂性表现在两个方面:第一,尽管步骤2到步骤17是相同的,但是所使用的密钥是不同的,虽然它们是从同一个密钥继承来的。第二,前一个步骤的输出是后一个步骤的输入。
图11-14 DES加密过程
11.5.1.5密钥的分配
如何把解密密钥告诉接收者又不让他人知道,特别是在地理上分散的网络环境中,由人工传送密钥是很不方便的,例如,银行系统可能有成百上千个分支机构,并且可能经常更换密钥,由人工传送密钥很困难。这就提出了如何通过系统本身传送密钥的问题,问题在于这种传送必须是保密的。
现在我们就讨论这个问题。设两个用户要进行对话,对话之前要商定一个对话密钥,由于它们之间没有一个对话密钥,密钥的传送是不能用明文的形式传送的,所以需要一个双方都信任的第三者帮助他们完成密钥的分配,将这个第三者叫做网络安全中心(NSC)。现在假定用户A和用户B均已经和NSC建立了保密的通信信道,使用的密钥分别为ka和kb,A用ka和NSC进行保密通信,B用kb和NSC进行保密通信。A和B要进行保密通信,必须向NSC申请一个对话密钥Ks,假设A是对话的发起者,那么A先向NSC申请一个对话密钥Ks,NSC将Ks以密文的形式E(ka,ks)和E(kb,ks)分别传送给A和B。密码可能含有其它数据,例如,消息的序号与时间。A和B解密得到ks,对话结束后消除它,NSC也不保留ks的副本。
NSC向A和B传送密钥Ks的方法有两种,如图11-15(a)和图11-15(b)所示。
图11-15(a)的方法存在一个问题:如果A是对话的发起者,A要向B发送一个消息M,那么接收者B应该先从NSC那里获得密钥E(kb,ks),然后从A那里获得消息E(ks,M),这样B才能解密获得M。但是B有可能先得到加密的消息,后得到密钥,从而不能对消息解密。图11-15(b)的方法则比较方便:NSC先把E(ka,ks)和E(kb,ks)传送给A,再由A把E(kb,ks)传送给B,A和B都得到密钥ks后,A再向B发送消息。
图11-15 密钥的分配
11.5.2公开密钥加密方法
11.5.2.1公开密钥系统加密模型
在传统的加密方法中,密钥的分配需要NSC,并且它已经跟通信双方A和B分别建立了保密通信的条件下才可实现。那么分属于不同的组织机构的两个用户,它们之间没有一个NSC,这样的用户怎样进行保密通信呢?
在传统的加密系统中只使用一个密钥,它既是加密密钥,同时也是解密密钥,因此密钥必须保密。所以传统的加密系统又称为对称加密系统或单密钥加密系统。如果使用不保密的加密密钥,则不必用保密信道来传送加密密钥。这样的加密系统称为非对称加密系统或公开密钥加密系统。这种加密思想是Diffie和Hellman在1976年提出的,这一思想使人们对加密系统有了新的认识。主要思想是,加密算法E和解密算法D无法保持秘密,不如干脆公开,但是使用两个密钥:加密密钥Ke和解密密钥Kd。加密密钥是不保密的,谁都可以使用,所以叫做公开密钥;解密密钥是保密的,只有接收密文的一方才知道,所以叫做专用密钥或保密密钥。选择某种类型的算法E和算法D,使得局外人即使知道了加密密钥Ke,也推算不出来解密密钥Kd。图11-16描述了公开密钥系统的加密模型。
在图11-16中,A有两个密钥:加密密钥KeA,这个密钥是公开的,B如果要向A发送保密报文,可用此密钥对报文加密;A还有一个解密密钥KdA,此密钥是保密的,除A之外,其它任何人都不知道,A用此密钥解密B发送来的保密报文。如图11-16(b)所示。
同样,B也有两个密钥:加密密钥KeB,这个密钥是公开的,A如果要向B发送保密报文,可用此密钥对报文加密;B还有一个解密密钥KdB,此密钥是保密的,除B之外,其它任何人都不知道,B用此密钥解密A发送来的保密报文。如图11-16(a)所示。
采用这种方法,所有网络用户登录时应该公布其加密密钥和加密函数,让所有人知道。这种方法使得密钥的分配工作大大简化。其缺点是加密和解密算法都很复杂,相当耗费时间和主存空间。算法的选择很困难,从1976年到现在,实用的算法很少。
图11-16 公开密钥加密模型
11.5.2.2RSA加密
RSA加密技术是一种公开密钥加密技术,它的名字来自于最初的三个发明者(Rivest,Shamir, Adleman)。下面我们简要的介绍如何使用这种方法。
首先计算出一些参数:
1. 选择两个,p和q。
2. 计算n=p×q和z=(p-1) ×(q-1)。
3. 选择一个与z互质的数d。
4. 找出e,使得e×d=1 mod z。
确定公开密钥和保密密钥:公开密钥由(e,n)构成,保密密钥由(d,n)构成。
加密时可以把明文看成一个比特串,把明文划分成大小为k比特的块,每块可以看作是一个正整数m,其中0≤m 加密的过程非常简单:设X是要加密的明文信息,计算Y=Xe(mod n),则Y就是加密后得到的密文。 解密的过程也非常简单:设Y是要解密的密文信息,计算X=Yd(mod n),则X就是解密后得到的明文。 算法的安全性:此算法的安全性建立在难于对大数进行分解的基础上。如果破译者能够对公开的n作因子分解,那么就能够找出p和q,并从中得到z。如果知道了z和e,就能够用欧几里德算法很容易地求出d。可是,对于一个很大的数n,对它做因子分解是极其困难的问题,也就是说,很难从n求出p和q。 根据Rivest及其同事们的研究,如果使用最好的算法,在指令时间为1us的计算机上对200位的数分解因子需要40亿年的时间,对500位的数分解因子需要1025年。 我们用表10-3的例子来说明如何对明文“SUZANNE”进行加密。在此例子中我们只考虑英文大写字母的加密,我们可以对英文字母A~Z按顺序编码为1~26。在该例子中,我们选择p=3,q=11,得到n=33,z=(p-1)×(q-1)=2×10=20。由于7和20互为质数,故可以设d=7。对于所选的d=7,解方程7e=1(mod 20),可以得到e=3。 在我们的例子中,由于所选的p和q太小,破译当然很容易,我们的例子只是用来说明此算法的原理。同时,也由于所选的p和q太小,所以每个明文块所对应的数值要小于33(p×q=33),所以每个明文块只能包含一个字符。 加密 解密 明文(X) X3 密文(Y) Y7 Y7(mod 33) 符号 符号 数值 X3(mod 33) S 19 6859 28 13492928512 19 S U 21 9261 21 1801088541 21 U Z 26 17576 20 1280000000 26 Z A 1 1 1 1 1 A N 14 2744 5 78125 14 N N 14 2744 5 78125 14 N E 5 125 26 8031810176 5 E 11.5.2.3认证 认证意味着确认发送者的身份。换句话说,一种认证技术需要确认一条消息是来自一个确认的发送者而不是一个冒充者。认证常通过数字签名的方式来实现。 在日常生活中很多场合需要签名原本文件。例如,为了从你的银行账户中提取大笔现金,你首先到银行填写一张取款单,银行需要你签署这张单子,同时保留签署的单子作为纪录。假如你日后说你从来没有提取过这样金额的现金,银行就可以拿出你的签名,证明你确实提取过。在网络交易中,虽然你不能签署单子,但是,你可以在发送数据的同时创建一个等价的数字签名。 实现数字签名,要解决两个问题:第一,接收者能验证所要求的发送者的身份;第二,发送者在发送已经签名的报文后不能否认。 加密技术不仅能够用于保密,而且还能够用于鉴别身份,即能够实现数字签名。 11.5.2.4公开密钥加密技术实现数字签名 使用公开密钥加密技术实现数字签名要求加密函数E和解密函数D满足下列条件: E(D(P))=P, 当然同时还有D(E(P))=P 也就是E和D可以互换。 现在,假定A向B发送一个签名报文P。A的公开密钥为KeA,保密密钥为KdA。B的公开密钥为KeB,保密密钥为KdB。 图11-17使用公开密钥实现数字签名 11.5.2.5单密钥加密技术实现数字签名 使用单密钥系统也可以实现数字签名,参见图11-18。 为获得保密性,NSC有一个对任何人都保密的密钥X。由两个用户A和B,A和NSC之间用密钥KA进行保密通信,B和NSC之间用密钥KB进行保密通信。当A要发送一个报文P给B时,如果按照一下过程进行,B可以认定报文P是A向它发送的: 1. A向NSC发送加密报文,简记成KA(P)=E(KA,P)。 2. NSC解密KA(P)得到P,然后在报文P上加上发送者的名字A和日期D,产生一个新报文,用X加密这个新报文,得到的形式记为X(A+D+P),然后送回给A。这里注意,NSC可以证明P确实是从A发送来的,因为只有A和NSC知道KA。A把X(A+D+P)送给B。 3. B保留一个X(A+D+P)的副本,然后把X(A+D+P)送给NSC,NSC将它换成KB(A+D+P)再送回给B。 4. B把NSC送来的KB(A+D+P)解密,得到A、D、P。 如果A否认曾向B发送P,则因B处有X(A+D+P),这可由NSC解密得到A、D、P,而B不知道X,因此不会伪造。 11.5.2.6使用报文摘要的数字签名 由于加密整个报文很慢,下面的数字签名方案不要求加密整个报文。这种方案是基于单向散列(hash)函数的思想,该函数从一段很长的明文中计算出固定长度的比特串。这个散列函数通常被称为报文摘要(message digest),它具有三个重要的属性: 1. 给出报文P就很容易计算出其报文摘要MD(P)。 2. 只给出MD(P),几乎无法推导出P。 3. 无法生成这样的两条报文,它们具有同样的报文摘要。 要满足第三个条件,散列至少长128比特,或者更长。 从一段明文中计算出一段报文摘要要比用公开密钥算法加密整个明文要快得多。因此,报文摘要可以用来加速数字签名算法。 例如,A向B发送签名的报文P,A首先计算P的报文摘要MD(P),A用自己的保密密钥对MD(P)进行加密以达到签名的目的,而不用对整个报文加密,所以签名的速度大大提高了。最后,A将报文摘要的签名形式DA(MD(P))连同明文P一起发送给B。B用A的公开密钥解密DA(MD(P)),从而得到MD(P),如果非法用户改变了明文P,B计算MD(P)时就会发现这一点。由于解密的数据长度大大地缩短,所以认证的速度也大大提高了。
图11-17显示了使用公开密钥加密技术实现数字签名的过程。图中DA(P)=D(KdA,P), EB(P)=E(KeB,P)。A先对报文P进行签名,形成P的签名形式DA(P);然后对报文的签名形式进行加密,形成签名报文的加密形式EB(DA(P));B收到这个加过密的签名报文后,使用自己的保密密钥解密,得到报文的签名形式DA(P)=DB(EB(DA(P)));最后B对签名的报文进行验证并获得报文的明文形式P=EA(DA(P)),同时保留P的签名形式DA(P)。B知道这个签名报文确实是从A那儿发送来的,因为只能用A的公开密钥KeA才能解密成功。同时,A不能否认,因为B保留有P的签名形式DA(P)。
图11-18 使用单密钥加密技术实现数字签名