參考了:Skynet服務器框架(六) Socket服務源碼剖析和應用(linshuhe1的專欄)以及Skynet 源碼學習 -- Socket Server 和 Skynet_socket(cchd0001的專欄)
用了Skynet下的Socket接口後,越發想看看它的底層實現。和我之前想的一樣,Skynet底層的網絡併發在Linux下使用的正是 epoll。
EPOLL封裝層:
./skynet-src/socket_poll.h 給了我答案:
#ifndef socket_poll_h
#define socket_poll_h
#include <stdbool.h>
typedef int poll_fd;
struct event {
void * s;
bool read;
bool write;
};
static bool sp_invalid(poll_fd fd);
static poll_fd sp_create();
static void sp_release(poll_fd fd);
static int sp_add(poll_fd fd, int sock, void *ud);
static void sp_del(poll_fd fd, int sock);
static void sp_write(poll_fd, int sock, void *ud, bool enable);
static int sp_wait(poll_fd, struct event *e, int max);
static void sp_nonblocking(int sock);
#ifdef __linux__
#include "socket_epoll.h"
#endif
#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined (__NetBSD__)
#include "socket_kqueue.h"
#endif
#endif
可以發現Skynet在Linux下使用了 epoll 來管理網絡併發,在FreeBSD等平臺下使用了 kqueue。該頭文件定義了一個結構體 event ,後面可以知道,該結構體就是對 epoll 下的 epoll_event 做了簡易封裝,拋棄了 epoll_event 下EPOLLPRI、EPOLLERR等不常用事件,僅僅保留了EPOLLIN(讀) 、EPOLLOUT(寫)兩個事件,分別用 read 和 write 兩個 bool 值來簡單標記。以上的函數實現在 socket_epoll.h 和 socket_kqueue.h 裏。
總體來說,Skynet在 epoll 的基礎上封裝了五層。本文先介紹最底下兩層,下一篇介紹上三層。
首先是 ./skynet-src/socket_epoll.h,這一層是對 epoll 的簡單封裝。
// 用於判斷產生的 epoll fd 是否有效
static bool
sp_invalid(int efd) {
return efd == -1;
}
// 用於產生一個 epoll fd,1024是用來建議內核監聽的數目,自從 linux 2.6.8 之後,該參數是被忽略的,即可以填大於0的任意值。
static int
sp_create() {
return epoll_create(1024);
}
// 釋放 epoll fd
static void
sp_release(int efd) {
close(efd);
}
/*
* 爲 epoll 添加一個監聽的文件描述符,僅監控讀事件
* fd : sp_create() 返回值
* sock : 待監聽的文件描述符
* ud : 自己使用的指針地址
* : 返回0表示添加成功, -1表示失敗
*/
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;
}
/*
* 刪除 epoll 中監聽的 fd
* fd : sp_create()創建的fd
* sock : 待刪除的fd
*/
static void
sp_del(int efd, int sock) {
epoll_ctl(efd, EPOLL_CTL_DEL, sock , NULL);
}
/*
* 修改 epoll 中已有 fd 的監聽事件
* efd : epoll fd
* sock : 待修改的fd
* ud : 用戶自定義數據指針
* enable: true表示開啓寫監聽, false表示還是讀監聽
*/
static void
sp_write(int efd, int sock, void *ud, bool enable) {
struct epoll_event ev;
ev.events = EPOLLIN | (enable ? EPOLLOUT : 0);
ev.data.ptr = ud;
epoll_ctl(efd, EPOLL_CTL_MOD, sock, &ev);
}
/*
* 輪詢fd事件
* efd : sp_create()創建的fd
* e : 一段struct event內存的首地址
* max : e內存能夠使用的最大值
* : 返回監聽到事件的fd數量,write與read分別對應寫和讀事件flag,值爲true時表示該事件發生
*/
static int
sp_wait(int efd, struct event *e, int max) {
struct epoll_event ev[max];
int n = epoll_wait(efd , ev, max, -1);
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) != 0;
}
return n;
}
/*
* 將fd設置爲非阻塞
*/
static void
sp_nonblocking(int fd) {
int flag = fcntl(fd, F_GETFL, 0);
if ( -1 == flag ) {
return;
}
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
接着是 ./skynet-src/socket_server.c
這一層對上一層的封裝較爲複雜。
socket_server 封裝:
先看幾個重要的結構體:
struct write_buffer {
struct write_buffer * next;
void *buffer;
char *ptr;
int sz;
bool userobject;
uint8_t udp_address[UDP_ADDRESS_SIZE];
};
#define SIZEOF_TCPBUFFER (offsetof(struct write_buffer, udp_address[0]))
#define SIZEOF_UDPBUFFER (sizeof(struct write_buffer))
//寫緩衝隊列
struct wb_list {
struct write_buffer * head; //寫緩衝區的頭指針
struct write_buffer * tail; //寫緩衝區的尾指針
};
struct socket {
uintptr_t opaque; //所屬服務在skynet中對應的handle
struct wb_list high;//高優先級寫隊列
struct wb_list low; //低優先級寫隊列
int64_t wb_size; //寫緩存尚未發送的數據大小
int fd;
int id; //用於索引socket_server裏的slot數組
uint16_t protocol; //使用的協議類型(TCP/UDP)
uint16_t type; //scoket的類型或狀態(讀、寫、監聽等)
union {
int size; //讀緩存預估需要的大小
uint8_t udp_address[UDP_ADDRESS_SIZE];
} p;
};
struct socket_server {
int recvctrl_fd; // pipe讀端
int sendctrl_fd; // pipe寫端
int checkctrl;
poll_fd event_fd; // epoll/kqueue的fd
int alloc_id;
int event_n; // epoll_wait 返回的事件數
int event_index; // 當前處理的事件序號
struct socket_object_interface soi;
struct event ev[MAX_EVENT]; // epoll_wait 返回的事件集合
struct socket slot[MAX_SOCKET]; // 每個socket_server可以包含多個socket,slot存儲這些socket
char buffer[MAX_INFO];
uint8_t udpbuffer[MAX_UDP_PACKAGE];
fd_set rfds; // select監測的fd集
};
struct request_open {
int id; // 用於在socket_server的slot找到對應的socket
int port;
uintptr_t opaque;
char host[1];
};
struct request_send {
int id;
int sz;
char * buffer;
};
struct request_send_udp {
struct request_send send;
uint8_t address[UDP_ADDRESS_SIZE];
};
struct request_setudp {
int id;
uint8_t address[UDP_ADDRESS_SIZE];
};
struct request_close {
int id;
uintptr_t opaque;
};
struct request_listen {
int id;
int fd;
uintptr_t opaque;
char host[1];
};
struct request_bind {
int id;
int fd;
uintptr_t opaque;
};
struct request_start {
int id;
uintptr_t opaque;
};
struct request_setopt {
int id;
int what;
int value;
};
struct request_udp {
int id;
int fd;
int family;
uintptr_t opaque;
};
/*
The first byte is TYPE
S Start socket
B Bind socket
L Listen socket
K Close socket
O Connect to (Open)
X Exit
D Send package (high)
P Send package (low)
A Send UDP package
T Set opt
U Create UDP socket
C set udp address
*/
struct request_package {
uint8_t header[8]; // 6 bytes dummy
union {
char buffer[256];
struct request_open open;
struct request_send send;
struct request_send_udp send_udp;
struct request_close close;
struct request_listen listen;
struct request_bind bind;
struct request_start start;
struct request_setopt setopt;
struct request_udp udp;
struct request_setudp set_udp;
} u;
uint8_t dummy[256];
};
struct socket_message {
int id;
uintptr_t opaque; // 在skynet中對應一個Actor實體的handle句柄
int ud; // 對於accept連接來說, ud是新連接的id;對於數據(data)來說, ud是數據的大小
char * data;
};
此外,還有幾個宏:
// 宏定義socket_server_poll()返回的socket消息類型
#define SOCKET_DATA 0 //數據data到來
#define SOCKET_CLOSE 1 //關閉連接
#define SOCKET_OPEN 2 //多處用到,參見代碼
#define SOCKET_ACCEPT 3 //被動連接建立
#define SOCKET_ERROR 4 //錯誤
#define SOCKET_EXIT 5 //退出socket
#define SOCKET_UDP 6 //udp通信
// socket狀態
#define SOCKET_TYPE_INVALID 0 // 無效的套接字
#define SOCKET_TYPE_RESERVE 1 // 預留,即將被使用
#define SOCKET_TYPE_PLISTEN 2 // 監聽套接字,尚未加入 epoll 管理
#define SOCKET_TYPE_LISTEN 3 // 監聽套接字,已加入 epoll 管理
#define SOCKET_TYPE_CONNECTING 4 // 嘗試連接中的套接字
#define SOCKET_TYPE_CONNECTED 5 // 已連接的套接字(主動或被動)
#define SOCKET_TYPE_HALFCLOSE 6 // 上層已發起關閉套接字請求,但發送緩衝區尚未發送完畢,未調用close
#define SOCKET_TYPE_PACCEPT 7 // accept()後的套接字,但尚未加入 epoll 管理
#define SOCKET_TYPE_BIND 8 // 已綁定其他類型描述符,如 stdin, stdout
先來看該層的初始化函數:
struct socket_server *
socket_server_create() {
int i;
int fd[2];
poll_fd efd = sp_create(); // 創建一個監聽 epoll,非常重要!
if (sp_invalid(efd)) {
fprintf(stderr, "socket-server: create event pool failed.\n");
return NULL;
}
if (pipe(fd)) { // 創建 pipe
sp_release(efd);
fprintf(stderr, "socket-server: create socket pair failed.\n");
return NULL;
}
if (sp_add(efd, fd[0], NULL)) { // 將 pipe 的讀端放入 epoll 中監聽,注意 pipe 消息是沒有 socket* 變量的,爲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)); // 創建 socket_server 實例,然後一系列初始化
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; // 所有socket的類型初始化爲SOCKET_TYPE_INVALID
clear_wb_list(&s->high);
clear_wb_list(&s->low);
}
ss->alloc_id = 0;
ss->event_n = 0;
ss->event_index = 0;
memset(&ss->soi, 0, sizeof(ss->soi));
FD_ZERO(&ss->rfds);
assert(ss->recvctrl_fd < FD_SETSIZE);
return ss;
}
接着是該層的核心代碼,該函數作爲中樞,掌管着內外數據流動。初始化的 epoll 和 pipe 都在該函數中扮演重要角色。以該代碼爲核心,我們可以畫出這樣一幅圖:
參照下面的源碼,我們知道 socket_server_poll 作爲任務處理分發器,處理着一個 socket_server 裏所有的事件。除了一些控制指令由 pipe 傳輸,還有一些其他事件由 epoll 監聽。不論是控制指令還是 epoll 監聽到的其他讀寫事件,都由 socket_server_poll 分發給相應的函數去處理。這裏的其他描述符事件主要是指socket連接的事件,比如TCP異步連接成功觸發的事件,socket的讀寫事件等等。
// return type
int
socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more) {
for (;;) {
if (ss->checkctrl) { // 每次處理完epoll的事件後會設置checkctrl=1
if (has_cmd(ss)) { // 檢測管道讀端是否可讀
//printf("has_cmd = 1\n");
int type = ctrl_cmd(ss, result); // 處理控制命令
if (type != -1) {
clear_closed_event(ss, result, type);
return type;
} else
continue;
} else {
//printf("has_cmd = 0\n");
ss->checkctrl = 0; // pipe 裏沒有數據,置爲0,此時如果有socket連接到來,接着可從sp_wait()獲取事件,
} // 當所有事件處理完畢後,會重新置爲1,然後再次調用非阻塞select接受pipe事件。處理
// 一批事件的過程是連續的,不會被pipe事件打斷,直到處理完。
}
printf("event: %d %d\n", ss->event_index, ss->event_n);
if (ss->event_index == ss->event_n) { // 相等說明事件處理完畢,可以調用 sp_wait() 接收新事件
ss->event_n = sp_wait(ss->event_fd, ss->ev, MAX_EVENT); // epoll 監聽很多東西,如 pipe 讀端, 標準輸出1,listen_fd, connect_fd 等等
printf("now we get: %d\n", ss->event_n);
ss->checkctrl = 1;
if (more) {
*more = 0;
}
ss->event_index = 0;
if (ss->event_n <= 0) {
ss->event_n = 0;
return -1;
}
}
struct event *e = &ss->ev[ss->event_index++];
struct socket *s = e->s;
if (s == NULL) { // s = NULL 說明是 pipe 消息,直接跳過。 待本批所有事件處理完畢後再交由 has_cmd 和 ctrl_cmd 處理
// dispatch pipe message at beginning
continue;
}
printf("get fd: %d, opa: %d, type: %d, read: %d, write: %d\n", s->fd, s->opaque, s->type, e->read, e->write);
switch (s->type) { // 處理 epoll 事件
case SOCKET_TYPE_CONNECTING: // 由於使用了異步tcp連接,連接成功後,客戶端connect_fd可寫
return report_connect(ss, s, result);
case SOCKET_TYPE_LISTEN:
if (report_accept(ss, s, result)) { // 由於使用了異步tcp連接,連接成功後,服務器listen_fd可讀。
return SOCKET_ACCEPT;
}
break;
case SOCKET_TYPE_INVALID:
fprintf(stderr, "socket-server: invalid socket\n");
break;
default:
if (e->read) {
int type;
if (s->protocol == PROTOCOL_TCP) {
type = forward_message_tcp(ss, s, result);
} else {
type = forward_message_udp(ss, s, result);
if (type == SOCKET_UDP) {
// try read again
--ss->event_index;
return SOCKET_UDP;
}
}
if (e->write) {
// Try to dispatch write message next step if write flag set.
e->read = false;
--ss->event_index;
}
if (type == -1)
break;
clear_closed_event(ss, result, type);
return type;
}
if (e->write) {
int type = send_buffer(ss, s, result);
if (type == -1)
break;
clear_closed_event(ss, result, type);
return type;
}
break;
}
}
}
除此之外,還有兩個函數協助:
/*
* 該函數使用非阻塞 select 來監測 pipe 讀端,當 pipe 中寫入數據後,pipe 將變爲可讀,返回 1 表明可讀,0 爲不可讀。
*/
static int
has_cmd(struct socket_server *ss) {
struct timeval tv = {0,0};
int retval;
FD_SET(ss->recvctrl_fd, &ss->rfds);
retval = select(ss->recvctrl_fd+1, &ss->rfds, NULL, NULL, &tv);
if (retval == 1) {
return 1;
}
return 0;
}
/*
* 該函數從 pipe 中讀取數據,首先讀2字節的頭,取出數據類型和大小後,讀取相應大小的數據後按消息類型發給相應的處理函數。result由socket_server_poll
* 傳入。依據不同的消息類型,交由 start_socket、bind_socket 等函數填寫。
*/
// return type
static int
ctrl_cmd(struct socket_server *ss, struct socket_message *result) {
int fd = ss->recvctrl_fd;
// the length of message is one byte, so 256+8 buffer size is enough.
uint8_t buffer[256];
uint8_t header[2];
block_readpipe(fd, header, sizeof(header));
int type = header[0];
int len = header[1];
block_readpipe(fd, buffer, len);
// ctrl command only exist in local fd, so don't worry about endian.
switch (type) {
case 'S':
return start_socket(ss,(struct request_start *)buffer, result);
case 'B':
return bind_socket(ss,(struct request_bind *)buffer, result);
case 'L':
return listen_socket(ss,(struct request_listen *)buffer, result);
case 'K':
return close_socket(ss,(struct request_close *)buffer, result);
case 'O':
return open_socket(ss, (struct request_open *)buffer, result);
case 'X':
result->opaque = 0;
result->id = 0;
result->ud = 0;
result->data = NULL;
return SOCKET_EXIT;
case 'D':
return send_socket(ss, (struct request_send *)buffer, result, PRIORITY_HIGH, NULL);
case 'P':
return send_socket(ss, (struct request_send *)buffer, result, PRIORITY_LOW, NULL);
case 'A': {
struct request_send_udp * rsu = (struct request_send_udp *)buffer;
return send_socket(ss, &rsu->send, result, PRIORITY_HIGH, rsu->address);
}
case 'C':
return set_udp_address(ss, (struct request_setudp *)buffer, result);
case 'T':
setopt_socket(ss, (struct request_setopt *)buffer);
return -1;
case 'U':
add_udp_socket(ss, (struct request_udp *)buffer);
return -1;
default:
fprintf(stderr, "socket-server: Unknown ctrl %c.\n",type);
return -1;
};
return -1;
}
該層的核心接口:
假如要在C語言中直接使用socket_server,基本上是用這些封裝好的接口基本上也就足夠了:
// 由於所有的接口實現都寫在頭文件裏面,全部聲明爲static。
//創建一個socket_server
struct socket_server * socket_server_create();
//釋放一個socket_server的資源佔用
void socket_server_release(struct socket_server *);
/*
* 封裝了的epoll或kqueue,用來獲取socket的網絡事件或消息
* (通常放在循環體中持續監聽網絡消息)
* socket_server : socket_server_create() 返回的socket_server實例
* result : 結果數據存放的地址指針
* : 返回消息類型,對應於宏定義中的SOCKET_DATA的類型
*/
int socket_server_poll(struct socket_server *, struct socket_message *result, int *more);
//退出socket_server
void socket_server_exit(struct socket_server *);
/*
* 關閉socket_server
* socket_server : socket_server_create() 返回的socket_server實例
* opaque : skynet中服務handle的句柄
* id : socket_server_listen() 返回的id
*/
void socket_server_close(struct socket_server *, uintptr_t opaque, int id);
/*
* 停止socket
* socket_server : socket_server_create() 返回的socket_server實例
* opaque : skynet中服務handle的句柄
* id : socket句柄
*/
void socket_server_shutdown(struct socket_server *, uintptr_t opaque, int id);
/*
* 將該socket放入epoll中監聽(啓動之前要先通過socket_server_listen()開啓TCP的socket(),bind(),listen()步驟)
* 或將服務器 report_accept() 後的socket放入epoll中監聽。總之,對於socket的fd,想要收發數據,都得先調用 socket_server_start()
* socket_server : socket_server_create() 返回的socket_server實例
* opaque : skynet中服務handle的句柄
* id : socket_server_listen() 返回的id
*/
void socket_server_start(struct socket_server *, uintptr_t opaque, int id);
/*
* 發送數據
* socket_server : socket_server_create() 返回的socket_server實例
* buffer : 要發送的數據
* sz : 數據的大小
* id : socket_server_listen() 返回的id
* : 假如返回-1表示error
*/
int64_t socket_server_send(struct socket_server *, int id, const void * buffer, int sz);
void socket_server_send_lowpriority(struct socket_server *, int id, const void * buffer, int sz);
/*
* 開啓TCP監聽,執行了socket(),bind(),listen() 步驟
* socket_server : socket_server_create() 返回的socket_server實例
* opaque : skynet中服務handle的句柄
* addr : ip地址
* port : 端口號
* : 返回一個id作爲操作此端口監聽的句柄
*/
int socket_server_listen(struct socket_server *, uintptr_t opaque, const char * addr, int port, int backlog);
/*
* 以非阻塞的方式連接服務器
* socket_server : socket_server_create() 返回的socket_server實例
* opaque : skynet中服務handle的句柄
* addr : ip地址
* port : 端口號
* : 返回一個id作爲操作此端口監聽的句柄
*/
int socket_server_connect(struct socket_server *, uintptr_t opaque, const char * addr, int port);
/*
* 並不對應bind函數,而是將stdin、stout這類IO加入到epoll中管理
* socket_server : socket_server_create() 返回的socket_server實例
* opaque : skynet中服務handle的句柄
* fd : socket的文本描述
*/
int socket_server_bind(struct socket_server *, uintptr_t opaque, int fd);
// for tcp
void socket_server_nodelay(struct socket_server *, int id);
/*
* 創建一個udp socket監聽,並綁定skynet服務的handle,udp不需要像tcp那樣要調用socket_server_start後才能接收消息
* 如果port != 0, 綁定socket,如果addr == NULL, 綁定 ipv4 0.0.0.0。如果想要使用ipv6,地址使用“::”,端口中port設爲0
*/
int socket_server_udp(struct socket_server *, uintptr_t opaque, const char * addr, int port);
// 設置默認的端口地址,返回0表示成功
int socket_server_udp_connect(struct socket_server *, int id, const char * addr, int port);
/*
* 假如 socket_udp_address 是空的, 使用最後最後調用 socket_server_udp_connect 時傳入的address代替
* 也可以使用 socket_server_send 來發送udp數據
*/
int64_t socket_server_udp_send(struct socket_server *, int id, const struct socket_udp_address *, const void *buffer, int sz);
// 獲取傳入消息的IP地址 address, 傳入的 socket_message * 必須是SOCKET_UDP類型
const struct socket_udp_address * socket_server_udp_address(struct socket_server *, struct socket_message *, int *addrsz);
// if you send package sz == -1, use soi.
void socket_server_userobject(struct socket_server *, struct socket_object_interface *soi);
以上函數有的會調用 reserve_id() 來獲取一個 socket_server 中的 slot[] 中的一個 socket,該 socket 會存儲很多相關信息。然而 reserve_id() 僅僅是初始化 socket 結構體,尚有很多其他變量並未被賦值。
這些函數並不是真正的執行者,它們會將任務消息寫入 pipe,然後由 socket_server_poll() 讀取 pipe 再將任務消息交給真正的執行者。與前面 reserve_id() 對應的,在這些真正的執行者中有的會調用new_fd() 函數,進一步對之前 reserve_id() 後的 socket 進一步賦值,並按需將 fd 加入 epoll 的監管下。正如之前分析的 socket_server_poll() 函數。這些消息以字符區分:
常用的指令有:
S Start socket 啓動一個Socket
B Bind socket 綁定一個Socket
L Listen socket 監聽一個Socket
K Close socket 關閉一個Socket
O Connect to (Open) 連接一個Socket
X Exit 退出一個Socket
D Send package (high) 發送數據
P Send package (low) (不常用,也用於發送數據)
A Send UDP package
T Set opt
U Create UDP socket
C set udp address
以上函數的執行伴隨着自定義 socket 結構體裏 type 改變,關係剖析如下:
下面嘗試用用這些API:
以雲風socket-server爲例,裏面有個test.c,爲了便於分析,我們稍作修改:
#include "socket_server.h"
#include <sys/socket.h>
#include <pthread.h>
#include <sys/select.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
int c;
static void *
_poll(void * ud) {
struct socket_server *ss = ud;
struct socket_message result;
for (;;) {
int type = socket_server_poll(ss, &result, NULL);
// DO NOT use any ctrl command (socket_server_close , etc. ) in this thread.
switch (type) {
case SOCKET_EXIT:
return NULL;
case SOCKET_DATA:
printf("message(%lu) [id=%d] size=%d data= %s\n",result.opaque,result.id, result.ud, result.data);
free(result.data);
break;
case SOCKET_CLOSE:
printf("close(%lu) [id=%d]\n",result.opaque,result.id);
break;
case SOCKET_OPEN:
printf("open(%lu) [id=%d] %s\n",result.opaque,result.id,result.data);
break;
case SOCKET_ERROR:
printf("error(%lu) [id=%d]\n",result.opaque,result.id);
break;
case SOCKET_ACCEPT:
printf("accept(%lu) [id=%d %s] from [%d]\n",result.opaque, result.ud, result.data, result.id);
break;
}
}
}
static void
test(struct socket_server *ss) {
pthread_t pid;
pthread_create(&pid, NULL, _poll, ss);
/*
int c = socket_server_connect(ss,100,"127.0.0.1",80);
printf("connecting %d\n",c);
*/
int l = socket_server_listen(ss,200,"127.0.0.1",8888,32); // 使用 127.0.0.1:8888 開啓TCP監聽
printf("listening %d\n",l);
socket_server_start(ss,201,l); // 讓epoll監聽該TCP
int b = socket_server_bind(ss,300,1); // 讓epoll監聽標準輸出
printf("binding stdin %d\n",b);
int i;
c = socket_server_connect(ss, 400, "127.0.0.1", 8888); // 異步連接 127.0.0.1:8888
//sleep(2);
char *data = (char *) malloc(sizeof(char) * 20);
memcpy(data, "hello world", 20);
socket_server_send(ss, c, data, strlen(data)); // 發送數據
/*
for (i=0;i<100;i++) {
socket_server_connect(ss, 400+i, "127.0.0.1", 8888);
}
socket_server_exit(ss);
*/
pthread_join(pid, NULL);
}
int
main() {
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &sa, 0);
struct socket_server * ss = socket_server_create();
test(ss);
socket_server_release(ss);
return 0;
}
編譯後運行,發現數據並未被服務器接收,排查半天,發現是 report_accept() 函數並未將 accept() 新創建的 fd 納入 epoll 監聽。這裏爲了便於分析,我們暫時修改成:將所有新 accept() 得到的 fd 都納入 epoll 監聽。修改僅一處,在 report_accept() 函數中:
struct socket *ns = new_fd(ss, id, client_fd, PROTOCOL_TCP, s->opaque, false);
修改爲:
struct socket *ns = new_fd(ss, id, client_fd, PROTOCOL_TCP, s->opaque, true);
重新編譯,這回可以正常接收。輸出如下(包含部分自己添加的調試信息o(╯□╰)o):
checkctrl = 1
has_cmd = 1
LLLLLLLLLLLLL
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 0 0
listening 1
binding stdin 2
now we get: 1
checkctrl = 1
has_cmd = 1
SSSSSSSSSSSS
s->opaque old: 200, new: 201
oooooooppppppppp3333333
open(201) [id=1] start
checkctrl = 1
has_cmd = 1
BBBBBBBBBBBB
oooooooppppppppp222222
open(300) [id=2] binding
checkctrl = 1
has_cmd = 1
OOOOOOOOOOOOOO
checkctrl = 1
has_cmd = 1
DDDDDDDDDDDDDD
s->type: 4, id = 3
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 1 1
now we get: 2
get fd: 6, opa: 201, type: 3, read: 1, write: 0
client_fd: 8
accept(201) [id=4 127.0.0.1:58840] from [1]
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 1 2
get fd: 7, opa: 400, type: 4, read: 0, write: 1
Error: 0
PTR: 1207962912, 0AAAAAAAAAAAAAAAAAAAAA
oooooooppppppppp5555
open(400) [id=3] 127.0.0.1
checkctrl = 0
event: 2 2
now we get: 1
get fd: 7, opa: 400, type: 5, read: 0, write: 1
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 1 1
now we get: 1
get fd: 8, opa: 201, type: 7, read: 1, write: 0
message(201) [id=4] size=11 data= hello world // 正常接收到數據
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 1 1
skynet是異步讀寫,這讓用戶在調用API時更加容易,不用考慮同步問題,同步問題由skynet內部解決。前面我們接觸了異步connect,現在我們簡單看看它是如何異步寫的。正如 test.c 裏的代碼,我們在調用 socket_server_send() 時並沒有考慮之前的 socket_server_connect() 是否完成。可見異步操作的便捷。socket_server_send() 將 send 消息寫入 pipe,然後由 send_socket() 來真正處理。send_socket()代碼如下:
/*
When send a package , we can assign the priority : PRIORITY_HIGH or PRIORITY_LOW
If socket buffer is empty, write to fd directly.
If write a part, append the rest part to high list. (Even priority is PRIORITY_LOW)
Else append package to high (PRIORITY_HIGH) or low (PRIORITY_LOW) list.
*/
static int
send_socket(struct socket_server *ss, struct request_send * request, struct socket_message *result, int priority, const uint8_t *udp_address) {
int id = request->id;
struct socket * s = &ss->slot[HASH_ID(id)];
struct send_object so;
send_object_init(ss, &so, request->buffer, request->sz);
if (s->type == SOCKET_TYPE_INVALID || s->id != id
|| s->type == SOCKET_TYPE_HALFCLOSE
|| s->type == SOCKET_TYPE_PACCEPT) {
so.free_func(request->buffer);
return -1;
}
assert(s->type != SOCKET_TYPE_PLISTEN && s->type != SOCKET_TYPE_LISTEN);
if (send_buffer_empty(s) && s->type == SOCKET_TYPE_CONNECTED) {
printf("s->type: %d, id = %d\n", s->type, id);
if (s->protocol == PROTOCOL_TCP) {
int n = write(s->fd, so.buffer, so.sz);
if (n<0) {
switch(errno) {
case EINTR:
case EAGAIN:
n = 0;
break;
default:
fprintf(stderr, "socket-server: write to %d (fd=%d) error :%s.\n",id,s->fd,strerror(errno));
force_close(ss,s,result);
return SOCKET_CLOSE;
}
}
if (n == so.sz) {
so.free_func(request->buffer);
return -1;
}
append_sendbuffer(ss, s, request, n); // add to high priority list, even priority == PRIORITY_LOW
} else {
// udp
if (udp_address == NULL) {
udp_address = s->p.udp_address;
}
union sockaddr_all sa;
socklen_t sasz = udp_socket_address(s, udp_address, &sa);
int n = sendto(s->fd, so.buffer, so.sz, 0, &sa.s, sasz);
if (n != so.sz) {
append_sendbuffer_udp(ss,s,priority,request,udp_address);
} else {
so.free_func(request->buffer);
return -1;
}
}
sp_write(ss->event_fd, s->fd, s, true);
} else {
printf("s->type: %d, id = %d\n", s->type, id);
if (s->protocol == PROTOCOL_TCP) {
if (priority == PRIORITY_LOW) {
append_sendbuffer_low(ss, s, request);
} else {
append_sendbuffer(ss, s, request, 0);
}
} else {
if (udp_address == NULL) {
udp_address = s->p.udp_address;
}
append_sendbuffer_udp(ss,s,priority,request,udp_address);
}
}
return -1;
}
send_socket()不經可以發送udp數據,也可發送tcp數據。send_socket() 首先判斷當前 socket 的狀態,如果連接尚未建立,如出於 CONNECTING,那麼將會調用 append 系列函數將數據暫時保存起來,待連接建立後再發送。特別需要格外小心的是一系列sp_write(ss->event_fd, s->fd, s, false) 及 sp_write(ss->event_fd, s->fd, s, true)的用法。對於 epoll 裏的 EPOLLOUT 事件,當發送緩衝有空間,可以被寫入數據時,該事件會一直被觸發(有很大機率一直觸發)。sp_write() 函數就是用於管理是否監聽 EPOLLOUT 事件的鑰匙。比如我們通過源碼可以發現,如果在連接建立之前我們就調用了 send 函數,epoll 就會持續監聽 EPOLLOUT 事件,直到被暫存的發送數據全部被 write() 成功(由 socket_server_poll 裏的 send_buffer 發起),纔會調用 sp_write(ss->event_fd, s->fd, s, false) 結束監聽。send_buffer() 會檢測自定義 socket 結構體裏的發送緩存是否全部發送完畢。
上面的例子中,只實現了客戶端向服務器發數據,如何實現雙向發數據呢?仔細看 send_socket() 這個函數,發現該函數僅保留對 CONNECTING 和 CONNECTED 兩種 socket 類型的處理,過濾掉了其他所有類型socket。那麼按之前的寫法, 服務器的 socket 在 report_accept() 後處於 PACCEPT 的狀態。難道是隻有連接發起者才能發數據?想想不對啊。不小心又看了看 start_socket() 函數,明白了。。
static int
start_socket(struct socket_server *ss, struct request_start *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) {
return SOCKET_ERROR;
}
if (s->type == SOCKET_TYPE_PACCEPT || s->type == SOCKET_TYPE_PLISTEN) {
if (sp_add(ss->event_fd, s->fd, s)) {
s->type = SOCKET_TYPE_INVALID;
return SOCKET_ERROR;
}
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) {
s->opaque = request->opaque;
result->data = "transfer";
return SOCKET_OPEN;
}
return -1;
}
start_socket() 不僅可以讓處於 PLISTEN 狀態的 socket 變爲 LISTEN 狀態,還可以讓處於 PACCEPT 狀態變爲 CONNECTED!!同時還能將其 fd 納入 epoll 的監控下。可見, start_socket() 就是像是使能鍵一樣。關於這一點,我們在之後上層的剖析中會有進一步介紹。由此推想到之前 report_accept() 函數對於 accept() 產生的新 fd,並未將其放在 epoll 的監控下,如果你想使用它進行 socket 通信,還需調用 socket_server_start() 函數來使能。結論:socket_server_listen() 及 report_accept() 新創建的 socket 都需要通過調用 socket_server_start() 來使能,才能收數據。於是,我們將 report_accept() 裏的修改回滾爲之前的:
struct socket *ns = new_fd(ss, id, client_fd, PROTOCOL_TCP, s->opaque, false);
然後,在收到 SOCKET_ACCEPT 消息後,調用 socket_server_start(),修改如下:
int c;
static void *
_poll(void * ud) {
struct socket_server *ss = ud;
struct socket_message result;
for (;;) {
int type = socket_server_poll(ss, &result, NULL);
// DO NOT use any ctrl command (socket_server_close , etc. ) in this thread.
switch (type) {
case SOCKET_EXIT:
return NULL;
case SOCKET_DATA:
printf("message(%lu) [id=%d] size=%d data= %s\n",result.opaque,result.id, result.ud, result.data);
free(result.data);
char *data = (char *) malloc(sizeof(char) * 20); // 新增
memcpy(data, "hello cxl", 20); // 新增
socket_server_send(ss, result.id, data, strlen(data)); // 新增
break;
case SOCKET_CLOSE:
printf("close(%lu) [id=%d]\n",result.opaque,result.id);
break;
case SOCKET_OPEN:
printf("open(%lu) [id=%d] %s\n",result.opaque,result.id,result.data);
break;
case SOCKET_ERROR:
printf("error(%lu) [id=%d]\n",result.opaque,result.id);
break;
case SOCKET_ACCEPT:
printf("accept(%lu) [id=%d %s] from [%d]\n",result.opaque, result.ud, result.data, result.id);
socket_server_start(ss, 600, result.ud); // 新增
break;
}
}
}
於是就能雙向通信了。(我這種寫法是無窮的回聲,根本停不下來。。。)輸出如下:
listening 1
binding stdin 2
checkctrl = 1
has_cmd = 1
LLLLLLLLLLLLL
checkctrl = 1
has_cmd = 1
SSSSSSSSSSSS
s->opaque old: 200, new: 201
oooooooppppppppp3333333
open(201) [id=1] start
checkctrl = 1
has_cmd = 1
BBBBBBBBBBBB
error(300) [id=2]
checkctrl = 1
has_cmd = 1
OOOOOOOOOOOOOO
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 0 0
now we get: 2
get fd: 6, opa: 201, type: 3, read: 1, write: 0
client_fd: 8
accept(201) [id=4 127.0.0.1:56814] from [1]
checkctrl = 1
has_cmd = 1
SSSSSSSSSSSS
s->opaque old: 201, new: 600
oooooooppppppppp3333333
open(600) [id=4] start
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 1 2
get fd: 7, opa: 400, type: 4, read: 0, write: 1
Error: 0
PTR: 0, 0AAAAAAAAAAAAAAAAAAAAA
oooooooppppppppp5555
open(400) [id=3] 127.0.0.1
checkctrl = 0
event: 2 2
now we get: 1
checkctrl = 1
has_cmd = 1
DDDDDDDDDDDDDD
s->type111111111111: 5, id = 3
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 1 1
now we get: 1
get fd: 8, opa: 600, type: 5, read: 1, write: 0
message(600) [id=4] size=11 data= hello world
checkctrl = 1
has_cmd = 1
DDDDDDDDDDDDDD
s->type111111111111: 5, id = 4
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 1 1
now we get: 1
get fd: 7, opa: 400, type: 5, read: 1, write: 0
message(400) [id=3] size=9 data= hello cxlld
checkctrl = 1
has_cmd = 1
DDDDDDDDDDDDDD
s->type111111111111: 5, id = 3
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 1 1
now we get: 1
get fd: 8, opa: 600, type: 5, read: 1, write: 0
message(600) [id=4] size=9 data= hello cxlld
checkctrl = 1
has_cmd = 1
DDDDDDDDDDDDDD
s->type111111111111: 5, id = 4
checkctrl = 1
has_cmd = 0
checkctrl = 0
event: 1 1
now we get: 1
get fd: 7, opa: 400, type: 5, read: 1, write: 0
message(400) [id=3] size=9 data= hello cxlld
.
.
.
附件:
關於socket_server源碼解析,還可以參考一下視頻: