boost.asio相信很多人听说过,作为一个跨平台的通信库,它的性能是很出色的,然而它却谈不上好用,里面有很多地方稍不注意就会出错,要正确的用好asio还是需要花一番精力去学习和实践的,本文将通过介绍如何写一个简单的通信程序来告诉读者如何使用asio,希望对asio的初学者有所帮助。由于只是介绍其基本用法,作为例子的简单示例并不考虑很多的业务逻辑和异常处理,只是介绍基本用法,让初学者入门。

  使用asio容易出错的一个主要原因是因为它是基于proactor模式实现的,asio有很多异步操作接口,这些异步接口稍不注意就会出现莫名奇妙的错误,所以要用好asio的第一步是理解其异步操思想。

异步操作思想

  用户发起异步事件,asio将这些异步事件投递到一个队列中,用户发起的操作就返回了,io_service::run会处理异步事件队列中的所有的异步事件,它会将这些事件交给操作系统处理,操作系统处理完成之后会丢到asio的事件完成的队列中,io_service发现有完成队列中有完成事件了,就会通知用户处理完成事件。 所以用户要发起一个异步操作需要做三件事:

  1. 调用asio异步操作接口,发起异步操作;如:async_connect、async_read、async_write,这些异步接口需要一个回调函数入参,这个回调函数在事件完成时,由io_service触发。
  2. 调用io_service::run处理异步事件;发起一个异步操作,必须要保证io_service::run,因为io_service通过一个循环去处理这些异步操作事件的,如果没有事件就会退出,所以要保证异步事件发起之后,io_service::run还在运行。要保证一直run的一个简单办法就是使用io_service::work,它可以保证io_service一直run。
  3. 处理异步操作完成事件;在调用异步接口时会传入一个回调函数,这个回调函数就是处理操作完成事件的,比如读完成了,用户需要对这些数据进行业务逻辑的处理。

  下图描述了一个异步操作的过程:

  asio的的核心是io_service, 理解了asio异步接口的机制就容易找出使用asio过程中出现的问题了,在这里把一些常见的问题列出来,并分析原因和提出解决方法。

  • 问题1:为什么我发起了异步操作,如连接或者写,对方都没有反应,好像没有收到连接请求或者没有收到数据? 答案:一个很可能的原因是io_service在异步操作发起之后没有run,解决办法是保持io_service run。
  • 问题2:为什么发送数据会报错? 答案:一个可能的原因是发送的数据失效了,异步发送要求发送的数据在回调完成之前都有效,异步操作只是将异步事件句柄投递到io_service队列中就返回了,并不是阻塞的,不注意这一点,如果是临时变量的数据,出了作用域就失效了,导致异步事件还没完成时数据就失效了。解决办法,保证发送数据在事件完成之前一直有效。
  • 问题3:为什么监听socket时,会报“函数不正确”的异常? 答案:因为监听时,也要保证这个socket一直有效,如果是一个临时变量socket,在调用异步监听后超出作用域就失效了,解决办法,将监听的socket保存起来,使它的生命周期和acceptor一样长。
  • 问题4:为什么连续调用异步操作时会报错? 答案:因为异步操作必须保证当前异步操作完成之后再发起下一次异步操作。解决办法:在异步完成事件处理完成之后再发起新的异步操作即可。
  • 问题5:为什么对方半天收不到数据,过了半天才一下子收到之前发送的数据? 答案:因为socket是流数据,一次发送多少数据不是外界能控制的,这也是所谓的粘包问题。解决办法,可以在接收时指定至少收多少的条件,或者做tcp分包处理。

  说了这么多,还是来看看例子吧,一个简单的通信程序:服务端监听某个端口,允许多个客户端连接上来,服务器将客户端发来的数据打印出来。 先看看服务端的需求,需求很简单,第一,要求能接收多个客户端;第二,要求把收到的数据打印出来。

  要求能接收多个客户端是第一个要解决的问题,异步接收需要用到acceptor::async_accept,它接收一个socket和一个完成事件的回调函数。前面的问题3中提到监听的这个socket不能是临时变量,我们要把它保存起来,最好是统一管理起来。可以考虑用一个map去管理它们,每次一个新连接过来时,服务器自动分配一个连接号给这个连接,以方便管理。然而,socket是不允许拷贝的,所以不能直接将socket放入容器中,还需要外面包装一层才可以。

  第二个问题是打印来自客户端的数据,既然要打印就需要异步读数据了。异步读是由socket完成,这个socket还要完成读写功能,为了简化用户操作,我将socket封装到一个读写事件处理器中,这个事件处理器只具备具备读和写的功能。服务器每次监听的时候我都会创建一个新的事件处理器并放到一个map中,客户端成功连接后就由这个事件处理器去处理各种读写事件了。 根据问题1,异步读写时要保证数据的有效性,这里我将一个固定大小的缓冲区作为读缓冲区。为了简单起见我使用同步发送,异步接收。

