C++ TCP 客户端发送大量数据,服务器来不及接收【一次详细的测试记录及分析】

如果客户端发送数据的速度远远大于服务器接收数据的速度,那么-->

客户端程序: 

#include <winsock.h> 
#include<iostream>
#pragma comment(lib,"ws2_32.lib") 

#include <Windows.h>
#include<string>

int main()
{
	//初始化Windows Socket Application
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsaData;

	//WinSock的注册函数,初始化底层的Windows Sockets DLL
	if (WSAStartup(sockVersion, &wsaData) != 0)
		return 0;

	//创建一个socket并返回socket的标识符
	SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sclient == INVALID_SOCKET)
	{
		std::cout << "invalid socket!" << std::endl;
		return 0;
	}
	sockaddr_in serAddr;
	serAddr.sin_family = AF_INET;
	serAddr.sin_port = htons(8888);
	serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
	{  			//连接失败 	
		std::cout << "connect error !" << std::endl;
		closesocket(sclient);
		return 0;
	}

	std::string data="abcdef_";

	char* chs = new char[1000000];
	for (int i = 0; i < 1000000; ++i)
		chs[i] = i;

	int n = 0;
	while (1)
	{
		//data = "abcdef_"+std::to_string(n);
		//std::cin >> data;
		const char * sendData=chs;
		//sendData = data.c_str();
		//string转const char* 		
		/*		send()用来将数据由指定的socket传给对方主机
		int send(int s, const void * msg, int len, unsigned int flags)
		s为已建立好连接的socket,msg指向数据内容,len则为数据长度,参数flags一般设0
		成功则返回实际传送出去的字符数,失败返回-1,错误原因存于error		*/

//这么干肯定会干爆的吧?
		send(sclient, sendData, 1000000, 0);//一次发送得多,接收得少

		//接收返回的数据
		char recData[255];
		int ret = recv(sclient, recData, 255, 0);//阻塞在此处等待接收
		if (ret > 0)
		{
			recData[ret] = 0x00;
			std::cout <<"第"<<n<<"回:"<< recData << std::endl;
		}

		if (n++>20)
		{
			break;
		}

		Sleep(1000);
	}

	delete[] chs;
	//关闭
	closesocket(sclient);

	WSACleanup();
	system("pause");
	return 0;
}

服务器端程序:

#include <winsock.h> 
#include<iostream>
#pragma comment(lib,"ws2_32.lib") 
#include <Windows.h>
#include <string>


DWORD WINAPI ThreadTCP(LPVOID pParam);

int main()
{
	//初始化Windows Socket Application
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsaData;

	//WinSock的注册函数,初始化底层的Windows Sockets DLL
	if (WSAStartup(sockVersion, &wsaData) != 0)
		return 0;

	//创建套接字 
	SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (slisten == INVALID_SOCKET)
	{
		std::cout << "create socket error!" << std::endl;
		return 0;
	}

	//绑定IP和端口  	
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	//
	sin.sin_port = htons(8888);//指定端口,将端口号转换为网络字节顺序
	sin.sin_addr.S_un.S_addr = INADDR_ANY;

	//bind()把socket绑定到特定的网络地址上
	if (bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		std::cout << "bind error!" << std::endl;
		return 0;
	}

	//开始监听
//启动指定的socket,监听到来的连接请求  
5指定了监听socket的等待连接缓冲区队列的最大长度,一般设为5	
	if (listen(slisten, 5) == SOCKET_ERROR)
	{
		std::cout << "listen error !" << std::endl;
		return 0;
	}

	//循环接收数据 
	SOCKET sClient;
	sockaddr_in remoteAddr;
	int nAddrlen = sizeof(remoteAddr);
	

	while (true)
	{
		std::cout << "阻塞。。。。等待连接。。。" << std::endl;
		//接收一个连接请求,并新建一个socket,原来的socket返回监听状态
		sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);
		if (sClient == INVALID_SOCKET)
		{
			std::cout << "accept error !" << std::endl;
			continue;
		}

		//inet_addr()把一个标准的点分十进制的IP地址转换成长整型的地址数据
		//inet_ntoa()把长整型的IP地址数据转换成点分十进制的ASCII字符串	
		std::cout << "接受一个连接:" << inet_ntoa(remoteAddr.sin_addr)<<":"<<remoteAddr.sin_port << std::endl;


		//创建一个线程与客户端进行会话
		CreateThread(NULL, 0, ThreadTCP, (LPVOID)sClient, 0, NULL);

	}

	closesocket(slisten);

	//winsock的注销函数,从底层的Windows Sockets DLL 中撤销注册
	WSACleanup();
	system("pause");
	return 0;
}

