1、背景
Nanomsg 库中的 usock 模块(src/core/usock.h 和 usock_posix.c)提供了一套优雅的异步 socket 抽象,基于状态机和 worker 线程模型,让上层协议无需关心底层 I/O 的复杂性。其设计目标是:
- 完全异步:所有操作(connect、accept、send、recv)均不阻塞调用线程
- 统一事件接口:通过状态机(FSM)向上层报告事件(连接成功、数据到达、错误等)
- 高效:优先尝试同步非阻塞操作,只在必要时才交给 worker 线程等待
- 跨平台:在 Windows 和 POSIX 系统上有对应实现
2、核心数据结构和函数
struct nn_usock 是一个较大的结构体,包含了状态机基类、底层 socket fd、worker 线程引用、I/O 缓冲区、任务和事件等成员,其定义如下:
c
struct nn_usock {
/* State machine base class. */
struct nn_fsm fsm; // 状态机基类,实现异步事件驱动,用于管理socket的状态和事件
int state; // 当前socket的状态
/* The worker thread the usock is associated with. */
struct nn_worker *worker; // 该socket所属的工作线程
/* The underlying OS socket and handle that represents it in the poller. */
int s; // 底层操作系统socket描述符
struct nn_worker_fd wfd; // 该socket在工作线程poller中的句柄,是对s的包装
/* Members related to receiving data. */
struct {
/* The buffer being filled in at the moment. */
uint8_t *buf; // 当前接收缓冲区
size_t len; // 缓冲区长度
/* Buffer for batch-reading inbound data. */
uint8_t *batch; // 批量读取数据的缓冲区
/* Size of the batch buffer. */
size_t batch_len; // 批量读取缓冲区长度
/* Current position in the batch buffer. The data preceding this
position were already received by the user. The data that follow
will be received in the future. */
size_t batch_pos; // 批量读取缓冲区当前位置
/* File descriptor received via SCM_RIGHTS, if any. */
int *pfd; // 用于接收带外文件描述符(SCM_RIGHTS), 这是做什么用的???
} in;
/* Members related to sending data. */
struct {
/* msghdr being sent at the moment. */
struct msghdr hdr; // 当前正在发送的消息头
/* List of buffers being sent at the moment. Referenced from 'hdr'. */
struct iovec iov [NN_USOCK_MAX_IOVCNT]; // 当前正在发送的缓冲区列表
} out;
/* Asynchronous tasks for the worker. */
// nn_worker_task 只是任务描述,并不是任务逻辑
// 任务描述本身并不包含任务逻辑,而是通过状态机和事件驱动机制来执行具体的任务逻辑
struct nn_worker_task task_connecting; // 对应连接任务
struct nn_worker_task task_connected; // 对应连接完成任务
struct nn_worker_task task_accept; // 对应accept任务
struct nn_worker_task task_send; // 对应发送任务
struct nn_worker_task task_recv; // 对应接收任务
struct nn_worker_task task_stop; // 对应停止任务
/* Events raised by the usock. */
struct nn_fsm_event event_established; // 连接建立事件
struct nn_fsm_event event_sent; // 发送完成事件
struct nn_fsm_event event_received; // 接收完成事件
struct nn_fsm_event event_error; // 错误事件
/* In ACCEPTING state points to the socket being accepted.
In BEING_ACCEPTED state points to the listener socket. */
// asock 是一个指针,在 ACCEPTING 状态下指向被 accept 的 socket,
// 在 BEING_ACCEPTED 状态下指向监听 socket
struct nn_usock *asock; // 指向正在被accept的socket,或者指向监听socket
/* Errno remembered in NN_USOCK_ERROR state */
int errnum; // 最近一次错误码
};
3、核心API解析
3.1、初始化和启动
c
void nn_usock_init(struct nn_usock *self, int src, struct nn_fsm *owner);
int nn_usock_start(struct nn_usock *self, int domain, int type, int protocol);
void nn_usock_start_fd(struct nn_usock *self, int fd);
void nn_usock_stop(struct nn_usock *self);
void nn_usock_async_stop (struct nn_usock *self)
这里需要关注的是,nn_usock_start_fd 函数末尾多调用了 nn_fsm_action(&self->fsm, NN_USOCK_ACTION_STARTED),而 nn_usock_start 没有,原因在于两者对文件描述符的预期状态不同:
- nn_usock_start,创建一个全新的 socket,此时它没有任何连接或绑定,需要等待上层进一步调用 nn_usock_listen、nn_usock_connect 等才能进入相应状态。因此状态机启动后停留在 STARTING 状态,不发送 ACTION_STARTED,否则会错误地将其直接转为 ACTIVE,虽然nn_usock_start 函数中确实没有直接对 self->state 赋值,因为状态机的状态转换是由 nn_fsm_start 触发的内部事件驱动的
- nn_usock_start_fd,接收一个已存在的文件描述符(例如通过 accept 得到的已连接 socket,或外部传入的已连接套接字)。该 fd 已经处于可读写状态,无需再经过绑定、监听或连接步骤。通过发送 NN_USOCK_ACTION_STARTED 动作,触发状态机从 STARTING 转换到 ACTIVE(参见状态机中 STARTING 状态对此动作的处理:将 fd 加入 worker 并切换到 ACTIVE 状态),从而立即进入数据传输阶段
除了这两个API需要注意,还需要注意的是: - nn_usock_stop,调用 nn_fsm_stop 启动状态机的正常停止流程。这是一个同步请求(但停止过程本身可能是异步的,比如等待 worker 清理资源)。它不会立即向上层发送 NN_USOCK_SHUTDOWN 事件,而是等待状态机完全进入 IDLE 状态后,才会通过 nn_fsm_stopped 上报 NN_USOCK_STOPPED。适用于外部主动关闭 socket 的正常路径
- nn_usock_async_stop,是一个内部使用的立即停止函数。它做了两件事:1、worker 提交 task_stop 任务(异步清理底层 fd 和 poller 注册);2、立即通过 nn_fsm_raise 向上层父状态机发送 NN_USOCK_SHUTDOWN 事件(封装在 event_error 中)。这样上层能马上知道 socket 已经"逻辑上停止",无需等待 worker 实际完成清理。常用于错误处理(如连接断开时快速通知上层)或状态机内部需要快速关闭并上报的场景
3.2、设置选项和地址
c
int nn_usock_setsockopt(struct nn_usock *self, int level, int optname,
const void *optval, size_t optlen);
int nn_usock_bind(struct nn_usock *self, const struct sockaddr *addr, size_t addrlen);
int nn_usock_listen(struct nn_usock *self, int backlog);
这些函数只能在 STARTING 或 ACCEPTED 状态下调用,保证 socket 尚未进入活跃 I/O
3.3、nn_usock_connect
c
void nn_usock_connect (struct nn_usock *self, const struct sockaddr *addr,
size_t addrlen)
{
int rc;
/* Notify the state machine that we've started connecting. */
// 告诉self中的fsm,这个socket正在尝试连接,socket的状态会被设置为NN_USOCK_STATE_CONNECTING
nn_fsm_action (&self->fsm, NN_USOCK_ACTION_CONNECT);
/* Do the connect itself. */
rc = connect (self->s, addr, (socklen_t) addrlen);
/* Immediate success. */
if (nn_fast (rc == 0)) {
nn_fsm_action (&self->fsm, NN_USOCK_ACTION_DONE);
return;
}
/* Immediate error. */
if (nn_slow (errno != EINPROGRESS)) {
self->errnum = errno;
nn_fsm_action (&self->fsm, NN_USOCK_ACTION_ERROR);
return;
}
/* Start asynchronous connect. */
nn_worker_execute (self->worker, &self->task_connecting);
}
- 调用非阻塞 connect。若立即成功,状态机直接进入 ACTIVE 并上报 CONNECTED 事件,这里其实是报给了父状态机
- 若返回 EINPROGRESS,则提交 task_connecting 给 worker,将 fd 加入 poller 并关注写事件。连接完成后 worker 触发 FD_OUT,状态机检查 SO_ERROR 并上报结果。这里其实是通过下面的代码完成设置的
c
case NN_USOCK_SRC_TASK_CONNECTING:
nn_assert (type == NN_WORKER_TASK_EXECUTE);
// 设置对写事件(OUT)的关注。对于非阻塞 connect,连接成功或失败时,socket 会变得可写(或产生错误事件)。
// worker 通过写就绪事件来通知 usock 状态机,从而在 CONNECTING 状态中检查连接结果(通过 getsockopt(SO_ERROR) 判断是否成功
nn_worker_add_fd (usock->worker, usock->s, &usock->wfd);
nn_worker_set_out (usock->worker, &usock->wfd);
3.4、nn_usock_accept
c
// self:用于存放新连接的 nn_usock 对象(调用前通常处于空闲状态)
// listener:已经处于监听状态(LISTENING)的 nn_usock 对象
void nn_usock_accept (struct nn_usock *self, struct nn_usock *listener)
{
int s;
/* Start the actual accepting. */
// 如果 self 处于空闲状态,则启动其状态机,并发送 NN_USOCK_ACTION_BEING_ACCEPTED 动作,使其进入 BEING_ACCEPTED 状态
if (nn_fsm_isidle(&self->fsm)) {
nn_fsm_start (&self->fsm);
nn_fsm_action (&self->fsm, NN_USOCK_ACTION_BEING_ACCEPTED);
}
// 让监听 socket 从 LISTENING 状态进入 ACCEPTING 状态,表示正在等待新的连接
nn_fsm_action (&listener->fsm, NN_USOCK_ACTION_ACCEPT);
/* Try to accept new connection in synchronous manner. */
// 使用 accept 或 accept4 尝试立即接受一个连接。监听 socket 已被设为非阻塞模式,因此如果没有新连接会返回 EAGAIN/EWOULDBLOCK
#if NN_HAVE_ACCEPT4
s = accept4 (listener->s, NULL, NULL, SOCK_CLOEXEC);
if ((s < 0) && (errno == ENOTSUP)) {
/* Apparently some old versions of Linux have a stub for this in libc,
without any of the underlying kernel support. */
s = accept (listener->s, NULL, NULL);
}
#else
s = accept (listener->s, NULL, NULL);
#endif
/* Immediate success. */
// 成功获得新的文件描述符 s >= 0,则走同步成功路径
if (nn_fast (s >= 0)) {
/* Disassociate the listener socket from the accepted
socket. Is useful if we restart accepting on ACCEPT_ERROR */
// 清除双方的 asock 指针(配对关系解除)
listener->asock = NULL;
self->asock = NULL;
nn_usock_init_from_fd (self, s);
// 通知监听 socket 接受完成(ACTION_DONE),使其回到 LISTENING 状态
nn_fsm_action (&listener->fsm, NN_USOCK_ACTION_DONE);
// 通知被接受 socket 完成(ACTION_DONE),使其从 BEING_ACCEPTED 进入 ACCEPTED 状态
nn_fsm_action (&self->fsm, NN_USOCK_ACTION_DONE);
return;
}
/* Detect a failure. Note that in ECONNABORTED case we simply ignore
the error and wait for next connection in asynchronous manner. */
// EAGAIN / EWOULDBLOCK:没有待处理的连接,最常见。
// ECONNABORTED:连接被对方提前中止,可忽略。
// 资源不足错误(ENFILE、EMFILE、ENOBUFS、ENOMEM),需要特殊处理
errno_assert (errno == EAGAIN || errno == EWOULDBLOCK ||
errno == ECONNABORTED || errno == ENFILE || errno == EMFILE ||
errno == ENOBUFS || errno == ENOMEM);
/* Pair the two sockets. They are already paired in case
previous attempt failed on ACCEPT_ERROR */
nn_assert (!self->asock || self->asock == listener);
self->asock = listener;
nn_assert (!listener->asock || listener->asock == self);
listener->asock = self;
/* Some errors are just ok to ignore for now. We also stop repeating
any errors until next IN_FD event so that we are not in a tight loop
and allow processing other events in the meantime */
// 这里处理的是资源不足错误(ENFILE、EMFILE、ENOBUFS、ENOMEM)
if (nn_slow (errno != EAGAIN && errno != EWOULDBLOCK
&& errno != ECONNABORTED && errno != listener->errnum))
{
listener->errnum = errno;
listener->state = NN_USOCK_STATE_ACCEPTING_ERROR;
nn_fsm_raise (&listener->fsm,
&listener->event_error, NN_USOCK_ACCEPT_ERROR);
return;
}
/* Ask the worker thread to wait for the new connection. */
// 这里最终会走到nn_internal_tasks函数,设置worker中epoll相关句柄属性,等待accept事件唤醒
nn_worker_execute (listener->worker, &listener->task_accept);
}
大体的思路是,listener 必须处于 LISTENING 状态,self 一般是空闲的;先尝试同步 accept,若成功则直接初始化 self 并通知双方;若失败(EAGAIN 等),则配对两个 socket,提交 task_accept 给 worker,监听读事件。当新连接到达时,worker 触发 FD_IN,状态机调用 accept 完成后续工作。
3.5、nn_internal_tasks
c
// 处理从 worker 线程反馈到 FSM 的内部任务事件
static int nn_internal_tasks (struct nn_usock *usock, int src, int type)
{
/******************************************************************************/
/* Internal tasks sent from the user thread to the worker thread. */
/******************************************************************************/
switch (src) {
case NN_USOCK_SRC_TASK_SEND:
nn_assert (type == NN_WORKER_TASK_EXECUTE);
nn_worker_set_out (usock->worker, &usock->wfd);
return 1;
case NN_USOCK_SRC_TASK_RECV:
nn_assert (type == NN_WORKER_TASK_EXECUTE);
nn_worker_set_in (usock->worker, &usock->wfd);
return 1;
case NN_USOCK_SRC_TASK_CONNECTED:
nn_assert (type == NN_WORKER_TASK_EXECUTE);
nn_worker_add_fd (usock->worker, usock->s, &usock->wfd);
return 1;
case NN_USOCK_SRC_TASK_CONNECTING:
nn_assert (type == NN_WORKER_TASK_EXECUTE);
// 设置对写事件(OUT)的关注。对于非阻塞 connect,连接成功或失败时,socket 会变得可写(或产生错误事件)。
// worker 通过写就绪事件来通知 usock 状态机,从而在 CONNECTING 状态中检查连接结果(通过 getsockopt(SO_ERROR) 判断是否成功
nn_worker_add_fd (usock->worker, usock->s, &usock->wfd);
nn_worker_set_out (usock->worker, &usock->wfd);
return 1;
case NN_USOCK_SRC_TASK_ACCEPT:
// 它将监听 socket 加入 worker 的监控集合并开启读事件监听。
// 之后当新连接到达时,worker 通过 FD_IN 事件触发真正的 accept 调用,完成整个异步 accept 流程
nn_assert (type == NN_WORKER_TASK_EXECUTE);
nn_worker_add_fd (usock->worker, usock->s, &usock->wfd);
nn_worker_set_in (usock->worker, &usock->wfd);
return 1;
}
return 0;
}
3.6、异步发送和接收
c
void nn_usock_send(struct nn_usock *self, const struct nn_iovec *iov, int iovcnt);
void nn_usock_recv(struct nn_usock *self, void *buf, size_t len, int *fd);
- 发送:先调用 sendmsg 尝试发送全部数据。若成功,立即上报 SENT;若遇到 EAGAIN,则提交 task_send,等待可写事件后继续发送
- 接收:先尝试从批量缓冲区或直接 recvmsg 读取数据。若一次读满,立即上报 RECEIVED;否则记录剩余位置,提交 task_recv,等待可读事件后继续填充
4、读懂这个模块需要区分事件和任务
- 事件(nn_fsm_event):由 usock 发出给上层状态机,如 NN_USOCK_CONNECTED、NN_USOCK_RECEIVED、NN_USOCK_ERROR。它们通过 nn_fsm_raise 传递
- 任务(nn_worker_task):由上层调用 API 时提交给 worker,用于执行将 fd 加入 poller、设置关注事件等操作。任务执行完成后,worker 通过事件通知 usock