asio

asio

qt和asio各有各的事件循环,如果要整合,一种方法是 asio run在另一个线程, qt gui跑在主线程,这样发起网络调用时后,返回的结果,asio会回调给你,但是这个回调是在asio的线程中调用的,所以不能直接在asio的线程中调用gui相关的函数,可以发起一个信息,然后主线程的槽函数会响应处理。如果想asio与qt跑在一个线程里,有个很简单,但比较土的方法,起一个QTimer定时器,间隔多少毫秒就发出一个信号,相关的槽函数 调用asio 的 poll_one函数。还有其它方法可以把asio与qt整在一线程里,就是重写QAbstractEventDispatcher,只不过比较麻烦了。可参考 https://github.com/peper0/qtasio/

https://zhuanlan.zhihu.com/p/39973955 C++网络编程之asio(二)

性能

这一篇本来想用asio做一下压测,然后分享压测的结果,可惜现在没时间,而且这样的压测需要申请十几台服务器作为客户端,要等到合适的时机。关于性能,我可以给出一个经验值。按照以前单线程epoll异步编程的经验,客户端可以发起几万连接、服务器可以接受几十万连接,我相信asio也不会差到哪里去,而且asio还支持多线程的模式,利用多核的特性在某些情况下性能应该有更大的提升。所以,90%以上的用户都不用担心吧。

asio是否过时

asio的第一个版本早在90年代末就出现了,我上次在某个论坛上看到一个老外说,asio的思想太老了。作为一个小白,看到这种言论心中有点慌,虽然不是追新族,但是对于新技术还是有点执念的。那么,asio真的老吗?最近学习python网络库的时候改变了我的看法。python3.4于2014年推出,包含了asyncio,我第一眼看到目录的时候吓了一跳,event loop、future、tasks、executor、handle,等等等等,这尼玛就是asio的翻版呀。当然了,虽然python的异步网络库比asio晚十几年才出,都是开源大作,应该不存在抄袭,唯一的解释就是,现在主流跨平台的异步网络编程思想就是这样的,所以python也是这么做的。所以呢,关于asio思想过时的想法包袱不存在了,可以更加放心花时间去学了。

源码阅读

asio的源码阅读起来有点困难,采用了太多的宏与模板,并且还有些代码是为了兼容以前的老版本而存在的,有点干扰视线。我大致浏览了一下,也作了一些初步的笔记,主要感受有2点:1、库中的很多代码都采用了多重继承+模板特化的方法,作者的功底很深,是一个值得学习的好库。更为强大的是,我目前发现除了最底层的kqueue_reactor/epoll_reactor/win_iocp_io_context等这几个类以外,其它的所有功能都是可以被定制的,而且不少功能的定制方法还不止一种。扩展方法主要有模板特化、从现有类继承、提供自定义的函数对象等等。就冲着如此强大的扩展性,进入C++标准库就实至名归了。2、asio的源码是跨平台多线程编程的经典例子。多线程编程其实有不少坑,asio内部的实现用到了多线程编程的各种特性,对学习多线程编程很有用。关于多线程我的经验不多,鉴于单线程异步编程的强大性能对于大部分的人都已经足够,我后面的例子都会以单线程为主。多线程的模式可能会放在最后的章节。由于我本身是做服务器的,一般会选用多进程而不是多线程。

同步和异步

asio也提供了同步编程的方法,但是同步的学起来很简单,不需要啥教程,而且同步的使用场景相对来说比较狭窄,所以我后面只举异步编程的例子。

基础类和函数

asio的库包含了很多类和函数,作为一般的应用,只要掌握常用的几个即可,列举如下。

asio::io_context类

基础设施,可以看作事件循环。socket、timer都需要它。io_context可以作为应用程序唯一的事件循环,也可以很容易地与qt等现有的event loop集成。

asio::io_context::run()成员函数

运行事件循环。

asio::ip::tcp::socket类

tcp socket类

asio::ip::tcp::acceptor类

tcp服务器用来接受客户端连接的类

asio::ip::tcp::endpoint

tcp地址+端口,用作参数

asio::buffer系列类

buffer,用来缓存需要收发的数据。buffer相关的类是asio中功能非常独立的部分,和其它的功能交集不多,所以掌握起来最为简单。

acceptor::async_accept成员函数