DWORD WINAPI ThreadTCP(LPVOID pParam)
{
	SOCKET sClient = (SOCKET)pParam;

	char revData[255];
	std::string data = "hello_";
	int n = 0;
	while (1)
	{
		data = "hello_"+std::to_string(n);
		//接收数据  
		//从一个已经与对方建立连接的socket接收数据	

		//快速发送与慢接收-->导致接收端满
		int ret = recv(sClient, revData, 255, 0);//阻塞于此进行接收
		if (ret > 0)
		{
			revData[ret] = 0x00;
			std::cout << "第" << n++ << "回:" << revData << std::endl;
			//printf(revData);
		}

		//发送数据  	
		const char * sendData = data.c_str();

		向一个已经与对方建立连接的socket发送数据	
		int nb=send(sClient, sendData, strlen(sendData), 0);
		if (nb != strlen(sendData))
		{
			break;
		}
		//Sleep(1000);
	}


	//断开连接
	std::cout << "断开连接" << std::endl;
	//关闭socket,释放相应的资源
	closesocket(sClient);
	return 0;
}

 wireshark抓包的显示:

在第23个数据包的时候,服务器端8888就告诉客户端5147自己不行了,然后就断开了

下面来分析具体过程:

首先前3个数据包是在建立TCP会话

然后在第4个数据包,客户端5147发送了65539字节的数据(包含44个字节的帧头,和65495字节数据),

但是程序中要求发送的是1000000个字节:send(sclient, sendData, 1000000, 0);注意数据包4的Data(65495bytes)

然后8888确认了接收【数据包5】

紧接着5147继续发送数据【数据包6】--【数据包16】,注意其数据的序号Seq从1->65496->130991->196486->....->720446

依次相差每次发送的数据量65495;

在第17个数据包,8888确认了接收,其确认号Ack为785941(即8888已经接收到785940字节,接下来应该接收785941字节了)

接下来5147继续发送数据【数据包18】--【数据包20】 ,数据序号Seq从785941->851436->916931,即982426之前的数据都已经发送了;离1000000数据还剩下一些,在第21个数据包继续发送

然后8888确认了接收【数据包22】其确认号Ack=1000001(我已经接收到1000000字节了,你该发送1000001啦!),

 

不过,控制台已经关闭了,程序崩了。。。

第23个数据包和第22个数据包中间隔了近5秒,8888已经关闭了会话

//后续

找到服务器控制台崩溃的原因了,应该是在定义char revData[255]; 这里的问题,应该改成比255大的数,比如256;

原因在于后面干了这么一件事情:revData[ret] = 0x00;

要防止索引数组溢出嘛!

 

》》》》》》》》》》》》》》》 》》》》》》》》》

重做实验

客户端程序:

#include <winsock.h> 
#include<iostream>
#pragma comment(lib,"ws2_32.lib") 

#include <Windows.h>
#include<string>

int main()
{
	//初始化Windows Socket Application
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsaData;

	//WinSock的注册函数,初始化底层的Windows Sockets DLL
	if (WSAStartup(sockVersion, &wsaData) != 0)
		return 0;

	//创建一个socket并返回socket的标识符
	SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sclient == INVALID_SOCKET)
	{
		std::cout << "invalid socket!" << std::endl;
		return 0;
	}
	sockaddr_in serAddr;
	serAddr.sin_family = AF_INET;
	serAddr.sin_port = htons(8888);
	serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
	{  			//连接失败 	
		std::cout << "connect error !" << std::endl;
		closesocket(sclient);
		return 0;
	}

	std::string data="abcdef_";

	char* chs = new char[1000000];
	for (int i = 0; i < 1000000; ++i)
		chs[i] = i;
	const char * sendData = chs;

	char recData[255];
	int n = 0;
	while (1)
	{
		//data = "abcdef_"+std::to_string(n);
		//std::cin >> data;
		
		//sendData = data.c_str();
		//string转const char* 		
		/*		send()用来将数据由指定的socket传给对方主机
		int send(int s, const void * msg, int len, unsigned int flags)
		s为已建立好连接的socket,msg指向数据内容,len则为数据长度,参数flags一般设0
		成功则返回实际传送出去的字符数,失败返回-1,错误原因存于error		*/

		send(sclient, sendData, 1000000, 0);//一次发送多,接收少

		//接收返回的数据
		
		//int ret = recv(sclient, recData, 255, 0);//阻塞在此处等待接收
		//if (ret > 0)
		//{
		//	recData[ret] = 0x00;
		//	std::cout <<"第"<<n<<"回:"<< recData << std::endl;
		//}

		if (n++>10)
		{
			break;
		}

		Sleep(1000);
	}

	delete[] chs;
	//关闭
	closesocket(sclient);

	WSACleanup();
	system("pause");
	return 0;
}

