前面几篇文章一直提到NFS客户端和服务器之间采用RPC进行通信,客户端向服务器发送RPC请求,服务器对客户端的身份进行验证,验证通过后处理RPC请求,最后将结果封装到应答消息中发送给客户端。这篇文章中就稍微讲讲RPC机制的工作原理。
1.RPC原理
RPC机制基于传统的函数调用原理。在编程过程中,我们会将一个大功能分割成若干个小功能,每个小功能用一个函数实现,通过函数调用将这些函数连接起来。一般而言,这些函数必须运行在同一台机器中。而RPC机制提供了一种方法,它可以将函数分散到不同的机器中,允许机器A中的main函数调用机器B中的func4。
这就很有意思了,一台机器上的函数竟然可以调用另一台机器上的函数。如果func4的功能是删除一个文件,那么我们不就可以在main函数中调用func4删除机器B中的文件了吗?事实上,NFS文件系统就是这么干的。那么func4是如何知道要删除哪个文件呢,对了,就是通过参数。main函数将文件路径作为参数传递给func4,func4删除文件后将结果作为返回值传回main函数,就是这么简单。
但是,我们不可能只删除文件,还可能创建新文件、读取文件数据、查看文件属性,我们有很多要求。没问题,我们可以定义一堆函数,这些函数构成了一个函数集合,每个请求都有相应的处理函数。机器A和机器B是两台不同的机器,可能运行不同的系统(如机器A运行Linux,机器B运行Windows),如果让Linus和Bill Gates就这些函数名称达成一致好像也不是一件容易的事情,而且他们也根本没有时间管这些小事。那好办,我们用编号表示函数,比如编号为0的函数负责删除文件,编号为1的函数负责创建文件,至于这些函数如何命名,就由操作系统决定了。这里还有一个问题,现实世界中有很多应用使用了RPC机制,如NFS、MOUNT、NLM服务,每个应用中的函数都从0开始编号,那么当机器A对机器B说:"请执行编号为1的函数",那么机器B执行哪个服务中编号为1的函数呢?为此,我们为各种服务分配了一个编号,比如NFS服务的编号是100003,MOUNT服务的编号是100005,NLM服务的编号是100021。最后一个问题,我们在编程时经常多函数进行修改,会形成不同的版本,RPC中的函数也会形成不同的版本。比如NFS已经开发了三个版本:NFSV2、NFSV3、NFSV4,这三个版本中都有删除文件的函数,但是实现过程多少有点不同,因此RPC调用中还需要指定程序版本编号。机器A应该这样对机器B说:"请执行编号为100003版本号为3例程号为1的函数",这样机器B就可以找到相应的函数了。对了,这里的编号、版本号和例程号不是我们随便定的,是IETF定的。
下面是NFSV2中所有例程的编号,摘自rfc1094。
program NFS_PROGRAM {
version NFS_VERSION {
void
NFSPROC_NULL(void) = 0; 这是一个空函数,测试NFS客户端和服务器是否连通
attrstat
NFSPROC_GETATTR(fhandle) = 1; 获取文件属性
attrstat
NFSPROC_SETATTR(sattrargs) = 2; 设置文件属性
void
NFSPROC_ROOT(void) = 3; 这个例程已经废弃了
diropres
NFSPROC_LOOKUP(diropargs) = 4; 查找指定名称的文件
readlinkres
NFSPROC_READLINK(fhandle) = 5; 读取符号链接文件链接到的实际文件的路径
readres
NFSPROC_READ(readargs) = 6; 读取文件内容
void
NFSPROC_WRITECACHE(void) = 7; 这个例程也已经废弃了
attrstat
NFSPROC_WRITE(writeargs) = 8; 向文件中写入数据
diropres
NFSPROC_CREATE(createargs) = 9; 创建一个新文件
stat
NFSPROC_REMOVE(diropargs) = 10; 删除一个文件
stat
NFSPROC_RENAME(renameargs) = 11; 修改文件名称
stat
NFSPROC_LINK(linkargs) = 12; 创建链接文件
stat
NFSPROC_SYMLINK(symlinkargs) = 13; 创建符号链接文件
diropres
NFSPROC_MKDIR(createargs) = 14; 创建一个新目录
stat
NFSPROC_RMDIR(diropargs) = 15; 删除一个目录
readdirres
NFSPROC_READDIR(readdirargs) = 16; 读取指定目录中包含的文件和子目录信息
statfsres
NFSPROC_STATFS(fhandle) = 17; 获取文件系统属性
} = 2;
} = 100003;
因此,如果你使用NFSV2挂载文件系统,那么所有的操作都是通过这几个函数实现的。
2.RPC报文格式
RPC是一种应用层协议,RPC协议可以封装在TCP或者UDP中进行传输,RPC报文由RPC报文头和净荷构成。
RPC报文头由8部分构成:
XID 这是一个RPC报文的编号,每次递增1。
Message Type 这是RPC报文的类型。RPC报文分为两种:请求消息用0表示,应答消息用1表示。上图是RPC请求报文的结构。
RPC Version 这是RPC协议本身的版本编号,目前通用的是版本2。
Program 这是RPC程序编号。比如请求的是NFS服务,则这个字段为100003。
Version 这是RPC程序版本。比如请求的是NFSV2,则这个字段为2。
Procedure 这是RPC例程编号。比如请求删除一个文件,则这个字段为10。
Credentials 这是包含了用户信息,供服务器进行验证。RPC支持多种认证方式,不同认证方式中这个字段的内容不同,后面的文章会讲解UNIX认证。
Verifier 这是认证信息的验证值,这个字段也跟认证方式有关。
在我们这个例子中,RPC报文的净荷由NFS数据构成,如果这一个删除文件的请求,则NFS数据中就包含了文件的路径。这些内容由NFS程序处理。
RPC应答报文格式如下:
XID 这是RPC报文的编号,取值与请求报文中的XID一致,客户端靠这个字段分配应答消息对应哪次RPC请求。
Message Type RPC消息类型,这里取值为1,表示应答消息。
Reply State 是一个应答标志。当服务器端接收到RPC请求后会对报文格式进行检查,如果格式正确,是一个有效的RPC请求,则将Reply State设置为0,表示格式正确。
Verifier 服务器端会解析RPC请求消息中的用户信息,对用户进行验证,根据结果填写Verifier字段,这个字段中包含了身份验证后的信息,这个字段和认证类型有关。
Accept State 这个字段表示服务器是否可以处理一个RPC请求,假如服务器不支持NFS服务,那么当接收到一个Program为100003的RPC消息后,就会拒绝这个请求。如果服务器中包含RPC消息中请求的处理程序,则接收这个请求。0表示接收请求,其他值表示不接收请求。
3.UNIX认证
RPC机制支持多种认证方式,根据认证方式不同,RPC请求消息的Credentials字段和Verifier字段的内容不同。这一节我们以UNIX认证为例来说说这两个字段的内容。
Credentials字段由下列内容构成
(1) Flavor 表示认证类型,UNIX认证的编号为1。
(2) Length 表示认证信息的长度,Length后面就是UNIX认证信息了。
(3) Stamp 这是一个时间戳,记录的是UNIX认证信息的组装时间。
(4) Machine Nmae 这是RPC客户端的主机名
(5) UID 这是RPC客户端中当前用户的UID
(6) GID 这是RPC客户端中当前用户的GID
(7) Auxiliary GIDS 这是RPC客户端中当前用户所属于的所有用户组的数量
(8) GID0 GID1 ... 这里就列出了用户所属用户组的ID
Verifier字段由下列内容构成
(1) Flavor 表示RPC应答消息中的认证类型,UNIX应答消息中的认证类型固定为NULL认证,其值为0。
(2)Length 表示RPC应答消息中认证信息的长度,由于NULL认证中不包含认证信息,因此这个值为0,表示没有认证信息。