内容纲要

1. 异步 IO

skynet 用 C 编写的 sokcet 模块使用异步回调机制,通过 lualib-src/lua-socket.c 导出为 socketdriver 模块。skynet socket C API 使用的异步回调方式是:在启动 socket 时,记录当前服务 handle,之后该 socket 上面的消息 (底层使用 epoll 机制) 通过 skynet 消息的方式发往该服务。这里的当前服务指的是 socket 启动时所在的服务,对于被请求方来说,为调用socketdriver.start(id)的服务,对于请求方来说,为调用socketdriver.connect(addr,port)的服务。skynet 不使用套接字 fd 在上层传播,因为在某些系统上 fd 的复用会导致上层遇到麻烦,skynet socket C API 为每个 fd 分配一个 ID,是自增不重复的。

socket C API 的核心是三个 poll:

socket poll

位于 skynet-src/socket_poll.h 底层异步 IO,监听可读可写状态,对于 linux 系统,使用的是 epoll 模型。

socket_server_poll

位于 skynet-src/socket_server.c 使用 socket poll,处理所有套接字上的 IO 事件和控制事件。socket_server_poll 处理这些事件,并返回处理结果 (返回一个 type 代表事件类型,通过 socket_message* result 指针参数返回处理结果)。

IO 事件主要包括可读,可写,新连接到达,连接成功。对于可读事件,socket_server_poll 会读取对应套接字上的数据,如果读取成功,返回 SOCKET_DATA 类型,并且通过 result 参数返回读取的 buffer。同样对于可写事件,会尝试发送缓冲区中的数据,并返回处理结果。

而控制事件指的是上层调用,由于 skynet 上层使用的是一个 id 而不是 socket fd 来代表一个套接字。skynet 在该 id 上做的所有操作 (如设置套接字属性,接受连接,关闭连接,发送数据等等) 都会被写入特殊的 ctrl 套接字 (recvctrl_fd sendctrl_fd),这些 ctrl fd 位于 socket_server 结构中,是唯一的,因此写入 ctrl 的控制信息要包括被操作的套接字 ID。这些控制信息统一通过 socket poll 来处理。再在 socket_server_poll 中,根据 id 提出对应的 socket fd 来完成操作。

skynet_socket_poll

通过 socket poll 和 socket_server_poll,此时数据已就绪,新连接也已经被接受,需要通知上层处理这些数据,而 skynet_socket_poll 就是来完成这些工作的。它调用 socket_server_poll,根据其返回的 type 和 result 来将这些套接字事件发送给套接字所属服务 (服务 handle 已由 socket_server_poll 填充在 result->opaque 字段中)。skynet_socket_poll 将 socket_message* result 和 type 字段组装成 skynet_socket_message,并且通过 skynet_message 消息发送给指定服务,消息类型为 PTYPE_SOCKET。这样一次异步 IO 就完成了。

skynet_socket_poll 通过一个单独的线程跑起来,线程入口为_socket 函数,位于 skynet-src/skynet_start.c。

2. lua 层封装

skynet socket C API 提供的是异步 IO,为方便使用,在 lua 层提供了一个 socket(lualib/socket.lua) 模块来实现阻塞读写。该模块是对 socketdriver 的封装。它通过 lua 协程模拟阻塞读写。

和 gateserver 模块一样,socket 模块对 PTYPE_SOCKET 类型的消息进行了注册处理,它使用 socketdriver.unpack 作为该类型消息的 unpack 函数。socketdriver.unpack 并不进行实际的分包,它只解析出原始数据,socket 模块会缓存套接字上收到的数据。缓存结构由 socketdriver 提供。当调用 socket.readline 时,将通过 socketdriver.readline 尝试从缓冲区中读取一行数据。如果缓冲区数据不足,则挂起自身,待数据足够时唤醒。虽然底层仍然是异步,但是由于协程的特性,对上层体现为同步。通过 socket 模块的 API 读到的数据可以看做原始数据。

3. 消息分包

大多数时候,在收到套接字数据时,要按照消息协议进行消息分包。skynet 提供一个 netpack 库用于处理分包问题,netpack 由 C 编写,位于 lualib-src/lua-netpack.c。skynet 范例使用的包格式是两个字节的消息长度 (Big-Endian) 加上消息数据。netpack 根据包格式处理分包问题,netpack 提供一个netpack.filter(queue, msg, size)接口,它返回一个 type(“data”, “more”, “error”, “open”, “close”) 代表具体 IO 事件,其后返回每个事件所需参数。

对于 SOCKET_DATA 事件,filter 会进行数据分包,如果分包后刚好只有一条完整消息,filter 返回的 type 为”data”,其后跟 fd msg size。如果不止一条消息,那么消息将被依次压入 queue 参数中,并且仅返回一个 type 为”more”。queue 是一个结构体指针,可以通过netpack.pop弹出 queue 中的一条消息。

其余 type 类型”open”,”error”, “close” 分别对应于 socket_message 中的 SOCKET_ACCEPT SOCKET_ERROR SOCKET_CLOSE 事件。netpack 的使用者可以通过 filter 返回的 type 来进行事件处理。

netpack 会尽可能多地分包,交给上层。并且通过一个哈希表保存每个套接字 ID 对应的粘包,在下次数据到达时,取出上次留下的粘包数据,重新分包。

发表评论

电子邮件地址不会被公开。 必填项已用*标注