【Skynet】Socket源碼剖析一

參考了: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源碼解析,還可以參考一下視頻:


發佈了129 篇原創文章 · 獲贊 47 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章