skynet是基于reactor网络模型的,并且他的网络层使用单线程。因为云风大大认为即使是代码量稍大一些的单线程程序,也会比代码量较小的多线程程序更容易理解,出 bug 的机会也更少。而且经典的网络服务程序,如 redis nginx 并没有因为单线程处理网络 IO 而变现得不堪,反而有不错的口碑。
既然是reactor模式,就必然有同步事件分离器Synchronous Event Demultiplexer,初始分派器Initiation Dispatcher,事件处理器的接口Event Handler。同步事件分离器负责等待新的事件发生。在linux网络模型中,有三种等待网络事件发生的模型,分别是select,poll,epoll。当然skynet选择了最为高效的epoll模型(关于epoll的理解可以参看我的这篇文章:epoll EPOLLL、EPOLLET模式与阻塞、非阻塞)。skynet没有严格的分派器,但是有事件处理器接口。处理epoll事件的函数为:
int
socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more) {
for (;;) {
if (ss->checkctrl) {
if (has_cmd(ss)) { //管道内是否有命令数据
int type = ctrl_cmd(ss, result); //只有SOCKET_DATA,SOCKET_CLOSE,SOCKET_OPEN,SOCKET_ACCEPT等返回的不是-1 listen命令是
if (type != -1) {
clear_closed_event(ss, result, type);
return type;
} else
continue;
} else {
ss->checkctrl = 0;
}
}
if (ss->event_index == ss->event_n) { //刚开始或者所有消息都已处理
ss->event_n = sp_wait(ss->event_fd, ss->ev, MAX_EVENT);
ss->checkctrl = 1;
if (more) {
*more = 0;
}
ss->event_index = 0;
if (ss->event_n <= 0) {
ss->event_n = 0;
if (errno == EINTR) {
continue;
}
return -1;
}
}
struct event *e = &ss->ev[ss->event_index++];
struct socket *s = e->s;
if (s == NULL) {
// dispatch pipe message at beginning
continue;
}
struct socket_lock l;
socket_lock_init(s, &l);
switch (s->type) {
case SOCKET_TYPE_CONNECTING:
return report_connect(ss, s, &l, result);
case SOCKET_TYPE_LISTEN: { //有客户端连接消息
int ok = report_accept(ss, s, result);
if (ok > 0) {
return SOCKET_ACCEPT;
} if (ok < 0 ) {
return SOCKET_ERR;
}
// when ok == 0, retry
break;
}
case SOCKET_TYPE_INVALID:
fprintf(stderr, "socket-server: invalid socket\n");
break;
default: //一般是读,写,错误消息
if (e->read) {
int type;
if (s->protocol == PROTOCOL_TCP) { //有tcp协议的数据触发
type = forward_message_tcp(ss, s, &l, result);
} else {
type = forward_message_udp(ss, s, &l, result);
if (type == SOCKET_UDP) {
// try read again
--ss->event_index;
return SOCKET_UDP;
}
}
if (e->write && type != SOCKET_CLOSE && type != SOCKET_ERR) {
// Try to dispatch write message next step if write flag set.
e->read = false;
--ss->event_index;
}
if (type == -1)
break;
return type;
}
if (e->write) {
int type = send_buffer(ss, s, &l, result);
if (type == -1)
break;
return type;
}
if (e->error) {
// close when error
int error;
socklen_t len = sizeof(error);
int code = getsockopt(s->fd, SOL_SOCKET, SO_ERROR, &error, &len);
const char * err = NULL;
if (code < 0) {
err = strerror(errno);
} else if (error != 0) {
err = strerror(error);
} else {
err = "Unknown error";
}
force_close(ss, s, &l, result);
result->data = (char *)err;
return SOCKET_ERR;
}
if(e->eof) {
force_close(ss, s, &l, result);
return SOCKET_CLOSE;
}
break;
}
}
}
当有网络事件触发时,我们会填充一个事件event的指针:
static int
sp_wait(int efd, struct event *e, int max) {
struct epoll_event ev[max];
int n = epoll_wait(efd , ev, max, -1);
int i;
for (i=0;i<n;i++) {
e[i].s = ev[i].data.ptr;
unsigned flag = ev[i].events;
e[i].write = (flag & EPOLLOUT) != 0;
e[i].read = (flag & (EPOLLIN | EPOLLHUP)) != 0;
e[i].error = (flag & EPOLLERR) != 0;
e[i].eof = false;
}
return n;
}
epoll得到的data.ptr是在事先填充好的,他是在把socket fd纳入epoll监管中填充的,前篇文章已经讲了监听socket和连接socket纳入epoll监管的时机,参见:skynet网络部分剖析(一) socket的状态。
得到了事件event,根据event是否可读可写或者有错误,我们就好处理事件了。这里没有像其他项目一样调用回调函数,而是就地调用处理函数。例如有tcp数据时调用forward_message_tcp。但是处理写数据却和其他的项目不一样。
skynet是面向lua脚本写逻辑的。lua逻辑调用写时socket.write(id, buf)会先发送到管道中,与epoll处理数据不在一个线程里。使用管道的好处是,其他线程向socket线程发送数据包,socket线程能够像处理网络消息一样,处理来自其他线程的请求,并且完全不用加任何锁,保证了线程安全,也简化了逻辑复杂度。当然管道也在epoll的监管之中。socket_server.c的ctrl_cmd函数就是用来处理管道消息的。当收到发送消息时,在这里处理了发送消息逻辑,只有当发送缓冲区满时才会保留发送缓存并添加可写的事件到epoll,这样epoll就可以处理发送缓存消息了。
欢迎加入QQ群 858791125 讨论skynet,游戏后台开发,lua脚本语言等问题。
参看:
https://blog.codingnow.com/2017/06/skynet_socket.html
https://blog.csdn.net/u010168160/article/details/53019039
https://www.jianshu.com/p/eef7ebe28673