- 区块链
- 金融科技
【免费下载链接】diem
Diem’s mission is to build a trusted and innovative financial network that empowers people and businesses around the world.
DiemNet 是 Diem 生态中任意两个节点之间通信的主网络协议,而消息协议(Messaging Protocol v1)定义了连接建立后所有应用数据在网线上的字节形态与交互语义:NetworkMessage消息类型、ProtocolId应用协议标识、RPC 与 DirectSend 两种消息模式、优先级字段、错误码以及 4 字节长度前缀 + 8 MiB 上限的帧定界规则。本文以 messaging-v1.md 规格文档为骨架,对照仓库中 wire/messaging/v1/mod.rs 的真实实现与测试用例,逐层拆解这套协议的版本协商、消息编码、发送接收链路与边界约束,帮助读者既掌握协议本身的实战细节,也能在源码中准确找到对应落点。
DiemNet 与消息协议的整体定位
在 DiemNet README 中,DiemNet 被定义为主要承载 Diem 生态内任意两个节点间通信的网络协议,它只描述网线上消息的结构与顺序,实际投递依赖底层 TCP 传输,并且所有通信必须经过 Noise 协议 的加密与认证。DiemNet 支持在单条连接上多路复用多个应用协议,并为每个应用协议提供两种消息语义:
- DirectSend:单向、fire-and-forget(发出即忘)的消息投递;
- RPC:一元(unary)请求-响应调用。
这两者正是本文主角NetworkMessage枚举所承载的两类核心消息。一条 DiemNet 连接从建立到可收发消息需要依次经过:TCP 握手 → Noise IK 安全握手 → 版本握手(handshake-v1.md)→ 消息协议。也就是说,消息协议是连接建立与升级完成后、节点间交换共识、mempool、状态同步等业务数据的最终载体。
从仓库源码看,消息协议的完整落点在 network/src/protocols/wire/messaging/v1/mod.rs,其模块注释明确写道:该模块定义了 DiemNet v1 消息类型、序列化/反序列化方式,并为在一个抽象 IO 对象(通常是一个 socket)上发送NetworkMessage提供了Sink与Stream实现,且直接引用了本文所依据的这份规格文档。
版本化机制:MessagingProtocolVersion 与握手协商
消息协议通过MessagingProtocolVersion进行版本化,该版本在连接建立与升级过程中由 DiemNet 握手协议 协商确定。
/// Enum representing different versions of the Diem network protocol. These /// should be listed from old to new, old having the smallest value. We derive /// [`PartialOrd`] since nodes need to find highest intersecting protocol version. pub enum MessagingProtocolVersion { V1 = 0, }从源码注释可以确认两个关键设计点:
- 版本枚举从旧到新排列,旧版本值最小(
V1 = 0),并且派生PartialOrd,因为节点之间需要找出“双方共同支持的最高版本”。 - 协商逻辑位于
HandshakeMsg::perform_handshake(network/src/protocols/wire/handshake/v1/mod.rs#L229-L276):先校验双方chain_id与network_id一致,再对supported_protocols映射按版本从大到小迭代,寻找两个节点都能支持的最高MessagingProtocolVersion;找不到交集则返回HandshakeError::NoCommonProtocols。
握手消息本身(HandshakeMsg)是“从MessagingProtocolVersion到SupportedProtocols的映射”,其中SupportedProtocols是一个 bit-vector,第i位为 1 当且仅当该节点支持第i个ProtocolId变体。握手完成后,双方必须只使用接收方声明支持的ProtocolId,否则接收方可以返回ErrorCode::NotSupported错误消息(详见下文“错误码”一节)。
NetworkMessage:最原始的网线消息类型
规格文档开篇给出了消息协议的核心数据结构——NetworkMessage枚举,所有 DiemNet 端点都必须能够处理接收到的全部这些消息:
/// Most primitive message type set on the network. Note this can only support up to 127 message /// types. The first byte in any message is the message type itself starting from 0. enum NetworkMessage { Error(ErrorCode), RpcRequest(RpcRequest), RpcResponse(RpcResponse), DirectSendMsg(DirectSendMsg), }对应的真实实现位于 network/src/protocols/wire/messaging/v1/mod.rs#L38-L46,与规格文档完全一致,并派生Clone, Debug, PartialEq, Eq, Deserialize, Serialize以支持 BCS 序列化。
这里有两个值得注意的底层约束:
- 最多 127 种消息类型:
NetworkMessage枚举变体本身编码在消息的第一个字节中(从 0 开始编号),由于 Rust 枚举判别式在 BCS 中通常占用一个字节,理论上限被限制在 127 个变体以内。 - 线编码为 BCS:规格文档明确所有 DiemNet 消息在网线上使用 [bcs] 编码。仓库测试 test.rs 中的
network_message_canonical_serialization用 proptest 断言任意NetworkMessage的规范化编码-解码往返一致,保证编码的确定性。
消息在网线上的字节布局示例
测试文件 libranet_wire_test_vectors 给出了一个非常直观的线上字节示例:发送一个DirectSendMsg(protocol_id = MempoolDirectSend、priority = 0、raw_msg = "hello world"),实际产生的完整字节流为:
[0, 0, 0, 15] // 帧长度前缀(4 字节大端 u32)= 15 [3] // NetworkMessage 类型:DirectSendMsg 的判别值 [2] // protocol_id:MempoolDirectSend [0] // priority [11] // raw_msg 长度 [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] // "hello world" 的 ASCII 字节这个测试同时验证了NetworkMessageStream能正确反序列化这些字节、NetworkMessageSink能把这些消息序列化成完全相同的字节,是理解整个帧定界与编码规则的绝佳起点。
ProtocolId:应用协议标识符
每个应用协议都有一个唯一的ProtocolId标识。规格文档给出的枚举包含 8 个变体(ConsensusRpc、ConsensusDirectSend、MempoolDirectSend、StateSyncDirectSend、DiscoveryDirectSend、HealthCheckerRpc、IdentityDirectSend、OnchainDiscoveryRpc)。仓库当前版本的实际实现(network/src/protocols/wire/handshake/v1/mod.rs#L36-L45)略有演进:
#[repr(u8)] pub enum ProtocolId { ConsensusRpc = 0, ConsensusDirectSend = 1, MempoolDirectSend = 2, StateSyncDirectSend = 3, DiscoveryDirectSend = 4, HealthCheckerRpc = 5, // json provides flexibility for backwards compatible upgrade ConsensusDirectSendJSON = 6, }与规格文档相比,仓库实现将IdentityDirectSend与OnchainDiscoveryRpc移除/替换,新增了ConsensusDirectSendJSON = 6,并在注释中说明这是为了向后兼容升级而提供的 JSON 序列化变体。从ProtocolId::to_bytes/from_bytes(network/src/protocols/wire/handshake/v1/mod.rs#L73-L89)可以看到,除ConsensusDirectSendJSON使用serde_json外,其余协议统一使用 BCS 序列化。
由于#[repr(u8)],ProtocolId在 BCS 编码中恰好占用 1 字节,测试protocol_id_serialization(test.rs#L12-L17)专门断言了ProtocolId::ConsensusRpc编码为单个字节0x00。ProtocolId的值同时也被用于握手阶段SupportedProtocolsbit-vector 的位索引,因此协议 ID 的数值一旦分配就应保持稳定。
错误码:ParsingError 与 NotSupported
ErrorCode枚举定义了两类协议级错误:
enum ErrorCode { /// Failed to parse NetworkMessage, the entries are the first two bytes of the message: /// NetworkMessage type and possibly the ProtocolId ParsingError(u8, u8), /// A message was received for a message / protocol that is not supported over this connection: /// The NetworkMessage type is encoded as a u8. NotSupported(u8, ProtocolId), }仓库实现(mod.rs#L48-L79)对这两个变体做了结构化细化,语义保持一致:
ParsingError(ParsingErrorType { message: u8, protocol: u8 }):无法解析对方消息的头信息,携带尽可能多的头信息(NetworkMessage 类型字节与可能的 ProtocolId 字节)以便对方诊断;NotSupported(NotSupportedType::RpcRequest(ProtocolId) | NotSupportedType::DirectSendMsg(ProtocolId)):消息可以被解析出头信息,但当前连接上不支持该协议(例如收到了未在握手中向对方通告的ProtocolId)。
规格文档给出了一个典型场景:如果收到了针对某个未向对方通告的ProtocolId的RpcRequest,则发送错误消息ErrorCode::NotSupported(1, ProtocolId),其中1是RpcRequest在NetworkMessage枚举中的索引。测试error_code(test.rs#L19-L27)验证了ParsingError编码为[0, 9, 5](错误码类型 + message + protocol 两个字节)。
RPC 协议:请求-响应语义
RPC 协议的流程非常直接:
- 请求方向响应方发送
NetworkMessage::RpcRequest,携带一个request_id; - 响应方发送
NetworkMessage::RpcResponse,request_id原样复制自对应的请求。
struct RpcRequest { protocol_id: ProtocolId, // 应用协议标识符 request_id: RequestId, // RequestId = u32 priority: Priority, // 0..=255 raw_request: Vec<u8>, // 请求负载,由应用层 handler 解析 } struct RpcResponse { request_id: RequestId, // 与请求中的 request_id 完全一致 priority: Priority, raw_response: Vec<u8>, // 响应负载 }两个要点:
protocol_id只出现在请求中,响应对象不包含该字段——request_id是唯一将响应关联回请求的纽带;- 任何应用层处理错误都应包装在
RpcResponse消息内部,即“应用错误不占用协议级错误码”,ErrorCode仅用于传输/解析层的错误。
RpcRequest的 BCS 编码顺序由测试 rpc_request 完整展示:protocol_id(1 字节)→request_id(4 字节小端 u32,例如 25 编码为[25, 0, 0, 0])→priority(1 字节)→raw_request长度(4 字节)→raw_request字节。这个字节序与RequestId = u32的类型别名、Priority = u8的类型别名(mod.rs#L81-L85)一一对应。
DirectSend 协议:单向即发即忘
DirectSend 提供单向、fire-and-forget 式的消息投递:
struct DirectSendMsg { protocol_id: ProtocolId, // 应用协议标识符 priority: Priority, // 0..=255 raw_msg: Vec<u8>, // 消息负载 }发送方将消息负载装入NetworkMessage::DirectSendMsg发出,接收方根据protocol_id将负载交给对应的应用层 handler;协议本身不保证送达、不提供确认与重传。这也是共识(Consensus)、mempool、状态同步等模块中广播类业务所依赖的基础通道。
消息优先级:best-effort 的调度提示
RpcRequest、RpcResponse与DirectSendMsg都带有priority字段(类型Priority = u8,取值范围0..=255),其语义是:
- 优先级是一个best-effort信号,数值越高表示越紧急;
- 发送端与接收端都可以据此进行消息调度;
- 对于 RPC,接收方可以尊重请求的优先级,并为出站响应附加相同的优先级值;
- 待处理的入站/出站消息 MAY 根据优先级被重排或丢弃(
MAY是规格中的可选能力,不是强制要求)。
规格文档特别指出:虽然协议允许按优先级重排或丢弃消息,但DiemNet 参考实现目前并不执行priority("the DiemNet reference implementation does not currently respectpriority")。这一点在源码中同样可以看到——messaging/v1/mod.rs 中的NetworkMessageStream/NetworkMessageSink只是按序读取、序列化并发送消息帧,并没有针对priority做任何排队或抢占逻辑;优先级字段目前仅作为协议预留的、面向未来的调度扩展点。
错误处理与流控
关于协议级错误与背压,规格文档明确了三条规则:
- 不强制响应错误:收到错误消息的一方不要求必须回复;
- 最小触发长度:一条消息至少要有 2 字节长度才会触发错误响应,否则错误信息本身会因数据不足而失去意义(例如
ParsingError至少要能带上消息类型字节); - 无内置流控:DiemNet 不定义任何背压/流控(back-pressure / flow-control)机制或策略,每个端点可以自由实现本地策略来防御“话痨邻居”(chatty neighbors),例如通过不发放 TCP window update 来限制对方的发送速率。
最后一点在实际实现中有一个有趣的对偶:虽然协议层不做流控,但 NetworkMessageStream/Sink 在构造时会包一层AsyncRateLimiter(来自diem_rate_limiter),接收Option<SharedBucket>参数——也就是说速率限制(rate limiting)作为一个可选能力被注入到消息收发路径中,而不是协议本身的强制机制,这与规格“每个端-point 自行决定本地策略”的表述一致。
帧定界:u32 长度前缀 + BCS 消息
每条序列化后的 DiemNet 消息以4 字节大端(big-endian)u32长度前缀进行帧定界,然后这些消息帧被送入 Noise 加密 socket(Noise 层有自己内部的成帧、加密与解密)。因此:
- 单个消息帧可能横跨多个 Noise 帧;
- 单个 Noise 帧也可能包含多个消息帧。
忽略底层加密与成帧后,网线上序列化的NetworkMsg序列看起来就是“长度前缀 + 消息字节”的重复:
[u32-length-prefix] || [serialized-message-bytes] || ..源码中的对应实现是network_message_frame_codec(mod.rs#L148-L154),它基于tokio_util::codec::LengthDelimitedCodec构建:
pub fn network_message_frame_codec(max_frame_size: usize) -> LengthDelimitedCodec { LengthDelimitedCodec::builder() .max_frame_length(max_frame_size) .length_field_length(4) .big_endian() .new_codec() }length_field_length(4)与.big_endian()正是规格文档中“big-endian encoded u32 (4-bytes) length prefix”的代码实现。关于长度前缀与噪声层的协作,DiemNet README 还补充了一个重要细节:Noise 层将单帧大小限制在至多65535字节,其中16字节始终保留给 AES-GCM 认证标签,因此发送一条序列化NetworkMsg(含 4 字节长度前缀)时,需要先将其切成不超过65535 - 16 = 65519字节的块,逐块加密后再发送。
最大帧大小:8 MiB 的硬性边界
规格文档对帧大小给出了强制约束:
每条
serialized-message-bytes必须小于或等于 8 MiB(8388608 字节)。注意该长度不包含u32长度前缀。DiemNet 服务器必须拒绝超过 8 MiB 上限的入站消息,DiemNet 客户端不得发送超过 8 MiB 上限的出站消息。
源码中该常量定义在 network/src/constants.rs#L20:
pub const MAX_FRAME_SIZE: usize = 8 * 1024 * 1024; /* 8 MiB */规格文档同时给出读取单条 DiemNet 消息的参考伪代码:
const MAX_DIEMNET_FRAME_LEN: u32 = 8388608; // 8 MiB // read the 4-byte length prefix first let length_prefix: u32 = noise_socket.read(4).to_host_endian(); // reject messages that are too large if length_prefix > MAX_DIEMNET_FRAME_LEN { reject; } // read the actual bcs-serialized message let message_bytes = noise_socket.read(length_prefix); // deserialize the message let message = bcs::from_bytes(message_bytes);这一约束在测试中有两处直接验证(test.rs):
send_fails_when_larger_than_frame_limit(test.rs#L90-L103):用 64 字节的帧上限构造NetworkMessageSink,发送 123 字节负载的消息,send返回Err——即发送端拒绝超大出站消息;recv_fails_when_larger_than_frame_limit(test.rs#L105-L123):发送端帧上限 128 字节、接收端帧上限 64 字节,发送 80 字节负载的消息后,接收端读取返回Err——即接收端拒绝超大入站消息。
这两条测试共同印证了规格文档“客户端不得发送 / 服务器必须拒绝”的双向约束,而LengthDelimitedCodec的max_frame_length正是这一上限在实际读写路径上的强制者。
发送与接收的实现:NetworkMessageSink 与 NetworkMessageStream
在 wire/messaging/v1/mod.rs 中,消息协议收发被封装为两个异步组件:
NetworkMessageSink<TWriteSocket>:实现Sink<&NetworkMessage>,负责把NetworkMessage用bcs::to_bytes序列化、交给LengthDelimitedCodec成帧后写到底层 socket。序列化失败产生WriteError::SerializeError,IO 失败产生WriteError::IoError(mod.rs#L136-L144);NetworkMessageStream<TReadSocket>:实现Stream<Item = Result<NetworkMessage, ReadError>>,从底层 socket 读出帧后用bcs::from_bytes反序列化;反序列化失败时保留帧长度与前 8 字节便于调试,产生ReadError::DeserializeError(mod.rs#L126-L134)。
两者都接受max_frame_size与可选的速率限制桶(Option<SharedBucket>)作为构造参数。proptest 用例network_message_socket_roundtrip(test.rs#L190-L221)在读写两端同时开启/关闭碎片化(fragmented read/write)的情况下,验证Sink与Stream能够互相理解并完整保留所有NetworkMessage——这也回应了规格文档中“一个消息帧可能横跨多个 Noise 帧”的描述:即使底层字节被切碎,长度前缀定界依然能正确还原出完整消息。
总结:一份协议、一套实现、一组测试
通过对照 messaging-v1.md 规格与仓库实现可以总结出消息协议 v1 的全貌:
- 消息模型:
NetworkMessage四种变体(Error / RpcRequest / RpcResponse / DirectSendMsg),BCS 编码,首字节为消息类型,上限 127 种; - 协议标识:
ProtocolId以#[repr(u8)]单字节编码,在握手中通过SupportedProtocolsbit-vector 通告,消息协议版本由HandshakeMsg::perform_handshake协商出最高交集; - 两种语义:RPC 靠
request_id关联请求与响应;DirectSend 即发即忘,protocol_id决定负载去向; - 质量信号:
priority(0..=255)是 best-effort 调度提示,参考实现暂未执行; - 错误处理:
ParsingError/NotSupported覆盖解析与协议不支持两类场景,应用错误一律封装在RpcResponse内,错误响应非强制,消息长度至少 2 字节才触发错误; - 帧定界:4 字节大端
u32长度前缀 + BCS 消息,Noise 层分包上限 65519 字节,消息帧上限 8 MiB(MAX_FRAME_SIZE),发送与接收两端分别由NetworkMessageSink/NetworkMessageStream强制。
无论读者是要实现一个兼容的 DiemNet 对端、排查线上消息解析问题,还是为应用接入某个ProtocolId通道,都可以以本文的协议语义为纲、以 wire/messaging/v1/mod.rs 与 test.rs 为实现的参照与验证基准。
- 区块链
- 金融科技
【免费下载链接】diem
Diem’s mission is to build a trusted and innovative financial network that empowers people and businesses around the world.
相关推荐
TorchSharp神经网络模块实战:搭建ResNet与MobileNet模型
TorchSharp神经网络模块实战:搭建ResNet与MobileNet模型 TorchSharp是一个强大的.NET库,它提供了对PyTorch核心功能的访
人工智能深度学习机器学习Temporal 消息协议(Message Protocol)深度解析:Workflow Update 的可插拔消息机制
Temporal 消息协议(Message Protocol)深度解析:Workflow Update 的可插拔消息机制 导读 本文围绕 Temporal 服务
后端工作流自动化任务调度Celery 消息协议深度解析:Task 消息 V1/V2 与 Event 事件的传输规范
Celery 消息协议深度解析:Task 消息 V1/V2 与 Event 事件的传输规范 本文基于 Celery 官方内部协议文档( docs/interna
任务调度后端消息队列
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考