高层 rdmanet
底层那一长串样板,rdmanet 全替你做了:内存注册、工作请求投递、完成队列轮询、流控。
它提供三个 net 风格的核心类型,让你像写 TCP/UDP 一样写 RDMA。
三个核心类型
| 类型 | 语义 | 主要方法 |
|---|---|---|
Conn | RC 可靠连接端点 | SendMsg / RecvMsg / Read / Write / Close |
Listener | 接受 RC 连接 | Accept / Close / Addr |
PacketConn | UD 数据报端点 | ReadFrom / WriteTo |
消息语义 vs 字节流
RC 本质是面向消息的,所以 Conn 的主 API 是保留消息边界的:
一次 SendMsg 对应一次 RecvMsg,无论多大——大消息会被透明地分片/重组。
// 消息语义:边界保留
conn.SendMsg([]byte("hello"))
msg, _ := conn.RecvMsg() // 恰好收到 "hello"
// 也可以收进调用方缓冲区,拿到写入长度
n, _ := conn.RecvMsgBuf(buf)
在此之上还叠了一层字节流适配器 Read/Write,让 Conn 满足
io.ReadWriteCloser,于是已有的 io.Reader/io.Writer 代码(比如 io.Copy)能直接用:
// 字节流:Conn 即 io.ReadWriteCloser,可被 io.Copy 消费
io.Copy(conn, file) // 发文件
io.Copy(os.Stdout, conn) // 收并打印
Read 和 RecvMsg——字节流适配器内部可能缓存了半条消息,
后续 RecvMsg 看不到那部分。一个 Conn 只选一种接收风格。
服务端 / 客户端骨架
// 服务端
ln, _ := rdmanet.Listen("0.0.0.0:18515")
defer ln.Close()
for {
conn, err := ln.Accept() // 可循环服务多个客户端
if err != nil { break }
go serve(conn)
}
// 客户端
conn, _ := rdmanet.Dial("10.0.0.1:18515")
defer conn.Close()
conn.SendMsg([]byte("ping"))
UD 数据报:PacketConn
UD 是无连接的,所以用 PacketConn 的 WriteTo/ReadFrom,对端用结构化的 Addr 命名
(格式 gid%qpn[#qkey],String()/ResolveAddr 互转)。一条数据报超过路径 MTU 会被
拒绝而非截断。配套的可选 Registry 是个轻量的「名字→Addr」目录,用来带外发现对端。
只做双边操作(Send/Recv)
第一部分讲过 RDMA 有双边(Send/Recv)和单边(RDMA Write/Read)两类操作。
这里要明确一点:rdmanet 全程只用双边 Send/Recv,不使用单边 Write/Read。
消息、字节流、批量、零拷贝、UD 数据报、内部的信用流控与 FIN,底层投递的工作请求清一色是 OpSend。
它的双边模型由三块撑起来:
- 接收方预挂接收 WR:连接建好就把所有接收槽
post_recv挂满,每消费完一帧再补挂回去——保证对端发来时总有落点(否则触发 RNR)。 - 发送方
post_send:所有发送路径都用Opcode: OpSend。 - 完成分发:轮询 CQ,按
WCSend/WCRecv分流——发送完成唤醒发送方,接收完成则解析帧头、交付数据、补挂 recv。
AllocBuffer+SendBuffer 的「零拷贝」指的是省掉到 bounce buffer 的内存拷贝
(用户 MR 直接作为 SGE 被 DMA),它的 Opcode 仍是 OpSend,依然是双边操作,
不是单边 RDMA Write。
为什么不在高层做单边
单边 Write/Read 需要发起方知道「对端哪块内存 + rkey」,这要么得持续交换内存布局,要么得有一套远端缓冲区
管理协议,和 rdmanet 想要的「net 风格、保留消息边界、自带流控」语义不搭。
双边 Send/Recv 则天然契合消息语义——一次 Send 一个落点,配上信用流控做背压、帧头做大消息分片/重组,
正好实现 SendMsg/RecvMsg 这套抽象。
gordma 包自己组织:SendWR 里有 Opcode
(OpWrite/OpRead)与 RemoteAddr+RKey 字段。
参考 perftest/write.go 和 perftest/read.go——它们正是 go_write_* / go_read_*
工具背后的单边实现。
并发约定
Conn 上同时一个并发读 + 一个并发写是安全的(典型的收发分离,见 chat 示例)。
关闭连接会让阻塞中的 RecvMsg 以 io.EOF 唤醒返回——便于优雅退出。