zoukankan      html  css  js  c++  java
  • 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, 广告太多, 想封别人就封别人,坑, ╮(╯▽╰)╭.   拜~~

  • 相关阅读:
    Begin Example with Override Encoded SOAP XML Serialization
    State Machine Terminology
    How to: Specify an Alternate Element Name for an XML Stream
    How to: Publish Metadata for a WCF Service.(What is the Metadata Exchange Endpoint purpose.)
    Beginning Guide With Controlling XML Serialization Using Attributes(XmlSerializaiton of Array)
    Workflow 4.0 Hosting Extensions
    What can we do in the CacheMetaData Method of Activity
    How and Why to use the System.servicemodel.MessageParameterAttribute in WCF
    How to: Begin Sample with Serialization and Deserialization an Object
    A Test WCF Service without anything of config.
  • 原文地址:https://www.cnblogs.com/life2refuel/p/5412934.html
Copyright © 2011-2022 走看看