skynet网络基础剖析

千呼万唤始出来,终于有时间开始分析skynet网络部分的代码了。skynet是个网络服务器框架,网络才是他的核心所在,读懂了网络模块的代码才算是对skynet有深入的了解。

花了两天时间读了网络部分的代码,底层网络模型是基于epoll的,这个在llinux高并发上是最高效的模型。底层网络的各种操作或响应(诸如accept,msg,close)也是通过发送消息告知lua层的回调函数,这点与前面讲的服务之间通信统一起来了。

网络服务这块有个很重要的数据结构,socket_server,他的定义如下:

struct socket_server {
	int recvctrl_fd;
	int sendctrl_fd;
	int checkctrl;
	poll_fd event_fd;
	int alloc_id;
	int event_n;
	int event_index;
	struct socket_object_interface soi;
	struct event ev[MAX_EVENT];
	struct socket slot[MAX_SOCKET];
	char buffer[MAX_INFO];
	uint8_t udpbuffer[MAX_UDP_PACKAGE];
	fd_set rfds;
}

比较重要的有epoll句柄event_fd,管道相关的recvctrl_fd,sendctrl_fd,socket池等等。

他的一个对象SOCKET_SERVER,始终贯穿所有网络服务。他是在进程开始,初始化各个模块时创建的。

网络服务模块通常会有一个大的循环来读取网络消息,skynet也不例外,socket_server_poll函数就是来干这事的。在这个循环中将会有两个不同来源的消息系统,一个是管道消息,另一个则是网络消息了。管道消息后面会提到。网络消息是通过epoll模型的epoll_wait来读取的,采用默认的水平触发模式,这样连续读取数据较为简单。

由于网络部分比较复杂,我通过一个小示例来说明一个典型的网络通信是如何开展的,为了简化消息过程,我直接在bootstrap里面写了代码,只是简单的监听某个端口号,然后等待连接和接收数据,代码如下:

local skynet = require "skynet"
local socket = require "socket"

skynet.start(function ()
    local id = socket.listen('127.0.0.1', 8876)

    socket.start(id, function(id, addr)
            print('connnect from '..addr..' '..id)
            socket.start(id)
            local ret = socket.read(id)
        end
        )
end)

socket.listen会调用c层的listen函数,c层要做的是绑定并监听了端口号,然后发送了一个监听相关的消息。消息发送给谁,发送的什么内容?

消息发送给管道的一端,即socket_server的sendctrl_fd结构体。什么是管道呢?管道有两端,往一头发送消息,另一头则可以获取消息。为什么要发送给管道呢?为了统一接收和管理,在网络消息循环socket_server_poll函数中有对这个管道消息的读取。读取分为两步,首先读取类型,然后读取数据,例如上面的listen,他会发送type为'L',数据包含listen_fd,socket的唯一标识id等数据。解析这个数据会创建一个新的socket 文件描述符。说新建也不是很准确,因为他是从socket_server里的socket 结构体池中取出的,类似于线程池,内存池等等,都是为了效率或方便统一管理。

这个socket结构体有哪些字段?文件描述符字段fd肯定是有的,还有一个id字段,用于lua层的的标识,所以这个id是全局唯一的,他通过reserve_id(socket_server) 递增id获得。还有type字段,用来表明这个socket的状态,如监听状态,已连接状态。

此时,要监听socket的type为SOCKET_TYPE_PLISTEN(2),然后返回那个唯一标识id给lua层。lua层必须调用socket.start开启这个监听socket才能继续后面的网络服务。lua层的socket.start()又是调用c层start()。类似于listen,他也给管道发送了相应的消息数据,包括id,通过这个可以获得其对应的socket。我们可以在start函数里设置一个回调函数,这个回调函数是与id号相关联的。然后当前协程挂起,为什么挂起?因为他在等要监听的socket变为监听状态。

在socket_server_poll消息循环中,管道收到start这个消息后,将会调用start_socket(),他会给这个socket注册epoll事件,目前只有读事件。相当于就把这个socket纳入epoll的监听中了。此时的socket type变为SOCKET_TYPE_LISTEN(3),也就是进入真正的监听状态。socket_server_poll返回SOCKET_OPEN(2),这个时候给消息队列发送一个SKYNET_SOCKET_TYPE_CONNECT(2)的消息,连接状态变为true,lua层收到之后就会恢复之前的协程。

我们用一个客户端(随便找一个tcp调试工具,或者用python写简单的连接即可)连接这个socket,epoll_wait会返回数据。由于之前socket type为SOCKET_TYPE_LISTEN(3),那么他会调用report_accept。在这里就会正在的调用socket api accept,返回一个新的socket fd。根据这个新的fd再从SOCKET_SERVER的socket池中获得一个socket对象,产生一个新的全局唯一id,将这个socket设为非阻塞模式等等操作。这个新的socket type为SOCKET_TYPE_PACCEPT(7)。socket_server_poll返回SOCKET_ACCEPT(3),这样给消息队列发送一条SKYNET_SOCKET_TYPE_ACCEPT(4)的消息,该消息中包含了新连接socket的id号。注意是针对监听socket关联的消息队列,而不是新的socket。

lua层收到SKYNET_SOCKET_TYPE_ACCEPT(4)的消息之后,会根据id号找到之前关联的回调函数,去执行。我们看到在这个回调函数中我们继续start(new id ),过程与上面的start监听socket一样,作用是把新连接的socket加入到epoll读的监控之中。我们注意到start函数给队列发送SKYNET_SOCKET_TYPE_CONNECT(2)这个消息,连接状态变为true。这里的连接是指socket加入epoll的监控之中,而不是客服端连接的意思,因为这个适用于那个监听的socket。

之后就是读数据。客服端发送数据,我们正确的读取了数据。关于数据的发送和读取也是比较复杂,我们下篇再讲。

总结一下,为什么listen的过程有点小复杂,曲折,还需要发送'L'指令,然后经lua层的start调用才真正开始监听。我的理由是,方便lua层的逻辑业务编写。因为脚本语言适合回调而不适合自上而下的函数调用,也可以把第一个socket.start理解为socket.accept(id, func)。socket的绑定,监听,accept等待都是lua层发送指令来驱动的,c底层不会主动去干这些事情。建立连接后才会主动接收数据,并保存在缓存中。

短短的一篇文章,差不到写了快一整天,剖析原理实在是辛苦,因为每个细节都要去推敲,保证正确性。希望我能坚持,继续加油。










猜你喜欢

转载自blog.csdn.net/zxm342698145/article/details/80421581