本篇主要介绍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逻辑层如何使用。