基于ASIO的异步IO编程

系列文章目录



前言

异步IO介绍见references
asio和boost.asio的区别
https://think-async.com/Asio/AsioAndBoostAsio.html


1. asio基础概念

host <-> hostname / domain / url
service <-> port

1. 同步操作

asio::io_context io_ctx;  // io上下文相当于asio上层与操作系统之间的一个桥梁、中间件
asio::ip::tcp::socket socket(io_ctx);
socket.connect(server_endpoint);  // 同步操作
// 应用通过通过connect接口告诉socket对象建立一个tcp链接,socket对象将其转发到io_context
// io execution context调用系统接口建立连接,将结果告诉socket对象,然年后它再返回告诉应用连接结果

2. 异步操作

asio::io_context io_ctx;  // io上下文相当于asio上层与操作系统之间的一个桥梁、中间件
asio::ip::tcp::socket socket(io_ctx);
std::function<void(const asio::error_code&)> handler;
handler = [&](const asio::error_code& ec){ }  // 在io_context::run()调用线程执行
socket.async_connect(server_endpoint, handler);  // 异步操作分两步,1.初始化 2.执行handler函数
io_ctx.run();  // 执行流阻塞在这里,直到没有新的异步io handler可供处理

每调用一次async_X操作时好像入队(生产一个),每次事件来了时都会出队(消费一个),若 .run()时 队中有操作则阻塞,无操作则返回。

3. 异步代理

由于在调用handler之前,所有申请的资源都已释放,因此在handler函数内可继续执行初始化(回调函数注册)操作,此为异步代理。
异步代理包含多个异步操作

handler = [&](const asio::error_code& ec){
    socket.async_connect(server_endpoint, handler);  // 异步代理
}  // 在io_context::run()调用线程执行

4. 相关特征

包括执行器用哪种,内存该如何分配,取消slot如何等

异步操作的实现类似下面:

template <typename CompletionToken, completion_signature... Signatures>
struct async_result
{
    template <typename Initiation, completion_handler_for<Signatures...> CompletionHandler, typename... args>
    static void initiate(Initiation&& initiation, CompletionHandler&& completion_handler, Args&&... args)
    {
        template <typename T> using sfw = std::forward<T>;
        sfw<Initiation>(Initiation)(sfw<CompletionHandler>(completion_handler), sfw<Args>(args)...);
    }
};


using FnType = void(asio::error_code, size_t);
template< FnType CompletionToken>
auto async_read_some(asio::ip::tcp& s, const asio::mutable_buffer& b, FnType&& token)
{
    auto init = [](auto completion_handler, asio::ip::tcp::socket* s, const asio::mutable_buffer& b){
        std::thread(
            [](auto completion_handler, asio::ip::tcp::socket* s, const asio::mutable_buffer &b) {
                asio::error_code ec;
                size_t n = s->read_some(b, ec);
                std::move(completion_handler)(ec, n);
            }, std::move(completion_handler), s, b
        ).detach();
    };
    return asio::async_result<typename std::decay_t<FnType>, void(asio::error_code, size_t)>
        ::initiate(init, std::forward<CompletionToken>(token), &s, b);
}

高层次级别抽象
在这里插入图片描述

proactor模式和reactor模式的区别

reactor为同步IO模型,主线程负责通知子线程,子线程负责读写以及业务逻辑处理
proactor为异步IO模型,主线程和内核负责通知和读写数据,子线程负责业务逻辑处理

不同对象是线程安全的,同一个对象不能多线程同时读写,他不是安全的

Strands
保证strand中的异步操作不会并发执行,这在服务端处理用户可以避免数据竞争

#include <functional>
#include <iostream>
#include <asio.hpp>
#include <vector>
#include <thread>

using namespace std;
using namespace asio;

