网络开发库从libuv说到epoll

ぃ、小莉子 提交于 2020-08-17 04:12:44

引言

  这篇博文可能有点水,主要将自己libuv的学习过程和理解. 简单谈方法. 有点杂. 那我们开始吧.

首先介绍 github . 这个工具特别好用. 代码托管. 如果不翻墙可能有点卡. 但是应该试试. 这里扯一点, 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基础案例能够让你至少听过

,有了装逼的方向.

 

后记

  错误是难免的,有问题再交流....

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!