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逻辑层如何使用。

浙公网安备 33010602011771号