服务器端程序:

#include <winsock.h> 
#include<iostream>
#pragma comment(lib,"ws2_32.lib") 
#include <Windows.h>
#include <string>


DWORD WINAPI ThreadTCP(LPVOID pParam);

int main()
{
	//初始化Windows Socket Application
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsaData;

	//WinSock的注册函数,初始化底层的Windows Sockets DLL
	if (WSAStartup(sockVersion, &wsaData) != 0)
		return 0;

	//创建套接字 
	SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (slisten == INVALID_SOCKET)
	{
		std::cout << "create socket error!" << std::endl;
		return 0;
	}

	//绑定IP和端口  	
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	//
	sin.sin_port = htons(8888);//指定端口,将端口号转换为网络字节顺序
	sin.sin_addr.S_un.S_addr = INADDR_ANY;

	//bind()把socket绑定到特定的网络地址上
	if (bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		std::cout << "bind error!" << std::endl;
		return 0;
	}

	//开始监听
//启动指定的socket,监听到来的连接请求  
5指定了监听socket的等待连接缓冲区队列的最大长度,一般设为5	
	if (listen(slisten, 5) == SOCKET_ERROR)
	{
		std::cout << "listen error !" << std::endl;
		return 0;
	}

	//循环接收数据 
	SOCKET sClient;
	sockaddr_in remoteAddr;
	int nAddrlen = sizeof(remoteAddr);
	

	while (true)
	{
		std::cout << "阻塞。。。。等待连接。。。" << std::endl;
		//接收一个连接请求,并新建一个socket,原来的socket返回监听状态
		sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);
		if (sClient == INVALID_SOCKET)
		{
			std::cout << "accept error !" << std::endl;
			continue;
		}

		//inet_addr()把一个标准的点分十进制的IP地址转换成长整型的地址数据
		//inet_ntoa()把长整型的IP地址数据转换成点分十进制的ASCII字符串	
		std::cout << "接受一个连接:" << inet_ntoa(remoteAddr.sin_addr)<<":"<<remoteAddr.sin_port << std::endl;


		//创建一个线程与客户端进行会话
		CreateThread(NULL, 0, ThreadTCP, (LPVOID)sClient, 0, NULL);

	}

	closesocket(slisten);

	//winsock的注销函数,从底层的Windows Sockets DLL 中撤销注册
	WSACleanup();
	system("pause");
	return 0;
}

DWORD WINAPI ThreadTCP(LPVOID pParam)
{
	SOCKET sClient = (SOCKET)pParam;

	//char* revData=new char[100000];
	char revData[1000];
	std::string data = "hello_";
	int n = 0;
	while (1)
	{
		data = "hello_"+std::to_string(n);
		//接收数据  
		//从一个已经与对方建立连接的socket接收数据	
		
		//快速发送与慢接收-->导致接收端满
		int ret = recv(sClient, revData, 100, 0);//阻塞于此进行接收
		if (ret > 0)
		{
			revData[ret] = 0x00;
			std::cout << "第" << n++ << "回:" << revData << std::endl;
			//printf(revData);
		}

		//发送数据  	
		//const char * sendData = data.c_str();

		//向一个已经与对方建立连接的socket发送数据	
		//int nb=send(sClient, sendData, strlen(sendData), 0);
		//if (nb != strlen(sendData))
		//{
		//	break;
		//}

		if (n++ > 100)
			break;
		Sleep(1000);
	}


	//断开连接
	std::cout << "断开连接" << std::endl;
	//关闭socket,释放相应的资源
	closesocket(sClient);
	return 0;
}

 