接受一个连接。注意只有一个。如果要接受多个,在回调函数中再次调用此函数。

socket::async_read_some成员函数

接收一次数据,收到多少是多少。

socket::async_write_some成员函数

发送一次数据,需要发的数据未必一次就可以发完。

asio::async_read全局函数

读取指定字节数的数据。这个函数是asio对socket.async_read_some的高级封装,在很多场合用这个函数可以节省很多代码。

asio::async_read_until全局函数

读取数据直到满足某个条件为止。

asio::async_write全局函数

发送指定字节的数据,直到发完为止。

本节未介绍udp相关的类。实际上,udp在不少场合的表现远优于tcp,而且已经有各种可靠udp的实现。

异步编程还有一个很重要的设施就是timer,即定时器,关于timer后面再介绍。asio的定时器管理采用的是堆结构,复杂度为O(logN),效率较低,实测每秒处理5万个左右。前面提到了,asio的可扩展性极高,对于定时器的管理,也可以用自定义的类。大量的定时器可以采用一种叫做时间轮的数据结构来实现,复杂度为O(1)。

asio相关的C++知识

使用asio需要熟悉C++11的lambda、std::function以及智能指针std::shared_ptr、std::enable_shared_from_this等。

asio初步实践

最后来写一个http服务器。http服务器是学习网络编程的好素材。http协议的文档很全,且主要是使用文本协议,测试简单,用浏览器就可以测试。http协议不仅仅只能用来做web服务器,也可以直接拿来在项目中作为通信协议,比如说网络游戏也可以直接用http协议。
http服务器的编程具有下限低上限高的特点,非常适合用来作为学习编程的素材。所谓的下限低是指几分钟就能写一个,上限高是指如果要打磨、完善、优化可能需要几年的时间。用C写的极简http服务器只需要200行代码,这里就有一个。用asio来实现一个类似的只要100行以内。我的实现如下。

#include <iostream>
#include <string>
#include <memory>
#include "asio.hpp"

using namespace std;

class HttpConnection: public std::enable_shared_from_this<HttpConnection>
{
public:
  HttpConnection(asio::io_context& io): socket_(io) {}

