skynet源码分析之网络层——底层介绍(转)

本篇主要介绍skynet网络层底层,主要代码在socket_server.c,skynet_socket.c,socket_epoll.h。通过该篇的介绍,了解skynet网络层的运作原理,比如工作线程与socket线程如何通信,如何处理网络收发数据等。之后会介绍skynet的服务怎么跟网络层交互以及在Lua逻辑层如何使用。

介绍时会涉及到unix网络编程相关知识请自己查阅。Linux I/O复用模式有三种:select,poll,epoll,这里用到select,epoll两种,稍后会介绍。

1. 主要数据结构

复制代码
// skynet-src/socket_server.c
struct socket { //单个socket结构
    uintptr_t opaque; //该socket关联的服务地址,收到的网络数据最终会传送给服务,如果socket是与客户端连接的,该服务通常是"logind"
    struct wb_list high; //高优先级发送队列
    struct wb_list low; //低优先级发送队列
    int64_t wb_size; //发送数据大小
    int fd; //socket文件描述符
    int id; //该socket在socket池中索引
    uint8_t protocol; //协议,TCP or UDP?
    uint8_t type; //socket状态,listen,connecting,connected等?
    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; //立刻发送缓冲区大小
};

struct socket_server { //skynet socket全局的结构,包含socket池,epoll监听的事件列表等
    int recvctrl_fd; //接收管道fd
    int sendctrl_fd; //发送管道fd
    int checkctrl; //标记管道里是否有数据
    poll_fd event_fd; //epoll实例id
    int alloc_id; //已经分配的socket池中的索引
    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;
};
复制代码

 2. 初始化

skynet专门创建一个线程处理socket连接(socket线程),工作线程通过管道与socket线程通信。

第6行,创建epoll实例(mac下用kqueue),epoll是Linux里最高效的I/O复用模式,当创建一个新的socket时,会加入到epoll里。

第8行,创建管道,工作线程向发送管道写数据,socket线程从接收管道读数据。这样做的好处是,简化了处理逻辑,且不用加锁,保证线程安全。

之后,初始化socket_server(ss)的各个成员。

复制代码
 1 // skynet-src/socket_server.c
 2 struct socket_server * 
 3 socket_server_create() {
 4     int i;
 5     int fd[2];
 6     poll_fd efd = sp_create();
 7     ...
 8     if (pipe(fd)) {
 9         sp_release(efd);
10         fprintf(stderr, "socket-server: create socket pair failed.\n");
11         return NULL;
12     }
13     if (sp_add(efd, fd[0], NULL)) {
14         // add recvctrl_fd to event poll
15         ...
16     }
17 
18     struct socket_server *ss = MALLOC(sizeof(*ss));
19     ss->event_fd = efd;
20     ss->recvctrl_fd = fd[0];
21     ss->sendctrl_fd = fd[1];
22     ss->checkctrl = 1;
23     ...
24     return ss;
25 }
复制代码

 3. socket线程工作流程概述

socket线程执行skynet_socket_poll,当返回值>0时,唤醒工作线程(第14行);否则,继续执行skynet_socket_poll

复制代码
 1 // skynet-src/skynet_start.c
 2 static void *
 3 thread_socket(void *p) {
 4     struct monitor * m = p;
 5     skynet_initthread(THREAD_SOCKET);
 6     for (;;) {
 7         int r = skynet_socket_poll();
 8         if (r==0)
 9             break;
10         if (r<0) {
11             CHECK_ABORT
12             continue;
13         }
14         wakeup(m,0);
15     }
16     return NULL;
17 }
复制代码

啥时候大于0,接着看skynet_socket_poll接口,当more为0时返回-1(第10行),否则返回1(第13行)

复制代码
 1 // skynet-src/skynet_socket.c
 2 int 
 3 skynet_socket_poll() {
 4     struct socket_server *ss = SOCKET_SERVER;
 5     assert(ss);
 6     struct socket_message result;
 7     int more = 1;
 8     int type = socket_server_poll(ss, &result, &more);
 9     ...
10     if (more) {
11         return -1;
12     }
13     return 1;
14 }
复制代码

 接着看socket_server_poll,当epoll事件全部处理完(第6行)且epoll有新事件到时才有可能返回0。

小结:当epoll有事件到达时,sp_wait返回,之后依次处理各个事件,通常是把网络数据传送给关联服务,即push到服务的消息队列中,此时就需要唤醒工作线程去处理。在sp_wait刚返回的那一帧,网络数据还没传送到关联服务,则不需要唤醒工作线程。

