基于 RDMA-CM(librdmacm)的聊天程序

一个基于 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  轮询完成事件

两个值得注意的点:

  1. RECV 必须预投递 ------RDMA 没有 socket 那样的内核缓冲,收到消息前必须先把接收缓冲区挂到 QP 上,所以每收到一条就要 post_recv 补一个;
  2. 同机双卡自环时,绑定不同卡的 IP 后 rdma_cm 会为两端 QP 解析出不同端口的 GID,流量必须出光纤绕一圈才能到达对端------这是它区别于普通 socket 聊天(会被内核回环短路)的关键。

如果跑的时候 rdma_bind_addr 报错,先检查 ip -br addr | grep f1 确认两个口的 IP 还在;rdma_connect 失败则用 show_gids 确认两端都有 RoCEv2 的 GID。

相关推荐
WUYOUGYLU1 小时前
大模型:人工智能时代的关键引擎
人工智能
2601_950760791 小时前
BCMA:急性髓系白血病免疫治疗的新型可行靶点
人工智能·算法·蛋白
vickycheung31 小时前
启扬I.MX8MPlus开发板如何在NPU上适配Movenet 模型
人工智能·arm 嵌入式开发
刘一说1 小时前
AI科技热点日报 | 2026年07月17日
人工智能·科技
海盗12341 小时前
AI新闻日报_2026-07-17
人工智能
(Charon)1 小时前
【C/C++】手写内存池(三):大块内存的申请、记录与单独释放
网络
Together_CZ1 小时前
GKDT: General Keypoint Detection Transformer——通用关键点检测 Transformer
人工智能·transformer·通用关键点检测·gkdt·keypoint·detection
学术小白人1 小时前
第二届智能制造、机器人与自动化国际学术会议(IMRA 2026)
运维·人工智能·机器人·自动化·制造
2601_960906722 小时前
网络防御等最先进AI模型领域
人工智能·json·hbase·flume·storm