void multi_context()
{
  vector<io_context*> ctxs;
  for (int i = 0; i < 100; ++i)
  {
    ctxs.push_back(new io_context);
  }
  for (int i = 0; i < 100; ++i)
  {
    asio::post( *ctxs[i] , [](){
      cout << gettid() << "]" << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << endl;
    });  
  }
  vector<std::thread> ths;
  for (int i = 0; i < 100; ++i)
  {
    ths.emplace_back([i, &ctxs](){ ctxs[i]->run(); });
  }
  for (int i = 0; i < 100; ++i)
  {
    ths[i].join();
  }
}
void no_strand()
{
  io_context io_ctx;
  // auto strand = asio::make_strand(io_ctx);
  for (int i = 0; i < 100; ++i)
  {
    asio::post( io_ctx , [](){
      cout << gettid() << "]" << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << endl;
    });  
  }
  vector<std::thread> ths;
  for (int i = 0; i < 100; ++i)
  {
    ths.emplace_back([i, &io_ctx](){ io_ctx.run(); });
  }

  for (int i=0; i < 100; ++i)
  {
    ths[i].join();
  }
}
void has_strand()
{
  io_context io_ctx;
  auto strand = asio::make_strand(io_ctx);
  for (int i = 0; i < 100; ++i)
  {
    asio::post( strand , [](){
      cout << gettid() << "]" << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << endl;
    });  
  }
  vector<std::thread> ths;
  for (int i = 0; i < 100; ++i)
  {
    ths.emplace_back([i, &io_ctx](){ io_ctx.run(); });
  }

  for (int i=0; i < 100; ++i)
  {
    ths[i].join();
  }
}

Buffers

  • A scatter-read receives data into multiple buffers.
  • A gather-write transmits multiple buffers.

stream-oriented I/O objects

  • There are no message boundaries. The data being transferred is a continuous sequence of bytes.
  • Read or write operations may transfer fewer bytes than requested. This is referred to as a short read or short write.

read_some()

async_read_some()

write_some()

async_write_some()

ip::tcp::socket

read(), async_read(), write() and async_write().

ip::tcp::socket socket(my_io_context);
...
socket.non_blocking(true);
...
socket.async_wait(ip::tcp::socket::wait_read, read_handler);
...
void read_handler(asio::error_code ec)
{
  if (!ec)
  {
    std::vector<char> buf(socket.available());
    socket.read_some(buffer(buf));
  }
}

异步读,直到读到xxx

read_until() and async_read_until()

class http_connection
{
  ...

  void start()
  {
    asio::async_read_until(socket_, data_, "\r\n",
        boost::bind(&http_connection::handle_request_line, this, _1));
  }

  void handle_request_line(asio::error_code ec)
  {
    if (!ec)
    {
      std::string method, uri, version;
      char sp1, sp2, cr, lf;
      std::istream is(&data_);
      is.unsetf(std::ios_base::skipws);
      is >> method >> sp1 >> uri >> sp2 >> version >> cr >> lf;
      ...
    }
  }

  ...

  asio::ip::tcp::socket socket_;
  asio::streambuf data_;
};

// read_until支持多个重载
typedef asio::buffers_iterator<
    asio::streambuf::const_buffers_type> iterator;

std::pair<iterator, bool>
match_whitespace(iterator begin, iterator end)
{
  iterator i = begin;
  while (i != end)
    if (std::isspace(*i++))
      return std::make_pair(i, true);
  return std::make_pair(i, false);
}
...
asio::streambuf b;
asio::read_until(s, b, match_whitespace);

// 可调用类,特性萃取
class match_char
{
public:
  explicit match_char(char c) : c_(c) {}

  template <typename Iterator>
  std::pair<Iterator, bool> operator()(
      Iterator begin, Iterator end) const
  {
    Iterator i = begin;
    while (i != end)
      if (c_ == *i++)
        return std::make_pair(i, true);
    return std::make_pair(i, false);
  }

private:
  char c_;
};
namespace asio {
  template <> struct is_match_condition<match_char>
    : public boost::true_type {};
} // namespace asio
...
asio::streambuf b;
asio::read_until(s, b, match_char('a'));

定制内存分配器, cancellation_slot

当使用用户定义的对象 Handler h时,获取分配器使用如下方法

asio::associated_allocator_t<Handler> a = asio::get_associated_allocator(h);
class my_handler
{
public:
  // Custom implementation of Allocator type requirements.
  typedef my_allocator allocator_type;

  // Return a custom allocator implementation.
  allocator_type get_allocator() const noexcept
  {
    return my_allocator();
  }

  void operator()() { ... }
};
namespace asio {

  template <typename Allocator>
  struct associated_allocator<my_handler, Allocator>
  {
    // Custom implementation of Allocator type requirements.
    typedef my_allocator type;

    // Return a custom allocator implementation.
    static type get(const my_handler&,
        const Allocator& a = Allocator()) noexcept
    {
      return my_allocator();
    }
  };

} // namespace asio


