掌握框架的开发思路
skynet 设计原理总结
actor 内部若涉及多线程应考虑加自旋锁或原子操作;避免在工作线程执行过程中被切换;
actor 内部若涉及多线程应考虑临界区域操作不能过于耗时;避免长期占用工作线程让同消息队列中其他消息得不到及时执行;
actor 单个消息业务应避免阻塞线程(注意不是协程)的操作;如果这个操作是必不可少,另起一个外部进程,skynet 进程用 socket 与之通信;这种阻塞或者耗时操作的任务交由外部服务来处理;
我们首先要思考几个问题?
事件与消息的关系?
消息与actor如何绑定?具体接口绑定流程?
消息与协程的关系?
我们看到上图,事件控制,epoll_ctrl是生成注册销毁更改一个事件,然后事件循环通过epoll_wait去阻塞调用我们拿到的数据,我们利用循环依次去处理。
io操作主要是连接的建立与断开的功能
skynet是单reactor的封装;是一个异步事件
reactor由io多路复用与非阻塞io
抓住actor,消息和协程
// 在 linux 系统中,采用 epoll 来检测管理网络事件;
int epoll_create(int size);
// 对红黑树进行增删改操作
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
通过epoll_ctl设置struct epoll_event中data.ptr = (struct socket *)ud;来完成fd与actor绑定
skynet通过socket.start(fd, func)来完成actor与fd的绑定;
网络单线程的封装
这个s就是我们的socket_s,再往里边进去我们就把这个ud传进去了,这就是actor的绑定
如果有读事件,把事件进行转发
发现是读事件,就会把读出来的数据放到result当中去,推到相应的actor中去,读事件的触发,从网络当中读取数据然后包装成一个消息再推到消息队列当中。
这样就实现了actor与fd的绑定,当事件触发,做相应的io操作,把处理的结果以及事件包装成消息,推到相应的actor消息队列
一个事件循环可以拿到很多消息,消息里边有很多种类型
从网络当中拿到的事件类型
消息类型解读
这里先创建一个协程,yeild跳出协程,resume唤醒协程
协程挂起,然后唤醒协程将arg作为参数传出去
读固定数量,读分隔符
这就是读固定数量
这里会进入一个条件,表示什么时候把协程唤醒
然后协程让出,这个wait里边调用的接口就是yeild
skynet.wait这里边有个sleep休眠,就是那个suspend_sleep
里边有个coroutine_yeild “SUSPEND”,这就是协程的让出
然后唤醒是要从读数据那里去唤醒,什么地方去唤醒呢,当然是那个poll,数据到达。
最后是这个read里边的suspend这里被唤醒了,然后就会从里边读数据,然后返回
读写端都关闭
我们要关闭一个端,我们就要使用shutdown()
当我们的c调用shutdown把写端关闭的时候,服务器对应的是把读端关闭,read=0就是读端关闭。
read关闭,shutdown(写)马上标记写端关闭,此时会发生什么,会收到epollhub,也就是当s把fin发出去的时候我们就会收到epollhub。
close(fd)这一步只是把所属的资源清除掉了,协议栈还是工作正常的,只是这个消息不能传输到用户层去了,这个时候协议栈会把读写都关闭,这里也会收到epollhub,这里不是调用close后立马收到,而是要等一会才收到
// 事件:1. EPOLLHUP 读写段都关闭
e[i].eof = (flag & EPOLLHUP) != 0;
// 处理:直接关闭并向 actor 返回事件 SOCKET_CLOSE
int halfclose = halfclose_read(s);
force_close(ss, s, &l, result);
if (!halfclose) {
// 如果前面因为关闭读端已经发送 SOCKET_CLOSE,在这里避免重复
SOCKET_CLOSE
return SOCKET_CLOSE;
}
读端关闭
int n = (int)read(s->fd, buffer, sz);
// 事件:2. 读端关闭 注意:EPOLLRDHUP 也可以检测,但是这个 read = 0 更为及时;因为事件 处理先处理读事件,再处理异常事件
if (n == 0) { if (s->closing) { // 如果该连接的 socket 已经关闭
// Rare case : if s->closing is true, reading event is disable, and SOCKET_CLOSE is raised.
if (nomore_sending_data(s)) {
force_close(ss,s,l,result);
}
return -1;
}
int t = ATOM_LOAD(&s->type);
if (t == SOCKET_TYPE_HALFCLOSE_READ) {
// 如果已经处理过读端关闭
// Rare case : Already shutdown read.
return -1;
}
if (t == SOCKET_TYPE_HALFCLOSE_WRITE)
{ // 如果之前已经处理过写端关闭,则直接 close
// Remote shutdown read (write error) before.
force_close(ss,s,l,result);
} else {
// 如果之前没有处理过,则只处理读端关闭
close_read(ss, s, result);
}
return SOCKET_CLOSE;
}
写端关闭
for (;;) {
ssize_t sz = write(s->fd, tmp->ptr, tmp->sz);
if (sz < 0) {
switch(errno) {
case EINTR: continue;
case AGAIN_WOULDBLOCK:
return -1;
}// sz < 0 && errno = EPIPE, fd is connected to a pipe or socket whose reading end is closed. // 在这里的处理是只要sz < 0,且不是被中断打断以及写缓冲满的情况下,直接关闭本地 写端
return close_write(ss, s, l, result);
}...
}
服务设置(这里的服务就是我们的actor)
服务的确定需要考虑以下几点:
功能独立性,可独立测试;这也是分布式开发要考虑的
需要大致估计它的运算程度;如果密集计算,需要考虑拆分成多个服务;功能抽象与算力抽象
需要考虑lua gc 压力;通常服务中存放的对象数据越多,gc 压力越大;
服务拆分
如果一个服务涉及的功能太多,不能用简单案例来测试的时候,那么服务设置有问题,此时要按功能拆分;
如果某个服务功能检测,也可以用简单案例来测试,但是计算比较密集,此时要将该服务拆分成核心数*N个服务;
设置核心数,是希望它们调度的时候能得到公平处理;为什么是N,根据lua gc压力,拆分成核心数的倍数;
目的:掌握 actor 模型开发思路;
游戏:猜数字的游戏;
条件:满3⼈开始游戏,游戏开始后不能退出,直到这个游戏结束;
规则:系统当中会随机 1-100 之间的数字,参与游戏的玩家依次猜测规定范围内的数字;如果猜测正确那么该玩家就输了,如果猜测错误,游戏继续;直到有玩家猜测成功,游戏结束,该玩家失败;
我们要先编译skynet然后是cservice,以及lualib
简单可用,持续优化,而不是一开始就过度优化;
网关去接收客户端连接,把这个连接生成agent,来一个玩家生成一个agent,然后进入大厅做准备,大厅会做一个用户匹配当满足三个人的时候,把三个用户推到房间中去
如果 room(lua虚拟机)功能比较简单,那么可以创建固定数量的 room;
如果是万人同时在线游戏,agent room 需要预先分配,长时间运行会让服务内存膨胀,同时也会造成 lua gc 负担会加重;
重启服务策略,创建同样数量的 agent 服务组,新进来的玩家,分配到新的服务组;而旧的玩家在旧的服务组操作结束后,就淘汰该玩家,直到旧的服务组没有玩家,这时旧服务组退出;保证旧的服务组只处理旧的任务,新连接进来的用户在新的服务组进行工作;
因篇幅问题不能全部显示,请点此查看更多更全内容