zoukankan      html  css  js  c++  java
  • 基于libevent, libuv和android Looper不断演进socket编程

    最近在做websocket  porting的工作中,需要实现最底层socket读和写,基于同步读,libevent, libuv和android Looper都写了一套,从中体会不少。

    1)同步阻塞读写

    最开始采用同步阻塞读写,主要是为了快速实现来验证上层websocket协议的完备性。优点仅仅是实现起来简单,缺点就是效率不高,不能很好利用线程的资源,建立连接这一块方法都是类似的,主要的区别是在如何读写数据,先看几种方法共用的一块:

        int n = 0;
        struct sockaddr_in serv_addr;
        event_init();
        if((mSockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
            //TODO error
            return;
        }
        memset(&serv_addr, '0', sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(url.port());
        if(inet_pton(AF_INET, url.host().utf8().data(), &serv_addr.sin_addr)<=0){
            return;
        }
        if( connect(mSockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0){
            return;
        }


    这里由于是client,所以比较简单,当然缺失了DNS解析这一块。然后,就是要监视读数据,由于是同步阻塞读,所以需要在循环里不断地去read/recv:

        while (1) {
            ssize_t result = recv(fd, buf, sizeof(buf), 0);
            if (result == 0) {
                break;
            } else if (result < 0) {
                perror("recv");
                close(fd);
                return 1;
            }
            fwrite(buf, 1, result, stdout);
        }


    缺点就显而易见,此线程需要不断轮询。当然,这里是个例子程序,正式代码中不会处理这么草率。

    2)libevent

    对上面的改进方法就是基于异步非阻塞的方式来处理读数据,在linux上一般是通过epoll来做异步事件侦听,而libevent是一个封装了epoll或其他平台上异步事件的c库,所以基于libevent来做异步非阻塞读写会更简单,也能跨平台。重构的第一个步是设置socketFD为非阻塞:

    static int setnonblock(int fd)
    {
        int flags;
        flags = fcntl(fd, F_GETFL);
        if (flags < 0){
            return flags;
        }
        flags |= O_NONBLOCK;
        if (fcntl(fd, F_SETFL, flags) < 0){
            return -1;
        }
        return 0;
    }


    然后需要在单独的线程中维护event loop,并添加read事件侦听:

    static void* loopListen(void *arg)
    {
        SocketStreamHandle *handle = (SocketStreamHandle *)arg;
        struct event_base* base = event_base_new();
        struct event ev_read;
        handle->setReadEvent(&ev_read);
        setnonblock(handle->getSocketFD());
        event_set(&ev_read, handle->getSocketFD(), EV_READ|EV_PERSIST, onRead, handle);
        event_base_set(base, &ev_read);
        event_add(&ev_read, NULL);
        event_base_dispatch(base);
    }
        pthread_t pid;
        pthread_create(&pid, 0, loopListen, this);
    

    然后在onRead方法中处理数据读取:

    static void onRead(int fd, short ev, void *arg)
    {
        while(true){
            char *buf = new char[1024];
            memset(buf, 0, 1024);
            int len = read(fd, buf, 1024);
            SocketStreamHandle *handle = (SocketStreamHandle *)arg;
            if(len > 0){
                SocketContext *context = new SocketContext;
                context->buf = buf;
                context->readLen = len;
                context->handle = handle;
                WTF::callOnMainThread(onReadMainThread, context);
                if(len == 1024){
                    continue;
                }else{
                    break;
                }
            }else{
                if(errno == EAGAIN || errno == EWOULDBLOCK){
                    return;
                }else if(errno == EINTR){
                    continue;
                }
                __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "onCloseMainThread, len:%d, errno:%d", len, errno);
                WTF::callOnMainThread(onCloseMainThread, handle);
                event_del(handle->getReadEvent());
            }
        }
    }


    这里比较有讲究的是:

    1)当一次buf读不完,需要在循环里再次读一次

    2)当read到0时,表示socket被关闭,这时需要删除事件侦听,不然会导致cpu 100%

    3)当read到-1时,不完全是错误情况,比如errno == EAGAIN || errno == EWOULDBLOCK表示暂时不可读,歇一会后面再读。errno == EINTR表示被系统中断,应重读一遍

    4)onRead是被libevent中专门做事件侦听的线程调用的,所以有的时候需要回到主线程,比如: WTF::callOnMainThread(onReadMainThread, context);这里就需要注意多线程间的同步问题。

    3)libuv

    libuv在libevent更进一步,它不但有event loop,并且把socket的各种操作也覆盖了,所以代码会更简洁,比如最开始的创建连接和创建loop:

        uv_loop_t *loop = uv_default_loop();
        uv_tcp_t client;
        uv_tcp_init(loop, &client);
        struct sockaddr_in req_addr = uv_ip4_addr(url.host().utf8().data(), url.port());
        uv_connect_t *connect_req;
        connect_req->data = this;
        uv_tcp_connect(connect_req, &client, req_addr, on_connect);
        uv_run(loop);


    在on_connect中创建对read的监听:

    static void* on_connect(uv_connect_t *req, int status)
    {
        SocketStreamHandle *handle = (SocketStreamHandle *)arg;
        uv_read_start(req->handle, alloc_buffer, on_read);
    }


    on_read就和前面类似了。所以libuv是最强大的,极大的省略了socket相关的开发。

    4)Android Looper

    Android提供一套event loop的机制,并且可以对FD进行监听,所以如果基于Android Looper,就可以省去对第三方lib的依赖。并且Android也是对epoll的封装,既然如此,值得试一试用Android原生的looper来做这块的event looper。socket连接这块和最开始是一样的,关键是在创建looper的地方:

    static void* loopListen(void *arg)
    {
        SocketStreamHandle *handle = (SocketStreamHandle *)arg;
        setnonblock(handle->getSocketFD());
        Looper *looper = new Looper(true);
        looper->addFd(handle->getSocketFD(), 0, ALOOPER_EVENT_INPUT, onRead, handle);
        while(true){
            if(looper->pollOnce(100) == ALOOPER_POLL_ERROR){
                __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "ALOOPER_POLL_ERROR");
                break;
            }
        }
    }


    代码比较简单就不多说,详细使用方法可以查看<utils/Looper.h>的API。

    综上所述,如果是在Android上做,可以直接基于原生的Looper,如果需要跨平台可以基于libuv。总之,要避免同步阻塞,因为这样会导致线程设计上的复杂和低效。

    在Java里也有类似的概念,可以参见以前的博文:

    从Jetty、Tomcat和Mina中提炼NIO构架网络服务器的经典模式(一)
    从Jetty、Tomcat和Mina中提炼NIO构架网络服务器的经典模式(二)
    从Jetty、Tomcat和Mina中提炼NIO构架网络服务器的经典模式(三)

  • 相关阅读:
    poj 3068 Bridge Across Islands
    XidianOJ 1086 Flappy v8
    XidianOJ 1036 分配宝藏
    XidianOJ 1090 爬树的V8
    XidianOJ 1088 AK后的V8
    XidianOJ 1062 Black King Bar
    XidianOJ 1091 看Dota视频的V8
    XidianOJ 1098 突击数论前的xry111
    XidianOJ 1019 自然数的秘密
    XidianOJ 1109 Too Naive
  • 原文地址:https://www.cnblogs.com/javawebsoa/p/3098130.html
Copyright © 2011-2022 走看看