tcpServer_test学习

目录

简介

流程介绍

收获

1.使用网络库libhv开发程序

2.多进程/多线程编程模式

3.lambda表达式的好处

4.原子操作std::atomic 

5.将类的成员函数作为线程函数的方法


简介

本博文是对开源网络库libhv中的示例程序tcpServer_test的学习记录。

流程介绍

1.打开TcpServer_test.cpp文件,看到main()

//运行程序时需要指定端口
int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("Usage: %s port\n", argv[0]);
        return -10;
    }
    int port = atoi(argv[1]);

    hlog_set_level(LOG_LEVEL_DEBUG);

    TcpServer srv;//创建TcpServer
    int listenfd = srv.createsocket(port);//创建socket,调用了socket()、bind()、listen()
    if (listenfd < 0) {
        return -20;
    }
    printf("server listen on port %d, listenfd=%d ...\n", port, listenfd);
    //设置连接回调函数
    srv.onConnection = [](const SocketChannelPtr& channel) {
        std::string peeraddr = channel->peeraddr();
        if (channel->isConnected()) {
            printf("%s connected! connfd=%d id=%d tid=%ld\n", peeraddr.c_str(), channel->fd(), channel->id(), currentThreadEventLoop->tid());
        } else {
            printf("%s disconnected! connfd=%d id=%d tid=%ld\n", peeraddr.c_str(), channel->fd(), channel->id(), currentThreadEventLoop->tid());
        }
    };
    srv.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) {
        // echo
        printf("< %.*s\n", (int)buf->size(), (char*)buf->data());
        channel->write(buf);
    };
    srv.setThreadNum(4);//设置线程数量
    srv.setLoadBalance(LB_LeastConnections);//设置负载均衡模式

    srv.start();//开启服务

    std::string str;
    while (std::getline(std::cin, str)) {
        if (str == "close") {
            srv.closesocket();
        } else if (str == "start") {
            srv.start();
        } else if (str == "stop") {
            srv.stop();
            break;
        } else {
            srv.broadcast(str.data(), str.size());
        }
    }

    return 0;
}

   main主要有如下步骤

   1.创建TcpServer

   2.创建socket

   3.设置onConnection()、onMessage()这两个回调函数,比较简单,不需要单独分析。

   4.开启服务

   下面挨个对main中的主要步骤进行说明

2. 创建TcpServer

    也就是这行代码

TcpServer srv;//创建TcpServer

    TcpServer定义

typedef TcpServerTmpl<SocketChannel> TcpServer;

  接着看看TcpServerTmpl类的定义及构造函数

class TcpServerTmpl : private EventLoopThread, public TcpServerEventLoopTmpl<TSocketChannel> {
public:
    TcpServerTmpl(EventLoopPtr loop = NULL)
        : EventLoopThread(loop)
        , TcpServerEventLoopTmpl<TSocketChannel>(EventLoopThread::loop())
        , is_loop_owner(loop == NULL)
    {}

    ......

}

TcpServerTmpl类继承EventLoopThread类和TcpServerEventLoopTmpl类。调用子类的构造函数时需要先通过初始化列表来构造父类,因此我们先来看看EventLoopThread类和TcpServerEventLoopTmpl类的定义及构造函数。

3. EventLoopThread类

class EventLoopThread : public Status {
public:
    // Return 0 means OK, other failed.
    typedef std::function<int()> Functor;

    EventLoopThread(EventLoopPtr loop = NULL) {
        setStatus(kInitializing);
        loop_ = loop ? loop : std::make_shared<EventLoop>();
        setStatus(kInitialized);
    }

    ......
}

EventLoopThread类继承Status类,Status类封装了状态操作。

EventLoopThread构造函数中,判断loop是否为空,loop不为空则将其赋值给成员变量loop_,若为空则创建一个EventLoopPtr并赋值给loop_。EventLoopPtr是make_shared<EventLoop>()。

接下来看看EventLoop。

EventLoop类简单来说就是一个事件循环类,创建一个EventLoop对象就创建一个事件循环。EventLoop类是对libhv的底层结构体hloop_t的封装,封装了开始事件循环、停止事件循环、暂停事件循环、发送客户消息等函数。

EventLoopThread类就介绍这么多,简单来说TcpServerTmpl的初始列表中的EventLoopThread(loop)就是创建了一个事件循环。

回到步骤2中,接下来我们来看看初始化列表中的TcpServerEventLoopTmpl<TSocketChannel>(EventLoopThread::loop())

4.TcpServerEventLoopTmpl类

template<class TSocketChannel = SocketChannel>
class TcpServerEventLoopTmpl {
public:
    typedef std::shared_ptr<TSocketChannel> TSocketChannelPtr;

