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. Arduino的光敏传感器和超声波测距传感器测试代码

    光敏传感器测试代码 使用的是一个未知名4pin模块, 4pin依次为Vcc, Gnd, DO, AO, 板载一个可调电阻以及一个LED, 调节电阻大小, 可以控制LED亮灭和DO输出变化对光亮的灵敏度 ...

  2. Java用freemarker导出word

    概述 最近一个项目要导出word文档,折腾老半天,发现还是用freemarker的模板来搞比较方便省事,现总结一下关键步骤,供大家参考,这里是一个简单的试卷生成例子. 详细 代码下载:http://w ...

  3. PL/SQL报无效的窗口句柄的解决办法

    在远程服务器上使用pl sql developer查询oralce数据库的时候,遇到很长的文本变量想点开小窗口看下具体内容, 但系统弹窗提示“无效的窗口句柄”,听同事介绍原来需要开启一个windows ...

  4. (原)ubuntu中安装tensorflow

    转载请注明出处: http://www.cnblogs.com/darkknightzh/p/6592052.html 参考网址: https://www.tensorflow.org/install ...

  5. 进阶之路(基础篇) - 001 亮一个led灯

    /********************************* 代码功能:点亮一个led灯 使用函数: pinMode(引脚号,模式); digitalWrite(引脚号,电平状态); //默认 ...

  6. Linux下mysql的远程连接(转)

    转载:http://www.cnblogs.com/fnlingnzb-learner/p/5830661.html 如果Mysql是按上篇的方法进行安装和设置的话,那进行远程连接就会稍微简单一点.我 ...

  7. CentOS 开启防火墙 firewall ,mysql 远程访问

    最近在阿里云服务器centos上安装了mysql数据库,默认是不开启远端访问功能,需要设置一下防火墙,在开放默认端口号 3306时提示FirewallD is not running,经过排查发现是防 ...

  8. Java 图片矢量压缩

    直接贴出工具类源码 package com.snow.web.util.publics; import java.awt.Image; import java.awt.image.BufferedIm ...

  9. C# 使用Xamarin开发Android应用程序

    C#使用Xamarin开发可移植移动应用终章(11.获取设备信息与常用组件,开源一个可开发模版.) 摘要: 前言 系列目录 C#使用Xamarin开发可移植移动应用目录 源码地址:https://gi ...

  10. Win7系统计算机中Msvcr100.dll丢失的解决办法

    1.使用安全卫士里的人工服务. 在搜索框里输入msvcr100.dll. 点击查找方案. 2.点击msvcr100.dll问题后面的立即修复. 只要等待片刻就好了.