引言
最經看cloud wind 的 skynet服務器設計. 覺得特別精妙. 想來個專題先剖析其通信層服務器內核
的設計原理. 最後再優化.本文是這個小專題的第一部分, 重點會講解對於不同平台通信基礎的接口封裝.
linux是epoll, unix是 kqueue. 沒有封裝window上的iocp模型(了解過,沒實際用過).
可能需要以下關於 linux epoll 基礎. 請按個參照.
1. Epoll在LT和ET模式下的讀寫方式 http://www.ccvita.com/515.html
上面文字寫的很好, 讀的很受用. 代碼外表很漂亮. 但是不對. 主要是 buf越界沒考慮, errno == EINTR要繼續讀寫等沒處理.
可以適合初學觀摩.
2. epoll 詳解 http://blog.csdn.net/xiajun07061225/article/details/9250579
總結的很詳細, 適合面試. 可以看看. 這個是csdn上的. 扯一點
最經在csdn上給一個大牛留言讓其來博客園, 結果被csdn禁言發評論了. 感覺無辜. 內心很受傷, csdn太武斷了.
3. epoll 中 EWOULDBLOCK = EAGAIN http://www.cnblogs.com/lovevivi/archive/2013/06/29/3162141.html
這個兩個信號意義和區別.讓其明白epoll的一些注意點.
4. epoll LT模式的例子 http://bbs.chinaunix.net/thread-1795307-1-1.html
網上都是ET模式, 其實LT不一定就比ET效率低,看使用方式和數量級.上面是個不錯的LT例子.
到這裡基本epoll就會使用了. epoll 還是挺容易的. 復雜在於 每個平台都有一套基礎核心通信接口封裝.統一封裝還是麻煩的.
現在到重頭戲了. ※skynet※ 主要看下面文件

再具體點可以看 一個cloud wind分離的 githup 項目
cloudwu/socket-server https://github.com/cloudwu/socket-server
引言基本都講完了.
這裡再扯一點, 對於服務器編程,個人認識. 開發基本斷層了. NB的框架很成熟不需要再瘋狂造輪子. 最主要的是 難,見效慢, 風險大, 待遇低.
前言
我們先看cloud wind的代碼. 先分析一下其中一部分.