下面进行详细分析:

前3个数据包建立TCP连接,然后客户端程序发送send(1000000)个字节

第4个数据包 客户端6324发送发送65495个字节,8888返回一个确认,然后第6个数据包到第16个数据包,每次发送65495个字节,第17个数据包是8888的确认,第18个数据包到第21个数据包,完成了1000000个字节的发送

此时客户端程序Sleep(1000)

而服务器程序则recv(100)个字节,并显示100个字节的内容到控制台,然后它也Sleep(1000),所以其接收缓存区还得有(1000000-100)字节待取出

客户端程序Sleep完毕,再次send(1000000)个字节

从第23个数据包直到第38个数据包,6324发送完1000000个字节

 

服务器端从Sleep醒来,接收recv(100)个字节后,接收缓存区有(2000000-200)个字节,它又Sleep(1000)

客户端程序Sleep醒来,继续发送1000000个字节,从第40个数据包到第50个数据包,每个数据包65495个字节

第51个数据包8888返回一个确认告知:它已经接收到27720445字节,准备接收27720446字节,但是Win=30208,

第52个数据包告知:[TCP Window Full]

第53个数据包8888告知:[TCP ZeroWindow] Win=0了

第54个数据包6324发送了一个零窗口探测? [TCP ZeroWIndowProbe]其中仅仅包含1个字节的数据

 

通信在第68个数据包结束了,8888->6324[RST,ACK]

 

下面给出一个更好的实验样例:

客户端程序:

#include <winsock.h> 
#include<iostream>
#pragma comment(lib,"ws2_32.lib") 

#include <Windows.h>
#include<string>

int main()
{
	//初始化Windows Socket Application
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsaData;

	//WinSock的注册函数,初始化底层的Windows Sockets DLL
	if (WSAStartup(sockVersion, &wsaData) != 0)
		return 0;

	//创建一个socket并返回socket的标识符
	SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sclient == INVALID_SOCKET)
	{
		std::cout << "invalid socket!" << std::endl;
		return 0;
	}
	sockaddr_in serAddr;
	serAddr.sin_family = AF_INET;
	serAddr.sin_port = htons(8888);
	serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
	{  			//连接失败 	
		std::cout << "connect error !" << std::endl;
		closesocket(sclient);
		return 0;
	}

	std::string data="abcdef_";

	char* chs = new char[1000000];
	for (int i = 0; i < 1000000; ++i)
		chs[i] = i;
	const char * sendData = chs;

	char recData[255];
	int n = 0;
	while (1)
	{
		//data = "abcdef_"+std::to_string(n);
		//std::cin >> data;
		
		//sendData = data.c_str();
		//string转const char* 		
		/*		send()用来将数据由指定的socket传给对方主机
		int send(int s, const void * msg, int len, unsigned int flags)
		s为已建立好连接的socket,msg指向数据内容,len则为数据长度,参数flags一般设0
		成功则返回实际传送出去的字符数,失败返回-1,错误原因存于error		*/
		std::cout << "第" << n << "个发送" << std::endl;
		int sn=send(sclient, sendData, 1000000, 0);//一次发送多,接收少
		if (sn <= 0)
			std::cout << "数据发送失败" << std::endl;
		//接收返回的数据
		
		//int ret = recv(sclient, recData, 255, 0);//阻塞在此处等待接收
		//if (ret > 0)
		//{
		//	recData[ret] = 0x00;
		//	std::cout <<"第"<<n<<"回:"<< recData << std::endl;
		//}

		if (n++>20)
		{
			break;
		}

		Sleep(500);
	}

	delete[] chs;
	//关闭
	closesocket(sclient);

	WSACleanup();
	system("pause");
	return 0;
}

 服务器端程序:

#include <winsock.h> 
#include<iostream>
#pragma comment(lib,"ws2_32.lib") 
#include <Windows.h>
#include <string>

//采用select实现多个连接同时处理