    TcpServerEventLoopTmpl(EventLoopPtr loop = NULL) {
        acceptor_loop = loop ? loop : std::make_shared<EventLoop>();
        port = 0;
        listenfd = -1;
        tls = false;
        tls_setting = NULL;
        unpack_setting = NULL;
        max_connections = 0xFFFFFFFF;
        load_balance = LB_RoundRobin;//设置负载均衡
    }

    ...

}

  该类是一个模板类,关于此处模板的作用,我们后面再分析。

  该类的构造函数接收一个EventLoopPtr,

TcpServerTmpl(EventLoopPtr loop = NULL)
        : EventLoopThread(loop) //创建一个事件循环
        , TcpServerEventLoopTmpl<TSocketChannel>(EventLoopThread::loop())//EventLoopThread::loop()表示调用父类EventLoopThread的loop()
        , is_loop_owner(loop == NULL)
    {}

 通过上面的代码可知,EventLoopPtr是通过EventLoopThread的loop()函数获取的,而loop()函数返回的EventLoopPtr是在EventLoopThread类的构造函数中创建的。

在TcpServerEventLoopTmpl类的构造函数中,将EventLoopPtr赋值给成员变量acceptor_loop,将该事件循环作为accept消息循环,它专门用来处理客户的连接事件。在构造函数中还有一个重要操作是设置了负载均衡策略为LB_RoundRobin,关于负载均衡后面还会提及到。

分析完了EventLoopThread类和TcpServerEventLoopTmpl类的定义及构造函数也就将TcpServerTmpl类的构造函数分析完了,总结一下,创建TcpServerTmpl类对象包括创建一个事件循环,然后将该事件循环的指针拷贝到TcpServerEventLoopTmpl类中进行管理,将该消息循环作为专门的accept消息循环。

回到步骤1中,现在应该分析创建socket了

5.创建socket

创建socket也就是main()中的这行代码

int listenfd = srv.createsocket(port);//调用了socket()、bind()、listen()

createsocket()实际也就是调用了socket()、bind()、listen()这几个系统调用,将创建的fd保存在TcpServerEventLoopTmpl类的成员变量中。对于阻塞IO来创建TCPServer的流程来说,调用socket()、bind()、listen()之后,接着就应该调用accept()来等待客户连接(listen()不是阻塞的),但该例程使用的是非阻塞IO,fd的非阻塞属性并不是在该函数中设置的,而是在将fd放入epoll监听之前设置的,后面的分析中还会提到这一点的。

回到步骤1中,现在应该分析创建服务了。

6.开启服务

也就是main()中的这行代码

srv.start();//开启服务

看看start()的实现,分别调用了父类TcpServerEventLoopTmpl和父类EventLoopThread的start()。参数wait_threads_stopped的含义是创建子线程时是否等待子线程运行起来。

// start thread-safe
    void start(bool wait_threads_started = true) {
        TcpServerEventLoopTmpl<TSocketChannel>::start(wait_threads_started);
        EventLoopThread::start(wait_threads_started);
    }

 我们分别看看父类TcpServerEventLoopTmpl的start()和父类EventLoopThread的start()。

7. TcpServerEventLoopTmpl的start()

    先看看start()的实现

    void start(bool wait_threads_started = true) {
        if (worker_threads.threadNum() > 0) 
        {
            //启动线程池
            worker_threads.start(wait_threads_started);
        }
        //启动accept事件循环
        acceptor_loop->runInLoop(std::bind(&TcpServerEventLoopTmpl::startAccept, this));
    }

start()中主要做了两个工作

一是启动线程池,worker_threads是类TcpServerEventLoopTmpl的成员, 其类型是EventLoopThreadPool,EventLoopThreadPool是一个事件循环线程池类,管理多个EventLoopThread,管理线程池的启停和各个事件循环的负载均衡。worker_threads.threadNum在main()中设置,worker_threads.start()即启动事件循环线程池,在worker_threads.start()中的主要工作是创建并启动事件循环,所有的事件循环放在worker_threads的成员loop_threads_中,loop_threads_是一个集合。

二是启动accept事件循环