具体看看这个读写事件处理器是怎么写的:

const int MAX_IP_PACK_SIZE = ;
const int HEAD_LEN = ;
class RWHandler
{
public: RWHandler(io_service& ios) : m_sock(ios)
{
} ~RWHandler()
{
} void HandleRead()
{
//三种情况下会返回:1.缓冲区满;2.transfer_at_least为真(收到特定数量字节即返回);3.有错误发生
async_read(m_sock, buffer(m_buff), transfer_at_least(HEAD_LEN), [this](const boost::system::error_code& ec, size_t size)
{
if (ec != nullptr)
{
HandleError(ec);
return;
} cout << m_buff.data() + HEAD_LEN << endl; HandleRead();
});
} void HandleWrite(char* data, int len)
{
boost::system::error_code ec;
write(m_sock, buffer(data, len), ec);
if (ec != nullptr)
HandleError(ec);
} tcp::socket& GetSocket()
{
return m_sock;
} void CloseSocket()
{
boost::system::error_code ec;
m_sock.shutdown(tcp::socket::shutdown_send, ec);
m_sock.close(ec);
} void SetConnId(int connId)
{
m_connId = connId;
} int GetConnId() const
{
return m_connId;
} template<typename F>
void SetCallBackError(F f)
{
m_callbackError = f;
} private:
void HandleError(const boost::system::error_code& ec)
{
CloseSocket();
cout << ec.message() << endl;
if (m_callbackError)
m_callbackError(m_connId);
} private:
tcp::socket m_sock;
std::array<char, MAX_IP_PACK_SIZE> m_buff;
int m_connId;
std::function<void(int)> m_callbackError;
};

  这个读写事件处理器有四个成员变量,第一个是socket它是具体的读写执行者;第二个是固定长度的读缓冲区,用来读数据;第三个是连接id,由连接管理层分配;第四个是回调函数,读写发生错误时回调到上层。当然还可以加一个tcp分包之后的回调函数,将应用层数据回调到应用层,这里简单起见,只是将其打印出来。

再来看看Server是如何写的:

