笔者不再赘述分包组包算法是什么和为什么了,直接上代码,注释已经写的非常完整了,也希望大家测试使用,并提出好的建议。
凡是有关提交bug、性能提升、算法优化方面好的建议,可以直接在本文下留言,或者私信作者,邮箱:yiyefangzhou24@qq.com。
一、C语言版
只有两个入口函数,发送数据SendString和接受数据RecvString,注意RecvString的数据需要手动free,该方法不适合dll使用,不符合谁申请谁释放原则。
// #
// # WinSocket TCP协议 分包组包算法
// #
// # 文件名socket.h
// #
// # 作者: yiyefangzhou24
// # 修改时间: 2021/6/1
// #
// # 仅供交流学习使用,未经允许,请勿商用
// #
#define PACKSIZE 1024 //定义默认单个包数据部分字节数
/* Socket数据包格式 */
//#pragma pack(1) //取消编译器默认8字节对齐
typedef struct MySocket
{
int total_package; //一次传输分包个数
int total_len; //一次传输总字节数
int index; //当前包序号
char data[PACKSIZE]; //数据内容
}MySocket;
/*
* 自定义发送函数主入口
* 参数[SOCKET] hSocket Socket句柄
* 参数[char*] pData 待发送的数据指针
* 参数[int] len 待发送数据字节数
* 返回值[int] 实际发送数据字节数
*/
int SendString(SOCKET hSocket, char * pData, int len);
/*
* 自定义接收函数主入口
* 参数[SOCKET] hSocket Socket句柄
* 参数[char*&] pData 用于接收数据的空指针
* 返回值[int] 实际接收数据字节数
*
* 注意:pData指针需要手动释放(free),该函数不能再dll外释放避免造成内存泄露
*/
int RecvString(SOCKET hSocket, char *& pData);
// #
// # WinSocket TCP协议 分包组包算法
// #
// # 文件名socket.cpp
// #
// # 作者: yiyefangzhou24
// # 修改时间: 2021/6/1
// #
// # 仅供交流学习使用,未经允许,请勿商用
// #
#pragma comment(lib,"Ws2_32.lib")
#include <winsock2.h>
#include "socket.h"
/*
* 此函数为内部函数,不对外提供调用接口,用于确保一次recv接收一个完整的socket_data分包
* 参数[SOCKET] hSocket Socket句柄
* 参数[socket_data*] pData 用于存放一个socket_data数据的指针,该指针应当提前分配好内存
* 返回值[int] 实际接收数据字节数
*/
int _PackageRecv(SOCKET hSocket, MySocket * pData);
/*
* 此函数为内部函数,不对外提供调用接口,用于确保一次send接收一个完整的socket_data分包
* 参数[SOCKET] hSocket Socket句柄
* 参数[socket_data*] pData 待发送socket_data指针
* 返回值[int] 实际发送数据字节数
*/
int _PackageSend(SOCKET hSocket, MySocket * pData);
int SendString(SOCKET hSocket, char * pData, int len)
{
unsigned int total_sended_buf = 0; //实际发送的字节数
int package_num = (len + PACKSIZE - 1) / PACKSIZE; //计算要分割为多少个包
MySocket package_data; //申请1个分包的内存,用于拷贝存放每次发送的数据
for (int i = 0; i < package_num; i++) //依次发送分包
{
//printf("total:%d---now:%d\n",package_data->total_package , package_data->index);
memset(&package_data, 0, sizeof(MySocket)); //重新初始化数据包数据
package_data.total_package = package_num; //一次传输包总数
package_data.total_len = len; //一次传输总字节数
package_data.index = i; //包序号
if (package_data.index == package_num - 1) //如果是最后一个分包拷贝实际包数据
{
memcpy(package_data.data, pData + i * PACKSIZE, package_data.total_len - i * PACKSIZE);
}
else //否则拷贝PACKSIZE字节数据
{
memcpy(package_data.data, pData + i * PACKSIZE, PACKSIZE); //包数据
}
int once_send = _PackageSend(hSocket, &package_data); //发送数据
if (once_send <= 0)
{
return once_send; //为保证每次发送数据的完整和正确,如果有一次SOCKET出错,就返回错误
}
total_sended_buf += once_send;
}
return total_sended_buf;
}
int RecvString(SOCKET hSocket, char *& pData)
{
int package_num = 0; //一次传输包数量
unsigned int total_len = 0; //一次传输数据字节总数
unsigned int total_recved_buf = 0; //实际接收的字节数
MySocket package_data; //申请一个分包内存用于每次接收数据
memset(&package_data, 0, sizeof(MySocket));
/* 接收第一个数据包 */
int once_recv = _PackageRecv(hSocket, &package_data);
if (once_recv <= 0)
{
return once_recv;
}
if (package_data.index != 0) //如果第一个包序号不是0则丢弃该包,直到找到序号为0的第一个包重新开始一次传输
{
pData = NULL;
return 0;
}
//printf("total:%d---now:%d\n",package_data->total_package , package_data->index);
package_num = package_data.total_package;
total_len = package_data.total_len;
pData = (char *)malloc(total_len); //为一次传输总数据申请内存
if (pData == NULL)
{
return 0;
}
memset(pData, 0, total_len);
if (package_data.index == package_num - 1) //如果是最后一个分包拷贝实际包数据
{
memcpy(pData, package_data.data, total_len);
}
else //否则拷贝PACKSIZE字节数据
{
memcpy(pData, package_data.data, PACKSIZE);
}
total_recved_buf += once_recv;
/* 循环接收剩下的分包 */
for (int i = 1; i < package_num; i++)
{
memset(&package_data, 0, sizeof(MySocket));
int once_recv = _PackageRecv(hSocket, &package_data);
if (once_recv <= 0)
{
return once_recv;
}
if (package_data.total_package != package_num || package_data.index != i) //验证分包逻辑是否正确
{
return 0;
}
total_recved_buf += once_recv;
if (package_data.index == package_num - 1) //如果是最后一个分包拷贝实际包数据
{
memcpy(pData + i * PACKSIZE, package_data.data, total_len - i * PACKSIZE);
}
else //否则拷贝PACKSIZE字节数据
{
memcpy(pData + i * PACKSIZE, package_data.data, PACKSIZE);
}
}
return total_len;
}
int _PackageRecv(SOCKET hSocket, MySocket * pData)
{
int once_lanth = 0; //实际接收到的数据总字节数
int one_package_size_left = sizeof(MySocket); //等待接收的字节数
int one_package_size = sizeof(MySocket); //一个完整封包字节数
do{
int once_recv = recv(hSocket, (char *)(pData + once_lanth), one_package_size_left, 0);
if (once_recv <= 0)
{
return once_recv;
}
once_lanth += once_recv;
one_package_size_left = one_package_size - once_lanth;
} while (once_lanth < one_package_size); //采用循环接收的方式,直到一个封包的数据收满
return once_lanth;
}
int _PackageSend(SOCKET hSocket, MySocket * pData)
{
int once_lanth = 0; //实际发送的数据总字节数
int one_package_size_left = sizeof(MySocket); //等待发送的字节数
int one_package_size = sizeof(MySocket); //一个完整封包字节数
do{
int once_send = send(hSocket, (char *)(pData + once_lanth), one_package_size_left, 0);
if (once_send <= 0)
{
return once_send;
}
once_lanth += once_send;
one_package_size_left = one_package_size - once_lanth;
} while (once_lanth < one_package_size); //采用循环接收的方式,直到一个封包的数据收满
return once_lanth;
}
二、C++版
该版本对高速接受的数据使用了内存池缓冲,避免多次申请开辟内存用于接收数据带来的效率低下问题。
该版本有3个入口函数,SendString用法不变,接收数据需要RecvString和GetPoolData匹配使用,先用RecvString接收数据,如果返回大于0,则用GetPoolData将内存池数据取出,数据长度取决于RecvString返回值。
因使用内存池,该类在程序中建议实例化一次,全局使用。
// #
// # WinSocket TCP协议 分包组包算法 c++版本
// #
// # 文件名SocketCpp.h
// #
// # 作者: yiyefangzhou24
// # 修改时间: 2021/6/1
// #
// # 因使用内存池,建议一个程序将该类实例化为唯一全局变量
// #
// # 仅供交流学习使用,未经允许,请勿商用
// #
#include "MemoryPool.h"
#define PACKSIZE 2048 //定义默认单个包数据部分字节数
/* Socket数据包格式 */
typedef struct PackData
{
int total_package; //一次传输分包个数
int total_len; //一次传输总字节数
int index; //当前包序号
char data[PACKSIZE]; //数据内容
}PackData;
class MySocket
{
public:
/*
* 类初始化函数
* 参数[SOCKET] hSocket Socket句柄
*/
MySocket(SOCKET h);
~MySocket();
/*
* 自定义发送函数主入口
* 参数[char*] pData 待发送的数据指针
* 参数[int] len 待发送数据字节数
* 返回值[int] 实际发送数据字节数
*/
int SendString(char * pData, int len);
/*
* 自定义接收函数主入口,该函数会将接受的内容存放在内存池中,配套GetPoolData函数使用
* 返回值[int] 实际接收数据字节数
*/
int RecvString();
/*
* RecvString接收成功后,调用该函数读取内存池中数据
* 返回值[char*] 指向内存池数据指针
*/
char * GetPoolData();
private:
/*
* 此函数为内部函数,不对外提供调用接口,用于确保一次recv接收一个完整的socket_data分包
* 返回值[int] 实际接收数据字节数
*/
int _PackageRecv();
/*
* 此函数为内部函数,不对外提供调用接口,用于确保一次send接收一个完整的socket_data分包
* 返回值[int] 实际发送数据字节数
*/
int _PackageSend();
public:
MemPool mPool; //定义一个内存池
private:
PackData pPackData; //一个封包完整数据,只随类申请并初始化一次,避免多次申请和销毁内存
SOCKET hSocket; //SOCKET句柄,类初始化时赋值
};
#pragma comment(lib,"Ws2_32.lib")
#include <winsock2.h>
#include "SocketCpp.h"
MySocket::MySocket(SOCKET h)
{
hSocket = h;
mPool.PoolInit(); //初始化内存池
}
MySocket::~MySocket()
{
}
int MySocket::SendString(char * pData, int len)
{
unsigned int total_sended_buf = 0; //实际发送的字节数
int package_num = (len + PACKSIZE - 1) / PACKSIZE; //计算要分割为多少个包
for (int i = 0; i < package_num; i++) //依次发送分包
{
//printf("total:%d---now:%d\n",package_data->total_package , package_data->index);
memset(&pPackData, 0, sizeof(PackData)); //重新初始化数据包数据
pPackData.total_package = package_num; //一次传输包总数
pPackData.total_len = len; //一次传输总字节数
pPackData.index = i; //包序号
if (pPackData.index == package_num - 1) //如果是最后一个分包拷贝实际包数据
{
memcpy(pPackData.data, pData + i * PACKSIZE, pPackData.total_len - i * PACKSIZE);
}
else //否则拷贝PACKSIZE字节数据
{
memcpy(pPackData.data, pData + i * PACKSIZE, PACKSIZE); //包数据
}
int once_send = _PackageSend(); //发送数据
if (once_send <= 0)
{
return once_send; //为保证每次发送数据的完整和正确,如果有一次SOCKET出错,就返回错误
}
total_sended_buf += once_send;
}
return total_sended_buf;
}
int MySocket::RecvString()
{
int package_num = 0; //一次传输包数量
unsigned int total_len = 0; //一次传输数据字节总数
unsigned int total_recved_buf = 0; //实际接收的字节数
memset(&pPackData, 0, sizeof(PackData));
/* 接收第一个数据包 */
int once_recv = _PackageRecv();
if (once_recv <= 0)
{
return once_recv;
}
if (pPackData.index != 0) //如果第一个包序号不是0则丢弃该包,直到找到序号为0的第一个包重新开始一次传输
{
return 0;
}
//printf("total:%d---now:%d\n",package_data->total_package , package_data->index);
package_num = pPackData.total_package;
total_len = pPackData.total_len;
if (pPackData.index == package_num - 1) //如果是最后一个分包拷贝实际包数据
{
mPool.PutData((BYTE*)pPackData.data, total_len);
}
else //否则拷贝PACKSIZE字节数据
{
mPool.PutData((BYTE*)pPackData.data, PACKSIZE);
}
total_recved_buf += once_recv;
/* 循环接收剩下的分包 */
for (int i = 1; i < package_num; i++)
{
memset(&pPackData, 0, sizeof(PackData));
int once_recv = _PackageRecv();
if (once_recv <= 0)
{
return once_recv;
}
if (pPackData.total_package != package_num || pPackData.index != i) //验证分包逻辑是否正确
{
return 0;
}
total_recved_buf += once_recv;
if (pPackData.index == package_num - 1) //如果是最后一个分包拷贝实际包数据
{
mPool.AppendData((BYTE*)pPackData.data, total_len - i * PACKSIZE);
}
else //否则拷贝PACKSIZE字节数据
{
mPool.AppendData((BYTE*)pPackData.data, PACKSIZE);
}
}
return total_len;
}
char * MySocket::GetPoolData()
{
return (char*)mPool.GetDataHandle();
}
int MySocket::_PackageRecv()
{
int once_lanth = 0; //实际接收到的数据总字节数
int one_package_size_left = sizeof(PackData); //等待接收的字节数
int one_package_size = sizeof(PackData); //一个完整封包字节数
do{
int once_recv = recv(hSocket, (char *)(&pPackData + once_lanth), one_package_size_left, 0);
if (once_recv <= 0)
{
return once_recv;
}
once_lanth += once_recv;
one_package_size_left = one_package_size - once_lanth;
} while (once_lanth < one_package_size); //采用循环接收的方式,直到一个封包的数据收满
return once_lanth;
}
int MySocket::_PackageSend()
{
int once_lanth = 0; //实际发送的数据总字节数
int one_package_size_left = sizeof(PackData); //等待发送的字节数
int one_package_size = sizeof(PackData); //一个完整封包字节数
do{
int once_send = send(hSocket, (char *)(&pPackData + once_lanth), one_package_size_left, 0);
if (once_send <= 0)
{
return once_send;
}
once_lanth += once_send;
one_package_size_left = one_package_size - once_lanth;
} while (once_lanth < one_package_size); //采用循环接收的方式,直到一个封包的数据收满
return once_lanth;
}
以下为内存池代码:
// #
// # 内存池算法 c++版本
// #
// # 文件名MemoryPool.h
// #
// # 作者: yiyefangzhou24
// # 修改时间: 2021/6/1
// #
// # 仅供交流学习使用,未经允许,请勿商用
// #
#define MEMSIZE 1024 * 1024 //定义默认内存池大小为1M
class MemPool
{
public:
MemPool();
~MemPool();
/*
* 内存池初始化函数,使用该类时应当先调用该函数
* 返回值[bool] 初始化成功返回true,否则返回false
*/
bool PoolInit();
/*
* 从内存池开始地址,向内存池中覆盖存放数据
* 参数[BYTE *] 要写入内存池数据指针
* 参数[unsigned long] 要写入内存池数据字节数
* 返回值[unsigned long] 实际写入内存池中数据字节数
*/
unsigned long PutData(BYTE * data, unsigned long size);
/*
* 向内存池中添加数据
* 参数[BYTE *] 要写入内存池数据指针
* 参数[unsigned long] 要写入内存池数据字节数
* 返回值[unsigned long] 实际写入内存池中数据字节数
*/
unsigned long AppendData(BYTE * data, unsigned long size);
/*
* 获取内存池中所有数据指针
* 返回值[BYTE *] 初始化成功内存池数据指针
*/
BYTE * GetDataHandle();
/*
* 获得当前内存池大小
* 返回值[unsigned long] 当前内存池大小
*/
unsigned long GetPoolSize();
/*
* 获得当前内存池中数据大小
* 返回值[unsigned long] 当前内存池中数据大小
*/
unsigned long GetDataSize();
private:
/*
* 增加内存池大小
* 参数[unsigned long] 新的内存池大小
* 返回值[bool] 初始化成功返回true,否则返回false
*/
bool AddPoolSize(unsigned long size);
private:
BYTE * pPoolData; //内存池中的数据
unsigned long PoolSize; //内存池总大小
unsigned long DataSize; //内存池中数据大小
};
#include <windows.h>
#include "MemoryPool.h"
MemPool::MemPool()
{
PoolSize = MEMSIZE;
DataSize = 0;
}
MemPool::~MemPool()
{
if (pPoolData)
free(pPoolData);
}
bool MemPool::PoolInit()
{
pPoolData = (BYTE *)malloc(PoolSize);
if (!pPoolData)
{
pPoolData = NULL;
return false;
}
memset(pPoolData, 0, PoolSize);
return true;
}
bool MemPool::AddPoolSize(unsigned long size)
{
pPoolData = (BYTE *)realloc(pPoolData, size);
if (!pPoolData)
{
pPoolData = NULL;
return false;
}
memset(pPoolData + PoolSize, 0, size - PoolSize);
PoolSize = size;
return true;
}
unsigned long MemPool::PutData(BYTE * data, unsigned long size)
{
if (size > PoolSize) //如果数据比现有内存池大,先扩大内存池
{
if (!AddPoolSize(size))
{
return 0;
}
PoolSize = size;
}
memcpy(pPoolData, data, size);
DataSize = size;
return size;
}
unsigned long MemPool::AppendData(BYTE * data, unsigned long size)
{
if (size + DataSize > PoolSize) //如果内存池大小不够,先扩大内存池
{
if (!AddPoolSize(size + DataSize))
{
return 0;
}
PoolSize = size + DataSize;
}
memcpy(pPoolData + DataSize, data, size);
DataSize = size + DataSize;
return size;
}
BYTE * MemPool::GetDataHandle()
{
return pPoolData;
}
unsigned long MemPool::GetPoolSize()
{
return PoolSize;
}
unsigned long MemPool::GetDataSize()
{
return DataSize;
}