runInloop()中只是选择在哪执行startAccept(),看看startAccept()的代码

    int startAccept() 
    {
        if (listenfd < 0) {
            listenfd = createsocket(port, host.c_str());
            if (listenfd < 0) {
                hloge("createsocket %s:%d return %d!\n", host.c_str(), port, listenfd);
                return listenfd;
            }
        }
        hloop_t* loop = acceptor_loop->loop();
        if (loop == NULL) return -2;
        hio_t* listenio = haccept(loop, listenfd, onAccept);//调用haccpet()
        assert(listenio != NULL);
        hevent_set_userdata(listenio, this);//设置用户数据,用户数据是当前对象
        if (tls) {
            hio_enable_ssl(listenio);
            if (tls_setting) {
                int ret = hio_new_ssl_ctx(listenio, tls_setting);
                if (ret != 0) {
                    hloge("new SSL_CTX failed: %d", ret);
                    closesocket();
                    return ret;
                }
            }
        }
        return 0;
    }

在haccept()中创建了hio_t对象,该对象保存IO的信息,比如fd、读回调函数、写回调函数、关闭连接回调函数、用户数据等;将socket设置成非阻塞;将acceptfd放入epoll中进行监听;设置连接回调函数onAccept()。

TcpServerEventLoopTmpl的start()函数就介绍完了,总结一下,开启了一个线程池;开启了accept事件循环、设置了连接回调函数、将acceptfd放入到epoll中进行监听。记住这几点,后面还会提到。同时我们也看出了TcpServerEventLoopTmpl类的主要作用,它管理一个线程池和一个accept事件循环。

回到步骤6中,接下来应该介绍父类EventLoopThread的start()了。

8.EventLoopThread的start()

    void start(bool wait_thread_started = true,
               Functor pre = Functor(),
               Functor post = Functor()) 
    {
        //判断线程是否启动,若没有启动则启动线程
        if (status() >= kStarting && status() < kStopped) return;
        setStatus(kStarting);

        thread_ = std::make_shared<std::thread>(&EventLoopThread::loop_thread, this, pre, post);

        if (wait_thread_started) {
            while (loop_->status() < kRunning) {
                hv_delay(1);
            }
        }
    }

EventLoopThread的start()比较简单,无需过多介绍。

介绍完父类TcpServerEventLoopTmpl的start()和父类EventLoopThread的start()步骤6也就介绍完了,至此main()也就介绍完了。那么这个程序接下来是怎么运行的了?还记得我们步骤7中说的将acceptfd放入epoll中进行监听嘛?对喽,接下来就是等待客户连接了,有客户连接时epoll就会触发,事件循环就该登场来处理这个连接了,从epoll触发到事件循环的处理不是一两句能说清楚,后面我会用一章专门来介绍这一点,现在你只需要知道最终调用了步骤7中提到的连接回调函数onAccept(),所以接下来我们应该来看看onAccept()。

9.onAccept()

   分两步来介绍onAccept(),第一onAccept()是在哪被调用的?第二onAccept()函数中做了什么?

   onAccept()在哪被调用?

   从步骤7中的代码我们可以看到,onAccept函数作为参数传递给函数haccept(),所以我们来看看该函数的代码

hio_t* haccept(hloop_t* loop, int listenfd, haccept_cb accept_cb) {
    hio_t* io = hio_get(loop, listenfd);//创建IO
    assert(io != NULL);
    if (accept_cb) {
        io->accept_cb = accept_cb;//将连接回调函数accept_cb保存在io中
    }
    if (hio_accept(io) != 0) return NULL;
    return io;
}

接下来看看hio_accept(),该函数定义在nio.c中,即非阻塞io,

int hio_accept(hio_t* io) {
    io->accept = 1;//设置该io是是用来accept客户端的
    /*hio_add就是将io加入到io事件监视器中,并指定自己需要关注的事件类型是HV_READ可读属性。
      因为当客户端连接服务器时,会令监听套接字成为可读的。当监听套接字的可读事件触发时,
      会调用hio_handle_events函数
    */
    //向事件轮中添加IO读写事件,hio_handle_events()为IO事件回调函数,在其内部进行读、写、accept事件的处理
    return hio_add(io, hio_handle_events, HV_READ);
}

当epoll被触发之后,事件循环流程会先调用hio_handle_events(),在hio_handle_events()中进行一些操作之后再调用hio_accept()。

    onAccept()函数中做了什么?

     看看代码

    static void onAccept(hio_t* connio) 
    {
        TcpServerEventLoopTmpl* server = (TcpServerEventLoopTmpl*)hevent_userdata(connio);
        // NOTE: detach from acceptor loop
        hio_detach(connio);//将连接进来的io从accept loop中剥离出来
        //通过负载均衡机制从事件循环线程池中选择一个工作线程出来
        EventLoopPtr worker_loop = server->worker_threads.nextLoop(server->load_balance);
        if (worker_loop == NULL) {
            worker_loop = server->acceptor_loop;
        }
        ++worker_loop->connectionNum;
        //通过调用newConnEvent完成的工作有:
        //1.将connio加入到工作线程的记录表中,
        //2.以connio构造一个通道,在构造通道时为connio设置好了可读、可写、关闭回调函数
        //3.将io放入到工作线程的epoll中监听可读事件
        worker_loop->runInLoop(std::bind(&TcpServerEventLoopTmpl::newConnEvent, connio));
    }

