skynet-轻量级的后台服务器框架源码阅读(二)
skynet源码阅读:
3rd //有动态内存分配源码,lua动态库供c语言程序调用;
Cservice//c语言写的actor 编译生成的动态库,skynet 中actor被称为服务;
examples//测试用例程序
luaclib// c语言写的辅助库,来供lua调用;
lualib//是Lua写的辅助库,
lualib-src//c语言写的源码,供lua调用;
service// 每一个文件都是一个actor服务;
service-src// c语言写的服务,内存块+隔离环境+消息队列;而lua是lua虚拟机;
skynet-src//核心代码,解决的是消息调度,定时器,网络相关的工作;
在阅读源码前先搞明白这两个问题:
1,阻塞IO与非阻塞IO区别:(从三个角度来看)
1),socket阻塞的是网络进程
2),区别在于在没有网络事件(没有数据到达的时候)IO是否立即返回;没有立即返回是阻塞IO,立即返回是非阻塞IO区别。
3),fnctl fd noblock 设置IO非阻塞,循环检测网络io中是否有数据,通过epoll_wait()来检测那个连接中IO可读可写的事件,然后依次处理(read,write)各个就绪事件。
fnctl 来设置recv/send read/write是阻塞的还是非阻塞的。
2,多路io复用select,poll,epoll是阻塞io模型;select,poll的监听集合数据结构不同,但都会有1024的限制。
Select 轮询监听集合中的每一个io是否有可读可写事件;
Poll 字典监听集合,io处理机制和select相同;
epoll只处理有网络事件的fd,不区分事件,将他们统一处理,处理流程如下。
A, int efd = epoll_create();//创建红黑树
B, epoll_ctl();//可以注册时间、删除事件、更改事件,主要操作对象是红黑树;
C,int nevents = epoll_wait(efd, evs,ET...);//操作对象是就绪队列;将就绪事件放入就绪队列epoll_event[],再采用循环去依次处理这些就绪的事件;
epoll工作原理图解:
开发过程中不建议直接修改skynet源码;在skynet同级目录下新建test目录,采用config.test来配置;
skynet启动配置文件,config.test
thread=8 --工作线程数目 logger=nil harbor=0--集群 slave start="main" -- 启动服务 lua_path="./skynet/lualib/?.lua;".."./test/lualib/?.lua;".."./skynet/lualib/?/init.lua" luaservice = "./skynet/service/?.lua;".."./test/?.lua;".."./skynet/test/?.lua;" lualoader = "./skynet/lualib/loader.lua" cpath = "./skynet/cservice/?.so;" --actor 内存块 lua_cpath = "./skynet/luaclib/?.so;"--c语言辅助服务
main.lua//这是实现了一个echo服务
local skynet = require "skynet" local socket = require "skynet.socket" --echo service local function event_loop(clientfd) while true do local data = socket.readline(clientfd)--从网络获取 以\n为分隔符的数据包 if not data then return end print(clientfd, "recv:", data) socket.write(clientfd, data.."\n") end end local function accept(clientfd, addr)-- 回调函数的作用 就是可以将 fd绑定到其他actor print("accept a connect:", clientfd, addr) socket.start(clientfd) -- 将clientfd注册到epoll skynet.fork(event_loop, clientfd) -- 实现一个简单的echo服务,可以通过 telnet 127.0.0.1 8001来连接skynet end skynet.start(function ()--skynet.start service start command local listenfd = socket.listen("0.0.0.0", 8001) -- socket bind listen socket.start(listenfd, accept) -- 将listenfd注册到epoll,收到连接会回调accept函数 end)
下面我们来加载consig.test配置文件,运行调试一下main.lua服务;
文件结构是这样子的:
skynet->skynet(make linux 编译生成的可执行文件)
consig.test
mkdir test
将main.lua放在新建的test目录下运行命令:
./skynet/skynet ./config.test
新开启终端:telnet 127.0.0.1 8001
根据提示命令安装:
apt-get install inetutils-telnet
apt-get install telnet-ssl
apt-get install telnet
解决方案失败;
在ubuntu下使用telnet:
sudo apt-get install openbsd-inetd
sudo apt-get install telnetd
sudo /etc/init.d/openbsd-inetd restart
# 查看 telnet服务是否开启,tcp 23端口启动
sudo netstat -a | grep telnet
再去尝试连接;telnet 127.0.0.1 8001成功
Skynet网络层工作原理图解:
Skynet网络层源码总结:
skynet网络层源码阅读:
worker进程的数据发送到网络中,需要与socket进程进行通信,worker进程与socket进程通信是通过管道来实现的。worker 进程writefd管道;socket进程readfd管道
见socket_server.c源码
struct socket_server { volatile uint64_t time; int recvctrl_fd;//接收管道文件描述符,这个管道是一个单向管道,goelang中实现的管道是双向的; int sendctrl_fd;//发送管道文件描述符 int checkctrl;//检测其他线程是否通过管道向socket线程发送消息了 poll_fd event_fd;//epoll 实例id int alloc_id;//已分配的slot列表ID int event_n;//本次epoll事件数量 int event_index;//下一个未处理的epoll事件索引 struct socket_object_interface soi; struct event ev[MAX_EVENT];//epoll事件列表 struct socket slot[MAX_SOCKET];//socket列表 char buffer[MAX_INFO]; uint8_t udpbuffer[MAX_UDP_PACKAGE]; fd_set rfds; }; struct socket { uintptr_t opaque;//actor地址 struct wb_list high; struct wb_list low; int64_t wb_size; struct socket_stat stat; volatile uint32_t sending; int fd; //socket文件描述符 int id; //slot列表索引 uint8_t protocol; uint8_t type; uint16_t udpconnecting; int64_t warn_size; union { int size; uint8_t udp_address[UDP_ADDRESS_SIZE]; } p; struct spinlock dw_lock; int dw_offset; const void * dw_buffer; size_t dw_size; };
socket_server_create(uint64_t time) { int i; int fd[2];//读写2个fd poll_fd efd = sp_create();//创建epoll文件描述符 if (sp_invalid(efd)) { fprintf(stderr, "socket-server: create event pool failed.\n"); return NULL; } if (pipe(fd)) {//多进程通信,管道 sp_release(efd); fprintf(stderr, "socket-server: create socket pair failed.\n"); return NULL; } if (sp_add(efd, fd[0], NULL)) {//加入读端到epoll中管理 // add recvctrl_fd to event poll fprintf(stderr, "socket-server: can't add server fd to event pool.\n"); close(fd[0]); close(fd[1]); sp_release(efd); return NULL; } struct socket_server *ss = MALLOC(sizeof(*ss)); ss->time = time; ss->event_fd = efd; ss->recvctrl_fd = fd[0];//存储读写fd ss->sendctrl_fd = fd[1]; ss->checkctrl = 1; for (i=0;i<MAX_SOCKET;i++) { struct socket *s = &ss->slot[i]; s->type = SOCKET_TYPE_INVALID; clear_wb_list(&s->high); clear_wb_list(&s->low); spinlock_init(&s->dw_lock); } ss->alloc_id = 0; ss->event_n = 0; ss->event_index = 0; memset(&ss->soi, 0, sizeof(ss->soi)); FD_ZERO(&ss->rfds); assert(ss->recvctrl_fd < FD_SETSIZE); return ss; }
socket_epoll.h
sp_add(int efd, int sock, void *ud) //注册事件 sp_del(int efd, int sock)//删除事件 sp_write(int efd, int sock, void *ud, bool enable) sp_wait(int efd, struct event *e, int max)//epoll_wait()
在sp_add()通过调用epoll_ctl()方法将fd加入到epoll中管理
sp_add(int efd, int sock, void *ud) { struct epoll_event ev; ev.events = EPOLLIN; ev.data.ptr = ud; if (epoll_ctl(efd, EPOLL_CTL_ADD, sock, &ev) == -1) { return 1; } return 0; }
这是skynet启动的网络线程,不断的调用skynet_socket_poll()->socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more)来获取网络数据。ss->checkctrl的值来判断其他线程是否向管道发送数据,如果发送数据了通过ss->event_n = sp_wait(ss->event_fd, ss->ev, MAX_EVENT);从epoll中取出改管道的消息,然后去处理。
static void * thread_socket(void *p) { struct monitor * m = p; skynet_initthread(THREAD_SOCKET); for (;;) { int r = skynet_socket_poll(); if (r==0) break; if (r<0) { CHECK_ABORT continue; } wakeup(m,0); } return NULL; }
结论:多个worker线程,skynet中的读操作都是在socket线程中,写时worker线程会通过try_spinlock自旋锁来检测,如果socket线程没有对该IO操作的话,worker线程try_spinlock成功了,就是worker线程可以获取到自旋锁,直接在worker线程中将数据发送;try_spinlock失败了,通过管道将数据发送到socket线程,epoll_wait()IO会出现一个可写状态,write将数据发送出去,在socket线程中进行数据发送。
使用lua语言在main.lua写以下代码:
skynet.start(function ()--skynet.start service start command local listenfd = socket.listen("0.0.0.0", 8001) -- socket bind listen socket.start(listenfd, accept) -- 将listenfd注册到epoll,收到连接会回调accept函数 skynet.fork(function () local connfd = socket.open("127.0.0.1", 6379)-- skynet作为客户端连接 redis-server;连接成功 fd 注册到epoll if not connfd then print("网络连接失败") return end socket.write(connfd, "*1\r\n$4\r\nPING\r\n")--这里实现了一个简单的ping pong心跳包功能 local data = socket.readline(connfd, "\r\n")--redis 协议以\r\n为分隔符 print("recv redis-server data:", data) end) end)
Skynet作为客户端,访问redis service
telnet 127.0.0.1 8001
返回结果:recv redis-server data: +PONG
skynet源码socket_poll.h中
用户数据ev,是epoll中event结构
struct event { void * s; bool read; bool write; bool error; bool eof; };
查看sp_wait()源码:
static int sp_wait(int efd, struct event *e, int max) { struct epoll_event ev[max]; int n = epoll_wait(efd , ev, max, -1);//从epoll中取出就绪队列e[n]; 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; }
fd的3种绑定方式:
1,注册listenfd; sp_add()注册事件;
2,skynet作为客户端的连接fd;
3,skynet作为客户端连接redis-server的fd;
从sp_add() 可以看出用户数据存储在ev.data.ptr这里
static int sp_add(int efd, int sock, void *ud) { struct epoll_event ev; ev.events = EPOLLIN; ev.data.ptr = ud; if (epoll_ctl(efd, EPOLL_CTL_ADD, sock, &ev) == -1) { return 1; } return 0; }
源码中socket_create.c,if (sp_add(efd, fd[0], NULL))是那个管道fd,这是一个worker线程,sp_add传入NULL,是不需要上下文。
struct socket_server * socket_server_create(uint64_t time) { int i; int fd[2]; poll_fd efd = sp_create(); if (sp_invalid(efd)) { fprintf(stderr, "socket-server: create event pool failed.\n"); return NULL; } if (pipe(fd)) { sp_release(efd); fprintf(stderr, "socket-server: create socket pair failed.\n"); return NULL; } if (sp_add(efd, fd[0], NULL)) {//fd[0]是那个管道fd,这是一个worker线程,sp_add传入NULL,是不需要上下文。 // add recvctrl_fd to event poll fprintf(stderr, "socket-server: can't add server fd to event pool.\n"); close(fd[0]); close(fd[1]); sp_release(efd); return NULL; } struct socket_server *ss = MALLOC(sizeof(*ss)); ss->time = time; ss->event_fd = efd; ss->recvctrl_fd = fd[0]; ss->sendctrl_fd = fd[1]; ss->checkctrl = 1; for (i=0;i<MAX_SOCKET;i++) { struct socket *s = &ss->slot[i]; s->type = SOCKET_TYPE_INVALID; clear_wb_list(&s->high); clear_wb_list(&s->low); spinlock_init(&s->dw_lock); } ss->alloc_id = 0; ss->event_n = 0; ss->event_index = 0; memset(&ss->soi, 0, sizeof(ss->soi)); FD_ZERO(&ss->rfds); assert(ss->recvctrl_fd < FD_SETSIZE); return ss; }
在new_fd()中,if (sp_add(ss->event_fd, fd, s))传入的s,s传递的是一个socket指针。
static struct socket * new_fd(struct socket_server *ss, int id, int fd, int protocol, uintptr_t opaque, bool add) { struct socket * s = &ss->slot[HASH_ID(id)]; assert(s->type == SOCKET_TYPE_RESERVE); if (add) { if (sp_add(ss->event_fd, fd, s)) { s->type = SOCKET_TYPE_INVALID; return NULL; } } s->id = id; s->fd = fd; s->reading = add ? READING_RESUME : READING_PAUSE; s->sending = ID_TAG16(id) << 16 | 0; s->protocol = protocol; s->p.size = MIN_READ_BUFFER; s->opaque = opaque; s->wb_size = 0; s->warn_size = 0; check_wb_list(&s->high); check_wb_list(&s->low); s->dw_buffer = NULL; s->dw_size = 0; memset(&s->stat, 0, sizeof(s->stat)); return s; }
在其他版本是start_socket(),if (sp_add(ss->event_fd, s->fd, s))出入的s是一个socket指针,
static int resume_socket(struct socket_server *ss, struct request_resumepause *request, struct socket_message *result) { int id = request->id; result->id = id; result->opaque = request->opaque; result->ud = 0; result->data = NULL; struct socket *s = &ss->slot[HASH_ID(id)]; if (s->type == SOCKET_TYPE_INVALID || s->id !=id) { result->data = "invalid socket"; return SOCKET_ERR; } struct socket_lock l; socket_lock_init(s, &l); if (s->reading == READING_PAUSE) { if (sp_add(ss->event_fd, s->fd, s)) { force_close(ss, s, &l, result); result->data = strerror(errno); return SOCKET_ERR; } s->reading = READING_RESUME; } if (s->type == SOCKET_TYPE_PACCEPT || s->type == SOCKET_TYPE_PLISTEN) { s->type = (s->type == SOCKET_TYPE_PACCEPT) ? SOCKET_TYPE_CONNECTED : SOCKET_TYPE_LISTEN; s->opaque = request->opaque; result->data = "start"; return SOCKET_OPEN; } else if (s->type == SOCKET_TYPE_CONNECTED) { // todo: maybe we should send a message SOCKET_TRANSFER to s->opaque s->opaque = request->opaque; result->data = "transfer"; return SOCKET_OPEN; }// if s->type == SOCKET_TYPE_HALFCLOSE , SOCKET_CLOSE message will send later return -1; }
从以上可以看出skynet中的data.ptr是socket指针,socket结构:
struct socket { uintptr_t opaque; //actor地址 struct wb_list high; struct wb_list low; int64_t wb_size; struct socket_stat stat; volatile uint32_t sending; int fd; //socket文件描述符 int id; //slot列表索引 uint8_t protocol; uint8_t type; uint16_t udpconnecting; int64_t warn_size; union { int size; uint8_t udp_address[UDP_ADDRESS_SIZE]; } p; struct spinlock dw_lock; int dw_offset; const void * dw_buffer; size_t dw_size; };
从以上的源码中分析可以知道:actor模型将连接转换为事件处理。 uintptr_t opaque可以找到actor,actor将连接转换为事件来处理。
socket结构体中的id是slot列表索引,用来存储socket。在epoll_data中的u32可以存储id也就是slot,通过id也可以找到socket.epoll_data的作用就是根据连接找到事件,再找到用户数据socketfd处理,进而找到actor,通过actor的逻辑进行处理。
总结一下流程:
1.actor模型将连接转换为事件;
2.epoll_data通过事件找到用户数据,用户数据内容有{fd,actorid,发送缓冲区,接收缓冲区};
3.再通过用户数据找到actor,
4.将用户数据在actor中处理;
lua就是一个actor,业务与逻辑分离,在actor中实现的是逻辑代码,使用Lua语言实现具体的业务开发;