#include <boost/asio/buffer.hpp>
#include <boost/unordered_map.hpp>
#include "Message.hpp"
#include "RWHandler.hpp" const int MaxConnectionNum = ;
const int MaxRecvSize = ;
class Server
{
public: Server(io_service& ios, short port) : m_ios(ios), m_acceptor(ios, tcp::endpoint(tcp::v4(), port)), m_cnnIdPool(MaxConnectionNum)
{
int current = ;
std::generate_n(m_cnnIdPool.begin(), MaxConnectionNum, [&current]{return ++current; });
} ~Server()
{
} void Accept()
{
cout << "Start Listening " << endl;
std::shared_ptr<RWHandler> handler = CreateHandler(); m_acceptor.async_accept(handler->GetSocket(), [this, handler](const boost::system::error_code& error)
{
if (error)
{
cout << error.value() << " " << error.message() << endl;
HandleAcpError(handler, error);
} m_handlers.insert(std::make_pair(handler->GetConnId(), handler));
cout << "current connect count: " << m_handlers.size() << endl; handler->HandleRead();
Accept();
});
} private:
void HandleAcpError(std::shared_ptr <RWHandler> eventHanlder, const boost::system::error_code& error)
{
cout << "Error,error reason:" << error.value() << error.message() << endl;
//关闭socket,移除读事件处理器
eventHanlder->CloseSocket();
StopAccept();
} void StopAccept()
{
boost::system::error_code ec;
m_acceptor.cancel(ec);
m_acceptor.close(ec);
m_ios.stop();
} std::shared_ptr<RWHandler> CreateHandler()
{
int connId = m_cnnIdPool.front();
m_cnnIdPool.pop_front();
std::shared_ptr<RWHandler> handler = std::make_shared<RWHandler>(m_ios); handler->SetConnId(connId); handler->SetCallBackError([this](int connId)
{
RecyclConnid(connId);
}); return handler;
} void RecyclConnid(int connId)
{
auto it = m_handlers.find(connId);
if (it != m_handlers.end())
m_handlers.erase(it);
cout << "current connect count: " << m_handlers.size() << endl;
m_cnnIdPool.push_back(connId);
} private:
io_service& m_ios;
tcp::acceptor m_acceptor; boost::unordered_map<int, std::shared_ptr<RWHandler>> m_handlers; list<int> m_cnnIdPool;
};

这个Server具备连接管理功能,会统一管理所有连上来的客户端。 其中的Message类是boost官网中的那个char_message

class Message
{
public:
enum { header_length = };
enum { max_body_length = }; Message()
: body_length_()
{
} const char* data() const
{
return data_;
} char* data()
{
return data_;
} size_t length() const
{
return header_length + body_length_;
} const char* body() const
{
return data_ + header_length;
} char* body()
{
return data_ + header_length;
} size_t body_length() const
{
return body_length_;
} void body_length(size_t new_length)
{
body_length_ = new_length;
if (body_length_ > max_body_length)
body_length_ = max_body_length;
} bool decode_header()
{
char header[header_length + ] = "";
std::strncat(header, data_, header_length);
body_length_ = std::atoi(header);
if (body_length_ > max_body_length)
{
body_length_ = ;
return false;
}
return true;
} void encode_header()
{
char header[header_length + ] = "";
std::sprintf(header, "%4d", body_length_);
std::memcpy(data_, header, header_length);
} private:
char data_[header_length + max_body_length];
std::size_t body_length_;
};

  

  至此一个简单的服务端程序写完了,还要把这个Server运行起来。

void TestServer()
{
io_service ios;
//boost::asio::io_service::work work(ios);
//std::thread thd([&ios]{ios.run(); }); Server server(ios, );
server.Accept();
ios.run(); //thd.join();
}

注意看这个TestServer函数,看我是如何保证io_service::run一直运行的, 我这里没有使用io_service::work来保证,用了一种更简单的方法,具体方法读者看代码便知。

现在可以写一个简单的客户端来测试一下,看看服务器能否正常工作,下一篇再继续写如何写一个简单的客户端程序。

如果你觉得这篇文章对你有用,可以点一下推荐,谢谢。

c++11 boost技术交流群:296561497,欢迎大家来交流技术。

