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

 

posted on 2016-04-20 16:50  喜欢兰花山丘  阅读(1132)  评论(0编辑  收藏  举报