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 项目
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
这个没有使用, 感兴趣可以到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, 广告太多, 想封别人就封别人,坑, ╮(╯▽╰)╭. 拜~~