复制代码
 1 // return type
 2 int 
 3 socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more) {
 4     for (;;) {
 5     ...
 6     if (ss->event_index == ss->event_n) {
 7         ss->event_n = sp_wait(ss->event_fd, ss->ev, MAX_EVENT);
 8         ss->checkctrl = 1;
 9         if (more) {
10             *more = 0;
11         }
12     ...
13 end
复制代码

 4. 工作线程与socket线程如何通信

工作线程与socket线程通过管道通信的。初始化时创建管道,之后工作线程向发送管道写数据,socket线程从接收管道读数据。

写数据:通过send_request这个api向发送管道写数据,数据额外包含类型type(一个字节第4行)和长度(一个字节第5行)

复制代码
 1   // skynet-src/socket_server.c
 2   static void
 3   send_request(struct socket_server *ss, struct request_package *request, char type, int len) {
 4       request->header[6] = (uint8_t)type;
 5       request->header[7] = (uint8_t)len;
 6       for (;;) {
 7           ssize_t n = write(ss->sendctrl_fd, &request->header[6], len+2);
 8           ...
 9      }
10  }
复制代码

比如,工作线程listen一个地址,最后会调用到socket_server_listen,

第4行,调用unix系统接口bind,listen获取一个fd

第9行,从ss的socket池中获取空闲的socket id

14-16行,保存关联的服务地址,socket池的id,socket套接字fd

17行,调用send_request,向发送管道写数据

复制代码
 1 // skynet-src/socket_server.c
 2 int 
 3 socket_server_listen(struct socket_server *ss, uintptr_t opaque, const char * addr, int port, int backlog) {
 4     int fd = do_listen(addr, port, backlog);
 5     if (fd < 0) {
 6         return -1;
 7     }
 8     struct request_package request;
 9     int id = reserve_id(ss);
10     if (id < 0) {
11         close(fd);
12         return id;
13     }
14     request.u.listen.opaque = opaque;
15     request.u.listen.id = id;
16     request.u.listen.fd = fd;
17     send_request(ss, &request, 'L', sizeof(request.u.listen));
18     return id;
19 }
复制代码

 读数据,由于接收管道ss->recvctrl_fd在初始化时加入到epoll管理,当有数据时,sp_wait会返回。socket线程在下一帧socket_server_poll中,通过has_cmd(第6行)判断接收管道是否有数据,若有则执行ctrl_cmd接口。

在has_cmd里,通过select模式检测fd是否有数据,注:设置的时间是0,所以select不会阻塞(第27行)。

在ctrl_cmd里,从管道里读取数据,然后根据类型type做对应的处理。

复制代码
 1 // skynet-src/socket_server.c
 2 int 
 3 socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more) {
 4     for (;;) {
 5         if (ss->checkctrl) {
 6             if (has_cmd(ss)) {
 7                 int type = ctrl_cmd(ss, result);
 8                 if (type != -1) {
 9                     clear_closed_event(ss, result, type);
10                     return type;
11                 } else
12                     continue;
13                 } else {
14                     ss->checkctrl = 0;
15                 }
16             }
17     ...
18 }
19 
20 static int
21 has_cmd(struct socket_server *ss) {
22     struct timeval tv = {0,0};
23     int retval;
24 
25     FD_SET(ss->recvctrl_fd, &ss->rfds);
26 
27     retval = select(ss->recvctrl_fd+1, &ss->rfds, NULL, NULL, &tv);
28     if (retval == 1) {
29         return 1;
30     }
31     return 0;
32 }
33 
34 // return type
35 static int
36 ctrl_cmd(struct socket_server *ss, struct socket_message *result) {
37     int fd = ss->recvctrl_fd;
38     // the length of message is one byte, so 256+8 buffer size is enough.
39     uint8_t buffer[256];
40     uint8_t header[2];
41     block_readpipe(fd, header, sizeof(header));
42     int type = header[0];
43     int len = header[1];
44     block_readpipe(fd, buffer, len);
45     // ctrl command only exist in local fd, so don't worry about endian.
46     switch (type) {
47     ...
48 }
复制代码

 5. 如何处理网络收发数据

通过epoll模式管理所有socket套接字fd,当一个连接建立时,会将fd加入到epoll中(第8行),并且将该socket对象传递给epoll事件集,目的是当epoll事件触发时可以找到对应的socket对象而做对应的处理。

复制代码
 1 // skynet-src/socket_server.c
 2 static struct socket *
 3 new_fd(struct socket_server *ss, int id, int fd, int protocol, uintptr_t opaque, bool add) {
 4     struct socket * s = &ss->slot[HASH_ID(id)];
 5     assert(s->type == SOCKET_TYPE_RESERVE);
 6 
 7     if (add) {
 8         if (sp_add(ss->event_fd, fd, s)) {
 9             s->type = SOCKET_TYPE_INVALID;
10             return NULL;
11         }
12     }
13 
14     ...
15 }
16 
17 // skynet-src/socket_epoll.h
18 static int 
19 sp_add(int efd, int sock, void *ud) {
20     struct epoll_event ev;
21     ev.events = EPOLLIN;
22     ev.data.ptr = ud;
23     if (epoll_ctl(efd, EPOLL_CTL_ADD, sock, &ev) == -1) {
24         return 1;
25     }
26     return 0;
27 }
复制代码

socket_server_poll除了处理接收管道数据外,还需要接收和发送网络数据。

6-7行,从epoll事件中获取对应的socket

第14行,根据socket状态做相应的处理

第31行,如果socket已连接且事件可读,通过forward_message_tcp接收数据

第38行,如果socket已连接且事件可写,通过send_buffer发送数据。

复制代码
 1 // skynet-src/socket_server.c
 2 int 
 3 socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more) {
 4     for (;;) {
 5         ...
 6         struct event *e = &ss->ev[ss->event_index++];
 7         struct socket *s = e->s;
 8         if (s == NULL) {
 9             // dispatch pipe message at beginning
10             continue;
11         }
12         struct socket_lock l;
13         socket_lock_init(s, &l);
14         switch (s->type) {
15             case SOCKET_TYPE_CONNECTING:
16                 return report_connect(ss, s, &l, result);
17             case SOCKET_TYPE_LISTEN: {
18                 int ok = report_accept(ss, s, result);
19                 if (ok > 0) {
20                     return SOCKET_ACCEPT;
21                 } if (ok < 0 ) {
22                     return SOCKET_ERR;
23                 }
24                     // when ok == 0, retry
25                     break;
26                 }
27             case SOCKET_TYPE_INVALID:
28                 fprintf(stderr, "socket-server: invalid socket\n");
29                 break;
30             default:
31                 if (e->read) {
32                     int type;
33                     if (s->protocol == PROTOCOL_TCP) {
34                         type = forward_message_tcp(ss, s, &l, result);
35                     } else {
36                         type = forward_message_udp(ss, s, &l, result);
37                         ...
38                     if (e->write) {
39                         int type = send_buffer(ss, s, &l, result);
40                         if (type == -1)
41                             break;
42                             return type;
43                         }
44           ...
45         }
46 }
复制代码

 6. 数据发送流程

 skynet之前版本发送数据流程是:工作线程把数据发到发送管道,socket线程从接收管道读取数据再发给对端。后来,有人建议工作线程立刻把数据发给对端,而不经过管道https://github.com/cloudwu/skynet/issues/646。于是就有了现有高效的做法,工作线程要发送数据时,先判断是否可以立刻发送,否则再走管道流程。

工作调用socket_server_send发送数据:

第5行,是否可以立刻发送数据,当该socket的发送队列缓冲区为空,且立刻写的缓冲区也为空时,可直接发送。

第10行,立刻发送数据。

第19行,把要发送的数据写入发送管道,交给socket线程去发送。

复制代码
 1 // skynet-src/socket_server.c
 2 int 
 3 socket_server_send(struct socket_server *ss, int id, const void * buffer, int sz) {
 4     ...
 5     if (can_direct_write(s,id) && socket_trylock(&l)) {
 6         // may be we can send directly, double check
 7         if (can_direct_write(s,id)) {
 8             // send directly
 9             ...
10             n = write(s->fd, so.buffer, so.sz);
11             ...
12         }
13     }
14     struct request_package request;
15     request.u.send.id = id;
16     request.u.send.sz = sz;
17     request.u.send.buffer = (char *)buffer;
18 
19     send_request(ss, &request, 'D', sizeof(request.u.send));
20     return 0;
21 }
复制代码

 socket线程最终通过send_buffer_发送数据。每个socket包含高优先级和低优先级两个发送队列,流程是:

(1). 优先发送高优先级队列里的数据

(2). 若高优先级为空,发送低优先级里的数据

(3). 把低优先级的数据移入到高优先级里

(4). 高低优先级队列都为空,重新加入到epoll事件里

复制代码
 1  // skynet-src/socket_server.c
 2  /*
 3           Each socket has two write buffer list, high priority and low priority.
 4   
 5           1. send high list as far as possible.
 6           2. If high list is empty, try to send low list.
 7           3. If low list head is uncomplete (send a part before), move the head of low list to empty high list (call raise_uncomplete) .
 8           4. If two lists are both empty, turn off the event. (call check_close)
 9    */
10  static int
11  send_buffer_(struct socket_server *ss, struct socket *s, struct socket_lock *l, struct socket_message *result) {
12      assert(!list_uncomplete(&s->low));
13      // step 1
14      if (send_list(ss,s,&s->high,l,result) == SOCKET_CLOSE) {
15          return SOCKET_CLOSE;
16      }
17      if (s->high.head == NULL) {
18          // step 2
19          if (s->low.head != NULL) {
20              if (send_list(ss,s,&s->low,l,result) == SOCKET_CLOSE) {
21                  return SOCKET_CLOSE;
22              }
23              // step 3
24              if (list_uncomplete(&s->low)) {
25                  raise_uncomplete(s);
26                  return -1;
27              }
28              if (s->low.head)
29                  return -1;
30             }
31           // step 4
32           assert(send_buffer_empty(s) && s->wb_size == 0);
33           sp_write(ss->event_fd, s->fd, s, false);                        
34  
35           if (s->type == SOCKET_TYPE_HALFCLOSE) {
36                force_close(ss, s, l, result);
37                return SOCKET_CLOSE;
38           }
39           ...
40      return -1;
41  }
复制代码

 这就是skynet网络层底层知识,之后会介绍skynet的服务如何跟网络层交互以及在Lua逻辑层如何使用。

posted on 2022-02-28 13:57  &大飞  阅读(465)  评论(0编辑  收藏  举报

导航