  void Start()
  {
    auto p = shared_from_this();
    asio::async_read_until(socket_, asio::dynamic_buffer(request_), "

",
        [p, this](const asio::error_code& err, size_t len) {
      if(err)
      {
        cout<<"recv err:"<<err.message()<<"
";
        return;
      }
      string first_line = request_.substr(0, request_.find("
")); // should be like: GET / HTTP/1.0
      cout<<first_line<<"
";
      // process with request
      // ...

      char str[] = "HTTP/1.0 200 OK

"
          "<html>hello from http server</html>";
      asio::async_write(socket_, asio::buffer(str), [p, this](const asio::error_code& err, size_t len) {
        socket_.close();
      });
    });
  }

  asio::ip::tcp::socket& Socket() { return socket_; }
private:
  asio::ip::tcp::socket socket_;
  string request_;
};

class HttpServer
{
public:
  HttpServer(asio::io_context& io, asio::ip::tcp::endpoint ep): io_(io), acceptor_(io, ep) {}

  void Start()
  {
    auto p = std::make_shared<HttpConnection>(io_);
    acceptor_.async_accept(p->Socket(), [p, this](const asio::error_code& err) {
      if(err)
      {
        cout<<"accept err:"<<err.message()<<"
";
        return;
      }
      p->Start();
      Start();
    });
  }
private:
  asio::io_context& io_;
  asio::ip::tcp::acceptor acceptor_;
};

int main(int argc, const char* argv[])
{
  if(argc != 3)
  {
    cout<<"usage: httpsvr ip port
";
    return 0;
  }

  asio::io_context io;
  asio::ip::tcp::endpoint ep(asio::ip::make_address(argv[1]), std::stoi(argv[2]));
  HttpServer hs(io, ep);
  hs.Start();
  io.run();
  return 0;
}

async_***函数的回调函数是asio里面的一个重要部分,叫做Completion handler,后面打算专门介绍。这次的代码很少,我全部贴这里了。以后代码较多的时候就放在github上。

https://zhuanlan.zhihu.com/p/46116528 C++网络编程之asio(三)

asio::post是线程安全的,使用起来很简单,asio系列文章的第三篇结合一个自己实现的redis client来展示其用法;状态机是网络编程中协议解析常用的工具,这里也简单展示一下。

redis是一个流行的数据库,过去几年获得了巨大的成功,当下互联网很多编程语言的技术栈都包含了它。redis的协议基于文本格式,人肉可读性很好,因为redis的流行,很多服务程序都支持redis格式的协议。

在网络编程时,对于协议的解析可以采用状态机的思想。状态机的全名叫有限状态机(finite state machine),简称fsm,知乎上面关于fsm的话题很少,有兴趣的可以自己研究,简单的fsm可以理解为一个用来表示状态的变量(一般是int、bool、enum类型)加上一堆switch...case或一些if...else语句。我这里演示采用fsm+面向对象的方法来解析redis的协议,现实起来简单清晰。

redis的安装非常简单,mac和linux都可以一键安装,知乎上面关于redis的话题非常多,这里就不介绍了。redis有一个命令行client和一组C语言的api,我这里用asio来实现一个c++的api,然后再用对应的api实现一个类似的命令行客户端。

因为调用std::cin的时候会阻塞,标准库中没有异步iostream的api,我不想引入太多第三方库,所以对于用户的输入专门放在一个线程中,将asio的接收和发送数据放在另一个线程中。asio对于对线程的支持极好,很多时候不需要自己加锁,可以刚好借这个例子演示一下。

相关的代码在这里:

只需要安装cmake和支持c++11的编译器,把整个network取下来即可:

git clone --recursive https://github.com/franktea/network.git;
cd network;
mkdir -p build;
cd build;
cmake ..;
make;

这样就可以编译整个目录,我前阵子学习aiso的例子都在这里面。我只在mac/linux下面编译过,windows里面如果有警告可以自己处理一下。

redis的协议非常简单,文档几分钟就可以看完,在这里:,格式共分为5种。

以+开头的简单字符串Simple Strings;
以-开头的,为错误信息Errors;
以:开头的,为整数Integers;
以$开头的,表示长度+字符串内容的字符串,redis叫它Bulk Strings;
以*开头的,redis协议里面叫Arrays,表示一个数组,是上面多种元素的组合。Arrays是可以嵌套的,相当于一个树状结构,前面四种格式都是叶子节点,Arrays是非叶子节点。

对于请求协议,全部打包成多Arrays格式即可。比如说set aaa bbb,打包成:*3 $3 set $3 aaa $3 bbb 。从命令行中获得输入,然后转换成对应的格式,这种转换只需要写一个很简单的类即可搞定。对于redis的回包,则可能包含5种协议的若干种,需要每种都能解析,因为有5种,所以定义一个父类AbstractReplyItem,然后对于每种具体的协议实现一个子类。我的实现类图如下:

其中有3种协议都只有一行,所以定义了一个中间的父类OneLineString。AbstractReplyItem用一个工厂方法,创建对应的子类:

std::shared_ptr<AbstractReplyItem> AbstractReplyItem::CreateItem(char c)
{
    std::map<char, std::function<AbstractReplyItem*()>> factory_func = {
   		 {'*', []() { return new ArrayItem; } },
   		 {'+', []() { return new SimpleStringItem; } },
   		 {'-', []() { return new ErrString; } },
   		 {':', []() { return new NumberItem; } },
   		 {'$', []() { return new BulkString; } },
    };

    auto it = factory_func.find(c);
    if(it != factory_func.end())
    {
   	 return std::shared_ptr<AbstractReplyItem>(it->second());
    }

    return nullptr;
}

在解析回包时,调用Feed函数,每次解析一个字符,在单元测试的response_parser_test.cpp中也可以看到其用法。

上面简单地说了一下解析协议的类的结构,现在来看看如何用状态机进行解析。以OneLineString为例,该类解析前面5中格式中的以+、-、:三种字符开头的回包协议,例如:

"+OK
"
"-Error message
"
":456789
"

最前面一个字符用来标识协议的种类,最后一个 表示解析完毕,对于这几种只有一行文本的协议,我定义了2个状态:

enum class OLS_STATUS { PARSING_STRING,
    EXPECT_LF // got 
, expect 

    };

用enum来定义意义更清晰,其实用int也可以。PARSING_STRING表示正在解析文本,如果碰到 ,就变成EXPECT_LF状态,表示接下来必须是 。

在OneLineString中只有两种状态的状态机,这应该是全世界最简单的状态机了,其实只要用一个if...else就可以实现。在解析Arrays的时候,就需要更多种状态才可以描述了。Arrays格式的一个例子如下:

*5

:1

:2

:3

:4

$6

foobar

首先要解析*后面的数字,这个数字就是对应子协议的条数,上面这个例子的条数为5,然后再依次解析每条子协议。我定义了4种状态:

	enum class AI_STATUS { PARSING_LENGTH,
   	 EXPECT_LF, // parsing length, got 
, expect 

   	 PARSING_SUB_ITEM_HEADER, // expect $ + - :
   	 PARSEING_SUB_ITEM_CONTENT
    };

可见,所谓的状态机就是定义一组状态,然后根据输入事件(在解析协议的时候输入事件就是一个个的字符)和当前状态,进行不同的处理,处理的时候可能发生状态切换。四种状态机也不算非常复杂,想关的实现可以直接看github上的代码,关于状态机这里就不多说了。

解析协议相关的类实现以后,就可以用asio来实现client api了,有了client api,就可以拿来发送redis的请求了。general-client.cpp里面实现了一个非常简单的client类,叫OneShotClient,每个实例只发送-接受一次请求,用完即销毁,多个请求就要创建多个实例,这种方法比较适合短链接。

在multi-thread-client.cpp里面我实现了一个和redis-cli类似的一个命令行工具。 在main函数里面创建了两个线程,一个线程(主线程)用来从命令行读取数据,然后将数据发送到asio数据收发的线程,发送数据的代码如下:

void Send(const string& line)
{
    auto self = shared_from_this();
    asio::post(io_context_, [self, this, line]() { // 此函数会在io_context::run的线程中执行
        requests_.push_back(line); // 将数据放入队列
        SendQueue(); // asio从队列里面取出数据发送给redis 
    });
}

调用asio::post,不需要加锁,post参数中lambda函数是在另一个线程中执行的。注意一下,需要发送的数据const string& line,在Send函数里面传的是引用,可以避免一次拷贝,但是在将其post到另一个线程中时,传的是拷贝(lambda中捕捉用的是line而不是&line),line被拷了一份,其生命周期和原来的参数就无关了。多线程编程重要的就是搞清楚对象的生命周期,避免出现空悬指针的情况。

asio对多线程支持很好,一般情况都不需要自己加锁,有asio::strand可以用实现常用的同步。在这个redis-client的例子中,主线程负责读取用户输入,另一个线程调用asio的收发函数,收到数据并输出到std::cout中,严格地说,也是需要同步的,为了简单,我没做同步。

但是在io_context目录中,演示了asio::strand的用法:

asio::io_context io; 
asio::strand<asio::io_context::executor_type> str(io.get_executor()); 

https://zhuanlan.zhihu.com/p/51216945  C++网络编程之asio(四)——reactor模式与libcurl

asio用的是proactor模式,于是reactor的粉丝就对asio无脑黑。其实asio功能很强大,直接支持reactor模式,满足各种不同强迫症玩家的胃口。

想要使用reactor模式,只需要调用下面两个函数:

socket::async_wait(tcp::socket::wait_read, handler)
socket::async_wait(tcp::socket::wait_write, handler)

在handler回调函数里面亲自调用read/write读写数据,与其它所有支持reactor的网络框架的用法如出一辙。

handler回调函数的格式如下:

void handler(const asio::error_code& error);

以读取数据为例,可以定义类似如如下格式的回调函数 。这里需要用到native_handle()函数,这个函数返回socket封装的底层的socket fd。

void Session::read_handler(const asio::error_code& error)
{
 if(!error)
 {
  int n = ::read(socket_.native_handle()buffer_, sizeof(buffer_);
 ……
 }
}

就这样,reactor模式的用法就已经演示完了。其实,为了使用reactor而使用reactor,在asio的世界里面是没有前途的。那啥时候必须要使用呢?答案就是:配合第三方软件的时候。现在以asio+libcurl的用法,来诠释asio的reactor模式的应用场合。

libcurl是一个功能极为强大的客户端网络库,无论是想做一个网络爬虫,还是想用c++去访问隔壁项目组提供的http服务,libcurl都是一个不二的选择。但是libcurl的文档比较晦涩,网上很多教程也都是盲人摸象,自说自话,想要真正用起来,需要费一番周折。

关于asio+libcurl的例子,目前能找到的例子都很老,而且有些bug,我这篇文章相关的代码在:,可以作为较新的参考。

关于libcurl结合外部eventloop的文档,地址在:。根据该文档所说,该用法的重点是二个回调函数,一个是在某个socket需要关注的事件(可读、可写)发生变化时的回调函数,另一个是在libcurl关注的超时发生变化时的回调函数。但是如何结合asio来使用,还有其它不少需要注意的地方。之前写过epoll+libcurl,这次重新写asio+libcurl,发现各有优缺点,当然理解也更深了一些。

关于libcurl的各种坑,在这里不详细介绍了,有兴趣的可以去github直接看我的代码。

https://zhuanlan.zhihu.com/p/58784652  C++网络编程之asio(五)——在asio中使用协程

在前不久关于C++20特性的最后一次会议上,coroutine ts终于通过投票。在语法上来说,协程的内容主要包括三个新的关键字:co_await,co_yield 和 co_return,以及std命名空间(编译器的实现目前还是在std::experimental)中的几个新类型:

coroutine_handle<P>、coroutine_traits<Ts...>、suspend_always、suspend_never

这些功能其实相当于实现协程的”汇编语言“,用起来很麻烦,它们主要是给库的作者使用的,比如说asio网络库的作者,用它来给asio加上协程的支持功能。

面向大众的协程日常功能需要再提供一套辅助的程序库,比如说std::generator、std::task之类的,只不过C++20的功能已经冻结了,在C++20中已经来不及加进去了,指望std中提供这套库估计需要到C++23才会有。但是github上面已经有了cppcoro库,可以先使用它,当然也可以自己实现,实现方法参考cppcoro。

asio中早就提供了对于coroutine ts的支持,而且在asio中使用协程相当简单。asio通过提供co_spawn函数来启动一个协程,而且asio的每个异步函数都有支持协程的重载版本,可以直接通过co_await来调用,使用起来就像在写同步程序一样。asio/src/examples/cpp17/coroutines_ts目录中有几个如何使用协程的例子,因为采用了协程的程序本身可读性非常好,只要按照这些例子就可以写出自己的协程程序出来。这个目录中已经有echo_server了,我们编译它,以它为服务器,自己来写一个客户端。直接命令行编译:

clang++ -std=c++2a -DASIO_STA_ALONE -fcoroutines-ts -stdlib=libc++ -I../../../../../../asio/asio/include echo_server.cpp

现在来用协程做一个客户端,客户端只有一个tcpsocket,使用一个协程即可,在此协程中异步连接,然后异步写数据,然后再异步读数据,实现如下:

awaitable<void> Echo()
{
	auto executor = co_await this_coro::executor;
	tcp::socket socket(executor);
	co_await socket.async_connect({tcp::v4(), 55555}, use_awaitable); // 异步执行连接服务器
	for(int i = 0; i < 100; ++i) // echo 100次
	{
		char buff[256];
		snprintf(buff, sizeof(buff), "hello %02d", i);
		size_t n = co_await socket.async_send(asio::buffer(buff, strlen(buff)), use_awaitable); // 异步写数据
		//assert(n == strlen(buff));
		n = co_await socket.async_receive(asio::buffer(buff), use_awaitable); // 异步读数据
		buff[n] = 0;
		std::cout<<"received from server: "<<buff<<"
";
	}
}

echo client的功能就实现完了,看起来确实和写同步程序一样简单。接下来只要在main函数中利用asio提供的co_spawn函数启动这个协程即可:

int main()
{
    asio::io_context ioc(1);
    co_spawn(ioc, Echo, detached);
    ioc.run();
    return 0;
}

代码在github上。编译运行,会输出100行类似received from server: hello **的字符串。输出100行以后,协程函数执行完成了,main函数中的ioc.run也返回,整个客户端也退出了。

co_spawn可以在循环中使用,启动多个并行的协程,也可以嵌套使用,在协程中再启动新的协程,asio的所有异步功能都可以用协程。asio未能进入C++20,协程功能没有早日定案也是其中的原因之一。现在协程已经落地了,asio会在接下来的时间内好好整理关于对于协程和executor的支持,到时候以更合理更优雅更高效的方式加入到C++23。

============= End

原文地址:https://www.cnblogs.com/lsgxeva/p/12881319.html