asio官方案例存在的隐患

简介

前文已经介绍了异步操作的api,今天写一个简单的异步echo服务器,以应答为主

Session类

Session类主要是处理客户端消息收发的会话类,为了简单起见,我们不考虑粘包问题,也不考虑支持手动调用发送的接口,只以应答的方式发送和接收固定长度(1024字节长度)的数据。
<!--more-->

  1. class Session
  2. {
  3. public:
  4. Session(boost::asio::io_context& ioc):_socket(ioc){
  5. }
  6. tcp::socket& Socket() {
  7. return _socket;
  8. }
  9. void Start();
  10. private:
  11. void handle_read(const boost::system::error_code & error, size_t bytes_transfered);
  12. void handle_write(const boost::system::error_code& error);
  13. tcp::socket _socket;
  14. enum {max_length = 1024};
  15. char _data[max_length];
  16. };

1   _data用来接收客户端传递的数据
2   _socket为单独处理客户端读写的socket。
3   handle_read和handle_write分别为读回调函数和写回调函数。
接下来我们实现Session类

  1. void Session::Start(){
  2. memset(_data, 0, max_length);
  3. _socket.async_read_some(boost::asio::buffer(_data, max_length),
  4. std::bind(&Session::handle_read, this, placeholders::_1,
  5. placeholders::_2)
  6. );
  7. }

在Start方法中我们调用异步读操作,监听对端发送的消息。当对端发送数据后,触发handle_read函数

  1. void Session::handle_read(const boost::system::error_code& error, size_t bytes_transfered) {
  2. if (!error) {
  3. cout << "server receive data is " << _data << endl;
  4. boost::asio::async_write(_socket, boost::asio::buffer(_data, bytes_transfered),
  5. std::bind(&Session::handle_write, this, placeholders::_1));
  6. }
  7. else {
  8. delete this;
  9. }
  10. }

handle_read函数内将收到的数据发送给对端,当发送完成后触发handle_write回调函数。

  1. void Session::handle_write(const boost::system::error_code& error) {
  2. if (!error) {
  3. memset(_data, 0, max_length);
  4. _socket.async_read_some(boost::asio::buffer(_data, max_length), std::bind(&Session::handle_read,
  5. this, placeholders::_1, placeholders::_2));
  6. }
  7. else {
  8. delete this;
  9. }
  10. }

handle_write函数内又一次监听了读事件,如果对端有数据发送过来则触发handle_read,我们再将收到的数据发回去。从而达到应答式服务的效果。

Server类

Server类为服务器接收连接的管理类

  1. class Server {
  2. public:
  3. Server(boost::asio::io_context& ioc, short port);
  4. private:
  5. void start_accept();
  6. void handle_accept(Session* new_session, const boost::system::error_code& error);
  7. boost::asio::io_context& _ioc;
  8. tcp::acceptor _acceptor;
  9. };

start_accept将要接收连接的acceptor绑定到服务上,其内部就是将accpeptor对应的socket描述符绑定到epoll或iocp模型上,实现事件驱动。
handle_accept为新连接到来后触发的回调函数。
下面是具体实现

  1. Server::Server(boost::asio::io_context& ioc, short port) :_ioc(ioc),
  2. _acceptor(ioc, tcp::endpoint(tcp::v4(), port)) {
  3. start_accept();
  4. }
  5. void Server::start_accept() {
  6. Session* new_session = new Session(_ioc);
  7. _acceptor.async_accept(new_session->Socket(),
  8. std::bind(&Server::handle_accept, this, new_session, placeholders::_1));
  9. }
  10. void Server::handle_accept(Session* new_session, const boost::system::error_code& error) {
  11. if (!error) {
  12. new_session->Start();
  13. }
  14. else {
  15. delete new_session;
  16. }
  17. start_accept();
  18. }

客户端

客户端的设计用之前的同步模式即可,客户端不需要异步的方式,因为客户端并不是以并发为主,当然写成异步收发更好一些。

  1. #include <iostream>
  2. #include <boost/asio.hpp>
  3. using namespace std;
  4. using namespace boost::asio::ip;
  5. const int MAX_LENGTH = 1024;
  6. int main()
  7. {
  8. try {
  9. //创建上下文服务
  10. boost::asio::io_context ioc;
  11. //构造endpoint
  12. tcp::endpoint remote_ep(address::from_string("127.0.0.1"), 10086);
  13. tcp::socket sock(ioc);
  14. boost::system::error_code error = boost::asio::error::host_not_found; ;
  15. sock.connect(remote_ep, error);
  16. if (error) {
  17. cout << "connect failed, code is " << error.value() << " error msg is " << error.message();
  18. return 0;
  19. }
  20. std::cout << "Enter message: ";
  21. char request[MAX_LENGTH];
  22. std::cin.getline(request, MAX_LENGTH);
  23. size_t request_length = strlen(request);
  24. boost::asio::write(sock, boost::asio::buffer(request, request_length));
  25. char reply[MAX_LENGTH];
  26. size_t reply_length = boost::asio::read(sock,
  27. boost::asio::buffer(reply, request_length));
  28. std::cout << "Reply is: ";
  29. std::cout.write(reply, reply_length);
  30. std::cout << "\n";
  31. }
  32. catch (std::exception& e) {
  33. std::cerr << "Exception: " << e.what() << endl;
  34. }
  35. return 0;
  36. }

运行服务器之后再运行客户端,输入字符串后,就可以收到服务器应答的字符串了。

隐患

