一个基于 RDMA-CM(librdmacm)的聊天程序------RDMA SEND/RECV 走 RoCEv2,流量真实经过光纤,而不是走内核 TCP。
下面是完整程序------数据面走 RDMA SEND/RECV(RoCEv2 over 光纤),控制面用 RDMA-CM 建连,API 风格接近 socket,适合作为 verbs 编程的入门模板。
rdma_chat.c
c
/*
* rdma_chat.c --- 基于 RDMA-CM 的双终端聊天程序
*
* 数据面走 RDMA SEND/RECV(RoCEv2),控制面由 rdma_cm 完成连接建立。
* 同机双卡自环:两端各自绑定一张卡的 IP,流量真实经过光纤。
*
* 用法:
* 服务端(卡2所在终端): ./rdma_chat 10.10.1.2 listen
* 客户端(卡1所在终端): ./rdma_chat 10.10.1.1 10.10.1.2
*
* 编译:
* gcc -O2 -Wall -o rdma_chat rdma_chat.c -lrdmacm -libverbs
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <rdma/rdma_cma.h>
#include <infiniband/verbs.h>
#define CHAT_PORT 7471
#define BUF_SIZE 1024
static struct ibv_pd *pd;
static struct ibv_cq *cq;
static struct ibv_mr *send_mr, *recv_mr;
static char *send_buf, *recv_buf;
/* 投递一个接收请求(RDMA 的 RECV 必须预投递) */
static int post_recv(struct rdma_cm_id *id)
{
struct ibv_sge sge = {
.addr = (uintptr_t)recv_buf,
.length = BUF_SIZE,
.lkey = recv_mr->lkey,
};
struct ibv_recv_wr wr = {
.wr_id = 1,
.sg_list = &sge,
.num_sge = 1,
};
struct ibv_recv_wr *bad;
return ibv_post_recv(id->qp, &wr, &bad);
}
/* 用 RDMA SEND 发送一条消息 */
static int post_send(struct rdma_cm_id *id, int len)
{
struct ibv_sge sge = {
.addr = (uintptr_t)send_buf,
.length = len,
.lkey = send_mr->lkey,
};
struct ibv_send_wr wr = {
.wr_id = 2,
.sg_list = &sge,
.num_sge = 1,
.opcode = IBV_WR_SEND,
.send_flags = IBV_SEND_SIGNALED,
};
struct ibv_send_wr *bad;
return ibv_post_send(id->qp, &wr, &bad);
}
/* 创建 PD/CQ/QP 并注册收发内存,然后预投递第一个 RECV */
static int setup_qp(struct rdma_cm_id *id)
{
pd = ibv_alloc_pd(id->verbs);
if (!pd) { perror("ibv_alloc_pd"); return -1; }
cq = ibv_create_cq(id->verbs, 64, NULL, NULL, 0);
if (!cq) { perror("ibv_create_cq"); return -1; }
struct ibv_qp_init_attr attr = {
.send_cq = cq,
.recv_cq = cq,
.cap = {
.max_send_wr = 32,
.max_recv_wr = 32,
.max_send_sge = 1,
.max_recv_sge = 1,
},
.qp_type = IBV_QPT_RC,
};
if (rdma_create_qp(id, pd, &attr)) { perror("rdma_create_qp"); return -1; }
send_buf = malloc(BUF_SIZE);
recv_buf = malloc(BUF_SIZE);
if (!send_buf || !recv_buf) { perror("malloc"); return -1; }
send_mr = ibv_reg_mr(pd, send_buf, BUF_SIZE, IBV_ACCESS_LOCAL_WRITE);
recv_mr = ibv_reg_mr(pd, recv_buf, BUF_SIZE, IBV_ACCESS_LOCAL_WRITE);
if (!send_mr || !recv_mr) { perror("ibv_reg_mr"); return -1; }
return post_recv(id);
}
/* 阻塞等待指定的 CM 事件 */
static struct rdma_cm_id *wait_event(struct rdma_event_channel *ec,
enum rdma_cm_event_type want)
{
struct rdma_cm_event *ev;
while (rdma_get_cm_event(ec, &ev) == 0) {
struct rdma_cm_id *id = ev->id;
enum rdma_cm_event_type got = ev->event;
rdma_ack_cm_event(ev);
if (got == want)
return id;
if (got == RDMA_CM_EVENT_REJECTED ||
got == RDMA_CM_EVENT_CONNECT_ERROR ||
got == RDMA_CM_EVENT_ADDR_ERROR ||
got == RDMA_CM_EVENT_ROUTE_ERROR) {
fprintf(stderr, "连接失败(CM 事件 %d)\n", got);
exit(1);
}
}
return NULL;
}
int main(int argc, char **argv)
{
if (argc != 3) {
fprintf(stderr,
"用法:\n"
" 服务端: %s <本端IP> listen\n"
" 客户端: %s <本端IP> <对端IP>\n", argv[0], argv[0]);
return 1;
}
int is_server = (strcmp(argv[2], "listen") == 0);
struct sockaddr_in local = {0}, peer = {0};
local.sin_family = AF_INET;
local.sin_port = htons(CHAT_PORT);
if (inet_pton(AF_INET, argv[1], &local.sin_addr) != 1) {
fprintf(stderr, "本端 IP 格式错误\n");
return 1;
}
struct rdma_event_channel *ec = rdma_create_event_channel();
if (!ec) { perror("rdma_create_event_channel"); return 1; }
struct rdma_cm_id *id, *conn;
if (rdma_create_id(ec, &id, NULL, RDMA_PS_TCP)) {
perror("rdma_create_id");
return 1;
}
if (is_server) {
if (rdma_bind_addr(id, (struct sockaddr *)&local)) {
perror("rdma_bind_addr(本端 IP 是否已配置在卡上?)");
return 1;
}
if (rdma_listen(id, 1)) { perror("rdma_listen"); return 1; }
printf("等待对端连接(%s:%d)...\n", argv[1], CHAT_PORT);
conn = wait_event(ec, RDMA_CM_EVENT_CONNECT_REQUEST);
if (setup_qp(conn)) return 1;
if (rdma_accept(conn, NULL)) { perror("rdma_accept"); return 1; }
wait_event(ec, RDMA_CM_EVENT_ESTABLISHED);
} else {
peer = local;
if (inet_pton(AF_INET, argv[2], &peer.sin_addr) != 1) {
fprintf(stderr, "对端 IP 格式错误\n");
return 1;
}
if (rdma_resolve_addr(id, (struct sockaddr *)&local,
(struct sockaddr *)&peer, 2000)) {
perror("rdma_resolve_addr");
return 1;
}
wait_event(ec, RDMA_CM_EVENT_ADDR_RESOLVED);
if (rdma_resolve_route(id, 2000)) { perror("rdma_resolve_route"); return 1; }
wait_event(ec, RDMA_CM_EVENT_ROUTE_RESOLVED);
conn = id;
if (setup_qp(conn)) return 1;
if (rdma_connect(conn, NULL)) { perror("rdma_connect"); return 1; }
wait_event(ec, RDMA_CM_EVENT_ESTABLISHED);
}
printf("已连接!输入文字回车发送,输入 quit 退出。\n> ");
fflush(stdout);
/* 握手完成后把事件通道设为非阻塞,主循环轮询:stdin + CQ + CM 事件 */
fcntl(ec->fd, F_SETFL, fcntl(ec->fd, F_GETFL) | O_NONBLOCK);
char line[BUF_SIZE];
int running = 1;
while (running) {
/* 1) 键盘输入 → RDMA SEND */
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(STDIN_FILENO, &rfds);
struct timeval tv = {0, 50000}; /* 50ms 轮询节拍 */
if (select(STDIN_FILENO + 1, &rfds, NULL, NULL, &tv) > 0 &&
FD_ISSET(STDIN_FILENO, &rfds)) {
if (!fgets(line, sizeof line, stdin))
break;
line[strcspn(line, "\n")] = 0;
if (strcmp(line, "quit") == 0)
break;
if (line[0] != '\0') {
strcpy(send_buf, line);
if (post_send(conn, (int)strlen(send_buf) + 1))
perror("post_send");
printf("> ");
fflush(stdout);
}
}
/* 2) 完成队列 → 打印收到的消息 */
struct ibv_wc wc[8];
int n = ibv_poll_cq(cq, 8, wc);
for (int i = 0; i < n; i++) {
if (wc[i].status != IBV_WC_SUCCESS) {
fprintf(stderr, "\nCQ 错误: %s\n",
ibv_wc_status_str(wc[i].status));
running = 0;
break;
}
if (wc[i].opcode == IBV_WC_RECV) {
printf("\r[对端] %s\n> ", recv_buf);
fflush(stdout);
post_recv(conn); /* 重新投递接收 */
}
}
/* 3) 对端断开检测 */
struct rdma_cm_event *ev;
if (rdma_get_cm_event(ec, &ev) == 0) {
if (ev->event == RDMA_CM_EVENT_DISCONNECTED) {
printf("\n对端已断开。\n");
running = 0;
}
rdma_ack_cm_event(ev);
}
}
rdma_disconnect(conn);
rdma_destroy_qp(conn);
ibv_dereg_mr(send_mr);
ibv_dereg_mr(recv_mr);
free(send_buf);
free(recv_buf);
ibv_destroy_cq(cq);
ibv_dealloc_pd(pd);
rdma_destroy_id(conn);
rdma_destroy_event_channel(ec);
return 0;
}
编译
bash
# 如果还没装开发库
sudo apt install libibverbs-dev librdmacm-dev
gcc -O2 -Wall -o rdma_chat rdma_chat.c -lrdmacm -libverbs
运行(两个终端,各绑一张卡)
bash
# 终端1 ------ 服务端,绑 18卡口2 的 IP
./rdma_chat 10.10.1.2 listen
# 终端2 ------ 客户端,绑 17卡口2 的 IP,连服务端
./rdma_chat 10.10.1.1 10.10.1.2
rdma_resolve_addr 会按本端 IP 选中对应的网卡/GID(自动选 RoCEv2 的 GID),连接建立后双向输入文字即聊,quit 退出。