int main()
{
	
	//初始化Windows Socket Application
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsaData;

	//WinSock的注册函数,初始化底层的Windows Sockets DLL
	if (WSAStartup(sockVersion, &wsaData) != 0)
		return 0;

	//创建服务器端监听套接字 
	SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (slisten == INVALID_SOCKET)
	{
		std::cout << "create socket error!" << std::endl;
		return 0;
	}

	//绑定IP和端口  	
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	//
	sin.sin_port = htons(8888);//指定端口,将端口号转换为网络字节顺序
	sin.sin_addr.S_un.S_addr = INADDR_ANY;

	//bind()把socket绑定到特定的网络地址上
	if (bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		std::cout << "bind error!" << std::endl;
		return 0;
	}

	//开始监听
	//启动指定的socket,监听到来的连接请求  
	//5指定了监听socket的等待连接缓冲区队列的最大长度,一般设为5	
	if (listen(slisten, 5) == SOCKET_ERROR)
	{
		std::cout << "listen error !" << std::endl;
		return 0;
	}

	std::cout << "服务器开启成功" << std::endl;

	FD_SET  read_set;//创建文件描述集合
	FD_ZERO(&read_set);//清空文件描述集合
	FD_SET(slisten, &read_set);//将监听套接字放入文件描述集合
	while (true)
	{
		FD_SET settmp;
		FD_ZERO(&settmp);
		settmp = read_set;

		//检查文件描述集合中的socket状态是否就绪
		int total = select(0, &settmp, nullptr, nullptr, nullptr);
		if (total == SOCKET_ERROR)
		{
			continue;
		}

		for (int i = 0; i < settmp.fd_count; i++)
		{
			SOCKET ss = settmp.fd_array[i];
			if (ss == slisten)//监听套接字有就绪
			{
				循环接收数据 
				SOCKET sClient;
				sockaddr_in remoteAddr;
				int nAddrlen = sizeof(remoteAddr);
				//接收一个连接请求,并新建一个socket,原来的socket返回监听状态
				sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);
				if (sClient == INVALID_SOCKET)
				{
					std::cout << "accept error !" << std::endl;
					continue;
				}
				//inet_addr()把一个标准的点分十进制的IP地址转换成长整型的地址数据
				//inet_ntoa()把长整型的IP地址数据转换成点分十进制的ASCII字符串	
				std::cout << "接受一个连接:" << inet_ntoa(remoteAddr.sin_addr) << ":" << remoteAddr.sin_port << std::endl;
				FD_SET(sClient, &read_set);
				
			}
			else
			{
				//客户端连接的socket
				char msg[10000];
				int ret=recv(ss, msg, 10000, 0);//接收数据
				if (ret == SOCKET_ERROR || ret == 0)
				{
					closesocket(ss);
					FD_CLR(ss, &read_set);
					std::cout << "结束一个连接" << std::endl;
				}
				else
				{
					std::cout << "接收到数据" << std::endl;
				}
				
			}


		}
		Sleep(100);

	}

	closesocket(slisten);

	//winsock的注销函数,从底层的Windows Sockets DLL 中撤销注册
	WSACleanup();
	system("pause");
	return 0;
}

提要:客户端每次发送100w个字节向服务器,然后暂停500ms,继续;

服务器端每次取出1w个字节,然后暂停100ms,显然服务器端是接收不及的;

当服务器端的接收缓存被充满之后,客户端的send()被阻塞,不再继续往客户端的发送缓存写入;

由此观察到的控制台显示情况是:客户端在显示了第0个发送、第1个发送、第2个发送、第3个发送后,就暂停了;而服务器端的控制台是飞快的显示接收到数据,其中每隔100ms稍微暂停一下,

过一会儿,客户端控制台则又显示第4个发送、第5个发送,然后就继续暂停(程序阻塞在send()位置);

整个发送和接收的过程持续时间在这里最终取决于服务器端接收的时间--客户端总共发送21*100w个字节;而服务器端接收的速率为1w字节/100ms,总的接收时间为(21*100w)/(10w/s)=210s时间,加上程序Sleep的一些时间,大概在230s左右

 

根据wireshark抓包的情况,也能与上面的分析匹配:

 

 

 

 

注意客户端每个数据包发送的时间,只有在客户端套接字探测到服务器接收缓存至少有100w字节的空余时,它才会send()一次【这里是瞎说的】;注意整个发送和接收过程中,服务器端的接收缓存处于取出和收入的动态竞争过程,在它刚好窗口不够的时候,它发出什么样的数据包,而在取出数据窗口变大时又发出什么样的数据包

 

  • 3
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值