该demo示例为仿照asio官网编写的,其中存在隐患,就是当服务器即将发送数据前(调用async_write前),此刻客户端中断,服务器此时调用async_write会触发发送回调函数,判断ec为非0进而执行delete this逻辑回收session。但要注意的是客户端关闭后,在tcp层面会触发读就绪事件,服务器会触发读事件回调函数。在读事件回调函数中判断错误码ec为非0,进而再次执行delete操作,从而造成二次析构,这是极度危险的。

总结

本文介绍了异步的应答服务器设计,但是这种服务器并不会在实际生产中使用,主要有两个原因:
1   因为该服务器的发送和接收以应答的方式交互,而并不能做到应用层想随意发送的目的,也就是未做到完全的收发分离(全双工逻辑)。
2   该服务器未处理粘包,序列化,以及逻辑和收发线程解耦等问题。
3   该服务器存在二次析构的风险。
这些问题我们会在接下来的文章中不断完善
源码链接
https://gitee.com/secondtonone1/boostasio-learn

热门评论
  • 迟增耀
    2024-07-02 16:40:18

    对,因为这里服务器收发数据是个同步的过程;但是当收发数据是个异步过程时就可能会出错。

  • stardust97
    2024-06-27 00:01:35

    async_write出错后,由于走到了delete this分支,不会再次调用


  •         _socket.async_read_some(boost::asio::buffer(_data, max_length), std::bind(&Session::handle_read,
  • this, placeholders::_1, placeholders::_2));
  • 注册读事件回调了吧?为什么还会进入读事件回调函数中判断错误码ec为非0呢?

热门文章

  1. 聊天项目(28) 分布式服务通知好友申请

    喜欢(507) 浏览(6093)
  2. 使用hexo搭建个人博客

    喜欢(533) 浏览(11888)
  3. Linux环境搭建和编码

    喜欢(594) 浏览(13540)
  4. Qt环境搭建

    喜欢(517) 浏览(24683)
  5. vscode搭建windows C++开发环境

    喜欢(596) 浏览(83343)

最新评论

  1. 聊天项目(7) visualstudio配置grpc diablorrr:cmake文件得改一下 find_package(Boost REQUIRED COMPONENTS system filesystem),要加上filesystem。在target_link_libraries中也同样加上
  2. protobuf配置和使用 熊二:你可以把dll放到系统目录,也可以配置环境变量,还能把dll丢到lib里
  3. boost::asio之socket的创建和连接 项空月:发现一些错别字 :每隔vector存储  是不是是每个. asio::mutable_buffers_1 o或者    是不是多打了个o
  4. 面试题汇总(一) secondtonone1:看到网络上经常提问的go的问题,做了一下汇总,结合自己的经验给出的答案,如有纰漏,望指正批评。
  5. 无锁并发队列 TenThousandOne:_head  和 _tail  替换为原子变量。那里pop的逻辑,val = _data[h] 可以移到循环外面吗
  6. 网络编程学习方法和图书推荐 Corleone:啥程度可以找工作
  7. 答疑汇总(thread,async源码分析) Yagus:如果引用计数为0,则会执行 future 的析构进而等待任务执行完成,那么看到的输出将是 这边应该不对吧,std::future析构只在这三种情况都满足的时候才回block: 1.共享状态是std::async 创造的(类型是_Task_async_state) 2.共享状态没有ready 3.这个future是共享状态的最后一个引用 这边共享状态类型是“_Package_state”,引用计数即使为0也不应该block啊
  8. Qt 对话框 Spade2077:QDialog w(); //这里是不是不需要带括号
  9. visual studio配置boost库 一giao里我离giaogiao:请问是修改成这样吗:.\b2.exe toolset=MinGW
  10. 构造函数 secondtonone1:构造函数是类的基础知识,要着重掌握
  11. Qt MVC结构之QItemDelegate介绍 胡歌-此生不换:gpt, google
  12. string类 WangQi888888:确实错了,应该是!isspace(sind[index]). 否则不进入循环,还是原来的字符串“some string”
  13. 再谈单例模式 secondtonone1:是的,C++11以后返回局部static变量对象能保证线程安全了。
  14. 类和对象 陈宇航:支持!!!!
  15. 聊天项目(9) redis服务搭建 pro_lin:redis线程池的析构函数,除了pop出队列,还要free掉redis连接把
  16. C++ 并发三剑客future, promise和async Yunfei:大佬您好,如果这个线程池中加入的异步任务的形参如果有右值引用,这个commit中的返回类型推导和bind绑定就会出现问题,请问实际工程中,是不是不会用到这种任务,如果用到了,应该怎么解决?
  17. 堆排序 secondtonone1:堆排序非常实用,定时器就是这个原理制作的。
  18. 创建项目和编译 secondtonone1:谢谢支持
  19. 处理网络粘包问题 zyouth: //消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里 if (bytes_transferred < data_len) { memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred); _recv_msg_node->_cur_len += bytes_transferred; ::memset(_data, 0, MAX_LENGTH); _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self)); //头部处理完成 _b_head_parse = true; return; } 把_b_head_parse = true;放在_socket.async_read_some前面是不是更好
  20. 利用栅栏实现同步 Dzher:作者你好!我觉得 std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); 这个例子中的assert fail并不会发生,原子变量设定了非relaxed内存序后一个线程的原子变量被写入,那么之后的读取一定会被同步的,c和d线程中只可能同时发生一个z++未执行的情况,最终z不是1就是2了,我测试了很多次都没有assert,请问我这个观点有什么错误,谢谢!
  21. 聊天项目(15) 客户端实现TCP管理者 lkx:已经在&QTcpSocket::readyRead 回调函数中做了处理了的。
  22. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  23. interface应用 secondtonone1:interface是万能类型,但是使用时要转换为实际类型来使用。interface丰富了go的多态特性,也降低了传统面向对象语言的耦合性。

个人公众号

个人微信