做了这么长时间的服务器,也在CSDN上发过不少帖子,谢谢那些热心的朋友!!!
在做服务器的时候,感觉这个 拆包粘包 有一点难度,现在呢,我就把我的拆包粘包过程分享,这个过程是经过测试的,在4个工作线程下连续发送大量消息,持续发送几分钟,每个包1000bytes左右,都是小包,测试了很多次,没有处理异常和错误崩溃。
下面代码我一段段的上,附上说明, 同时也会附上文件:http://download.csdn.net/detail/oiooooio/7774413
第一段代码,先上数据结构(单数据句柄):
typedef struct sPerHandleData
{
friend class::ReceiveManager;
friend class::SendManager;
friend class::SendOrRecvMngBase;
private:
boost::asio::ip::tcp::socket* _socket; //socket
char* _buffer; //消息缓冲区
unsigned __int32 _buffer_size; //缓冲区 【总】 大小
unsigned __int32 _offset;//消息长度
public:
boost::asio::ip::tcp::socket* Socket() const { return _socket; }
unsigned __int32 Buffer_size() const { return _buffer_size; }
unsigned __int32 Offset() const { return _offset; }
char* Buffer() const { return _buffer; }
public:
void Offset(unsigned __int32 val) { _offset = val; }
void Socket(boost::asio::ip::tcp::socket* val) { _socket = val; }
//还有一些方法这里就不在做介绍了
}PerHandleData, *PPerHandleData;
以上大家应该一看就懂,下来上包头结构:
typedef struct sPacket
{
unsigned __int8 _packet_type; //不介绍
unsigned __int8 _device_type; //不介绍
unsigned __int16 _the_packet_header_size; //包头大小
unsigned __int32 _the_whole_packet_size; //整包大小(包头+消息体)
}Packet, *PPacket;
看完以上结构,我就再附上真正的东西:
void ReceiveManager::AsynReceive( mc_system_msg::PPerHandleData handledata, const boost::system::error_code& error, std::size_t bytes_transferred )
{
//在拆包的时候,这个变量是有用的,因为拆包需要移动handledata->_buffer指针,使之不断指向下一个包
//当处理完毕后,需要对handledata->_buffer缓冲区指针还原,以免释放缓冲区时出现问题
char* tmpPtr = handledata->_buffer;
//设置总共收到的消息的长度,必须要用+=,而不是=
handledata->_offset += bytes_transferred;
if(Receive(handledata, bytes_transferred))
{
//投递接收操作
PostReceive(handledata->Socket());
}
//防止handledata->_buffer指针在Receive中被改变,还原指针值,希望指针被正确的释放
handledata->_buffer = tmpPtr;
ReleaseMemory(handledata);
}
/// <summary>
/// 此过程主要处理TCP消息的拆包和粘包过程,把处理完整的消息加入到消息队列
/// </summary>
/// <param name="handledata">The handledata.</param>
/// <param name="bytes_transferred">The bytes_transferred.</param>
/// <returns>int.</returns>
bool ReceiveManager::Receive( mc_system_msg::PPerHandleData handledata, std::size_t bytes_transferred )
{
if(bytes_transferred == 0){
//DoData(handledata, 0);
_msg_queue->Add(handledata);
return false;
}
using namespace mc_system_msg;
do
{
PPacket tmpPacket = (PPacket)handledata->_buffer;
if(handledata->_offset < PACKET_SIZE){
//消息不全,跳转到[2]继续读取消息
goto FALG_4;
}
else if(tmpPacket->_the_packet_header_size != PACKET_SIZE){
return false;
}
/*[1]*/if(tmpPacket->_the_whole_packet_size == handledata->_offset){
//DoData(handledata, bytes_transferred);
_msg_queue->Add(handledata);
return true;
}
/*[2]*/else if(handledata->_offset < tmpPacket->_the_whole_packet_size){
/*
* 代码执行到这里,说明已收到的数据不是一个完整的包,需要再次接收剩下的数据,这里或许有一个疑问,为什么重新获取数据句柄?
* 原因在于下面一个判断[3]:
* else if(handledata->_offset > tmpPacket->_the_whole_packet_size && tmpPacket->_the_whole_packet_size != 0)
* 这个判断的意思是,收到的数据是几个消息包的集合,需要拆分处理,那么可能正好是3个包的集合,也可能是3个半的消息包的集合,这样,剩下的1/2
* 数据包的处理将会走到这个if里,这时,并不能保证handledata->_buffer指针,还指在handledata->_buffer的头部,或许已经移动到了中部,
* 尾部等,基于这个原因,所以再次申请一个数据句柄,用于投递接收剩下的数据。
*
* 看了以上的话,那为什么不定义一个临时指针?这里可以不用重新申请数据句柄吗?
* 答:就算定义一个临时指针,那么在【3个半的消息包的集合,这样,剩下的1/2数据包的处理将会走到这个if里】这种情况下,还要把剩下的数据移动到
* handledata->_buffer头部,然后再去投递接收剩下的数据,这样的一个操作过程不比重新申请一个数据句柄简单
*/
FALG_4:
mc_system_msg::PPerHandleData tmp_perHandledata = GetData(handledata->_socket);
if(tmp_perHandledata == NULL){
return false;
}
else{
memcpy((tmp_perHandledata->_buffer), (handledata->_buffer), handledata->_offset);
tmp_perHandledata->_offset = handledata->_offset;
PostReceive(tmp_perHandledata);
}
//这里不需要投递接收消息
return false;
}
/*[3]*/else if(handledata->_offset > tmpPacket->_the_whole_packet_size && tmpPacket->_the_whole_packet_size != 0){
DoData(handledata, tmpPacket->_the_whole_packet_size);
//_msg_queue->Add(handledata);
char* tmpMovePtr = handledata->_buffer + tmpPacket->_the_whole_packet_size;
handledata->_buffer = tmpMovePtr;
bytes_transferred -= tmpPacket->_the_whole_packet_size;
handledata->_offset -= tmpPacket->_the_whole_packet_size;
continue;
}
/*[4]*/else{
//出现这种情况呢,一般来说,正常的情况下是收到的消息/或者可能是拆包后剩下的消息长度达不到包头的长度,导致消息解析失败
//跳转到[2]再次去读取消息
goto FALG_4;
}
} while (true);
assert(false);
throw std::exception("bool ReceiveManager::Receive(...)error");
return false;
}
代码已经上完了,废话不多...
在拆包粘包过程中,注释写的很清楚,整个项目依赖于boost库,这段代码其他跟boost库也没啥大关系,读者顶多就是看到有几个boost字眼,哪有怎么样...
我在处理完整的消息的时候,是把消息加入到了消息队列(加入的时候,是做了消息的一个副本),加入完毕后,把消息句柄释放。读者应该会看到有DoData函数,用一个子类继承这个函数,就可以直接对消息进行处理。
拆包粘包过程,2个函数,这2个函数是配合使用的,请仔细看...
---
好了,最后呢,欢迎大家交流讨论,有问题和疑问的尽管说,如果我知道,那么言无不尽,如有错误,欢迎指出,谢谢!
打个广告,我的群,欢迎各位朋友加入!
c/c++_发烧友_ 80416665
c/c++_发烧友_
80416665