SPDK 常用结构体
1.1 spdk_app_opts 用于配置 SPDK 应用程序的各个方面,包括内存分配、核心分配、日志记录等。通过设置这些字段
/**
* \brief 事件框架初始化选项
*/
struct spdk_app_opts {
const char *name; /**< 应用程序名称 */
const char *json_config_file; /**< JSON 配置文件路径 */
bool json_config_ignore_errors; /**< 是否忽略 JSON 配置文件中的错误 */
/* 在字节 17-23 之间留白. */
uint8_t reserved17[7];
const char *rpc_addr; /**< RPC 服务地址,可以是 UNIX 域套接字路径或 IP 地址 + TCP 端口 */
const char *reactor_mask; /**< 反应堆掩码 */
const char *tpoint_group_mask; /**< tracepoint 组掩码 */
int shm_id; /**< 共享内存标识 */
/* 在字节 52-55 之间留白. */
uint8_t reserved52[4];
spdk_app_shutdown_cb shutdown_cb; /**< 应用程序关闭回调函数 */
bool enable_coredump; /**< 是否启用核心转储 */
/* 在字节 65-67 之间留白. */
uint8_t reserved65[3];
int mem_channel; /**< 内存通道数量 */
int main_core; /**< 主核心编号 */
int mem_size; /**< 内存大小 */
bool no_pci; /**< 是否禁用 PCI */
bool hugepage_single_segments; /**< 是否使用单个巨页段 */
bool unlink_hugepage; /**< 是否取消链接巨页 */
/* 在字节 83-85 之间留白. */
uint8_t reserved83[5];
const char *hugedir; /**< 巨页目录路径 */
enum spdk_log_level print_level; /**< 日志打印级别 */
/* 在字节 100-103 之间留白. */
uint8_t reserved100[4];
size_t num_pci_addr; /**< PCI 地址数量 */
struct spdk_pci_addr *pci_blocked; /**< 阻塞的 PCI 地址列表 */
struct spdk_pci_addr *pci_allowed; /**< 允许的 PCI 地址列表 */
const char *iova_mode; /**< IOVA 模式 */
/**
* 当启用此标志时,在初始化子系统之前等待关联的 RPC。
*/
bool delay_subsystem_init;
/* 在字节 137-143 之间留白. */
uint8_t reserved137[7];
/**
* 每个核心分配的跟踪条目数量
*/
uint64_t num_entries;
/** 环境实现的不透明上下文。 */
void *env_context;
/**
* 用户提供的日志调用函数
*/
logfunc *log;
uint64_t base_virtaddr; /**< 基本虚拟地址 */
/**
* 用于 ABI 兼容性的选项大小字段。用于知道此库的调用者结构体中有多少字段是有效的。
* 库将使用此字段来知道该结构体的剩余字段并将其填充为默认值。
* 然后,新添加的字段应该放在 opts_size 之后。
*/
size_t opts_size;
/**
* 禁用默认信号处理程序。
* 如果设置为 `true`,则不会通过进程信号隐式启动关闭过程,
* 因此应用程序负责调用 spdk_app_start_shutdown()。
*
* 默认值为 `false`。
*/
bool disable_signal_handlers;
/* 在字节 185-191 之间留白. */
uint8_t reserved185[7];
/**
* 由线程库使用的消息池的分配大小。
*
* 默认值为 `SPDK_DEFAULT_MSG_MEMPOOL_SIZE`。
*/
size_t msg_mempool_size;
/*
* 如果非 NULL,允许的 RPC 方法的字符串数组。
*/
const char **rpc_allowlist;
/**
* 用于通过 DPDK 传递 vf_token 到 vfio_pci 驱动程序的 vf_token。
* vf_token 是 SR-IOV PF 和 VF 之间共享的 UUID
1.2 spdk_net_impl 设计允许用户定义和切换不同的网络实现,以满足特定应用场景的需求。每个函数指针都代表了网络实现中的不同功能,用户可以根据需要实现这些函数来提供自定义的网络协议栈。
/**
* \brief 网络实现结构体
*/
struct spdk_net_impl {
const char *name; /**< 网络实现的名称 */
int priority; /**< 网络实现的优先级 */
/**
* 获取套接字地址信息的函数指针。
*
* @param sock 套接字
* @param saddr 本地地址
* @param slen 本地地址长度
* @param sport 本地端口
* @param caddr 远程地址
* @param clen 远程地址长度
* @param cport 远程端口
* @return 成功返回 0,失败返回负数
*/
int (*getaddr)(struct spdk_sock *sock, char *saddr, int slen, uint16_t *sport,
char *caddr, int clen, uint16_t *cport);
/**
* 连接到指定 IP 地址和端口的函数指针。
*
* @param ip 目标 IP 地址
* @param port 目标端口
* @param opts 套接字选项
* @return 成功返回套接字,失败返回 NULL
*/
struct spdk_sock *(*connect)(const char *ip, int port, struct spdk_sock_opts *opts);
/**
* 创建监听套接字的函数指针。
*
* @param ip 本地 IP 地址
* @param port 本地端口
* @param opts 套接字选项
* @return 成功返回监听套接字,失败返回 NULL
*/
struct spdk_sock *(*listen)(const char *ip, int port, struct spdk_sock_opts *opts);
/**
* 接受连接请求的函数指针。
*
* @param sock 套接字
* @return 成功返回新套接字,失败返回 NULL
*/
struct spdk_sock *(*accept)(struct spdk_sock *sock);
/**
* 关闭套接字的函数指针。
*
* @param sock 套接字
* @return 成功返回 0,失败返回负数
*/
int (*close)(struct spdk_sock *sock);
/**
* 接收数据的函数指针。
*
* @param sock 套接字
* @param buf 接收缓冲区
* @param len 接收数据长度
* @return 成功返回接收的字节数,失败返回负数
*/
ssize_t (*recv)(struct spdk_sock *sock, void *buf, size_t len);
/**
* 使用散列/聚合 I/O 接收数据的函数指针。
*
* @param sock 套接字
* @param iov 数据块数组
* @param iovcnt 数据块数量
* @return 成功返回接收的字节数,失败返回负数
*/
ssize_t (*readv)(struct spdk_sock *sock, struct iovec *iov, int iovcnt);
/**
* 使用散列/聚合 I/O 发送数据的函数指针。
*
* @param sock 套接字
* @param iov 数据块数组
* @param iovcnt 数据块数量
* @return 成功返回发送的字节数,失败返回负数
*/
ssize_t (*writev)(struct spdk_sock *sock, struct iovec *iov, int iovcnt);
/**
* 接收下一个数据包的函数指针。
*
* @param sock 套接字
* @param buf 接收缓冲区
* @param ctx 上下文
* @return 成功返回 0,失败返回负数
*/
int (*recv_next)(struct spdk_sock *sock, void **buf, void **ctx);
/**
* 异步发送数据的函数指针。
*
* @param sock 套接字
* @param req 套接字请求
*/
void (*writev_async)(struct spdk_sock *sock, struct spdk_sock_request *req);
/**
* 异步接收数据的函数指针。
*
* @param sock 套接字
* @param req 套接字请求
*/
void (*readv_async)(struct spdk_sock *sock, struct spdk_sock_request *req);
/**
* 刷新套接字的函数指针。
*
* @param sock 套接字
* @return 成功返回 0,失败返回负数
*/
int (*flush)(struct spdk_sock *sock);
/**
* 设置套接字接收低水位标志的函数指针。
*
* @param sock 套接字
* @param nbytes 低水位标志的字节数
* @return 成功返回 0,失败返回负数
*/
int (*set_recvlowat)(struct spdk_sock *sock, int nbytes);
/**
* 设置套接字接收缓冲区大小的函数指针。
*
* @param sock 套接字
* @param sz 缓冲区大小
* @return 成功返回 0,失败返回负数
*/
int (*set_recvbuf)(struct spdk_sock *sock, int sz);
/**
* 设置套接字发送缓冲区大小的函数指针。
*
* @param sock 套接字
* @param sz 缓冲区大小
* @return 成功返回 0,失败返回负数
*/
int (*set_sendbuf)(struct spdk_sock *sock, int sz);
/**
* 检查套接字是否使用 IPv6 的函数指针。
*
* @param sock 套接字
* @return 使用 IPv6 返回 true,
1.3 spdk_sock_opts 定义了Spdk套接字初始化选项,允许用户请求非默认的套接字配置。该结构体包括套接字优先级、零拷贝支持、ACK超时等参数。此外,用户还可以指定套接字实现选项,并在使用后释放它们。这个结构体的设计允许用户根据特定需求定制套接字的行为
/**
* Spdk套接字初始化选项。
*
* 由spdk_sock_listen_ext()或spdk_sock_connect_ext()使用此结构体的指针,以允许用户请求套接字的非默认选项。
*/
struct spdk_sock_opts {
/**
* 根据库的调用者的大小来确定spdk_sock_opts的大小,用于ABI兼容性。
* 库使用此字段来了解此结构体中有多少个有效字段。库将使用默认值填充任何剩余的字段。
*/
size_t opts_size;
/**
* 套接字的优先级,默认值为零。
*/
int priority;
/**
* 用于在套接字层启用或禁用零拷贝。
*/
bool zcopy;
/* 字节偏移13-15处有空洞。 */
uint8_t reserved13[3];
/**
* 等待ACK的时间(以毫秒为单位),直到强制关闭连接。
*/
uint32_t ack_timeout;
/* 字节偏移20-23处有空洞。 */
uint8_t reserved[4];
/**
* 套接字实现选项。如果非NULL,这些选项将覆盖spdk_sock_impl_set_opts()设置的选项。
* 库在内部复制此结构,因此用户可以在spdk_sock_connect()/spdk_sock_listen()调用后立即释放它。
*/
struct spdk_sock_impl_opts *impl_opts;
/**
* impl_opts结构体的大小。
*/
size_t impl_opts_size;
};
1.4 spdk_sock_group_impl 用于表示Spdk套接字组的实现细节。它包括套接字网络实现的指针、套接字组的指针以及一个链表,该链表包含了与此套接字组关联的套接字。这个结构体的设计用于管理套接字组的内部结构,以支持高效的套接字管理和事件处理。
/**
* Spdk套接字组实现结构体。
*/
struct spdk_sock_group_impl {
struct spdk_net_impl *net_impl; /**< 套接字网络实现 */
struct spdk_sock_group *group; /**< 套接字组 */
TAILQ_HEAD(, spdk_sock) socks; /**< 套接字链表 */
STAILQ_ENTRY(spdk_sock_group_impl) link; /**< 链表链接 */
};
1.5 spdk_sock_request 用于表示Spdk套接字请求。它包括一个回调函数和相关参数,用于在请求完成时进行回调通知。此外,结构体还包含一些内部字段,用于套接字层的管理和控制。这个结构体的设计支持套接字操作的异步处理,并允许使用 zerocopy 进行数据传输。
/**
* Spdk套接字请求结构体。
*/
struct spdk_sock_request {
/**
* 当请求完成时,将调用此回调函数。
* 在成功时,err将为:
* - 对于写操作:0,
* - 对于读操作:读取的字节数。
* 在失败时:负的错误号值。
*/
void (*cb_fn)(void *cb_arg, int err);
void *cb_arg; /**< 回调函数的参数 */
/**
* 这些字段由套接字层使用,不应修改。
*/
struct __sock_request_internal {
TAILQ_ENTRY(spdk_sock_request) link; /**< 链表链接 */
#ifdef DEBUG
void *curr_list;
#endif
uint32_t offset; /**< 偏移量 */
/* 指示整个请求或其中一部分是否使用zerocopy发送 */
bool is_zcopy;
} internal;
int iovcnt; /**< iovec数组中的项数 */
/* struct iovec iov[]; */
};
1.6 spdk_sock_impl_opts 用于表示SPDK套接字实现选项,它允许用户请求套接字模块实现的各种选项。不同的套接字模块可以定义并使用这些选项中的不同部分。此结构体包括了一系列用于配置套接字行为的字段,如接收和发送缓冲区的大小、是否启用零拷贝、TLS设置等等。这些选项可用于调整套接字模块的行为,以满足特定的性能和功能需求
/**
* SPDK套接字实现选项。
*
* 指向此结构的指针将由spdk_sock_impl_get_opts()和spdk_sock_impl_set_opts()使用,
* 允许用户请求套接字模块实现的选项。
* 每个套接字模块定义了此结构中哪些选项适用于该模块。
*/
struct spdk_sock_impl_opts {
/**
* 套接字接收缓冲区的最小大小。由posix和uring套接字模块使用。
*/
uint32_t recv_buf_size;
/**
* 套接字发送缓冲区的最小大小。由posix和uring套接字模块使用。
*/
uint32_t send_buf_size;
/**
* 启用或禁用接收管道。由posix和uring套接字模块使用。
*/
bool enable_recv_pipe;
/**
* **已弃用,请改用enable_zerocopy_send_server或enable_zerocopy_send_client**
* 启用或禁用发送时的零拷贝流。由posix套接字模块使用。
*/
bool enable_zerocopy_send;
/**
* 启用或禁用快速ACK。由posix和uring套接字模块使用。
*/
bool enable_quickack;
/**
* 启用或禁用placement_id。由posix和uring套接字模块使用。
* 枚举spdk_placement_mode中的有效值。
*/
uint32_t enable_placement_id;
/**
* 启用或禁用服务器套接字的发送时零拷贝流。由posix和uring套接字模块使用。
*/
bool enable_zerocopy_send_server;
/**
* 启用或禁用客户端套接字的发送时零拷贝流。由posix和uring套接字模块使用。
*/
bool enable_zerocopy_send_client;
/**
* 设置零拷贝阈值(以字节为单位)。
* 在此阈值以下的连续一系列请求的iovec可以在不设置zerocopy标志的情况下发送。
*/
uint32_t zerocopy_threshold;
/**
* TLS协议版本。由ssl套接字模块使用。
*/
uint32_t tls_version;
/**
* 启用或禁用内核TLS。由ssl套接字模块使用。
*/
bool enable_ktls;
/**
* 设置默认的PSK密钥。由ssl套接字模块使用。
*/
uint8_t *psk_key;
/**
* psk_key的大小。
*/
uint32_t psk_key_size;
/**
* 设置默认的PSK身份。由ssl套接字模块使用。
*/
char *psk_identity;
/**
* 根据客户端的身份获取PSK的可选回调函数。
*
* \param out 用于填充找到的密钥的二进制格式的PSK的缓冲区。
* \param out_len "out"缓冲区的长度。
* \param cipher 由此回调设置的密码套件。
* \param psk_identity 需要找到密钥的PSK身份。
* \param get_key_ctx 此回调的上下文。
*
* \return 成功时的密钥长度,失败时为-1。
*/
int (*get_key)(uint8_t *out, int out_len, const char **cipher, const char *psk_identity,
void *get_key_ctx);
/**
* 传递给get_key()回调的上下文。
*/
void *get_key_ctx;
/**
* 密码套件。由ssl套接字模块使用。
* 对于连接方,它必须包含单个密码:
* 例如:"TLS_AES_256_GCM_SHA384"
*
* 对于监听方,它可以是以冒号分隔的密码列表:
* 例如:"TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"
*/
const char *tls_cipher_suites;
};
1.7 server_context_t 用于存储服务器的相关信息和状态,包括主机名、端口号、套接字实现名称、输入和输出字节数以及与服务器通信的套接字和套接字组等信息。这个结构体在服务器应用程序中用于管理和维护与客户端之间的通信
/**
* 服务器上下文结构体。
*
* 该结构体用于保存服务器的相关信息和状态,包括主机名、端口号、套接字实现名称、输入和输出字节数、套接字以及套接字组等。
*/
struct server_context_t {
char *host; // 服务器主机名
int port; // 服务器端口号
char *sock_impl_name; // 套接字实现名称
int bytes_in; // 接收的字节数
int bytes_out; // 发送的字节数
struct spdk_sock *sock; // 服务器套接字
struct spdk_sock_group *group; // 套接字组
};
SPDK 基础函数
2.1 spdk_app_start:初始化SPDK应用程序所需的各种资源和环境,包括全局结构、日志、锁、CPU核心分配、反应堆等:
参数检查和准备工作:在函数开始时,检查传入的参数和选项,确保它们的有效性。如果参数无效或选项设置有误,函数将返回1以表示错误。
日志设置:根据选项设置日志打印级别,并根据终端情况输出警告信息。
核心转储设置:如果启用了核心转储,设置核心转储的资源限制。
全局应用程序结构初始化:初始化全局的g_spdk_app结构体,包括配置文件、RPC地址、共享内存ID等信息。
全局日志级别设置:设置全局的日志打印级别。
环境初始化:调用app_setup_env函数来初始化SPDK环境。如果环境已经初始化过,表示重新初始化。
计算内存池大小:根据环境配置和用户选项计算内存池的大小。
日志打开:打开SPDK日志。
锁初始化:初始化每个锁为-1,表示"空"状态。
CPU核心锁定:如果未禁用CPU核心锁定,尝试获取核心掩码锁。
反应堆初始化:初始化SPDK反应堆。
应用线程创建:创建应用线程。
追踪设置:如果选项中指定了追踪项,尝试设置追踪。
信号处理程序设置:如果未禁用信号处理程序,尝试设置信号处理程序。
启动回调函数:设置延迟初始化标志和启动回调函数。
消息发送和阻塞:将消息发送到应用线程执行bootstrap_fn函数,并在之后阻塞直到spdk_app_stop被调用。
环境初始化标志设置:设置环境已初始化标志
// 启动SPDK应用程序
int spdk_app_start(struct spdk_app_opts *opts_user, spdk_msg_fn start_fn, void *arg1)
{
int rc;
char *tty;
struct spdk_cpuset tmp_cpumask = {};
static bool g_env_was_setup = false;
struct spdk_app_opts opts_local = {};
struct spdk_app_opts *opts = &opts_local;
uint32_t i;
// 检查opts_user是否为NULL
if (!opts_user) {
SPDK_ERRLOG("opts_user不应为NULL\n");
return 1;
}
// 检查opts_user结构体中的opts_size是否为零
if (!opts_user->opts_size) {
SPDK_ERRLOG("opts_user结构体中的opts_size不应为零\n");
return 1;
}
// 检查spdk_app_opts结构体中的name是否已指定
if (opts_user->name == NULL) {
SPDK_ERRLOG("spdk_app_opts::name未指定\n");
return 1;
}
// 复制opts_user中的选项到opts_local
app_copy_opts(opts, opts_user, opts_user->opts_size);
// 检查start_fn是否为NULL
if (!start_fn) {
SPDK_ERRLOG("start_fn不应为NULL\n");
return 1;
}
// 如果没有设置lcore_map或reactor_mask,设置默认CPU掩码
if (!(opts->lcore_map || opts->reactor_mask)) {
opts->reactor_mask = SPDK_APP_DPDK_DEFAULT_CORE_MASK;
}
// 检查并设置日志级别
tty = ttyname(STDERR_FILENO);
if (opts->print_level > SPDK_LOG_WARN &&
isatty(STDERR_FILENO) &&
tty &&
!strncmp(tty, "/dev/tty", strlen("/dev/tty"))) {
// 在终端输出警告信息
printf("警告:未指定-q选项的情况下将stderr输出到控制台终端。\n");
printf("建议使用--silence-noticelog禁用stderr日志输出,\n");
printf("并将stderr重定向到文件中。\n");
printf("(延迟10秒...)\n");
sleep(10);
}
// 设置日志打印级别
spdk_log_set_print_level(opts->print_level);
// 如果启用了核心转储,设置核心转储限制
#ifndef SPDK_NO_RLIMIT
if (opts->enable_coredump) {
struct rlimit core_limits;
core_limits.rlim_cur = core_limits.rlim_max = SPDK_APP_DEFAULT_CORE_LIMIT;
setrlimit(RLIMIT_CORE, &core_limits);
}
#endif
// 初始化全局的SPDK应用程序结构体g_spdk_app
memset(&g_spdk_app, 0, sizeof(g_spdk_app));
g_spdk_app.json_config_file = opts->json_config_file;
g_spdk_app.json_config_ignore_errors = opts->json_config_ignore_errors;
g_spdk_app.rpc_addr = opts->rpc_addr;
g_spdk_app.rpc_allowlist = opts->rpc_allowlist;
g_spdk_app.shm_id = opts->shm_id;
g_spdk_app.shutdown_cb = opts->shutdown_cb;
g_spdk_app.rc = 0;
g_spdk_app.stopped = false;
// 设置全局日志级别
spdk_log_set_level(SPDK_APP_DEFAULT_LOG_LEVEL);
// 初始化环境,如果已经初始化,则表示重新初始化
if (app_setup_env(g_env_was_setup ? NULL : opts) < 0) {
return 1;
}
// 计算内存池大小
calculate_mempool_size(opts, opts_user);
// 打开日志
spdk_log_open(opts->log);
// 初始化每个锁为-1,表示"空"状态
for (i = 0; i < MAX_CPU_CORES; i++) {
g_core_locks[i] = -1;
}
// 如果未禁用CPU核心锁定,尝试获取核心掩码锁
if (!g_disable_cpumask_locks) {
if (claim_cpu_cores(NULL)) {
SPDK_ERRLOG("无法获取分配的核心掩码锁 - 退出。\n");
return 1;
}
} else {
SPDK_NOTICELOG("CPU核心锁定已禁用。\n");
}
// 输出可用核心数量
SPDK_NOTICELOG("可用核心总数:%d\n", spdk_env_get_core_count());
// 初始化反应堆
if ((rc = spdk_reactors_init(opts->msg_mempool_size)) != 0) {
SPDK_ERRLOG("反应堆初始化失败:rc = %d\n", rc);
return 1;
}
// 设置tmp_cpumask
spdk_cpuset_set_cpu(&tmp_cpumask, spdk_env_get_current_core(), true);
// 创建应用线程
spdk_thread_create("app_thread", &tmp_cpumask);
if (!spdk_thread_get_app_thread()) {
SPDK_ERRLOG("无法创建用于初始化的spdk_thread\n");
return 1;
}
// 如果num_entries不为0,尝试设置追踪
if (opts->num_entries != 0 && app_setup_trace(opts) != 0) {
return 1;
}
// 如果未禁用信号处理程序,尝试设置信号处理程序
if (!opts->disable_signal_handlers && app_setup_signal_handlers(opts) != 0) {
return 1;
}
// 设置延迟初始化标志和启动回调函数
g_delay_subsystem_init = opts->delay_subsystem_init;
g_start_fn = start_fn;
g_start_arg = arg1;
// 发送消息到应用线程,执行bootstrap_fn函数
spdk_thread_send_msg(spdk_thread_get_app_thread(), bootstrap_fn, NULL);
// 阻塞直到spdk_app_stop被调用
spdk_reactors_start();
// 设置环境已初始化标志
g_env_was_setup = true;
// 返回SPDK应用程序的返回码
return g_spdk_app.rc;
}
2.2 spdk_app_opts_init
针对每个字段,设置默认值。这些默认值包括启用核心转储(enable_coredump)、共享内存标识(shm_id)、内存大小(mem_size)、主核心编号(main_core)、内存通道数量(mem_channel)、基本虚拟地址(base_virtaddr)、日志打印级别(print_level)、RPC 服务器地址(rpc_addr)、跟踪信息条目数量(num_entries)、延迟子系统初始化标志(delay_subsystem_init)、禁用信号处理程序标志(disable_signal_handlers)、RPC 允许列表(rpc_allowlist)等
spdk server实现实例
接下来实现一个spdk-server程序,其功能是接收客户端的数据,并将数据发送回客户端
先上伪代码
void spdk_server_callback(){
//接收数据
n = spdk_sock_recv();
if (n < 0) //接收失败
if (n = 0) //连接已被释放
else
spdk_sock_writev //成功接收数据,并发送数据到客户端
}
int main(){
//初始化spdk_app_opts opts
spdk_app_opts_init(&opts,sizeof(opts));
//启动线程执行spdk server 进程
int rc = spdk_app_start(&opts, spdk_server_start, &server_context);
spdk_server_start();
spdk_server_listen();
spdk_sock_listen();// 使用SPDK函数创建服务器监听套接字
//创建一个套接字组(通常使用epoll实现),用于管理多个套接字的事件。套接字组将结果存储在ctx->group中
spdk_sock_group_create();
//一个用于处理客户端连接和通信,另一个用于检查套接字事件。这种设计允许并发处理不同类型的任务,并确保服务器可以同时处理多个连接和事件
SPDK_POLLER_REGISTER(spdk_server_accept,ctx,2000 *1000);
SPDK_POLLER_REGISTER(spdk_server_group_poll,ctx,0);
//释放资源,退出程序
spdk_app_fini();
}
接下来是完整代码
#include "spdk/stdinc.h"
#include "spdk/thread.h"
#include "spdk/env.h"
#include "spdk/event.h"
#include "spdk/string.h"
#include "spdk/log.h"
#include "spdk/sock.h"
#define ADDR_STR_LEN INET6_ADDRSTRLEN
#define BUFFER_SIZE 1024
static char *g_host;
static int g_port;
static char *g_sock_impl_name;
static bool g_running;
struct server_context_t{
char *host;
int port;
char *sock_impl_name;
int bytes_in;
int bytes_out;
struct spdk_sock *sock;
struct spdk_sock_group *group;
};
static void spdk_server_shutdown_callback(void) {
g_running = false;
}
// -H 0.0.0.0 -P 8888
static int spdk_server_app_parse(int ch,char*arg){
printf("spdk_server_app_parse: %s\n",arg);
switch(ch){
case 'H':
g_host = arg;
break;
case 'P':
g_port = spdk_strtol(arg,10);
if(g_port < 0){
SPDK_ERRLOG("Invalid port ID\n");
return g_port;
}
break;
case 'N':
g_sock_impl_name = arg; //-N posix or -N uring
break;
default:
return -EINVAL;
}
return 0;
}
//help
static void spdk_server_app_usage(void){
printf("-H host_addr \n");
printf("-P host_port \n");
printf("-N sock_impl \n");
}
static void spdk_server_callback(void *arg, struct spdk_sock_group *group, struct spdk_sock *sock) {
struct server_context_t *ctx = arg;
char buf[BUFFER_SIZE];
struct iovec iov;
ssize_t n = spdk_sock_recv(sock, buf, sizeof(buf));
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 如果接收操作被阻塞,记录错误信息并返回
SPDK_ERRLOG("spdk_sock_recv 失败,错误码 %d %s\n", errno, spdk_strerror(errno));
return;
}
// 如果接收操作失败,记录错误信息并返回
SPDK_ERRLOG("spdk_sock_recv 失败,错误码:%d %s\n", errno, spdk_strerror(errno));
} else if (n == 0) {
// 如果连接被关闭,记录信息并从套接字组中移除套接字并关闭它
SPDK_NOTICELOG("连接已关闭\n");
spdk_sock_group_remove_sock(group, sock);
spdk_sock_close(&sock);
return ;
} else {
// 如果成功接收数据,记录接收字节数
ctx->bytes_in += n;
iov.iov_base = buf;
iov.iov_len = n;
// 将接收的数据回写到客户端套接字
int n = spdk_sock_writev(sock, &iov, 1);
if (n > 0) {
ctx->bytes_out += n;
}
return ;
}
return ;
}
static int spdk_server_accept(void *arg) {
struct server_context_t *ctx = arg;
char saddr[ADDR_STR_LEN], caddr[ADDR_STR_LEN];
uint16_t sport, cport;
int count = 0;
printf("spdk_server_accept\n");
if (!g_running) {
// 如果服务器未运行,执行相应的处理...
}
while (1) {
// 使用套接字接受客户端连接
struct spdk_sock *client_sock = spdk_sock_accept(ctx->sock);
if (client_sock == NULL) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
// 处理套接字接受错误...
}
break; // 如果没有新连接,跳出循环
}
// 获取客户端的地址和端口信息
int rc = spdk_sock_getaddr(client_sock, saddr, sizeof(saddr), &sport, caddr, sizeof(caddr), &cport);
if (rc < 0) {
// 获取地址信息失败,关闭套接字并返回空闲状态
SPDK_ERRLOG("无法获取连接地址\n");
spdk_sock_close(&ctx->sock);
return SPDK_POLLER_IDLE;
}
// 将客户端套接字添加到套接字组中,并设置回调函数spdk_server_callback
rc = spdk_sock_group_add_sock(ctx->group, client_sock, spdk_server_callback, ctx);
if (rc < 0) {
// 添加套接字到组失败,关闭客户端套接字并返回空闲状态
SPDK_ERRLOG("无法将套接字添加到组\n");
spdk_sock_close(&client_sock);
return SPDK_POLLER_IDLE;
}
count++; // 记录已接受连接的计数
}
// 如果有新连接被接受,返回忙碌状态;否则返回空闲状态
return count > 0 ? SPDK_POLLER_BUSY : SPDK_POLLER_IDLE;
}
static int spdk_server_group_poll(void *arg) {
struct server_context_t *ctx = arg;
// 使用套接字组进行轮询,返回需要处理事件的个数
int rc = spdk_sock_group_poll(ctx->group);
if (rc < 0) {
// 如果轮询失败,记录错误信息
SPDK_ERRLOG("无法轮询套接字组 = %p\n", ctx->group);
}
// 根据轮询结果决定函数的返回状态
return rc > 0 ? SPDK_POLLER_BUSY : SPDK_POLLER_IDLE;
}
//spdk sock
static int spdk_server_listen(struct server_context_t *ctx){
// 使用SPDK函数创建服务器监听套接字
ctx->sock = spdk_sock_listen(ctx->host, ctx->port, ctx->sock_impl_name);
if(ctx->sock == NULL){
// 如果套接字创建失败,则打印错误消息并返回-1
SPDK_ERRLOG("无法创建服务器套接字\n");
return -1;
}
// 创建套接字组,通常使用epoll来实现
ctx->group = spdk_sock_group_create(NULL); // epoll
//设置标志,表示服务器正在运行
g_running = true;
// 注册用于接受连接的轮询器(poller)
SPDK_POLLER_REGISTER(spdk_server_accept, ctx, 2000 * 1000);
// 注册用于套接字组轮询的轮询器(poller),检查套接字状态
SPDK_POLLER_REGISTER(spdk_server_group_poll, ctx, 0);
// 打印调试信息
printf("spdk_server_listen\n");
// 返回0表示成功
return 0;
}
static void spdk_server_start(void *arg){
struct server_context_t *ctx = arg;
printf("spdk_server_start\n");
int rc = spdk_server_listen(ctx);
if(rc){
spdk_app_stop(-1);
}
return ;
}
// ./sdpk_server -H 0.0.0.0 -P 8888 , -N
int main(int argc,char *argv[]){
struct spdk_app_opts opts = {};
//初始化spdk_app_opts opts
spdk_app_opts_init(&opts,sizeof(opts));
opts.name = "spdk_server";
opts.shutdown_cb = spdk_server_shutdown_callback;
printf("spdk_app_parse_args\n");
//解析命令行参数
spdk_app_parse_args(argc,argv,&opts,"H:P:N:SVzZ",NULL,
spdk_server_app_parse,spdk_server_app_usage);
printf("spdk_app_parse_args 11\n");
struct server_context_t server_context = {};
server_context.host = g_host;
server_context.port = g_port;
server_context.sock_impl_name = g_sock_impl_name;
printf("host: %s, port: %d, impl_name: %s\n", g_host, g_port, g_sock_impl_name);
//sdpk_server_start(&server_context);
//启动线程执行spdk server 进程
int rc = spdk_app_start(&opts, spdk_server_start, &server_context);
if (rc) {
SPDK_ERRLOG("Error starting application\n");
}
//释放资源,退出程序
spdk_app_fini();
}
最后看看结果吧
./scripts/setup.sh
./spdk_server -P 8888 -H 0.0.0.0 -N posix
然后可以看到spdk接收 并发送了数据