网络开发库从libuv说到epoll
引言
这篇博文可能有点水,主要将自己libuv的学习过程和理解. 简单谈方法. 有点杂. 那我们开始吧.
首先介绍 github . 这个工具特别好用. 代码托管. 如果不FQ可能有点卡. 但是应该试试. 这里扯一点, github
对代码水平提高 太重要了.还有一个解决疑难问题的论坛 stackoverflow http://stackoverflow.com/.
真的屌的不行.
附赠
github 简易教程, 不用谢 http://www.nowcoder.com/courses/2
国内还有一个 逼格特别高的论坛, 哪天你nb了, 也可以上去装逼, 以其中一个帖子为例
知乎epoll讨论 http://www.zhihu.com/question/21516827
到这里关于 引言就结束了.
前言
现在我们开始说libuv, 这是个网络跨平台的库,是C库.比其它同类的网络库, 多了个高效编程.不需要考虑太多细节.
是node.js的底层. 自己学习了一两周,发现, 功能挺强大的.通用性好. 但总觉得有点恶心.后面有时间说. 总的而言很优秀,很好,
但不喜欢.
下面我来分享怎么学习libuv 首先 你要去 官网下载libuv 代码.
libuv github 源码 https://github.com/libuv/libuv 这时候你需要在你的linux上编译安装.
参照步骤就是 readme.md
这时候你肯定会出故障. 怎么做呢. 去 stackoverflow 上 找答案. google搜一下,都能解决. 我当时遇到一个问题是网关超时. 修改网关就可以了. 自己尝试,提高最快.
安装折腾你半天. 那我们 测试一下. 按照 libuv 中文版最后一个demo 为例
#include <stdio.h> #include <string.h> #include <uv.h> uv_tty_t g_tty; uv_timer_t g_tick; int g_width, g_height, g_pos; static void __update(uv_timer_t* req) { uv_write_t wreq; char data[64]; const char* msg = " Hello TTY "; uv_buf_t buf; buf.base = data; buf.len = sprintf(data, "\033[2J\033[H\033[%dB\033[%luC\033[42;37m%s", g_pos, (g_width - strlen(msg))/2, msg); uv_write(&wreq, (uv_stream_t*)&g_tty, &buf, 1, NULL); if(++g_pos > g_height){ uv_tty_reset_mode(); uv_timer_stop(&g_tick); } } // 主函数检测 int main(void) { uv_loop_t* loop = uv_default_loop(); uv_tty_init(loop, &g_tty, 1, 0); uv_tty_set_mode(&g_tty, 0); if(uv_tty_get_winsize(&g_tty, &g_width, &g_height)){ puts("Could not get TTY information"); uv_tty_reset_mode(); return 1; } printf("Width %d, height %d\n", g_width, g_height); uv_timer_init(loop, &g_tick); uv_timer_start(&g_tick, __update, 200, 200); return uv_run(loop, UV_RUN_DEFAULT); }
测试的时候,运行会看见动画. 控制台动画
gcc -g -Wall -o uvtty.c uvtty.c -luv
运行截图是
运行看出来Hello TTY 会一直向下移动知道移动到底了.
好到这里,表示libuv 基本环境是好了,是可以开发了. 来上大头戏.国人有几个人翻译了一本 libuv 开发的书籍 ,
地址
libuv中文编程 拿走不谢 http://www.nowx.org/uvbook/
这里再扯一点, 对于别人的劳动成果, 还是表示感谢.没有他们我们只能是干等着 闭门造车. 外国技术至少领先国内5年.
你看上面书的时候需要对照下面代码看
libuv中文编程 演示代码 https://github.com/nikhilm/uvbook/tree/master/code
你至少需要看完那本书, 有问题翻libuv 源码, 对于书中的 demo code都需要敲一遍. 后面至少遇到libuv不在陌生.
上面能练习code都敲了一遍,临摹并且优化修改了.
到这里关于libuv 的学习思路基本就确定了. 就是 写代码.
好了简单提一下对libuv的理解.
1. libuv 最好的学习方法 看懂源码. ........
(源码能看懂的似懂非懂,目前还是写不出来.)
2.libuv 网络开发确实简单, 网络层 100-200行代码就可以了, 但是它提供了 例如线程池, 定时器揉在一起源码看起来就难一点了, 跨平台的终端控制.
3.libuv 开发全局变量 和 隐含的包头技术 太泛滥不好.....
总而言之C开发中没有一劳永逸的轮子. 否则就成为标准库了. 都有优缺点. 看自己应用领域. 喜欢看网络库的 强烈推荐libuv 比libevent和libuv要
封装的好写. 好久没用也都忘记了. .......
这里也快结束了. 最好的 还是 思想和 设计......
正文
到这里我想了一下,网络库看了有一些了, 但是还是封装不出来. 感觉基础还是不好. 说的太玄乎还是从基础开始吧. 这里就相当了epoll. 还是epoll做起吧.
对于socket 基础开发, 请参照的我的 博文资料 http://www.cnblogs.com/life2refuel/p/5240175.html
简单讲解socket开发 最后还举了个epoll的案例.
对于epoll 其实就 4个函数 man epoll_create 在linux系统上查看就可以了. 对于它怎么入门. 搜索10篇比较不错的epoll博文,看完写完.基本上
就会开发了.其它的就慢慢提升了. 这里我们 不细说epoll 是什么. 就举个简单例子帮助我和大家入门. epoll 本质就是操作系统轮询检测通知上层可以用了.
第一个例子监测 stdin输入
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/epoll.h> #define _INT_BUF (255) // epoll helloworld 练习 int main(void) { int epfd, nfds, i, len; char buf[_INT_BUF]; struct epoll_event ev; epfd = epoll_create(1); //监听一个描述符与. stdin ev.data.fd = STDIN_FILENO; ev.events = EPOLLIN; //使用默认的LT条件触发 epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); // 10 表示等待 30s,过了直接退出 for(;;){ nfds = epoll_wait(epfd, &ev, 1, -1); for(i=0; i<nfds; ++i){ if(ev.data.fd == STDIN_FILENO){ len = read(STDIN_FILENO, buf, sizeof buf - 1); buf[len] = '\0'; printf("%s" ,buf); } } //强加一个结束条件吧 if(random() % 100 >= 90) break; } puts("Epoll Hello world is end!"); // 只要是文件描述符都要释放 close(epfd); return 0; }
// 编译 gcc -g -Wall -o epoll_stdin.out epoll_stdin.c
运行结果是
当用户输入的时候,再读取输出一次.
这里再扯一点,关于 我们使用的 类vi 配置
在根目录, touch .vimrc写入下面信息
"设定默认解码 set fenc=utf-8 "设置默认字符集 set fencs=utf-8,usc-bom,euc-jp,gb18030,gbk,gb2312,cp936 " 用于关闭VI的兼容模式, 采用纯VIM, vi还是比较难搞 set nocompatible "显示行号 set number "vim使用自动对齐,也就是把当前行的对齐格式应用到下一行 set autoindent "依据上面的对齐格式,智能的选择对齐方式 set smartindent "设置tab键为4个空格 set tabstop=4 "设置当行之间交错时使用4个空格 set shiftwidth=4 "设置在编辑过程中,于右下角显示光标位置的状态行 set ruler "设置增量搜索,这样的查询比较smart set incsearch "高亮显示匹配的括号 set showmatch "匹配括号高亮时间(单位为 1/10 s) set ignorecase "在搜索的时候忽略大小写 set matchtime=1 "高亮语法 syntax on
还是比较好用的配合.
最后我们举一个简单的 epoll + pthread 案例, 有时候觉得 从底层做起, 一辈子就是水比. 太难搞了.上代码
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <pthread.h> #include <unistd.h> #include <fcntl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/time.h> #include <sys/resource.h> #include <sys/socket.h> #include <sys/epoll.h> //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.2 检查一行代码,测试结果 #define IF_CHECK(code) \ if((code) < 0) \ CERR_EXIT(#code) // 监听队列要比监听文件描述符epoll少一倍 #define _INT_EPL (8192) #define _INT_BUF (1024) #define _INT_PORT (8088) #define _STR_IP "127.0.0.1" // 待发送的数据 #define _STR_MSG "HTTP/1.0 200 OK\r\nContent-type: text/plain\r\nI am here, heoo...\r\n\r\n" // 线程执行的函数 void* message(void* arg); // 设置文件描述符为非阻塞的, 设置成功返回0 extern inline int setnonblocking(int fd); // 开启服务器监听 int openserver(const char* ip, unsigned short port); // 主逻辑,开启线程和epoll 监听 int main(int argc, char* argv[]) { int nfds, i, cfd; struct sockaddr_in caddr; socklen_t clen = sizeof caddr; pthread_t tid; struct epoll_event ev, evs[_INT_EPL]; int sfd = openserver(_STR_IP, _INT_PORT); int efd = epoll_create(_INT_EPL); if(efd < 0) { close(sfd); CERR_EXIT("epoll_create %d is error!", _INT_EPL); } ev.events = EPOLLIN | EPOLLET; ev.data.fd = sfd; if(epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &ev) < 0){ close(efd); close(sfd); CERR_EXIT("epoll_ctl is error!"); } // 这里开始等待操作系统通知文件描述符是否可以了 __startloop: if((nfds = epoll_wait(efd, evs, _INT_EPL, -1)) <= 0){ if(nfds == 0 || errno == EINTR) goto __startloop; // 这里出现错误,直接返回 CERR("epoll_wait is error nfds = %d.", nfds); goto __endloop; } // 这里是事件正确 for(i=0; i<nfds; ++i) { if(evs[i].data.fd == sfd) { // 新连接过来 // clen做输入和输出参数 cfd = accept(sfd, (struct sockaddr*)&caddr, &clen); if(cfd < 0) { CERR("accept is error sfd = %d.", sfd); goto __startloop; //继续其它服务 } CERR("[%s:%d] happy connected here.", inet_ntoa(caddr.sin_addr), htons(caddr.sin_port)); // 这里开始注册新的文件描述符过来 ev.events = EPOLLIN | EPOLLET; ev.data.fd = cfd; setnonblocking(cfd); if(epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev) < 0) { CERR("epoll_ctl add cfd : %d error.", cfd); // 这里存在一个cfd没有释放问题, 指望 exit之后帮我们释放吧 goto __endloop; } } else { // 这里是处理数据可读可写 // 速度太快,也存在数据异常问题 if(pthread_create(&tid, NULL, message, &evs[i].data.fd) < 0) { CERR("pthread_create is error!"); goto __endloop; } } } goto __startloop; __endloop: CERR("epoll server is error, to exit..."); close(efd); close(sfd); return 0; } // 线程执行的函数 void* message(void* arg) { char buf[_INT_BUF]; int cfd = *(int*)arg, rt; //得到文件描述符 // 设置线程分离属性,自己回收 pthread_detach(pthread_self()); // 数据循环读取, 非阻塞随便搞 for(;;) { rt = read(cfd, buf, _INT_BUF - 1); if(rt < 0){ if(errno == EINTR) //信号中断继续 continue; // 由于非阻塞模式,当缓冲区已无数据可以读写的时候,触发EAGAIN信号 if(errno == EAGAIN){ rt = 1; //标志客户端连接没有断 break; } // 下面就是错误现象 CERR("read cfd = %d, is rt = %d.", cfd, rt); break; } // 需要继续读取客户端数据 if(rt == _INT_BUF - 1) continue; // 下面表示客户端已经关闭 CERR("read end cfd = %d.", cfd); break; } // 给客户端 发送数据 if( rt > 0 ) { // 给客户端发送信息, 多个'\0'吧 write(cfd, _STR_MSG, strlen(_STR_MSG) + 1); } // 这里是处理完业务,关闭和服务器连接 close(cfd); return NULL; } // 设置文件描述符为非阻塞的, 设置成功返回0 inline int setnonblocking(int fd) { int zfd = fcntl(fd, F_GETFD, 0); if(fcntl(fd, F_SETFL, zfd | O_NONBLOCK) < 0){ CERR("fcntl F_SETFL fd:%d, zfd:%d.", fd, zfd); return -1; } return 0; } // 开启服务器监听 int openserver(const char* ip, unsigned short port) { int sfd, opt = SO_REUSEADDR; struct sockaddr_in saddr = { AF_INET }; struct rlimit rt = { _INT_EPL, _INT_EPL }; //设置每个进程打开的最大文件数 IF_CHECK(setrlimit(RLIMIT_NOFILE, &rt)); // 开启socket 监听 IF_CHECK(sfd = socket(PF_INET, SOCK_STREAM, 0)); //设置端口复用, opt 可以简写为1,只要不为0 IF_CHECK(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt)); // 设置bind绑定端口 saddr.sin_addr.s_addr = inet_addr(ip); saddr.sin_port = htons(port); IF_CHECK(bind(sfd, (struct sockaddr*)&saddr, sizeof saddr)); //开始监听 IF_CHECK(listen(sfd, _INT_EPL >> 1)); // 这时候服务就启动起来并且监听了 return sfd; }
这个服务器监测客户端连接发送报文给客户端
编译的时候需要加上 -lpthread
运行结果如下
客户端
上面的关于epoll案例,有机会一定要自己学学. 都挺耗时间的. 但是 不学也不见有什么更有意思的事. 到这里有机会继续分享那些开发中用到的基础
模型.网络开发确实不好搞, 细节太多, 但也容易都是套路...到这里说再见了,希望本文提供一些关于libuv的学习方法和epoll基础案例能够让你至少听过
,有了装逼的方向.
后记
错误是难免的,有问题再交流....