Nanomsg中的usock:一个高效的异步 Socket 封装

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
相关推荐
哎呦,帅小伙哦8 天前
Nanomsg中间件utils中部分工具学习记录
学习·中间件·nanomsg
哎呦,帅小伙哦7 个月前
Nanomsg库CMakeLists.txt文件阅读笔记
cmakelist.txt·nanomsg