• 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 项目

    /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 "
    ",
             __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] = '';
                        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
    ";
                    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] = '';
                        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
    ";
                    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, 广告太多, 想封别人就封别人,坑, ╮(╯▽╰)╭.   拜~~

  • 相关阅读:
    (转)性能测试---并发用户理解
    (转)基于DDD的现代ASP.NET开发框架--ABP分层架构
    (转)Web自动化测试中的接口测试
    (转) 一致性Hash算法在Memcached中的应用
    Memcached工作原理及常见问题
    Memcached介绍及相关知识
    .net 面试题总结
    使用IDEA工具配置和运行vue项目(详细其中的坑)
    关于伪分布zookeeper集群启动出错(Error contacting service. It is probably not running.)
    常用查找和排序
  • 原文地址:https://www.cnblogs.com/life2refuel/p/5412934.html
Copyright © 2020-2023  润新知