• 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.
    ");
    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
    ");
    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逻辑层如何使用。

  • 相关阅读:
    线程高级应用-心得2-同步锁讲解及面试题案例分析
    线程高级应用-心得1-传统线程和定时器讲解及案例分析
    Map拷贝 关于对象深拷贝 浅拷贝的问题
    HashMap对象的深层克隆
    java Collections.sort()实现List排序自定义方法
    java中观察者模式Observable和Observer
    mysql字符串函数(转载)
    CSS的三种样式表和优先级
    Android之微信支付
    Android之扫描二维码和根据输入信息生成名片二维码
  • 原文地址:https://www.cnblogs.com/RainRill/p/8670117.html
Copyright © 2020-2023  润新知