简介
本文主要描述TCP协议的实现,其他协议类似。
关于Boost Asio库是什么,请参考Boost Asio快速入门。这篇文章概述了Asio库的重点。
关于Boost Asio中提供的函数及使用,请参考Boost Asio 网络编程理论基础。该文可以快速预览,待到使用时再回来查询。另外,该文关于异步编程的关键注意点也有阐述。
本文关注于异步编程的实现(关于同步和异步的异同,请参考上文)。异步编程在于入门难,但一旦入门,你会发现网络编程如此简单。
场景
TCP网络编程分为客户端和服务端两个部分,一般情况下,服务端启动后等待客户端的连接到来,客户端启动后发送登录请求及其他数据。
根据协议的分类,对于服务端,又可以分为无协议的数据流接收和基于协议的数据接收,前者会连续接收,待数据完全后再解析,后者一般会先接收数据头,根据数据头计算出总体数据长度,再接收指定长度的数据。
用户需要根据项目实际需求调整相应的结节部分,总体上的程序框架是相同的。
本文示例基于协议的数据接收。
协议也非常简单,数据分为两部分:数据头(4字节)和数据体。4字节的数据头表示的是数据体的长度。数据全部接收完成后再解析处理。
其他协议可根据实际需求处理。
下面先从相对简单的客户端说起。
客户端
客户端执行的步骤如下:
- 向服务端建立异步连接
- 连接成功后开始准备接收数据
- 接收数据流程:先接收4个字节数据头,计算出数据体长度,再接收数据体
- 提供发送数据接口,并缓存不能及时发送的数据
示例代码:
// TcpMgr.h
#ifndef TCP_MGR_H_
#define TCP_MGR_H_
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/enable_shared_from_this.hpp>
#include "App.h" // 应用类,使用该TcpMgr的类,用于向应用类返回信息
using std::string;
class App;
// TcpMgr类需要一直存在,否则会发生错误。如把该实例建立在栈上,当作用域结束后,将被析构。所以类为static且返回智能指针
// 这里把构造方法设置成了私有而且不允许拷贝构造(继承自boost::noncopyable)
class TcpMgr : public boost::enable_shared_from_this<TcpMgr>, boost::noncopyable
{
public:
//
static boost::shared_ptr<TcpMgr> Init(const char *ip, const unsigned short port, App *pApp);
bool SendData(const uint8_t *pDataInfo, const size_t len, std::string errmsg); // 对外接口
void Finit();
private:
using error_code = boost::system::error_code;
string m_ip;
unsigned short m_port;
boost::asio::io_service m_ios;
boost::asio::io_service::work m_work;
boost::asio::io_service::strand m_strand; // 单线程时不必要,但为了扩展,这里直接都以strand包裹了
boost::asio::ip::tcp::socket m_sock; // 非线程安全的类,使用时务必注意
static const int kNumOfWorkThreads = 1; // 处理tcp的线程数量
boost::thread_group m_threads; // 用于创建线程
bool m_socketStarted;
enum
{
MSG_HEADER_LEN = 4 // 私有协议的数据头字节数
};
std::vector<char> m_recv_buffer; // 接收数据缓冲区
std::list<boost::shared_ptr<std::string>> m_pending_sends; // 发送数据缓冲区,待发送数据以List管理
App *m_pApp;
static const int kTcpRetryInterval = 1000; //ms,自动重连的时间间隔
TcpMgr();
void Start(const char *ip, const unsigned short port, App *pApp);
void OnConnect(const error_code &err); // 连接建立完成后会被调用
void OnReadHeader(const error_code &err, size_t bytes); // 接收到数据头后会被调用
void OnReadBody(const error_code &err, size_t bytes); // 接收到数据体后会被调用
void OnWrite(const error_code &err); // 数据发送完成后会被调用
void ReConnectServer();
void RecvHeader();
void RecvBody(int len);
void StartRecvHeader();
void StartRecvBody(int len);
void SendMsg(boost::shared_ptr<string> msg);
void StartSend();
void Run();
void CloseSocket();
void StartCloseSocket();
};
#endif
// TcpMgr.cpp
#include "TcpMgr.h"
#include <thread>
#include <chrono>
#include "flatbuffers/AppDataType_generated.h" // 使用了FlatBuffers对数据编码
using namespace AppDataType;
using namespace std;
TcpMgr::TcpMgr()
: m_work(m_ios),
m_strand(m_ios),
m_sock(m_ios),
m_socketStarted(false)
{
}
boost::shared_ptr<TcpMgr> TcpMgr::Init(const char *ip, const unsigned short port, App *pApp)
{
boost::shared_ptr<TcpMgr> newTcpMgr(new TcpMgr());
newTcpMgr->Start(ip, port, pApp);
return newTcpMgr;
}
void TcpMgr::Run()
{
m_ios.run();
}
void TcpMgr::Finit()
{
CloseSocket();
m_ios.stop();
m_threads.join_all();
}
void TcpMgr::Start(const char *ip, const unsigned short port, App *pApp)
{
m_ip = ip;
m_port = port;
m_pApp = pApp;
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(m_ip), m_port);
m_sock.async_connect(ep, m_strand.wrap(boost::bind(&TcpMgr::OnConnect, shared_from_this(), _1))); // 异步连接
for (auto i = 0; i < kNumOfWorkThreads; ++i)
{
m_threads.create_thread(boost::bind(&TcpMgr::Run, this));
}
}
void TcpMgr::OnConnect(const error_code &err)
{
if (!err && m_sock.is_open()) // 连接成功
{
m_pApp->OnServerConnected();
m_socketStarted = true;
RecvHeader(); // 开始接收数据头
}
else
{
m_pApp->OnServerDisConnected(err.message());
ReConnectServer();
}
}
void TcpMgr::ReConnectServer()
{
std::this_thread::sleep_for(std::chrono::milliseconds(kTcpRetryInterval));
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(m_ip), m_port);
m_sock.async_connect(ep, m_strand.wrap(boost::bind(&TcpMgr::OnConnect, shared_from_this(), _1)));
}
void TcpMgr::OnReadHeader(const error_code &err, size_t bytes)
{
if (!err)
{
size_t bodyLen = flatbuffers::ReadScalar<flatbuffers::uoffset_t>(m_recv_buffer.data()); // 解析实际数据长度
RecvBody(bodyLen); // 开始接收数据体
}
else
{
m_pApp->OnServerDisConnected(err.message());
CloseSocket();
ReConnectServer();
}
}
void TcpMgr::StartRecvHeader()
{
m_recv_buffer.resize(MSG_HEADER_LEN);
async_read(m_sock, boost::asio::buffer(m_recv_buffer, MSG_HEADER_LEN), m_strand.wrap(boost::bind(&TcpMgr::OnReadHeader, shared_from_this(), _1, _2)));
}
void TcpMgr::RecvHeader()
{
m_strand.post(boost::bind(&TcpMgr::StartRecvHeader, shared_from_this()));
}
void TcpMgr::OnReadBody(const boost::system::error_code &ec, size_t bytes_transferred)
{
if (!ec)
{
// 处理接收到的数据
{
// 数据交由子类处理,注意子类在处理中不能进行耗时操作,否则可能导致数据接收的阻塞
m_pApp->OnRecvedData(&m_recv_buffer[0], bytes_transferred + MSG_HEADER_LEN);
}
// 继续读取
RecvHeader();
}
else
{
m_pApp->OnServerDisConnected(ec.message());
CloseSocket();
ReConnectServer();
}
}
void TcpMgr::StartRecvBody(int len)
{
m_recv_buffer.resize(MSG_HEADER_LEN + len);
async_read(m_sock, boost::asio::buffer(&m_recv_buffer[MSG_HEADER_LEN], len), m_strand.wrap(boost::bind(&TcpMgr::OnReadBody, shared_from_this(), _1, _2)));
}
void TcpMgr::RecvBody(int len)
{
m_strand.post(boost::bind(&TcpMgr::StartRecvBody, shared_from_this(), len));
}
void TcpMgr::OnWrite(const error_code &err)
{
if (err)
{
m_pApp->OnServerDisConnected(err.message());
CloseSocket();
ReConnectServer();
}
else
{
m_pending_sends.pop_front();
StartSend();
}
}
void TcpMgr::StartSend()
{
if (!m_pending_sends.empty())
{
boost::asio::async_write(m_sock, boost::asio::buffer(*m_pending_sends.front().get()), m_strand.wrap(boost::bind(&TcpMgr::OnWrite, shared_from_this(), _1)));
}
}
void TcpMgr::SendMsg(boost::shared_ptr<string> pmsg)
{
bool should_start_send = m_pending_sends.empty();
m_pending_sends.emplace_back(pmsg);
if (should_start_send)
{
StartSend();
}
}
bool TcpMgr::SendData(const uint8_t *pDataInfo, const size_t len, std::string errmsg)
{
try
{
auto pmsg(boost::make_shared<string>((const char *)(pDataInfo), len));
m_strand.post(boost::bind(&TcpMgr::SendMsg, shared_from_this(), pmsg));
}
catch (const std::exception &e)
{
errmsg.assign("发送数据失败:" + string(e.what()));
return false;
}
return true;
}
void TcpMgr::CloseSocket()
{
m_strand.post(boost::bind(&TcpMgr::StartCloseSocket, shared_from_this()));
}
void TcpMgr::StartCloseSocket()
{
if (!m_socketStarted)
{
return;
}
boost::system::error_code ec;
m_sock.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
m_sock.close(ec);
m_socketStarted = false;
}
几点说明:
- strand是不必要的,因为只使用了一个线程,不会有并发。如果使用了多线程,则是必要的
- 数据的接收是顺序的,不需要缓冲区暂存。但如果应用处理较慢,会有TCP接收缓冲区数据堆积
- 发送数据是要管理缓冲区的,因为应用层可能要发送大量数据,而socket不是一直可供使用
- 数据的接收需要根据协议修改,应用层也根据需要修改
服务端
服务端比较复杂,执行的步骤如下:
- 在指定端口上异步接收连接请求,并为连接绑定TcpSession
- 连接到来后,需要做两件事:一是继续异步接收连接请求,二是让TcpSession开始接收数据
- 接收数据流程同客户端
- 发送数据流程同客户端
示例代码如下:
// ClientMgrBase.h
#ifndef CLIENT_MGR_BASE_H_
#define CLIENT_MGR_BASE_H_
// #define BOOST_ASIO_ENABLE_HANDLER_TRACKING
#include <string>
#include <boost/thread.hpp>
#include <boost/asio.hpp>
using std::string;
class AsioTcp; // 接收tcp连接请求的类
class TcpSession; // 用于跟踪每个已经连接的类,包括socket和数据缓冲区
// 子类从该类派生,即可使用服务
class ClientMgrBase
{
public:
ClientMgrBase();
virtual ~ClientMgrBase();
// 启动服务,开始在指定的端口监听连接,在程序启动时调用。如果返回值非0,则err存储为错误信息
int Start(const unsigned short port, std::string &err);
// 停止监听,释放所有资源,一般在程序退出时调用。如果返回值非0,则err存储为错误信息
int Release(std::string &err);
// 发送数据。如果返回值非0,则err存储为错误信息
int SendData(boost::shared_ptr<TcpSession> handler, const char *pData, const size_t nDataSize, std::string &err);
// 主动关闭会话。如果返回值非0,则err存储为错误信息
int CloseSession(boost::shared_ptr<TcpSession> handler, std::string &err);
// 回调函数,由子类实现
// 新连接建立
virtual void OnNewConnection(boost::asio::ip::tcp::socket &newSocket) = 0;
// 连接断开
virtual void OnDisConnection(boost::shared_ptr<TcpSession> handler) = 0;
// 接收到数据
virtual void OnRecvData(boost::shared_ptr<TcpSession> handler, const char *pData, const size_t nDataSize) = 0;
// 发送数据的结果,ec为0时发送成功,非0时发送失败。无论成功与否,均返回发送的数据,以便处理和调试
virtual void OnSendComplete(boost::shared_ptr<TcpSession> handler, const int ec, const std::string &em, const char *buf, const size_t len) = 0;
// 发生异常时, 向子类返回错误信息
virtual void OnRtnErrMsg(boost::shared_ptr<TcpSession> handler, const std::string &err) = 0;
private:
boost::shared_ptr<AsioTcp> m_pAsioTcp;
};
// 与每个已经连接的客户端关联的类,包括socket和数据缓冲区
class TcpSession : public boost::enable_shared_from_this<TcpSession>, boost::noncopyable
{
public:
TcpSession(boost::asio::io_service &ioService, ClientMgrBase *pClientMgrBase);
virtual ~TcpSession();
void ReadHeader();
bool SendData(const char *pData, const size_t nDataSize);
void CloseSocket();
boost::asio::ip::tcp::socket &GetSocket();
boost::asio::strand &GetStrand();
private:
enum
{
MSG_HEADER_LEN = 4
};
std::vector<char> m_recv_buffer;
std::list<boost::shared_ptr<std::string>> m_pending_sends;
boost::asio::ip::tcp::socket m_socket;
boost::asio::strand m_strand;
ClientMgrBase *m_pClientMgrBase;
bool m_started;
void HandleReadHeader(const boost::system::error_code &ec, size_t bytes_transferred);
void HandleReadBody(const boost::system::error_code &ec, size_t bytes_transferred);
void HandleWrite(const boost::system::error_code &ec, size_t bytes_transferred);
void StartCloseSocket();
void StartRecvHeader();
void RecvBody(int len);
void StartRecvBody(int len);
void SendMsg(boost::shared_ptr<string> msg);
void StartSend();
};
// 负责启动并准备接收新连接
class AsioTcp
{
public:
AsioTcp();
virtual ~AsioTcp();
void Start(ClientMgrBase *pClientMgrBase, const unsigned short port);
void SendMsg(boost::shared_ptr<TcpSession> handler, const char *pData, const size_t nDataSize);
int CloseConnection(boost::shared_ptr<TcpSession> handler, std::string &err);
void Stop();
private:
using TcpSessionPtr = boost::shared_ptr<TcpSession>;
using acceptor = boost::asio::ip::tcp::acceptor;
boost::asio::io_service m_ioservice;
boost::shared_ptr<boost::asio::io_service::work> m_work;
boost::shared_ptr<acceptor> m_pAcceptor;
ClientMgrBase *m_pClientMgrBase;
int m_numOfWorkThreads;
boost::thread_group m_workThreads;
void Run();
void StartAccept();
void HandleAccept(const boost::system::error_code &ec, TcpSessionPtr pTcpSession);
};
#endif
// ClientMgrBase.cpp
#include "ClientMgrBase.h"
#include <iostream>
#include <thread>
#include <string>
#include <boost/bind.hpp>
#include "AppDataType_generated.h"
using namespace AppDataType;
using namespace boost::asio;
using namespace std;
ClientMgrBase::ClientMgrBase()
{
}
ClientMgrBase::~ClientMgrBase()
{
}
int ClientMgrBase::Start(const unsigned short port, string &err)
{
try
{
m_pAsioTcp = boost::make_shared<AsioTcp>();
m_pAsioTcp->Start(this, port);
}
catch (const std::exception &e)
{
err.assign(e.what());
return -1;
}
return 0;
}
int ClientMgrBase::Release(string &err)
{
try
{
m_pAsioTcp->Stop();
}
catch (const std::exception &e)
{
err.assign(e.what());
return -1;
}
return 0;
}
int ClientMgrBase::SendData(boost::shared_ptr<TcpSession> handler, const char *pData, const size_t nDataSize, string &err)
{
try
{
m_pAsioTcp->SendMsg(handler, pData, nDataSize);
}
catch (const std::exception &e)
{
err.assign(e.what());
return -1;
}
return 0;
}
int ClientMgrBase::CloseSession(boost::shared_ptr<TcpSession> handler, string &err)
{
return m_pAsioTcp->CloseConnection(handler, err);
}
AsioTcp::AsioTcp() : m_work(new boost::asio::io_service::work(m_ioservice))
{
m_numOfWorkThreads = std::thread::hardware_concurrency();
}
AsioTcp::~AsioTcp(void)
{
}
void AsioTcp::Run()
{
m_ioservice.run();
}
void AsioTcp::Start(ClientMgrBase *pClientMgrBase, const unsigned short port)
{
m_pClientMgrBase = pClientMgrBase;
m_pAcceptor = boost::make_shared<acceptor>(m_ioservice, ip::tcp::endpoint(ip::tcp::v4(), port));
StartAccept();
// 启动任务处理线程
for (auto i = 0; i < m_numOfWorkThreads; i++)
{
m_workThreads.create_thread(boost::bind(&AsioTcp::Run, this));
}
}
void AsioTcp::StartAccept()
{
TcpSessionPtr pTcpSession = boost::make_shared<TcpSession>(m_ioservice, m_pClientMgrBase);
m_pAcceptor->async_accept(pTcpSession->GetSocket(), pTcpSession->GetStrand().wrap(boost::bind(&AsioTcp::HandleAccept, this, _1, pTcpSession)));
}
void AsioTcp::HandleAccept(const boost::system::error_code &ec, TcpSessionPtr pTcpSession)
{
if (ec)
{
string err = "AsioTcp处理连接请求失败:" + ec.message();
m_pClientMgrBase->OnRtnErrMsg(pTcpSession, err);
StartAccept();
return;
}
if (m_pClientMgrBase != NULL)
{
m_pClientMgrBase->OnNewConnection(pTcpSession->GetSocket());
}
try
{
StartAccept();
pTcpSession->ReadHeader();
}
catch (const std::exception &e)
{
string err = "AsioTcp重启连接失败:" + string(e.what());
m_pClientMgrBase->OnRtnErrMsg(pTcpSession, err);
}
}
void AsioTcp::SendMsg(boost::shared_ptr<TcpSession> handler, const char *pData, const size_t nDataSize)
{
handler->SendData(pData, nDataSize);
}
void AsioTcp::Stop()
{
m_ioservice.stop(); // 此操作会立即结束所有任务
// m_work.reset(); // 此操作会等待ios处理完所有任务后返回,有可能阻塞
m_workThreads.join_all();
}
int AsioTcp::CloseConnection(boost::shared_ptr<TcpSession> handler, string &err)
{
handler->CloseSocket();
(void)err;
return 0;
}
TcpSession::TcpSession(boost::asio::io_service &ioService, ClientMgrBase *pClientMgrBase)
: m_socket(ioService),
m_strand(ioService),
m_pClientMgrBase(pClientMgrBase)
{
m_started = true;
}
TcpSession::~TcpSession()
{
}
boost::asio::ip::tcp::socket &TcpSession::GetSocket()
{
return m_socket;
}
boost::asio::strand &TcpSession::GetStrand()
{
return m_strand;
}
void TcpSession::ReadHeader()
{
m_strand.post(boost::bind(&TcpSession::StartRecvHeader, shared_from_this()));
}
void TcpSession::StartRecvHeader()
{
m_recv_buffer.resize(MSG_HEADER_LEN);
async_read(m_socket, boost::asio::buffer(m_recv_buffer, MSG_HEADER_LEN), m_strand.wrap(boost::bind(&TcpSession::HandleReadHeader, shared_from_this(), _1, _2)));
}
void TcpSession::HandleReadHeader(const boost::system::error_code &ec, size_t bytes_transferred)
{
(void)bytes_transferred;
if (!ec)
{
size_t bodyLen = flatbuffers::ReadScalar<flatbuffers::uoffset_t>(m_recv_buffer.data()); // 解析实际数据长度
RecvBody(bodyLen);
}
else
{
// 发生错误
m_pClientMgrBase->OnRtnErrMsg(shared_from_this(), ec.message());
CloseSocket();
}
}
void TcpSession::RecvBody(int len)
{
m_strand.post(boost::bind(&TcpSession::StartRecvBody, shared_from_this(), len));
}
void TcpSession::StartRecvBody(int len)
{
m_recv_buffer.resize(MSG_HEADER_LEN + len);
async_read(m_socket, boost::asio::buffer(&m_recv_buffer[MSG_HEADER_LEN], len), m_strand.wrap(boost::bind(&TcpSession::HandleReadBody, shared_from_this(), _1, _2)));
}
void TcpSession::HandleReadBody(const boost::system::error_code &ec, size_t bytes_transferred)
{
if (!ec)
{
// 处理接收到的数据
if (m_pClientMgrBase != NULL)
{
// 数据交由子类处理,注意子类在处理中不能进行耗时操作,否则可能导致数据接收的阻塞
m_pClientMgrBase->OnRecvData(shared_from_this(), &m_recv_buffer[0], bytes_transferred + MSG_HEADER_LEN);
}
// 继续读取
ReadHeader();
}
else
{
m_pClientMgrBase->OnRtnErrMsg(shared_from_this(), ec.message());
CloseSocket();
}
}
bool TcpSession::SendData(const char *data, const size_t size)
{
try
{
auto pmsg(boost::make_shared<string>(data, size));
m_strand.post(boost::bind(&TcpSession::SendMsg, shared_from_this(), pmsg));
}
catch (const std::exception &e)
{
return false;
}
return true;
}
void TcpSession::SendMsg(boost::shared_ptr<string> pmsg)
{
bool should_start_send = m_pending_sends.empty();
m_pending_sends.emplace_back(pmsg);
if (should_start_send)
{
StartSend();
}
}
void TcpSession::StartSend()
{
if (!m_pending_sends.empty())
{
boost::asio::async_write(m_socket, boost::asio::buffer(*m_pending_sends.front().get()), m_strand.wrap(boost::bind(&TcpSession::HandleWrite, shared_from_this(), _1, _2)));
}
}
void TcpSession::HandleWrite(const boost::system::error_code &ec, size_t bytes_transferred)
{
if (!ec)
{
// 数据发送完成
m_pending_sends.pop_front();
StartSend();
}
else
{
// 发生错误
string err = "数据发送失败:" + ec.message();
m_pClientMgrBase->OnSendComplete(shared_from_this(), -1, err, m_pending_sends.front()->c_str(), bytes_transferred);
CloseSocket();
}
}
void TcpSession::CloseSocket()
{
m_strand.post(boost::bind(&TcpSession::StartCloseSocket, shared_from_this()));
}
void TcpSession::StartCloseSocket()
{
if (!m_started)
{
return;
}
boost::system::error_code ec;
m_pClientMgrBase->OnDisConnection(shared_from_this());
m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
m_socket.close(ec);
m_started = false;
}
说明:
- 异步操作的精髓与客户端类似,因为不知道具体的异步操作何时发生,所以要保证异步操作发生时所有相关的实例是活动的
- 正因为上述原因,所以在bind函数中,使用的都是shared_from_this()智能指针,如果使用普通指针,在相关回调函数中可能实例已经释放,就会崩溃
- 异步发送之前,要把数据缓存,不然可能会造成数据丢失
- 启动的线程数量为
std::thread::hardware_concurrency();
,它获取了操作系统的核心数,实际根据需要选择 #define BOOST_ASIO_ENABLE_HANDLER_TRACKING
宏可以打开Boost调试,当心,这会输出较多日志,方便调试- 封装的类可以根据需要简化
小结
Boost Asio异步网络编程强健、跨平台,但在使用时要特别注意一些事项:
- 要保证异步操作发生时,相关的实例是活动的
- 要注意socket类非线程安全,在多线程中要使用strand
- 接收和发送缓冲区要注意自行管理
- 注意异常处理
把握住了以上几点,先写个简单的应用,测试一下,改下代码,知道什么情况下会有问题,什么情况下是安全的,就差不多掌握了。