主要工作首先将connio从accept loop中剥离,因为并没有将connio放入到epoll中进行监听,所以这里的剥离仅仅是将connio从accept loop的记录表中删除,然后根据负载均衡策略从线程池中选择一个工作线程,再在该工作线程中执行newConnEvent()函数,newConnEvent()函数作用见上面代码注释,为connio设置的可读回调为Channel类的on_read(),设置的可写回调函数为Channel类的on_write(),记住这两个回调函数,后面还需要使用到他们。newConnEvent()函数将connio放入epoll中等待可读事件,所以接下来我们就应该看看on_read()函数,探究客户端发送数据来了服务端做了哪些操作。

10. on_read()

当有可读事件时,经过epoll、事件循环一系列的调用最终会调用到Channel.h中的on_read()函数(此处省略10000字,如果你真想找到这条调用流程,我还是简单给一个思路,newConnEvent函数创建通道时已经将io中的各种回调指向了Channel类中的函数,调用startRead函数将io放入epoll中进行监听时,将io的cb指针指向nio.c的hio_handle_events函数,通过分析事件循环代码可知,当有IO事件发送的时候首先调用的是io的cb指针指向的函数即hio_handle_events函数,再在hio_handle_events函数中根据不同的事件类型调用io中指针指向可读、可写、关闭等回调函数,所以最终调用到了Channel.h中的on_read()函数)。那么我们看看on_read函数

    static void on_read(hio_t* io, void* data, int readbytes) 
    {
        Channel* channel = (Channel*)hio_context(io);
        if (channel && channel->onread) {
            Buffer buf(data, readbytes);
            channel->onread(&buf);
        }
    }

再看看onread函数,噢,它是一个函数对象

std::function<void(Buffer*)> onread;

  它是在newConnEvent函数中被设置的,代码如下

static void newConnEvent(hio_t* connio) {
        TcpServerEventLoopTmpl* server = (TcpServerEventLoopTmpl*)hevent_userdata(connio);

        ...

        const TSocketChannelPtr& channel = server->addChannel(connio);
        channel->status = SocketChannel::CONNECTED;

        channel->onread = [server, &channel](Buffer* buf) {
            if (server->onMessage) {
                server->onMessage(channel, buf);
            }
        };
        channel->onwrite = [server, &channel](Buffer* buf) {
            if (server->onWriteComplete) {
                server->onWriteComplete(channel, buf);
            }
        };
        channel->onclose = [server, &channel]() {
            EventLoop* worker_loop = currentThreadEventLoop;
            assert(worker_loop != NULL);
            --worker_loop->connectionNum;

            channel->status = SocketChannel::CLOSED;
            if (server->onConnection) {
                server->onConnection(channel);
            }
            server->removeChannel(channel);
            // NOTE: After removeChannel, channel may be destroyed,
            // so in this lambda function, no code should be added below.
        };
       
       ...
    }

在onread()其实就是调用TcpServerEventLoopTmpl类的omMessage,而该函数在main()中定义,代码如下:

可以看到,onMessage函数先将接收的数据打印出来,然后调用write()将接收的数据发送出去,所以接下来我们应该看看channel->write()。

11. channel->write()

    int write(const void* data, int size) {
        if (!isOpened()) return -1;
        return hio_write(io_, data, size);
    }

    int write(Buffer* buf) {
        return write(buf->data(), buf->size());
    }

看来主要工作的函数是hio_write(),该函数在nio.c中,看看其代码

int hio_write (hio_t* io, const void* buf, size_t len) {
    if (io->closed) {
        hloge("hio_write called but fd[%d] already closed!", io->fd);
        return -1;
    }
    int nwrite = 0, err = 0;
    hrecursive_mutex_lock(&io->write_mutex);
#if WITH_KCP
    if (io->io_type == HIO_TYPE_KCP) {
        nwrite = hio_write_kcp(io, buf, len);
        // if (nwrite < 0) goto write_error;
        goto write_done;
    }
#endif
    //判断写队列是否为空,如果不为空,不能直接写,要先处理写队列中的数据,否则会照成数据乱序
    //所以当队列中有数据时,直接将本次的数据加入到队列尾
    if (write_queue_empty(&io->write_queue)) 
    {
try_write:
        //队列为空直接发送数据
        nwrite = __nio_write(io, buf, len);
        // printd("write retval=%d\n", nwrite);
        if (nwrite < 0) 
        {
            //如果是EAGAIN,那么需要之后再尝试发送,所以这里先入队列
            err = socket_errno();
            if (err == EAGAIN || err == EINTR) //EAGAIN:缓冲区已满 EINTR:系统调用被中断
            {
                //写失败将数据放入队列
                nwrite = 0;
                hlogw("try_write failed, enqueue!");
                goto enqueue;
            } 
            else 
            {
                // 出现错误,关闭连接
                // perror("write");
                io->error = err;
                goto write_error;
            }
        }
        if (nwrite == 0) {
            goto disconnect;
        }

        //如果一次性发送完成,直接就返回,不需要使用写队列
        if (nwrite == len) 
        {
            goto write_done;
        }
enqueue:
        hio_add(io, hio_handle_events, HV_WRITE);//注册写事件
    }

    //如果没有一次性发送完成,需要将数据加入写队列
    if (nwrite < len) 
    {
        if (io->write_bufsize + len - nwrite > io->max_write_bufsize) {
            hloge("write bufsize > %u, close it!", io->max_write_bufsize);
            io->error = ERR_OVER_LIMIT;
            goto write_error;
        }
        offset_buf_t remain;
        remain.len = len - nwrite;
        remain.offset = 0;
        // NOTE: free in nio_write
        HV_ALLOC(remain.base, remain.len);
        memcpy(remain.base, ((char*)buf) + nwrite, remain.len);
        if (io->write_queue.maxsize == 0) {
            write_queue_init(&io->write_queue, 4);
        }
        write_queue_push_back(&io->write_queue, &remain);
        io->write_bufsize += remain.len;
        if (io->write_bufsize > WRITE_BUFSIZE_HIGH_WATER) {
            hlogw("write len=%u enqueue %u, bufsize=%u over high water %u",
                (unsigned int)len,
                (unsigned int)(remain.len - remain.offset),
                (unsigned int)io->write_bufsize,
                (unsigned int)WRITE_BUFSIZE_HIGH_WATER);
        }
    }
write_done:
    hrecursive_mutex_unlock(&io->write_mutex);
    if (nwrite > 0) {
        __write_cb(io, buf, nwrite);
    }
    return nwrite;
write_error:
disconnect:
    hrecursive_mutex_unlock(&io->write_mutex);
    /* NOTE:
     * We usually free resources in hclose_cb,
     * if hio_close_sync, we have to be very careful to avoid using freed resources.
     * But if hio_close_async, we do not have to worry about this.
     */
    if (io->io_type & HIO_TYPE_SOCK_STREAM) {
        hio_close_async(io);
    }
    return nwrite < 0 ? nwrite : -1;
}

hio_write()函数的大致流程在代码中有详细注释,需要强调的是当写队列不为空或者一次没有将发送完时,需要将数据放入写队列,并添加可读事情到epoll中,当发送缓冲区有空间时就会触发可写事情,事件循环调用写函数将写队列中的数据发送出去。

至此,tcpServer_test流程介绍完毕

收获

//TODO 深入挖掘学习到的知识及收获

1.使用网络库libhv开发程序

熟悉了libhv网络库,可以利用它来开发跨平台的程序,而不用自己造基础轮子。事件循环是该库的核心,事件包括IO事件、定时器事件、空闲事件,利用该事件循环不只是开发网络程序,还能利用它来开发并发程序。libhv库中还封装一些好用的模块,比如说字符串操作、日志文件、线程、配置文件操作等。

2.多进程/多线程编程模式

常见的多进程/多线程编程模式有

  • one thread per connection(每个连接一个线程)
  • multi-acceptor-processes(多accept进程模式)
  • multi-acceptor-threads(多accept线程模式)
  • one-acceptor-multi-workers(一个accept线程+多worker线程)

下面3个都是使用非阻塞IO多路复用机制,每个进程/线程都有运行一个事件循环(hloop_run),accept请求,将连接上来的fd加入到IO多路复用中,监听读写事件。

3.lambda表达式的好处

lambda表达式的好处之一,能保存lambada定义时的上下文。//TODO lambada总结笔记

4.原子操作std::atomic 

以前只是在书上看到过原子操作,但实践中并没用使用,多线程访问同一个变量时,我一般是使用锁来保护,但是使用锁的性能比原子操作要低得多。

5.将类的成员函数作为线程函数的方法

我们知道类的成员函数第一个参数其实是this,这是编译器默认给加上的。在创建线程的时候需要在创建线程的函数中手动填写this。

6.

未完......

  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值