统计信息:

验证流量真的过了光纤
聊天时在第三个终端观察计数器增长:
bash
watch -n1 "sudo ethtool -S enp23s0f1np1 | grep -E '^ *(tx|rx)_packets' | head -4"
# 或看 RDMA 端口计数
watch -n1 'sudo cat /sys/class/infiniband/mlx5_1/ports/1/counters/port_rcv_packets 2>/dev/null'
程序结构说明(verbs 学习要点)
rdma_cm 控制面(类似 socket) verbs 数据面(RDMA 本体)
───────────────────────────── ────────────────────────────
create_id / bind_addr / listen alloc_pd 保护域
resolve_addr / resolve_route create_cq 完成队列
accept / connect create_qp 队列对(RC 模式)
│ reg_mr 注册内存(拿到 lkey)
▼ ibv_post_recv 预投递接收
ESTABLISHED 之后 ─────────────────► ibv_post_send RDMA SEND 发送
ibv_poll_cq 轮询完成事件
两个值得注意的点:
- RECV 必须预投递 ------RDMA 没有 socket 那样的内核缓冲,收到消息前必须先把接收缓冲区挂到 QP 上,所以每收到一条就要
post_recv补一个; - 同机双卡自环时,绑定不同卡的 IP 后 rdma_cm 会为两端 QP 解析出不同端口的 GID,流量必须出光纤绕一圈才能到达对端------这是它区别于普通 socket 聊天(会被内核回环短路)的关键。
如果跑的时候 rdma_bind_addr 报错,先检查 ip -br addr | grep f1 确认两个口的 IP 还在;rdma_connect 失败则用 show_gids 确认两端都有 RoCEv2 的 GID。