最近在做ROS与树莓派进行通信时,发现boost:asio库还蛮强大的,就试着用asio写了一个简单的使用tcp传输轨迹数据到树莓派,让树莓派控制步进电机旋转的代码。其实也就是将asio的功能封装成了两个类而已。asio_server类运行于树莓派上作为服务器,asio_client运行于PC上作为客户端。刚开始写的代码仅仅是客户端发送,服务器端接收这么简单。但后来一想,树莓派不是还要返回数据给ROS吗,比如电机的位置状态啊。所以呢又对程序进行了扩展。在扩展时就从一个新的高度来思考这个通信问题,只要建立了连接,是不是就可以不再区分客户端和服务器端了,两边应该是对等的关系。聊天软件不就是这样的例子吗。但C++程序中要实现这样的两个通信类并且很方便的使用也是要费点周折的。下面就论述一下实现的思路。
首先使用异步的方式来实现是很合理的选择,因为接收和发送都是同等的。如果用同步的方式处理,要么阻塞,要么轮询,要么开两个线程,总之处理会很麻烦。异步则可以设置callback函数,通过高效的poll方式来处理。通过分析asio中异步服务器的代码实现,其定义了一个tcp_connection类来处理每一个连接的收发,而asio_serve主类则只负责accept一个连接。这样的思路很好,所以就可以照搬采用。然后就是需要考虑数据的问题,所实现的tcp通信类应该是数据无关的,就是不管要发送的数据是多复杂的数据结果,在tcp通信类看来都是一串二进制。复杂的数据结构可以使用protobuf工具来进行描述和序列化。因此,实现的tcp通信类进行通信时,就简单的先发送四字节的数据大小,然后再发送数据即可,接收也是同样道理。
下面是该tcp通信框架的类图结构。
TcpDataHandler是一个基类,其中只实现setSocket和sendData,当成功建立连接时必须使用setSocket设置好socket,好让sendData能够成功发送数据。该类还有一个readCallback虚函数,作为实现具体功能的继承类完成该函数的实现,用于在收到数据后的处理。
AsioClient类只提供一个简单的start函数来启动于服务器端的连接即可,它关联一个TcpDataHandler的继承类来完成具体的收发任务。
AsioServer也只提供一个start函数使得服务器处于侦听状态,当有客户端连接时,它new出一个TcpConnection来处理与客户端的通信。所以TcpConnection与AsioClient是一个对等的关系。在创建TcpConnection实例的同时,也会创建一个与之对应的TcpDataHandler子类的实例。在C++实现中对AsioServer采用模板的方式创建TcpDataHandler子类,这样AsioServer就不需要关心具体是什么子类了。
客户端的代码非常简单,下面是一个示例:
#include "asio_tcp.h"
char data[4096]="hello";
class data_handler: virtual public TcpDataHandler
{
public:
data_handler() {}
~data_handler() {}
void readCallback(char*data, int size) {
cout<<"read "<<size<<" bytes"<<endl;
}
};
void main() {
data_handler handler;
asio_client client(&handler);
client.start("192.168.1.100",2000);
while(1) {
handler.sendData(data,6);
sleep(1);
}
}
服务器端的代码为:
#include "asio_tcp.h"
class data_handler: virtual public TcpDataHandler
{
public:
data_handler() {}
~data_handler() {}
void readCallback(char*data, int size) {
cout<<"read "<<size<<" bytes"<<endl;
}
};
void main(){
try{
asio_tcp_server<data_handler> srv(2000);
srv.start();
} catch(excetion &e) {
cerr<<e.what()<<endl;
}
}
完整的asio_tcp的实现代码如下:
#ifndef ASIO_TCP_H_
#define ASIO_TCP_H_
#include <string>
#include <memory>
#include <boost/asio.hpp>
#include <boost/make_shared.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/bind.hpp>
#include <boost/thread/thread.hpp>
#define BUFFER_SIZE 64*1024 //the required buffer size depends on applications.
#define TIMEOUT 10
using namespace std;
typedef boost::asio::io_service IoService;
typedef boost::asio::ip::tcp TCP;
class TcpDataHandler{
public:
TcpDataHandler(){ socket_=NULL; }
virtual ~TcpDataHandler(){}
void setSocket(TCP::socket *sock){ socket_=sock; }
virtual void readCallback(char* data, int size)=0;
bool sendData(char* data, int size)
{
if(socket_==NULL) return false;
boost::asio::write(*socket_, boost::asio::buffer(&size, sizeof(int)) );
int n,sent=0;
while(sent<size) {
n=boost::asio::write(*socket_, boost::asio::buffer(&data[sent],size-sent));
sent+=n;
}
return true;
}
private:
TCP::socket *socket_;
};
/****************************************************************************************************
// asio tcp client
*****************************************************************************************************/
class asio_client: public boost::enable_shared_from_this<asio_client>
{
public:
asio_client(TcpDataHandler *dataHandler): socket_(ios_),timer_(ios_), dataHandler_(dataHandler)
{ }
bool start(string srvip, int port) {
TCP::endpoint ep(boost::asio::ip::address::from_string(srvip), port);
try{
socket_.connect(ep);
cout<<"connected."<<endl;
}
catch(exception &ex)
{
cout<<ex.what()<<endl;
return false;
}
//start async read
if(dataHandler_!=NULL)
dataHandler_->setSocket(&socket_);
start_async_read(sizeof(int), false);
return true;
}
private:
void run_service()
{
ios_.run();
}
void start_async_read(int size, bool realData)
{
try{
// register read callback, receive the total length of data first
boost::asio::async_read(socket_, boost::asio::buffer(msg_,size),
boost::asio::transfer_exactly(size),
boost::bind(&asio_client::read_handle,
this, boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred, realData) );
}
catch(exception &ex){
cout<<ex.what()<<endl;
}
boost::thread(boost::bind(&asio_client::run_service,this));
}
void timeout_handle(const boost::system::error_code & ec)
{
if(ec==boost::system::errc::operation_canceled) return;
cerr<<"receive data time out"<<endl;
socket_.cancel();
}
void read_handle(const boost::system::error_code & ec,
size_t bytes_transferred, bool realData)
{
int total_size;
if(ec.value()!=0) {
cerr<<ec.message()<<endl;
socket_.close();
return;
}
if(!realData) {
total_size= *((int*)msg_);
//check whether total_size > BUFFER_SIZE;
if(total_size>BUFFER_SIZE) {
cerr<<"buffer is not enough to hold the data."<<endl;
socket_.close();
return;
}
cout<<"receiving "<<total_size<<" bytes..."<<endl;
//receive real data with known total_size
//setup a timer for timeout
timer_.expires_from_now(boost::posix_time::seconds(TIMEOUT));
timer_.async_wait(boost::bind(&asio_client::timeout_handle,this, boost::asio::placeholders::error));
//start async read again, read the real data
start_async_read(total_size,true);
}
else { //real data received.
cout<<"data transferred:"<<bytes_transferred<<endl;
timer_.cancel(); //cancel timer
if(dataHandler_!=NULL)
dataHandler_->readCallback(msg_, bytes_transferred);
//start async read again. read the length of data
start_async_read(sizeof(int),false);
}
}
private:
IoService ios_;
TCP::socket socket_;
boost::asio::deadline_timer timer_;
TcpDataHandler *dataHandler_;
};
/****************************************************************************************************
// asio tcp server
*****************************************************************************************************/
class tcp_connection
:public boost::enable_shared_from_this<tcp_connection>
{
public:
typedef boost::shared_ptr<tcp_connection> pointer;
static pointer create(IoService &io_service, boost::shared_ptr<TcpDataHandler> dataHandler) {
return pointer(new tcp_connection(io_service,dataHandler) );
}
TCP::socket &socket() {
return socket_;
}
// register a read call back, then return.
void start_async_read(int size, bool realData)
{
// register read callback, receive the total length of data first
boost::asio::async_read(socket_, boost::asio::buffer(msg_,size),
boost::bind(&tcp_connection::read_handle,
shared_from_this(), boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred, realData) );
}
string remoteIp(){
return socket_.remote_endpoint().address().to_string();
}
int remotePort() {
return socket_.remote_endpoint().port();
}
private:
tcp_connection(IoService &io_service, boost::shared_ptr<TcpDataHandler> dataHandler)
:socket_(io_service),timer_(io_service)
,dataHandler_(dataHandler)
{ }
void timeout_handle(const boost::system::error_code & ec)
{
if(ec==boost::system::errc::operation_canceled) return;
cerr<<"receive data time out"<<endl;
socket_.cancel();
}
void read_handle(const boost::system::error_code & ec,
size_t bytes_transferred, bool realData)
{
int total_size;
if(ec.value()!=0) {
cerr<<ec.message()<<endl;
socket_.close();
return;
}
if(!realData) {
total_size= *((int*)msg_);
//check whether total_size > BUFFER_SIZE;
if(total_size>BUFFER_SIZE) {
cerr<<"buffer is not enough to hold the data."<<endl;
socket_.close();
return;
}
cout<<"receiving "<<total_size<<" bytes..."<<endl;
//receive realdata with known total_size.
//setup a timer for timeout.
timer_.expires_from_now(boost::posix_time::seconds(TIMEOUT));
timer_.async_wait(boost::bind(&tcp_connection::timeout_handle,this, boost::asio::placeholders::error));
//start async read again, read real data.
start_async_read(total_size,true);
}
else { //real data received.
//call dataHandler->Callback();
cout<<"data transferred:"<<bytes_transferred<<endl;
//if(dataHandler_!=NULL)
dataHandler_->readCallback(msg_, bytes_transferred);
timer_.cancel(); //cancel timer
//start async read again, read data length.
start_async_read(sizeof(int), false);
}
}
private:
TCP::socket socket_;
boost::asio::deadline_timer timer_;
boost::shared_ptr<TcpDataHandler> dataHandler_;
char msg_[BUFFER_SIZE];
};
template<typename DataHandler>
class asio_tcp_server
{
public:
//initialize an acceptor to listen on TCP port 2000
asio_tcp_server(int port ):
acceptor_(ios, TCP::endpoint(TCP::v4(), port) )
{
//dataHandler_=handler;
}
//this function create a socket and initiates an asynchronous accept operation to wait for a new connection.
void start()
{
boost::shared_ptr<TcpDataHandler> dataHandler(new DataHandler);
tcp_connection::pointer new_connection =
tcp_connection::create(acceptor_.get_io_service(), dataHandler);
cout<<"start listenning."<<endl;
//initiate an asynchronous accept operation to wait for a new connection.
acceptor_.async_accept(new_connection->socket(),
boost::bind(&asio_tcp_server::handle_accept, this, new_connection,
boost::asio::placeholders::error) ) ;
ios.run(); //will block here.
}
private:
//this function is called when the asynchronous accept operation initiated by start_accept() finishes.
// It serves the client request, and then calls start_accept() to initiate the next accept operation.
void handle_accept(tcp_connection::pointer new_connection, const boost::system::error_code& error)
{
if(!error){
cout<<"New connection from "<<new_connection->remoteIp()<<endl;
new_connection->start_async_read(sizeof(int), false); //do some read and write work.
}
start();
}
private:
IoService ios;
TCP::acceptor acceptor_;
};
#endif
当前这个实现在服务器端还没有保存各个客户端连接。如果进一步改进可以添加一个客户端连接索引功能,能够方便的取出客户连接对应的TcpDataHandler,实现对等的发送功能。