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语言写的服务,内存块+隔离环境+消息队列;而lualua虚拟机;

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, 0sizeof(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,是epollevent结构

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;
}

 

fd3种绑定方式:

  1,注册listenfd; sp_add()注册事件;

  2,skynet作为客户端的连接fd

  3,skynet作为客户端连接redis-serverfd

 

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, 0sizeof(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, 0sizeof(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.ptrsocket指针,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可以找到actoractor将连接转换为事件来处理。

socket结构体中的idslot列表索引,用来存储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语言实现具体的业务开发;

 

 

posted @ 2020-08-19 02:25  will287248100  阅读(629)  评论(0编辑  收藏  举报