引言
最經看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
#ifndef poll_socket_kqueue_h #define poll_socket_kqueue_h #include <unistd.h> #include <netdb.h> #include <fcntl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/event.h> /* * 統一的錯誤檢測接口. * fd : 檢測的文件描述符(句柄) * : 返回 true表示有錯誤 */ static inline bool sp_invalid(poll_t fd) { return fd < 0; } /* * 句柄創建函數.可以通過sp_invalid 檢測是否創建失敗! * : 返回創建好的句柄 */ static inline poll_t sp_create(void) { return kqueue(); } /* * 句柄釋放函數 * 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 kevent ke; EV_SET(&ke, sock, EVFILT_READ, EV_ADD, 0, 0, ud); if (kevent(fd, &ke, 1, NULL, 0, NULL) == -1) { return -1; } EV_SET(&ke, sock, EVFILT_WRITE, EV_ADD, 0, 0, ud); if (kevent(fd, &ke, 1, NULL, 0, NULL) == -1) { EV_SET(&ke, sock, EVFILT_READ, EV_DELETE, 0, 0, NULL); kevent(fd, &ke, 1, NULL, 0, NULL); return -1; } EV_SET(&ke, sock, EVFILT_WRITE, EV_DISABLE, 0, 0, ud); if (kevent(fd, &ke, 1, NULL, 0, NULL) == -1) { sp_del(fd, sock); return -1; } return 0; } /* * 在輪詢句柄fd中刪除注冊過的sock描述符 * fd : sp_create()創建的句柄 * sock : socket()創建的句柄 */ static inline void sp_del(poll_t fd, int sock) { struct kevent ke; EV_SET(&ke, sock, EVFILT_READ, EV_DELETE, 0, 0, NULL); kevent(fd, &ke, 1, NULL, 0, NULL); EV_SET(&ke, sock, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); kevent(fd, &ke, 1, NULL, 0, NULL); } /* * 在輪序句柄fd中修改sock注冊類型 * fd : 輪詢句柄 * sock : 待處理的句柄 * ud : 用戶自定義數據地址 * enable : true表示開啟寫, false表示還是監聽讀 */ static inline void sp_write(poll_t fd, int sock, void* ud, bool enable) { struct kevent ke; EV_SET(&ke, sock, EVFILT_WRITE, enable ? EV_ENABLE : EV_DISABLE, 0, 0, ud); kevent(fd, &ke, 1, NULL, 0, NULL); } /* * 輪詢句柄,等待有結果的時候構造當前用戶層結構struct event 結構描述中 * fd : sp_create 創建的句柄 * es : 一段struct event內存的首地址 * max : es數組能夠使用的最大值 * : 返回等待到的變動數, 相對於 es */ static int sp_wait(poll_t fd, struct event es[], int max) { struct kevent ev[max], *st = ev, *ed; int n = kevent(fd, NULL, 0, ev, max, NULL); for(ed = st + n; st < ed; ++st) { unsigned filter = st->filter; es->s = st->udata; es->write = EVFILT_WRITE == filter; es->read = EVFILT_READ == filter; ++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 View Code這個沒有使用, 感興趣可以到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, 廣告太多, 想封別人就封別人,坑, ╮(╯▽╰)╭. 拜~~