asio::async_read(my_socket, my_buffer,
    asio::bind_cancellation_slot(
      my_cancellation_slot,
      [](asio::error_code e, std::size_t n)
      {
        ...
      }
    )
  );

cancellation slot and signal. signal发送,slot接收(一个覆盖式handler)

class session
  : public std::enable_shared_from_this<proxy>
{
  ...

  void do_read()
  {
    auto self = shared_from_this();
    socket_.async_read_some(
        buffer(data_),
        asio::bind_cancellation_slot(
          cancel_signal_.slot(),
          [self](std::error_code error, std::size_t n)
          {
            ...
          }
        )
      );
  }

  ...

  void request_cancel()
  {
    cancel_signal_.emit(asio::cancellation_type::total);
  }

  ...

  asio::cancellation_signal cancel_signal_;
};

debugging

通过加入一些宏
https://think-async.com/Asio/asio-1.30.2/doc/asio/overview/core/handler_tracking.html
显示调试信息

异步操作组合

将几个异步操作组成一个简单的状态机

struct async_echo_implementation
{
    asio::ip::tcp::socket& socket_;
    asio::mutable_buffer buffer_;
    enum {starting, reading, writing} state_;
    
    template <typename Self> void 
    operator()(Self& self, asio::error_code error = {}, size_t n = 0)
    {
        switch (state_)
        {
        case starting:
            state_ = reading;
            socket_.async_read_some(buffer_, std::move(self));
            break;
        case reading:
            if (error)
            {
                self.complete(error, 0);
            }
            else
            {
                state_ = writing;
                asio::async_write(socket_, buffer_, asio::transfer_exactly(n), std::move(self));
            }
            break;
        case writing:
            self.complete(error, n);
            break;
        }
    }
};
template<typename CompletionToken>
auto async_echo(ip::tcp::socket& socket, mutable_buffer buffer, CompletionToken&& token) ->
    typename async_result<typename std::decay<CompletionToken>::type,
        void(error_code, size_t)>::return_type
{
    return async_compose<CompletionToken, void(error_code, size_t)>(
        async_echo_implementation{socket, buffer, async_echo_implementation::starting},
        token,
        socket
    );
}

Completion Token的适配器

适配器,保持接口不变而改变实现

bind_executor, bind_allocator, bind_cancellation_slot, and bind_immediate_executor

重定向错误适配器

asio::error_code my_error; // N.B. must be valid until operation completes
// ...
my_socket.async_read_some(my_buffer,
    asio::redirect_error(
      [](std::size_t bytes_transferred)
      {
        // ...
      }, my_error));

5 网络

在socket编程中,ip, tcp, udp, icmp协议在创建socket时通过AFINET(6),SOCKE_STREAM,SOCK_DGRAM等指定

asio通过namespace分离出ip协议,tcp,udp,imcp则定义在类中

通讯节点称为endpoint, 域名/主机名成为hostname, 端口号称为service

ip::tcp::resolver resolver(my_io_context);
ip::tcp::resolver::query query("www.boost.org", "http");
ip::tcp::resolver::iterator iter = resolver.resolve(query);
ip::tcp::resolver::iterator end; // End marker.
while (iter != end)
{
  ip::tcp::endpoint endpoint = *iter++;
  std::cout << endpoint << std::endl;
}
// 包含ipv4 和 ipv6
ip::tcp::socket socket(my_io_context);
asio::connect(socket, resolver.resolve(query)/*or endpoint*/); // 尝试每个endpoint,直到连接成功

asio::async_connect(socket_, iter,
    std::bind(&client::handle_connect, this, asio::placeholders::error));
// ...
struct client{
    void handle_connect(const error_code& error)
    {
        if (!error) // Start read or write operations.
        else        // Handle error.
    }    
};

receive(), async_receive(), send() or async_send()

数据量short: read(), async_read(), write() and async_write().

// icmp  udp
ip::icmp::resolver resolver(my_io_context);
ip::icmp::resolver::query query("localhost", "");
ip::icmp::resolver::iterator iter = resolver.resolve(query), end;

ip::icmp::endpoint endpoint(ip::icmp::v6(), 0);
ip::icmp::socket socket(my_io_context, endpoint);
// tcp


对sockets的高层次封装

ip::tcp::iostream stream("www.boost.org", "http");
if (!stream)
{
  // Can't connect.
  std::cout << "Error: " << stream.error().message() << "\n";
}

