程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C高級 服務器內核分析和構建 (一),內核構建

C高級 服務器內核分析和構建 (一),內核構建

編輯:關於C語言

C高級 服務器內核分析和構建 (一),內核構建


引言

   最經看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, 廣告太多, 想封別人就封別人,坑, ╮(╯▽╰)╭.   拜~~

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved