下文以TCP echo server为例,使用libhv创建TCP服务端。

文章目录

c版本

#include "hv/hloop.h"

void on_close(hio_t* io) {
}

void on_recv(hio_t* io, void* buf, int readbytes) {
	// 回显数据
    hio_write(io, buf, readbytes);
}

void on_accept(hio_t* io) {
	// 设置close回调
    hio_setcb_close(io, on_close);
    // 设置read回调
    hio_setcb_read(io, on_recv);
    // 开始读
    hio_read(io);
}

int main(int argc, char** argv) {
    if (argc < 2) {
        printf("Usage: cmd port\n");
        return -10;
    }
    int port = atoi(argv[1]);

    // 创建事件循环
    hloop_t* loop = hloop_new(0);
    // 创建TCP服务
    hio_t* listenio = hloop_create_tcp_server(loop, "0.0.0.0", port, on_accept);
    if (listenio == NULL) {
        return -20;
    }
    // 运行事件循环
    hloop_run(loop);
    // 释放事件循环
    hloop_free(&loop);
    return 0;
}

编译运行:

$ cc examples/tcp_echo_server.c -o bin/tcp_echo_server -I/usr/local/include/hv -lhv
$ bin/tcp_echo_server 1234

类unix系统可使用nc作为客户端测试:

$ nc 127.0.0.1 1234
< hello
> hello

windows端可使用telnet作为客户端测试:

$ telent 127.0.0.1 1234

更多TCP服务端示例参考:

hio_t更多实用接口:

  • hio_enable_ssl:启用SSL/TLS加密通信
  • hio_set_connect_timeout:设置连接超时(仅用作TCP客户端)
  • hio_set_read_timeout:设置读超时,一段时间没有数据接收,自动断开连接
  • hio_set_write_timeout:设置写超时,一段时间没有数据发送,自动断开连接
  • hio_set_keepalive_timeout:设置keepalive超时,一段时间没有数据读写,自动断开连接
  • hio_set_heartbeat: 设置心跳
  • hio_set_unpack:设置拆包规则,支持固定包长、分隔符、头部长度字段三种常见的拆包方式,内部根据拆包规则处理粘包与分包,保证回调上来的是完整的一包数据,大大节省了上层处理粘包与分包的成本
  • hio_read_until_length:读取数据直到指定长度才回调上来
  • hio_read_until_delim:读取数据直到遇到分割符才回调上来
  • hio_setup_upstream:设置转发

c++版本

代码示例参考evpp/TcpServer_test.cpp

#include "hv/TcpServer.h"

using namespace hv;

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("Usage: %s port\n", argv[0]);
        return -10;
    }
    int port = atoi(argv[1]);

    TcpServer srv;
    int listenfd = srv.createsocket(port);
    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\n", peeraddr.c_str(), channel->fd());
        } else {
            printf("%s disconnected! connfd=%d\n", peeraddr.c_str(), channel->fd());
        }
    };
    srv.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) {
        // echo
        printf("< %.*s\n", (int)buf->size(), (char*)buf->data());
        channel->write(buf);
    };
    srv.onWriteComplete = [](const SocketChannelPtr& channel, Buffer* buf) {
        printf("> %.*s\n", (int)buf->size(), (char*)buf->data());
    };
    srv.setThreadNum(4);
    srv.start();

    // press Enter to stop
    while (getchar() != '\n');
    return 0;
}

编译运行:

$ c++ -std=c++11 evpp/TcpServer_test.cpp -o bin/TcpServer_test -I/usr/local/include/hv -lhv -lpthread
$ bin/TcpServer_test 5678

TcpServer更多实用接口

  • setThreadNum:设置IO线程数
  • setMaxConnectionNum:设置最大连接数
  • setLoadBalance: 设置负载均衡策略(轮询、随机、最少连接数)
  • setUnpack:设置拆包规则(固定包长、分界符、头部长度字段)
  • withTLS:SSL/TLS加密通信
  • broadcast: 广播

Tips
以上示例只是简单的echo服务,TCP是流式协议,实际应用中请务必添加边界进行拆包。
文本协议建议加上\0或者\r\n分隔符,可参考 examples/jsonrpc
二进制协议建议加上自定义协议头,通过头部长度字段表明负载长度,可参考 examples/protorpc
然后通过hio_set_unpackTcpClient::setUnpackTcpServer::setUnpack设置拆包规则。
不想自定义协议和拆包组包的可直接使用现成的HTTP/WebSocket协议;

Tips:

  • hio_write是非阻塞的(事件循环异步编程里所有的一切都要求是非阻塞的),且多线程安全的,关于hio_write的实现这里简单介绍下,每个hio_t的内部都会维护一个写队列write_queue,调用hio_write时会上锁检查写队列是否为空,为空会先尝试发送,一般小于系统发送缓冲区的数据在这一步就直接写入了,如果没写完的就会放入hio_t的写队列(深拷贝,不依赖上层buffer),监听可写事件,在可写时再从写队列顺序取出数据发送,即使调用hio_write后马上hio_close(短连接发送完响应后马上close是常规操作,而实际期望是写数据完成再断开连接),如果写队列非空,也不会马上关闭连接,而是起了一个close_timer定时器,等待写完成后或者定时器超时(默认是一分钟,可以通过hio_set_close_timeout设置该值)才执行真正的close操作。hio_write的返回值也和阻塞里的write/send返回值有所不同,有效范围是[0, len]<0代表出错,=0表示该次一个字节也没有写入,并不是断链了,=len表示写入了全部数据到系统发送缓冲区,<len也是正常现象(一般发送几M以上的数据时就会发生),表示一部分写入了系统发送缓冲区,另外一部分写入了hio_t的写队列;所以hio_write的返回值并不需要关心和判断,如果是<0也会统一触发close回调的,不需要再次手动调用hio_close,而对于小数据量的发送(几十M以下),我们也根本就不需关心写完成回调,只有在发送大数据量时我们才需要做数据分片发送速率控制,避免全部积压到写队列导致内存爆炸;
  • hio_close也是多线程安全的,这可以让网络IO事件循环线程里接收数据、拆包组包、反序列化后放入队列消费者线程从队列里取出数据、处理后发送响应和关闭连接,变得更加简单;
  • 所以libhv根本用不着libeventbufferevent,也比libuvuv_write方便的多,我认为这正是libhvlibeventlibuv使用简单最重要的一点(并不是libhv还提供了c++封装HTTP/WebSocket实现,虽然这也是很重要的基础设施,libevent、libuv因为局限于c语言并没有提供官方c++封装,evhttp因为是c语言的接口也是相当难用,libwebsockets更是难用)。
  • 有人说没必要使用网络库,直接使用epoll不行吗?也就socket->bind->listen->acceptepoll_create、epoll_ctl、epoll_wait几个系统API,当然可以,但当你真正工程实践时,且不论跨平台定时心跳定时推送需要用到定时器write、close的线程安全问题非阻塞写队列的维护、读缓冲readbuf的自动扩缩容粘包分包的处理、负载均衡策略,哪一项不是有挑战性的难题,久而久之自然就形成了一层封装,形成所谓的网络库,而且网络库需要时间的沉淀和考验。
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