io_context ioc;
ip::tcp::endpoint endpoint(tcp::v4(), 80);
ip::tcp::acceptor acceptor(ios, endpoint);
for (;;)
{
  ip::tcp::iostream stream;
  acceptor.accept(stream.socket());
  ...
}


ip::tcp::iostream stream;
stream.expires_from_now(boost::posix_time::seconds(60));
stream.connect("www.boost.org", "http");
stream << "GET /LICENSE_1_0.txt HTTP/1.0\r\n";
stream << "Host: www.boost.org\r\n";
stream << "Accept: */*\r\n";
stream << "Connection: close\r\n\r\n";
stream.flush();
std::cout << stream.rdbuf();

Any socket operations that occur past the deadline will put the iostream into a “bad” state.

BSD socket API 和 asio的对应关系:https://think-async.com/Asio/asio-1.30.2/doc/asio/overview/networking/bsd_sockets.html

6 一些异步封装

计时器

io_context io_ctx;
steady_timer t1(io_ctx);
steady_timer::time_point time_of_expiry = t1.expiry();
t1.expires_after(asio::chrono::milliseconds(1000));
io_ctx.run();

文件

Files
Asio provides support for manipulating stream-oriented and random-access files. And this feature requires I/O completion ports on Windows, and io_uring on Linux (define ASIO_HAS_IO_URING to enable).

管道

Pipes

串口

Serial Ports

信号处理

// Construct a signal set registered for process termination.
asio::signal_set signals(io_context, SIGINT, SIGTERM);

// Start an asynchronous wait for one of the signals to occur.
signals.async_wait([](asio::error_code ec,int sig){if(!ec)cout<<sig<<'\n';});

不同线程使用不同的signal_set对象是安全的,使用同一个对象是不安全的。

    signal_set sin(io_ctx, SIGINT), sin2(io_ctx, SIGINT);
    sin.async_wait(handler);
    sin.async_wait(handler3);
    sin2.async_wait(handler2);
    sin2.async_wait(handler4);
// 所有注册的handler*好像队列依次入队,信号到来,依次被消费,若无handler则信号阻塞在队列中直到wait被调用

7 ssl支持

2. 计时器 & UDP/TCP编程

https://blog.youkuaiyun.com/surfaceyan/article/details/136457991

3. 系统提供的异步接口

https://blog.youkuaiyun.com/surfaceyan/article/details/134710393?sharetype=blogdetail&sharerId=134710393&sharerefer=PC&sharesource=surfaceyan&spm=1011.2480.3001.8118

4. 最佳实践

客户端

#include "asio.hpp"
#include <iostream>
#include <vector>
using namespace std;
using namespace asio;

int main()
{
    io_context io_ctx;
    ip::tcp::socket socket(io_ctx);
    ip::tcp::endpoint end_point(ip::make_address_v4("127.0.0.1"), 80);
    vector<char> vec_buf(1024, 0);
    mutable_buffer buffer(vec_buf.data(), vec_buf.size()-1);
    function<void(const error_code&, size_t)> read_callback;
    read_callback = [&](const error_code& ec, size_t size){
                        if (!ec)
                        {
                            vec_buf[size] = '\0';
                            cout << "read bytes: " << size << ":" 
                                << vec_buf.data()  << endl;  
                            socket.async_read_some(buffer, read_callback);                          
                        }
                        else
                        {
                            cout << "read failure: " << ec.message() << endl;
                        }
                    };
    socket.async_connect(end_point, [&](const error_code& ec){
        cout << "async conn: " << ec.message() << endl;
        if (!ec)
        {
            cout << "connected" << endl;
            using namespace buffer_literals;
            socket.async_write_some("GET / HTTP/1.1\r\n"
                "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\n"
                "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0\r\n"
                "Upgrade-Insecure-Requests: 1\r\n"
                "Connection: close\r\n"
                "\r\n"_buf, [&](const error_code& ec, size_t size){
                if (!ec)
                {
                    cout << "write ok " << size <<  endl;
                    socket.async_read_some(buffer, read_callback);
                }
                else
                {
                    cout << "write failure: " << ec.message() << endl;
                }
            });
        }
        else
        {
            cout << "connect failure: " << ec.message() << endl;
        }
    });
    io_ctx.run();
    cout << "process end" << endl;
}

modern C++ style server

