Alt-Sendme开发日志
Alt-Sendme前端使用typescript+tauri做成跨平台桌面应用。这是它的仓库链接:
如果你不关心具体的细节和原理,可以只看问题和解决部分
问题
项目的 Issue#9 请求了一个文件预览功能:在原来的版本中,接收者无从得知ticket指向的file blob包含什么文件内容。因此需要一个接收端预览功能。
Iroh
项目底层的数据传输依赖Rust Crate Iroh。它的大致工作原理是这样的:在现实世界中,计算机之间通过IP来寻址,但IP地址会随着你的网络环境变化而改变;而Iroh为每个节点(endpoint)创建一个公钥(公钥算法为Ed25519)作为这个节点的唯一身份ID(NodeID),无论网络IP地址如何变换,NodeID不变,就能够让其他节点与本节点连接。
P2P中的一大难题是NAT,所有设备都在路由器之后,没有公网IP,导致它们无法直接看到彼此。Iroh使用magicsock核心组件来解决这个问题。在创建节点时,Iroh默认开启打孔,它允许两个在NAT后的peer之间建立直接连接。如果打孔连接失败,会fallback到通过relay server建立连接和传输数据,但相比打孔这种方式有更高的延迟和更大的网络开销。
Iroh这样建立两个peer之间的打孔链接而不需要手动配置网络:
- 两个peers连接到一个具有共同可见性的Relay Server。在这里,Relay Server扮演一个两个Peer之间的相会点。Relay Server会根据他在Peer连接时收到的请求记录两个端点的反射地址(Reflective address),通过这个信息,远端的peer就能够让防火墙认为“这是预期的流量”而放行远端peer的数据。
- Relay Server中,两个peer会交换:
Public IP address公共IPPort numbers端口号Local addresses(如果两个端点在同一网域上)
- 接下来,每个Peer会向对方的reflective address发送UDP报文。由于两个端点都使用相同的4元组发送消息**(源IP、源端口、目标IP、目标端口),防火墙会认为这是预期的流量而放行这些消息。而在这个过程中,Relay Server只会在节点之间转发数据包**,而不会得知数据包中包含的是用于协调的消息还是真正需要传输的消息。
- A与B建立连接几乎是同时的。在A向B发送一个数据包的同时,A的NAT生成一个映射,防火墙生成一个临时规则允许来自B的响应;同时B向A发送数据包时也会进行同样的操作。到此为止,NAT映射规则已建立,防火墙规则也能够放行对方的流量,于是直连就建立了。
在进行文件传输时,Iroh也会对文件数据进行加工。借助Iroh-blob,Iroh能够增强数据传输的可靠性,尽管底层采用的是UDP。blob协议作用在一系列不透明的数据序列(或称为数据的分片),通常是一个文件的字节。该协议负责数据的储存和传送。
与docker镜像源Registry类似,iroh-blobs也是通过内容寻址的,数据通过它的加密哈希引用。数据的分片通过请求/响应来传输分片或一系列分片,使用哈希来校验数据的完整性:具体来说,分片在内部由它的BLAKE3哈希被引用,并使用哈希进行增量验证。
BLAKE3是一种树形哈希算法,将输入数据分成均匀大小的块,并将他们作为二叉树的叶节点。两个叶节点之间生成块哈希,块哈希组合成根哈希。Iroh使用32字节的根哈希作为blob的不可变标识符。blobs协议利用上述的树形哈希结构作为增量验证的基础。数据通过网络传输时,每块的完整性都会被发送者和接收者检查。在BLAKE3的论文6.4节“验证传输流”中所描述:
...由于BLAKE3是树形哈希,它支持序列哈希所不支持的新用途。其中之一就是传输流验证。假设有一个视频流应用从不可信源获取视频文件。应用知道它需要获取的文件哈希,而它获取到的数据的哈希需要与它期望的哈希匹配。
在序列哈希中,直到文件的所有内容都被传输完成(下载完成)后才能够进行哈希校验。但BLAKE3能够实现在传输过程中进行哈希校验。应用能够通过BLAKE3在数据到达时就对每个块进行独立的哈希校验。为了在不重新计算整个已接收部分的哈希的前提下对块进行校验,会从根节点开始沿着到达每个分块的二叉树路径逐步验证每一块数据的完整性。
假设我们将获取一个4块文件的所有块,那么步骤是这样的。首先获取第一块,为此需要先获取其父节点的哈希,则其父节点的哈希匹配根哈希的前32个字节。验证父节点后,第一块数据的哈希将匹配其父节点哈希的前32个字节。那么第二块自然对应其父节点的后32个哈希。同理地,我们可以通过这样的方式在传输时校验第三块、第四块的哈希。我们可以将这些待检验的哈希作为一个栈,就像遍历二叉树那样从栈中弹出下一块文件应当匹配的哈希值。
论文中也讨论到了“如果不知道文件长度”的校验情况,这时必须假定对方主机发送的长度数据是不可信的,在这里不做记录。
Iroh将所有数据块的哈希值缓存作为外部元数据,从而将未经修改的输入blob作为数据的标准来源。验证传输流也支持范围请求:通过仅流式传输验证指定子序列所需的BLAKE3二叉树部分,从而获取blob的可验证连续子序列。块哈希与根哈希有显著的区别,且它们只在数据传输过程中被应用。默认的BLAKE3块大小为1KB,将导致约6%的额外空间开销。增大块大小能够减小额外开销,但代价是在到达下一个增量验证检查点前需要传输的数据量增加。无论块大小如何设置,他们都不会影响根哈希。故能够实现在运行时的动态块大小设置。
更多blobs协议的内容可以前往crate文档页面查看。
如何构造一个最简单的文件传输工具?我们的目标是这样的:
在发送端,需要这样的应用来传输:
$ cargo run -- send ./file.txt
Indexing file.
File analyzed. Fetch this file by running:
cargo run -- receive blobabvojvy[...] file.txt
于是在接收端就可以这样接收文件:
$ cargo run -- receive blobabvojvy[...] file.txt
Starting download
Finished download
Copying to destination
Finished copying
Shutting down.
示例使用
创建端点
端点能够管理可能的网络交换,维护到最近的Relay Server的链接以及通过EndpointID寻找目标端点。
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 创建端点。端点允许来自iroh P2P的链接和创建到其他端点的链接
let endpoint = Endpoint::bind().await?;
// ...
Ok(())
}
这样我们就有了一个能够接受链接和创建链接的端点。
使用iroh-blobs协议
iroh-blobs协议能够从文件系统中加载文件,提供一个能够寻址和支持断点续传的协议:
#[tokio::main]
async fn main()-> anyhow::Reuslt<()>{
let endpoint = Endpoint::bind().await?;
// 在内存中创建iroh-blobs的缓存区域
let store = MemStore::new();
// 初始化能够通过iroh链接接受blob请求的结构体
let blobs = BlobsProtocol::new(&store, None);
// ...
Ok(())
}
解决
为了保证预览功能不会干扰到现存的功能,以及ticket在桌面端应用和命令行应用的通用性,决定不将任何信息编辑进ticket。相对于使用blobs协议,我们在alt-sendme中创建了一个全新的协议,ALPN是sendme/metadata/1。
在握手阶段,两个端点使用ALPN标识支持的协议进行协商,决定通信采用的协议内容,然后对应协议的请求将被route到对应的handler。新的metadata协议的工作方式是这样的:
在发送端:
在发送时生成FileMetadata结构体:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMetadata {
pub file_name: String,
pub size: u64,
#[serde(default, skip_seralizing_if = "Option::is_none")]
pub thumbnail: Option<String>,
#[serde(default, skip_seralizing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
metadata handler的行为是这样的:
- 等待1字节作为开始信号
- 返回
[4-bytes length prefix] [JSON payload]格式的响应 - 限制metadata的大小
在接收端:
-
解析ticket,获取发送者的address:
let ticket = BlobTicket::from_str(&ticket_str); let addr = ticket.addr().clone(); -
与发送者创建双端通信
open_bi() -
向发送者请求metadata
还存在的问题
在实际测试中,有时metadata协议不稳定,尤其是在跨度大的网络环境中,或存在限制的网络环境中(如企业NAT后的设备经常失效)。这很可能是由于依赖于双端链接的metadata传输在限制条件下稳定性欠佳,完全不如借助blobs协议进行的分布存储。如果能够利用blobs协议,将metadata以blob的形式传输,稳定性将进一步提升,且不会出现网络限制问题。
请见这份我正在进行的工作: