第二部分 · 4 / 7

高层 rdmanet

底层那一长串样板,rdmanet 全替你做了:内存注册、工作请求投递、完成队列轮询、流控。 它提供三个 net 风格的核心类型,让你像写 TCP/UDP 一样写 RDMA。

三个核心类型

类型语义主要方法
ConnRC 可靠连接端点SendMsg / RecvMsg / Read / Write / Close
Listener接受 RC 连接Accept / Close / Addr
PacketConnUD 数据报端点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)      // 收并打印
重要约束 同一个 Conn 上不要混用 ReadRecvMsg——字节流适配器内部可能缓存了半条消息, 后续 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 是无连接的,所以用 PacketConnWriteTo/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 这套抽象。

需要单边怎么办 想用 RDMA Write/Read,请下沉到底层 gordma 包自己组织:SendWR 里有 OpcodeOpWrite/OpRead)与 RemoteAddr+RKey 字段。 参考 perftest/write.goperftest/read.go——它们正是 go_write_* / go_read_* 工具背后的单边实现。

并发约定

线程安全 一个 Conn同时一个并发读 + 一个并发写是安全的(典型的收发分离,见 chat 示例)。 关闭连接会让阻塞中的 RecvMsgio.EOF 唤醒返回——便于优雅退出。
下一步 Dial/Listen 背后其实有两种建连方式。下一节讲 rdma_cm 与 TCP 握手
gordma 教程