#include <array>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <type_traits>
#include <utility>
#include "asio.hpp"

using asio::ip::tcp;
using namespace asio;


class session: public std::enable_shared_from_this<session>
{
public:
  session(tcp::socket socket)
    : socket_(std::move(socket))
  {
    printf("session construct: %p\n", this);
  }
  void start()
  {
    do_read();
  }
  ~session()
  {
    printf("session destruct: %p\n", this);
  }
private:
  void do_read()
  {
    auto self(shared_from_this());
    socket_.async_read_some(asio::buffer(data_),
          [this, self](std::error_code ec, std::size_t length)
          {
            if (!ec)
            {
              do_write(length);
            }
          });
  }
  void do_write(std::size_t length)
  {
    auto self(shared_from_this());
    asio::async_write(socket_, asio::buffer(data_, length),
          [this, self](std::error_code ec, std::size_t /*length*/)
          {
            if (!ec)
            {
              do_read();
            }
          });
  }
  // The socket used to communicate with the client.
  tcp::socket socket_;
  // Buffer used to store data received from the client.
  std::array<char, 1024> data_;
};

class server
{
public:
  server(asio::io_context& io_context, short port)
    : acceptor_(io_context, tcp::endpoint(tcp::v4(), port))
  {
    do_accept();
  }

private:
  void do_accept()
  {
    acceptor_.async_accept(
        [this](std::error_code ec, tcp::socket socket)
        {
          if (!ec)
          {
            std::make_shared<session>(std::move(socket))->start();
          }

          do_accept();
        });
  }

  tcp::acceptor acceptor_;
};
int main(int argc, char* argv[])
{
  try
  {
    if (argc != 2)
    {
      std::cerr << "Usage: server <port>\n";
      return 1;
    }
    asio::io_context io_context;
    asio::signal_set signals(io_context);
    signals.async_wait([&](asio::error_code ec, int sig){ io_context.stop(); });
    server s(io_context, std::atoi(argv[1]));
    io_context.run();
  }
  catch (std::exception& e)
  {
    std::cerr << "Exception: " << e.what() << "\n";
  }
  return 0;
}

native C++ style server

#include <cstdlib>
#include <iostream>
#include <memory>
#include <utility>
#include "asio.hpp"
using namespace std;
using asio::ip::tcp;
using namespace asio;


class Client
{
private:
  ip::tcp::socket socket_;
  std::array<char, 1024> buffer_;
public:
  Client(ip::tcp::socket socket) : socket_(std::move(socket)) { }
  ~Client()
  {
    socket_.close();
    cout << "connect closed: " << this << endl;
  }
  void do_read()
  {
    socket_.async_read_some(asio::buffer(buffer_), [this](const error_code& ec, size_t len){
      cout << ec.message() << " len: " << len << endl;
      if (!ec) this->do_write(len);
      else     delete this;
    });
  }
  void do_write(size_t len)
  {
    socket_.async_write_some(asio::buffer(buffer_.data(),len), [this](const error_code& ec, size_t len){
      cout << ec.message() << " write len: " << len << endl; 
      if (!ec)  this->do_read();
      else      delete this;
    });
  }
};

int main(int argc, char* argv[])
{
  try
  {
    if (argc != 2)
    {
      std::cerr << "Usage: async_tcp_echo_server <port>\n";
      return 1;
    }

    asio::io_context io_ctx;
    ip::tcp::endpoint endp = ip::tcp::endpoint(ip::tcp::v4(), atoi(argv[1]));
    ip::tcp::acceptor serv(io_ctx, endp);
    std::function<void(std::error_code, tcp::socket)> ac_cb;
    ac_cb = [&](std::error_code ec, tcp::socket socket){
        if (!ec)
        {
          auto p = new Client(std::move(socket));
          p->do_read();
        }
        serv.async_accept(ac_cb);
    };
    serv.async_accept(ac_cb);
    io_ctx.run();
  }
  catch (std::exception& e)
  {
    std::cerr << "Exception: " << e.what() << "\n";
  }

  return 0;
}


references

https://think-async.com/Asio/AsioAndBoostAsio.html
https://think-async.com/Asio/
https://think-async.com/Asio/asio-1.30.2/doc/
https://www.cnblogs.com/flashsun/p/14591563.html

BSD socket API 和 asio的对应关系:https://think-async.com/Asio/asio-1.30.2/doc/asio/overview/networking/bsd_sockets.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值