紅線標注的是本文要分析優化的文件. 那開始吧.
Makefile
socket-server : socket_server.c test.c
gcc -g -Wall -o $@ $^ -lpthread
clean:
rm socket-server
很基礎很實在生成編譯. 沒的說.
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
一眼看到這個頭文件, 深深的為這個設計感到佩服. 這個跨平台設計的思路真巧妙. 設計統一的訪問接口. 對於不同平台
采用不同設計. 非常的出彩. 這裡說一下. 可能在 雲風眼裡, 跨平台就是linux 和 ios 能跑就可以了. window 是什麼. 是M$嗎.
這是玩笑話, 其實 window iocp是內核讀取好了通知上層. epoll和kqueue是通知上層可以讀了. 機制還是很大不一樣.
老虎和禿鹫很難配對.window 網絡編程自己很不好,目前封裝不出來. 等有機會真的需要再window上設計再來個. (服務器linux和unix最強).
那我們開始吐槽雲風的代碼吧.
1). 代碼太隨意,約束不強
static void sp_del(poll_fd fd, int sock); static void sp_write(poll_fd, int sock, void *ud, bool enable);
上面明顯 第二個函數 少了 參數 ,應該也是 poll_fd fd.
2). 過於追求個人美感, 忽略了編譯速度
#ifdef __linux__ #include "socket_epoll.h" #endif #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined (__NetBSD__) #include "socket_kqueue.h" #endif
這個二者是 if else 的關系. 雙if不會出錯就是編譯的時候多做一次if判斷. c系列的語言本身編譯就慢. 要注意
設計沒的說. 好,真好. 多一份難受,少一份不完整.
socket_epoll.h
#ifndef poll_socket_epoll_h
#define poll_socket_epoll_h
#include <netdb.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
static bool
sp_invalid(int efd) {
return efd == -1;
}
static int
sp_create() {
return epoll_create(1024);
}
static void
sp_release(int efd) {
close(efd);
}
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;
}
static void
sp_del(int efd, int sock) {
epoll_ctl(efd, EPOLL_CTL_DEL, sock , NULL);
}
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);
}
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;
}
static void
sp_nonblocking(int fd) {
int flag = fcntl(fd, F_GETFL, 0);
if ( -1 == flag ) {
return;
}
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
#endif
這個代碼沒有什麼問題, 除非雞蛋裡挑骨頭. 就是前面接口層 socket_poll.h 中已經定義了變量名,就不要再換了.
fd -> efd. 例如最後一個將 sock 換成fd 不好.
static void
sp_nonblocking(int fd) {
可能都是大神手寫的. 心隨意動, ~~無所謂~~.
我後面會在正文部分開始全面優化. 保證有些變化. 畢竟他的代碼都是臨摹兩遍之後才敢說話的.
socket_kqueue.h
#ifndef poll_socket_kqueue_h
#define poll_socket_kqueue_h
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/event.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static bool
sp_invalid(int kfd) {
return kfd == -1;
}
static int
sp_create() {
return kqueue();
}
static void
sp_release(int kfd) {
close(kfd);
}
static void
sp_del(int kfd, int sock) {
struct kevent ke;
EV_SET(&ke, sock, EVFILT_READ, EV_DELETE, 0, 0, NULL);
kevent(kfd, &ke, 1, NULL, 0, NULL);
EV_SET(&ke, sock, EVFILT_WRITE, EV_DELETE, 0, 0, NULL);
kevent(kfd, &ke, 1, NULL, 0, NULL);
}
static int
sp_add(int kfd, int sock, void *ud) {
struct kevent ke;
EV_SET(&ke, sock, EVFILT_READ, EV_ADD, 0, 0, ud);
if (kevent(kfd, &ke, 1, NULL, 0, NULL) == -1) {
return 1;
}
EV_SET(&ke, sock, EVFILT_WRITE, EV_ADD, 0, 0, ud);
if (kevent(kfd, &ke, 1, NULL, 0, NULL) == -1) {
EV_SET(&ke, sock, EVFILT_READ, EV_DELETE, 0, 0, NULL);
kevent(kfd, &ke, 1, NULL, 0, NULL);
return 1;
}
EV_SET(&ke, sock, EVFILT_WRITE, EV_DISABLE, 0, 0, ud);
if (kevent(kfd, &ke, 1, NULL, 0, NULL) == -1) {
sp_del(kfd, sock);
return 1;
}
return 0;
}
static void
sp_write(int kfd, int sock, void *ud, bool enable) {
struct kevent ke;
EV_SET(&ke, sock, EVFILT_WRITE, enable ? EV_ENABLE : EV_DISABLE, 0, 0, ud);
if (kevent(kfd, &ke, 1, NULL, 0, NULL) == -1) {
// todo: check error
}
}
static int
sp_wait(int kfd, struct event *e, int max) {
struct kevent ev[max];
int n = kevent(kfd, NULL, 0, ev, max, NULL);
int i;
for (i=0;i<n;i++) {
e[i].s = ev[i].udata;
unsigned filter = ev[i].filter;
e[i].write = (filter == EVFILT_WRITE);
e[i].read = (filter == EVFILT_READ);
}
return n;
}
static void
sp_nonblocking(int fd) {
int flag = fcntl(fd, F_GETFL, 0);
if ( -1 == flag ) {
return;
}
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
#endif
unix 一套機制. 個人覺得比 epoll好,不需要設置開啟大小值. 真心話linux epoll 夠用了. 估計服務器開發用它也就到頭了.
上面代碼還是很好懂得單獨注冊讀寫. 後面再單獨刪除.用法很相似.
前言總結. 對於大神的代碼, 臨摹的效果確實很好, 解決了很多開發中的難啃的問題. 而自己只需要臨摹抄一抄就豁然開朗了.
他的還有一個, 設計上細節值得商榷, 條條大路通羅馬. 對於 函數返回值
......
if (kevent(kfd, &ke, 1, NULL, 0, NULL) == -1) {
sp_del(kfd, sock);
return 1;
}
return 0;
一般約定 返回0表示成功, 返回 -1表示失敗公認的. 還有一個潛規則是返回 <0的表示錯誤, -1, -2, -3 各種錯誤狀態.
返回 1, 2, 3 也表示成功, 並且有各種狀態.
基於上面考慮,覺得它返回 1不好, 推薦返回-1.
還有
static int
sp_create() {
return epoll_create(1024);
}
上面的代碼, 菜鳥寫也就算了. 對於大神只能理解為大巧若拙吧. 推薦用宏表示, 說不定哪天改了. 重新編譯.
這裡吐槽完了, 總的而言 雲風的代碼真的 很有感覺, 有一種細細而來的美感.
正文
到這裡我們開始優化上面的代碼.目前優化後結構是這樣的.

說一下, sckpoll.h 是對外提供的接口文件. 後面 sckpoll-epoll.h 和 sckpoll-kqueue.h 是sckpoll 對應不同平台設計的接口補充.
中間的 '-' 標志表示這個文件是私有的不完整(部分)的. 不推薦不熟悉的實現細節的人使用.
這也是個潛規則. 好 先看 sckpoll.h
#ifndef _H_SCKPOLL
#define _H_SCKPOLL
#include <stdbool.h>
// 統一使用的句柄類型
typedef int poll_t;
// 轉存的內核通知的結構體
struct event {
void* s; // 通知的句柄
bool read; // true表示可讀
bool write; // true表示可寫
};
/*
* 統一的錯誤檢測接口.
* fd : 檢測的文件描述符(句柄)
* : 返回 true表示有錯誤
*/
static inline bool sp_invalid(poll_t fd);
/*
* 句柄創建函數.可以通過sp_invalid 檢測是否創建失敗!
* : 返回創建好的句柄
*/
static inline poll_t sp_create(void);
/*
* 句柄釋放函數
* fd : 句柄
*/
static inline void sp_release(poll_t fd);
/*
* 在輪序句柄fd中添加 sock文件描述符.來檢測它
* fd : sp_create() 返回的句柄
* sock : 待處理的文件描述符, 一般為socket()返回結果
* ud : 自己使用的指針地址特殊處理
* : 返回0表示成功, -1表示失敗
*/
static int sp_add(poll_t fd, int sock, void* ud);
/*
* 在輪詢句柄fd中刪除注冊過的sock描述符
* fd : sp_create()創建的句柄
* sock : socket()創建的句柄
*/
static inline void sp_del(poll_t fd, int sock);
/*
* 在輪序句柄fd中修改sock注冊類型
* fd : 輪詢句柄
* sock : 待處理的句柄
* ud : 用戶自定義數據地址
* enable : true表示開啟寫, false表示還是監聽讀
*/
static inline void sp_write(poll_t fd, int sock, void* ud, bool enable);
/*
* 輪詢句柄,等待有結果的時候構造當前用戶層結構struct event 結構描述中
* fd : sp_create 創建的句柄
* es : 一段struct event內存的首地址
* max : es數組能夠使用的最大值
* : 返回等待到的變動數, 相對於 es
*/
static int sp_wait(poll_t fd, struct event es[], int max);
/*
* 為套接字描述符設置為非阻塞的
* sock : 文件描述符
*/
static inline void sp_nonblocking(int sock);
// 當前支持linux的epoll和unix的kqueue, window會error. iocp機制和epoll機制好不一樣呀
#if defined(__linux__)
# include "sckpoll-epoll.h"
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD) || defined(__NetBSD__)
# include "sckpoll-kqueue.h"
#else
# error Currently only supports the Linux and Unix
#endif
#endif // !_H_SCKPOLL
參照原先總設計沒有變化, 改變在於加了注釋和統一了參數名,還有編譯的判斷流程.
繼續看 epoll 優化後封裝的代碼 sckpoll-epoll.h
#ifndef _H_SCKPOLL_EPOLL
#define _H_SCKPOLL_EPOLL
#include <unistd.h>
#include <netdb.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
// epoll 創建的時候創建的監測文件描述符最大數
#define _INT_MAXEPOLL (1024)
/*
* 統一的錯誤檢測接口.
* fd : 檢測的文件描述符(句柄)
* : 返回 true表示有錯誤
*/
static inline bool
sp_invalid(poll_t fd) {
return fd < 0;
}
/*
* 句柄創建函數.可以通過sp_invalid 檢測是否創建失敗!
* : 返回創建好的句柄
*/
static inline poll_t
sp_create(void) {
return epoll_create(_INT_MAXEPOLL);
}
/*
* 句柄釋放函數
* fd : 句柄
*/
static inline
void sp_release(poll_t fd) {
close(fd);
}
/*
* 在輪序句柄fd中添加 sock文件描述符.來檢測它
* fd : sp_create() 返回的句柄
* sock : 待處理的文件描述符, 一般為socket()返回結果
* ud : 自己使用的指針地址特殊處理
* : 返回0表示成功, -1表示失敗
*/
static int
sp_add(poll_t fd, int sock, void* ud) {
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.ptr = ud;
return epoll_ctl(fd, EPOLL_CTL_ADD, sock, &ev);
}
/*
* 在輪詢句柄fd中刪除注冊過的sock描述符
* fd : sp_create()創建的句柄
* sock : socket()創建的句柄
*/
static inline void
sp_del(poll_t fd, int sock) {
epoll_ctl(fd, sock, EPOLL_CTL_DEL, 0);
}
/*
* 在輪序句柄fd中修改sock注冊類型
* fd : 輪詢句柄
* sock : 待處理的句柄
* ud : 用戶自定義數據地址
* enable : true表示開啟寫, false表示還是監聽讀
*/
static inline void
sp_write(poll_t fd, int sock, void* ud, bool enable) {
struct epoll_event ev;
ev.events = EPOLLIN | (enable? EPOLLOUT : 0);
ev.data.ptr = ud;
epoll_ctl(fd, EPOLL_CTL_MOD, sock, &ev);
}
/*
* 輪詢句柄,等待有結果的時候構造當前用戶層結構struct event 結構描述中
* fd : sp_create 創建的句柄
* es : 一段struct event內存的首地址
* max : es數組能夠使用的最大值
* : 返回等待到的變動數, 相對於 es
*/
static int
sp_wait(poll_t fd, struct event es[], int max) {
struct epoll_event ev[max], *st = ev, *ed;
int n = epoll_wait(fd, ev, max, -1);
// 用指針遍歷速度快一些, 最後返回得到的變化量n
for(ed = st + n; st < ed; ++st) {
unsigned flag = st->events;
es->s = st->data.ptr;
es->read = flag & EPOLLIN;
es->write = flag & EPOLLOUT;
++es;
}
return n;
}
/*
* 為套接字描述符設置為非阻塞的
* sock : 文件描述符
*/
static inline void
sp_nonblocking(int sock) {
int flag = fcntl(sock, F_GETFL, 0);
if(flag < 0) return;
fcntl(sock, F_SETFL, flag | O_NONBLOCK);
}
#endif // !_H_SCKPOLL_EPOLL
還是有些變化的. 看人喜好了. 思路都是一樣的. 這裡用了C99 部分特性. 可變數組, 數組在棧上聲明的 struct event ev[max]; 這樣.
還有特殊語法糖 for(int i=0; i<.......) 等. 確實挺好用的. 要是目前編譯器都支持C11(2011 年C指定標准)就更好了.
sckpoll-kqueue.h

這個沒有使用, 感興趣可以到unix上測試.
到這裡 那我們開始 寫測試文件了 首先是編譯的文件Makefile
test.out : test.c
gcc -g -Wall -o $@ $^
clean:
rm *.out ; ls
測試的 demo test.c. 強烈推薦值得參考
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "sckpoll.h"
// 目標端口和服務器監聽的套接字個數
#define _INT_PORT (7088)
#define _INT_LIS (18)
// 一次處理事件個數
#define _INT_EVS (64)
//4.0 控制台打印錯誤信息, fmt必須是雙引號括起來的宏
#define CERR(fmt, ...) \
fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
__FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)
//4.1 控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量
#define CERR_EXIT(fmt,...) \
CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
//4.3 if 的 代碼檢測
#define IF_CHECK(code) \
if((code) < 0) \
CERR_EXIT(#code)
/*
* 創建本地使用的服務器socket.
* ip : 待連接的ip地址, 默認使用NULL
* port : 使用的端口號
* : 返回創建好的服務器套接字
*/
static int _socket(const char* ip, unsigned short port) {
int sock, opt = SO_REUSEADDR;
struct sockaddr_in saddr = { AF_INET };
// 開啟socket 監聽
IF_CHECK(sock = socket(PF_INET, SOCK_STREAM, 0));
//設置端口復用, opt 可以簡寫為1,只要不為0
IF_CHECK(setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt));
// 設置bind綁定端口
saddr.sin_addr.s_addr = !ip || !*ip ? INADDR_ANY : inet_addr(ip);
saddr.sin_port = htons(port);
IF_CHECK(bind(sock, (struct sockaddr*)&saddr, sizeof saddr));
//開始監聽
IF_CHECK(listen(sock, _INT_LIS));
// 這時候服務就啟動起來並且監聽了
return sock;
}
/*
* 主邏輯, 測試sckpoll.h封裝的簡單讀取發送 服務器
* 需要 C99或以上
*/
int main(int argc, char* argv[]) {
int i, n, csock, nr;
char buf[BUFSIZ];
struct sockaddr_in addr;
socklen_t clen = sizeof addr;
struct event es[_INT_EVS];
// 開始創建服務器套接字和my poll監聽文件描述符
int sock = _socket(NULL, _INT_PORT);
poll_t fd = sp_create();
if(sp_invalid(fd)) {
close(sock);
CERR_EXIT("sp_create is error");
}
// 開始設置非阻塞調節字後面注冊監聽
sp_nonblocking(sock);
// sock 值需要客戶端下來, 這裡會有警告沒關系
if(sp_add(fd, sock, (void*)sock) < 0) {
CERR("sp_add fd,sock:%d, %d.", fd, sock);
goto __exit;
}
//開始監聽
for(;;) {
n = sp_wait(fd, es, _INT_EVS);
if(n < 0) {
if(errno == EINTR)
continue;
CERR("sp_wait is error");
break;
}
//這裡處理 各種狀態
for(i=0; i<n; ++i) {
struct event* e = es + i;
int nd = (int)e->s;
// 有新的鏈接過來,開始注冊鏈接
if(nd == sock) {
for(;;){
csock = accept(sock, (struct sockaddr*)&addr, &clen);
if(csock < 0 ) {
if(errno == EINTR)
continue;
CERR("accept errno = %d.", errno);
}
break;
}
// 開始設置非阻塞調節字後面注冊監聽
sp_nonblocking(csock);
// sock 值需要客戶端下來, 這裡會有警告沒關系
if(sp_add(fd, csock, (void*)csock) < 0) {
close(csock);
CERR("sp_add fd,sock:%d, %d.", fd, csock);
}
continue;
}
// 事件讀取操作
if(e->read) {
for(;;){
nr = read(nd, buf, BUFSIZ-1);
if(nr < 0 && errno != EINTR && errno != EAGAIN) {
CERR("read buf error errno:%d.", errno);
break;
}
buf[nr] = '\0';
printf("%s", buf);
if(nr < BUFSIZ-1) //讀取完畢也直接返回
break;
}
//添加寫事件, 方便給客戶端回復信息
if(nr > 0)
sp_write(fd, nd,(void*)nd, true);
}
if(e->write) {
const char* html = "HTTP/1.1 500 Internal Server Error\r\n";
int nw = 0, sum = strlen(html);
while(nw < sum) {
nr = write(nd, buf + nw, sum - nw);
if(nr < 0) {
if(errno == EINTR || errno == EAGAIN)
continue;
CERR("write is error sock:%d.", nd);
break;
}
nw += nr;
}
// 發送完畢關閉客戶端句柄
close(nd);
}
}
}
// 關閉打開的文件描述符
__exit:
sp_release(fd);
close(sock);
return 0;
}
一共才150行左右, 一般沒有封裝的epoll demo估計都250行. 上面可以再封裝.等第二遍會來個更好的(繼續臨摹優化).
演示結果 先啟動服務器

客戶端測試結果

測試顯示這個服務器處理收發數據都沒問題. 到這裡基本ok了. 上面 test.c 是采用 epoll LT觸發模式, 但是用了 ET的讀和寫方式.
讀 部分代碼
for(;;){
nr = read(nd, buf, BUFSIZ-1);
if(nr < 0 && errno != EINTR && errno != EAGAIN) {
CERR("read buf error errno:%d.", errno);
break;
}
buf[nr] = '\0';
printf("%s", buf);
if(nr < BUFSIZ-1) //讀取完畢也直接返回
break;
}
//添加寫事件, 方便給客戶端回復信息
if(nr > 0)
sp_write(fd, nd,(void*)nd, true);
寫的部分代碼
const char* html = "HTTP/1.1 500 Internal Server Error\r\n";
int nw = 0, sum = strlen(html);
while(nw < sum) {
nr = write(nd, buf + nw, sum - nw);
if(nr < 0) {
if(errno == EINTR || errno == EAGAIN)
continue;
CERR("write is error sock:%d.", nd);
break;
}
nw += nr;
}
// 發送完畢關閉客戶端句柄
close(nd);
對於特殊信號基本都處理了. 到這裡最後總結就是
熟能生巧,勤能補拙.
後記
錯誤是難免的, 交流會互相提高, 有機會繼續分享這個專題. 想吐槽CSDN, 廣告太多, 想封別人就封別人,坑, ╮(╯▽╰)╭. 拜~~