(原创)如何使用boost.asio写一个简单的通信程序(一)的更多相关文章

  1. (原创)如何使用boost.asio写一个简单的通信程序(二)

    先说下上一篇文章中提到的保持io_service::run不退出的简单办法.因为只要异步事件队列中有事件,io_service::run就会一直阻塞不退出,所以只要保证异步事件队列中一直有事件就行了, ...

  2. Python3的tkinter写一个简单的小程序

    一.这个学期开始学习python,但是看了python2和python3,最后还是选择了python3 本着熟悉python的原因,并且也想做一些小程序来增加自己对python的熟练度.所以写了一个简 ...

  3. [pixhawk笔记]4-如何写一个简单的应用程序

    本文主要内容来自于:https://dev.px4.io/en/tutorials/tutorial_hello_sky.html,并对文档中的部分问题进行更正. 本文假设已经建立好开发环境并能正确编 ...

  4. 用c++写一个简单的计算器程序

    // 050305.cpp : 定义控制台应用程序的入口点.// // 050304.cpp : 定义控制台应用程序的入口点.////四则运算#include "stdafx.h" ...

  5. python3 写一个简单的websocket程序(转)

    原贴:https://segmentfault.com/q/1010000009284816?_ea=1883181 也是找了好久 #! /usr/bin/env python # -*- codin ...

  6. 如何用 Python 写一个简易的抽奖程序

    不知道有多少人是被这个头图骗进来的:) 事情的起因是这样的,上周有同学问小编,看着小编的示例代码敲代码,感觉自己也会写了,如果不看的话,七七八八可能也写的出来,但是一旦自己独立写一段程序,感觉到无从下 ...

  7. linux设备驱动第三篇:如何写一个简单的字符设备驱动?

    在linux设备驱动第一篇:设备驱动程序简介中简单介绍了字符驱动,本篇简单介绍如何写一个简单的字符设备驱动.本篇借鉴LDD中的源码,实现一个与硬件设备无关的字符设备驱动,仅仅操作从内核中分配的一些内存 ...

  8. laravel学习:php写一个简单的ioc服务管理容器

    php写一个简单的ioc服务管理容器 原创: 陈晨 CoderStory 2018-01-14 最近学习laravel框架,了解到laravel核心是一个大容器,这个容器负责几乎所有服务组件的实例化以 ...

  9. 用Python写一个简单的Web框架

    一.概述 二.从demo_app开始 三.WSGI中的application 四.区分URL 五.重构 1.正则匹配URL 2.DRY 3.抽象出框架 六.参考 一.概述 在Python中,WSGI( ...

随机推荐

  1. 基于源码编译openssl

    openssh依赖于openssl,由于ssl频繁曝出漏洞,牵扯到openssh.而自身也存在漏洞... 00.下载openssl https://www.openssl.org/source/ ht ...

  2. β particle, α particle, γ ray, ionization chamber

    Alpha particles consist of two protons and two neutrons bound together into a particle identical to ...

  3. MySQL的binlog操作

    1. MySQL的binlog有三种模式: statement, row and mixed, 从5.1开始支持row, 默认是row模式 2. 设置参数 # 要配置在mysqld下 [mysqld] ...

  4. requires the FLAG_ACTIVITY_NEW_TASK flag

    07-18 16:34:05.891: E/AndroidRuntime(18396): FATAL EXCEPTION: main 07-18 16:34:05.891: E/AndroidRunt ...

  5. JavaScript RegExp Object 正则表达式入门

    什么是 RegExp? RegExp 是regular expression的缩写. RegExp 对象表示正则表达式,它是对字符串执行模式匹配的强大工具. 当您检索某个文本时,可以使用一种模式来描述 ...

  6. java 结束程序进程 代码

    结束firefox的进程,一句代码就够了,如下: Runtime.getRuntime().exec("taskkill /F /IM firefox.exe"); 结束qq: R ...

  7. 浅谈iOS中MVVM的架构设计

    MVVM就是在MVC的基础上分离出业务处理的逻辑到viewModel层. M:  Model层是API请求的原始数据,充当DTO(数据传输对象),当然,用字典也是可以的,编程么,要灵活一些.Model ...

  8. session会话保持原理

    1. 什么是会话保持? 会话保持是负载均衡最常见的问题之一,也是一个相对比较复杂的问题.会话保持有时候又叫做粘滞会话(Sticky Sessions).会话保持是指在负载均衡器上的一种机制,可以识别客 ...

  9. nfs的优化

    总结和测试了一下自己的经验: NFS中的rsize.wsize rsize.wsize对于NFS的效能有很大的影响.wsize和rsize设定了SERVER和CLIENT之间往来数据块的大小,这两个参 ...

  10. ef SQL Server 版本不支持数据类型“datetime2”

    我遇到这个问题是在用数据库模型的时候.当时我电脑上是sql2008 通过vs2010建立了一个ADO.net数据库模型,之后在项目上线的时候,临时把数据库换成了sql2005,在添加新闻的时候出现了“ ...