博客
关于我
C高级 服务器内核分析和构建 (一)
阅读量:429 次
发布时间:2019-03-06

本文共 18260 字,大约阅读时间需要 60 分钟。

引言

   最经看cloud wind 的 skynet服务器设计. 觉得特别精妙. 想来个专题先剖析其通信层服务器内核

的设计原理. 最后再优化.本文是这个小专题的第一部分, 重点会讲解对于不同平台通信基础的接口封装.

linux是epoll, unix是 kqueue. 没有封装window上的iocp模型(了解过,没实际用过).

可能需要以下关于 linux epoll 基础. 请按个参照.

  1.  http://www.ccvita.com/515.html

上面文字写的很好, 读的很受用. 代码外表很漂亮. 但是不对. 主要是 buf越界没考虑, errno == EINTR要继续读写等没处理.

可以适合初学观摩.

  2.   http://blog.csdn.net/xiajun07061225/article/details/9250579

总结的很详细, 适合面试. 可以看看. 这个是csdn上的. 扯一点

最近在csdn上给一个大牛留言让其来博客园, 结果被csdn禁言发评论了. 感觉无辜. 内心很受伤, csdn太武断了.

  3. epoll 中  http://www.cnblogs.com/lovevivi/archive/2013/06/29/3162141.html

这个两个信号意义和区别.让其明白epoll的一些注意点.

  4.  http://bbs.chinaunix.net/thread-1795307-1-1.html

网上都是ET模式, 其实LT不一定就比ET效率低,看使用方式和数量级.上面是个不错的LT例子.

 

到这里基本epoll就会使用了. epoll 还是挺容易的. 复杂在于 每个平台都有一套基础核心通信接口封装.统一封装还是麻烦的. 

现在到重头戏了.   主要看下面文件

再具体点可以看 一个cloud wind分离的 githup 项目

/  https://github.com/cloudwu/socket-server

引言基本都讲完了.

 

  这里再扯一点, 对于服务器编程,个人认识. 开发基本断层了. NB的框架很成熟不需要再疯狂造轮子. 最主要的是 难,见效慢, 风险大, 待遇低.

 

前言

  我们先看cloud wind的代码. 先分析一下其中一部分.

 

   红线标注的是本文要分析优化的文件. 那开始吧.

Makefile

socket-server : socket_server.c test.c    gcc -g -Wall -o $@ $^ -lpthreadclean:    rm socket-server

很基础很实在生成编译. 没的说.

socket_poll.h

#ifndef socket_poll_h#define socket_poll_h#include 
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 
#include
#include
#include
#include
#include
#include
#include
static bool sp_invalid(int efd) { return efd == -1;}static intsp_create() { return epoll_create(1024);}static voidsp_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

这个代码没有什么问题, 除非鸡蛋里挑骨头. 就是前面接口层 socket_poll.h 中已经定义了变量名,就不要再换了.

fd -> efd. 例如最后一个将 sock 换成fd 不好.

static voidsp_nonblocking(int fd) {

可能都是大神手写的. 心随意动, ~~无所谓~~.

我后面会在正文部分开始全面优化. 保证有些变化. 毕竟他的代码都是临摹两遍之后才敢说话的.

socket_kqueue.h

#ifndef poll_socket_kqueue_h#define poll_socket_kqueue_h#include 
#include
#include
#include
#include
#include
#include
#include
static bool sp_invalid(int kfd) { return kfd == -1;}static intsp_create() { return kqueue();}static voidsp_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

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 intsp_create() {    return epoll_create(1024);}

上面的代码, 菜鸟写也就算了. 对于大神只能理解为大巧若拙吧. 推荐用宏表示, 说不定哪天改了. 重新编译.

这里吐槽完了, 总的而言 云风的代码真的 很有感觉, 有一种细细而来的美感. 

 

正文

  到这里我们开始优化上面的代码.目前优化后结构是这样的.

说一下, sckpoll.h 是对外提供的接口文件. 后面 sckpoll-epoll.h 和 sckpoll-kqueue.h 是sckpoll 对应不同平台设计的接口补充.

中间的 '-' 标志表示这个文件是私有的不完整(部分)的. 不推荐不熟悉的实现细节的人使用.  

这也是个潜规则. 好 先看 sckpoll.h

#ifndef _H_SCKPOLL#define _H_SCKPOLL#include 
// 统一使用的句柄类型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 
#include
#include
#include
#include
#include
#include
#include
// 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 
#include
#include
#include
#include
#include
#include
#include
/* * 统一的错误检测接口. * 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 
#include
#include
#include
#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
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, 广告太多, 想封别人就封别人,坑, ╮(╯▽╰)╭.   拜~~

 

你可能感兴趣的文章
Redis源码分析(七)--- zipmap压缩图
查看>>
Oracle 11G环境配置
查看>>
【Python】(十二)IO 文件处理
查看>>
【Oozie】(三)Oozie 使用实战教学,带你快速上手!
查看>>
师兄面试遇到这条 SQL 数据分析题,差点含泪而归!
查看>>
C语言的数值溢出问题(上)
查看>>
vue项目通过vue.config.js配置文件进行proxy反向代理跨域
查看>>
android:使用audiotrack 类播放wav文件
查看>>
ACM/NCPC2016 C Card Hand Sorting(upc 3028)
查看>>
SLAM学习笔记-求解视觉SLAM问题
查看>>
程序员应该知道的97件事
查看>>
shell编程(六)语言编码规范之(变量)
查看>>
vimscript学习笔记(二)预备知识
查看>>
Android数据库
查看>>
HTML基础,块级元素/行内元素/行内块元素辨析【2分钟掌握】
查看>>
23种设计模式一:单例模式
查看>>
C++&&STL
查看>>
基于单片机简易脉搏测量仪系统设计-毕设课设资料
查看>>
spring启动错误:Could not resolve placeholder
查看>>
invalid byte sequence for encoding
查看>>