【算法分享】基于WinSocket的TCP分包组包传输算法

笔者不再赘述分包组包算法是什么和为什么了,直接上代码,注释已经写的非常完整了,也希望大家测试使用,并提出好的建议。

凡是有关提交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;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值