C#/.NET 网络文件系统 (NFS) 服务器






4.86/5 (10投票s)
C# 中 NFS 服务器的基本实现。
引言
本文档简要介绍了用 C# 1.0 实现的一个 NFS 服务器。这是大约 8 年前为了解决当时存在的问题并作为学习 C# 的途径而编写的。这意味着其中没有使用泛型集合,而且代码也比较初级,所以请不要过于苛责。它确实可以工作,并且在个人生产模式下使用过,尽管有时会进入一个错误状态,目前只能通过重启进程来修复。重要的是,它支持创建和使用 UNIX 符号链接。它不实现任何安全措施,仅限于 NFS 的第 2 版本,并且只通过 UDP 运行。因此,它应该被视为一个原型,但如果需要通过 NFS 访问 Windows 机器,它可能可以作为一种权宜之计。
进行研究和编写代码已经是很久以前的事了。因此,本文档的很多内容基于我的记忆和对源代码的快速浏览。
我撰写本文档并将代码开源的原因是,最近我重新审视了 NFS 服务器项目。它基本可用,而且似乎没有可用的 C# 开源 NFS 服务器。
背景
我当时正在开发一个跨平台(Windows 和多种 UNIX 系统)应用程序。主要的开发环境是 Windows,但所有代码都需要在 UNIX 上编译和测试。最简单的方法是使用托管在 Windows 机器上的 VMWare 中的 Linux。这样,虚拟 Linux 机就可以通过 SAMBA 访问 Windows 机器上的源代码,进而访问 SMB/CIFS 共享。不幸的是,UNIX 的构建环境会创建符号链接。SAMBA 客户端和 SMB 都不支持这些链接。这留下了三个选择。第一,将源代码托管在 Linux 上,并使用 SAMBA 服务器模式共享。这并不吸引我,因为 8 年前 Windows 机器上的资源不足以一直运行一个后台虚拟机。第二,可以使用第三方 NFS 服务器,但当时没有免费或低成本的选择。最后一个选择是自己编写一个,这也可以作为一个锻炼 C# 技能的扎实项目。
使用代码
该解决方案包含以下二进制文件
- nfs.exe
- mountV1.dll
- nfsV2.dll
- portmapperV1.dll
- RPCV2Lib.dll
NFS 服务器通过运行 nfs.exe 来启动。这需要所有其他 DLL 文件存在。它会启动提供 NFS 支持所需的三种守护进程。
- 端口映射器 (Portmapper)
- mountd
- nfsd
由于 NFS 构建在 SunRPC 之上,这是一个通用的服务,RPC 客户端最初会连接到它,以发现请求的 RPC 服务可用在哪种协议(TCP 和/或 UDP)上,以及如果可用,使用哪个端口号。
出于某种原因,实际的初始访问远程文件系统的请求是发送给一个独立的进程来处理的,这个进程除了卸载操作之外,还负责处理 NFS 系统中的所有其他请求。mountd 就是这个进程。
这是主要的 NFS 服务器进程。一旦挂载,它将处理所有与远程文件系统相关的请求,例如列出目录、打开和关闭文件、访问文件内容等。
使用的端口分别是:111、635 和 2049。端口映射器 (111) 和 nfsd (2049) 的端口是标准的分配号。为了遵循约定,mountd 的 635 端口应该是 645。由于此值是从端口映射器请求的,因此这不成问题。目前这些值已硬编码为源代码中的常量。
为了避免必须启动三个独立的进程,inetd nfs.exe 启动了所有三个。它们不是独立的进程,而是独立的线程。因此,停止 nfs.exe 也会停止端口映射器和 mountd 服务以及 NFS 服务器。要将其提升为生产质量,这将成为一个服务或三个服务。
要从 Linux 使用,需要执行以下命令
mount -t nfs -o proto=udp -vers=2 <host>:<dir> <mountpoint>
例如:
mount -t nfs -o proto=udp -vers=2 192.168.0.1:C:/users/somebody/foo /foo
Windows 路径的大小写不敏感,但对于 UNIX 挂载点来说是敏感的。可以使用主机名代替 IP 地址。要在 UNIX 机器上卸载文件系统,请使用
umount <mountpoint>
例如:
umount /foo
服务器支持以下操作
- 列出文件和目录
- 创建和删除文件
- 创建和删除目录
- 重命名文件和目录
- 读写文件
- 创建、删除和使用符号链接
- 获取文件系统信息,例如,通过 df -k 获取可用磁盘空间
- 获取文件信息,例如,ls -l
注意:nfs.exe 在处理目录操作时似乎容易出现问题。另外,速度也不是很快。截图是在 Linux 上截取的,并保存到本地文件系统。然后通过 NFS 复制到 Windows。大小约为 280K,复制花费了几秒钟。
创建服务器的主要目的是允许从 UNIX 创建和操作符号链接到 Windows。这些是通过 NFS 服务器在 Windows 上创建 .sl 文件来实现的。这些文件包含指向实际文件的路径。当对符号链接执行操作时,NFS 服务器会将其应用于 '.sl' 文件中引用的文件,但删除操作会应用于符号链接本身。
文件和目录可以在双方创建、删除和操作,更改会反映在另一方。这包括手动创建 Windows 上的 '.sl' 文件。
快速看一下代码
该解决方案包含六个部分,其中一个部分是包含相关 RFC PDF 文件的解决方案文件夹。有四个 DLL 项目,最后是整合所有内容的 NFS 项目。
RPCV2Lib
三种守护进程都实现为 SunRPC 服务器。该库提供了通用的远程过程调用 (RPC) 基础设施,包括创建端点、监听传入请求、执行初步处理,以及最终分发到专用处理程序。处理请求是守护进程特定的,这就是为什么每个守护进程都继承自库中的 rpcd。
处理远程过程调用基本上就是反序列化传入数据、执行请求并返回需要序列化的结果。调用是以 SunRPC 请求的形式进行的,该请求本身使用 XDR 格式化;XDR 是一种中立的数据序列化格式。初步反序列化是通用的,由 rpcd 中的 CrackRPC 方法处理。一旦请求通过了有效性测试,就会调用 Proc 虚拟方法,该方法由 respective 守护进程实现以专门处理调用。
初始处理完成后,RPCV2Lib 仍用于特定处理程序需要反序列化剩余数据。初始处理仅用于验证和获取用于进一步分发的 RPC 编号。所有 RPC/XDR 反序列化代码都由该库提供。当调用 Proc 方法时,会传递一个对应于实际 RPC 的标识符,一个 rpcCracker 实例,用于执行反序列化。另一件传递的是一个 rpcPacker 实例。它用于序列化调用的响应。
portmapperV1
这很简单。它只实现了一个 RPC,该 RPC 返回 mountd 和 nfsd 的端口号。这些都是通过单独的请求获取的。调用者请求特定版本和协议的端口号。在本实现中,必须是 UDP 和 mount 协议版本 1,以及 NFS 版本 2。请求任何其他内容都会导致错误响应。
mountV1
它实现了 Mount 和 Unmount RPC。
nfsV2
这是大部分 RPC 实现的地方。如前所述,它处理除挂载和卸载之外的所有文件和目录操作。
nfs
这是一个直接的可执行文件。它依赖于 portmapperV1、mountV1 和 nfsV2 项目。它只需创建每个项目的实例,为每个项目创建一个线程,然后启动它们,每个线程都在自己的 UDP 端口上等待传入的 RPC。然后它会等待所有线程完成。除非出现未处理的异常,否则它们永远不会完成,因为它们都在无限循环中。通常通过控制台窗口中的 Ctrl+C 来终止它们!
句柄
到目前为止还没有提到 fileHandle.cs 文件的内容。它包含 FileTable 类。它主要由静态方法组成,尽管构造函数不是,因此它是一种单例;有点粗糙!撇开这一点不说,它将 mountd 和 nfsd 绑定在一起。nfs 创建了它的一个(唯一的)实例,结合静态方法使其对其他守护进程(特别是 mountd)可访问。
我不记得确切的细节了,但除了查看代码之外,该表还包含当前正在使用的任何文件和目录的条目。正在使用可能意味着已打开等,对于目录,则意味着当前是工作目录。一旦将文件或目录的条目添加到表中,就会创建一个文件句柄,这是一个唯一 ID(可用于映射到 UNIX inode),用于在所有后续 RPC 中标识文件或目录。路径仅在首次访问文件或目录时才需要。该类提供了根据路径名获取句柄的能力,反之亦然,具体取决于执行的操作类型,例如,“list”操作返回句柄列表,但如果客户端需要显示可读名称,它将请求与句柄关联的名称。
我认为条目永远不会从 FileTable 中移除,嗯,底层的 HandleTable(包含映射)除非实际文件或目录被删除。如果文件被关闭,则条目仍然存在,这可能是一个问题,因为表可能会不断增长直到耗尽所有内存。
查看 nfsd.cs 中实现的 RPC,奇怪的是没有 open 或 close 方法。相反,如果我没记错的话,Lookup RPC 用于发现文件或目录是否已被打开,如果没有,则打开它。此时会将一个条目添加到 FileTable。我认为对文件的每次操作都应该导致底层文件被打开、执行操作然后关闭,因此 NFS 规范中没有显式的 close 方法,因为所有操作都使文件保持关闭状态。这会导致明显的性能问题,因为同一个文件可能为连续操作被打开和关闭很多次。我认为任何优化都是服务器的实现细节,而不是 NFS 规范中描述的。
NFS(至少在版本 2 中)是一个无状态协议,因此服务器不需要维护任何文件的状态。这是客户端的责任。这也是唯一 ID 很重要的原因,因为这是客户端关联状态的对象。另外,NFS 具有 UNIX 背景,并且如前所述,每个 UNIX 文件都有一个唯一的 ID(来自 i-node 结构中的 i-number 成员),这使得实现 NFS 客户端更容易。
对于 C# 实现来说,唯一 ID 是一个问题,因为无法从 NTFS 获取正确格式的唯一 ID。相反,HandleTable 在首次访问时提供了从生成的 ID 到路径名的映射。这些是按会话生成的,这就是为什么添加到 FileTable 的文件或目录不会被删除(除非实体被删除或发生错误),因为它们必须在整个会话中保持不变。
如何构建
我首先阅读了相关的 RFC。
实际实现始于端口映射器,然后需要理解 RPC 并实现基本的 XDR 序列化和反序列化。一旦可以挂载文件系统,就添加了文件和目录列表功能,等等。实现 RFC 的一个简单方面是,您是根据规范进行编程。您仍然需要设计实现架构,但与某些项目不同的是,一些艰难的设计工作已经为您完成了。谢谢 IETF!
并非一切都一帆风顺,尤其是在 XDR 编码方面。很容易在那里犯错误,如果我没记错的话,尽管有规范,但一些 NFS 客户端的行为并不像它们应该的那样。这时 Ethereal 被证明非常有价值。这是一个免费的网络嗅探器,它不仅仅是转储 TCP/IP 级别的数据,它还理解包括 NFS 在内的各种更高级别的协议。这意味着我可以嗅探现有 NFS 服务器和客户端之间的通信,并将转储与我的服务器进行比较,从而找出问题所在。我认为没有它我无法走到这一步。
这还有必要吗?
NTFS 现在支持 Junction Points,这使得可以在 NTFS 上创建符号链接。因此,当通过 smbmount 将 NTFS 文件系统挂载到 Linux 时,可能可以实现此功能。快速调查显示并非如此,尽管可以创建硬链接。进一步调查后发现,这似乎是通过简单地复制挂载的 NTFS 上的原始文件来实现的。
摘要
基本上就是这样。一个基本可用的 NFS V2 over UDP 实现,没有安全性,有一些 bug,并且有很多重构的潜力。当从 UNIX 挂载 NTFS 时需要符号链接时,它可能提供短期解决方案。此外,它还可以作为任何人想要构建更完整和/或更现代的 NFS 服务器的参考。
所有源代码和二进制文件均以配套的 zip 文件提供。源代码是开源的,可在 GitHub 上找到:https://github.com/petebarber/NFS。