项目(百万并发网络通信架构)7.1---客户端与服务端的粘包现象演示(网络缓冲区)

  • 客户端代码和服务端代码都使用前面文章所封装的class
  • 本文先对缓冲区做各种测试,文章最后会介绍粘包现象。接下来的几篇文章中会介绍客户端与服务端如何处理这种粘包现象

一、测试1:循环发送与接收少量数据(8kb)

  • 测试1的过程为:
    • 服务端:服务端代码基本不变,其接收客户端发送过来的数据,在收到数据之后将相应数据回送给客户端
    • 客户端:客户端使用while()循环一直向服务端发送程序
    • 客户端与服务端的每次交互的单个数据包比较小,只有8字节

服务端与客户端代码

  • 服务端代码如下:
#include "EasyTcpServer.hpp"
#include "MessageHeader.hpp"

int main()
{

	EasyTcpServer server1;
	server1.Bind("192.168.0.105", 4567);//IP地址在内外网测试时更改
	server1.Listen(5);

	while (server1.isRun())
	{
		server1.Onrun();
		//std::cout << "空闲时间,处理其他业务..." << std::endl;
	}

	server1.CloseSocket();
	std::cout << "服务端停止工作!" << std::endl;

	getchar();  //防止程序一闪而过
	return 0;
}
  • 客户端代码如下:
#include "EasyTcpClient.hpp"

int main()
{
	EasyTcpClient client1;
	client1.ConnectServer("192.168.0.105", 4567);//IP地址在内外网测试时更改

	Login login;
	strcpy(login.userName, "dongshao");
	strcpy(login.PassWord, "123456");
    //一直发送Login类型的报文,之后服务端会给自己回送LoginResult类型的报文
	while (client1.isRun())
	{
		client1.Onrun();
		client1.SendData(&login);
	}

	client1.CloseSocket();
	std::cout << "客户端停止工作!" << std::endl;
	getchar();  //防止程序一闪而过
	return 0;
}

内网测试结果

  • ①先测试服务端和客户端都属于同一系统中的测试,结果如下:
    • 左侧为服务端(Windows系统),测试显示一直接收客户端发送的数据并回送数据
    • 右侧为客户端(Windows系统),测试显示一直向服务端发送程序并且接收到服务端回送的数据

  • 通过查看资源管理器可以看到客户端与服务端的发送与接收速率相对稳定(并且两者的发送与接收都是对称的,并且总数都相同)。网络中的数据传输带宽大概有3Mbps左右

  • ②再测试服务端与客户端不属于同一系统中的测试,结果如下:
    • 左侧为服务端(ubuntu系统),测试显示一直接收客户端发送的数据并回送数据
    • 右侧为客户端(Windows系统),测试显示一直向服务端发送程序并且接收到服务端回送的数据

外网测试结果

  • 此处我个人有一个远程云服务器(IP为111.229.177.161),测试结果如下:
    • 左侧为服务端(ubuntu系统),右侧为客户端(Windows系统)
    • 当运行几秒后,服务端程序崩溃

二、测试2:循环发送与接收大量数据(1032kb)

  • 测试1的过程为:
    • 服务端:服务端代码基本不变,其接收客户端发送过来的数据,在收到数据之后将相应数据回送给客户端
    • 客户端:客户端使用while()循环一直向服务端发送程序
    • 我们更改了服务端给客户端回送数据的报文(LogoutResult)的大小,更改为了1032KB

服务端与客户端代码

  • 需要更改一下MessageHeader.hpp头文件中LogoutResult报文的大小,更改如下,为其添加了一个1024kb的data成员
//其余代码同之前
struct DataHeader
{
	short cmd;
	short dataLength;
};

struct LogoutResult :public DataHeader
{
	LogoutResult() :result(0) {
		cmd = CMD_LOGOUT_RESULT;
		dataLength = sizeof(LogoutResult);
	}
	int result;
	char data[1024]; //添加的,为了测试大量数据的传输
};
  • 服务端代码如下:
#include "EasyTcpServer.hpp"
#include "MessageHeader.hpp"

int main()
{

	EasyTcpServer server1;
	server1.Bind("192.168.0.105", 4567);//IP地址在内外网测试时更改
	server1.Listen(5);

	while (server1.isRun())
	{
		server1.Onrun();
		//std::cout << "空闲时间,处理其他业务..." << std::endl;
	}

	server1.CloseSocket();
	std::cout << "服务端停止工作!" << std::endl;

	getchar();  //防止程序一闪而过
	return 0;
}
  • 客户端代码如下:
#include "EasyTcpClient.hpp"
#include <thread>

int main()
{
	EasyTcpClient client1;
	client1.ConnectServer("192.168.0.105", 4567);//IP地址在内外网测试时更改

	Login login;
	strcpy(login.userName, "dongshao");
	strcpy(login.PassWord, "123456");
    //while循环中不断的向服务端发送数据
	while (client1.isRun())
	{
		client1.Onrun();
		client1.SendData(&login);
	}

	client1.CloseSocket();
	std::cout << "客户端停止工作!" << std::endl;
	getchar();  //防止程序一闪而过
	return 0;
}

内网测试结果

  • ①先测试服务端和客户端都属于同一系统中的测试,结果如下:
    • 左侧为服务端(Windows系统),右侧为客户端(Windows系统)
    • 测试时,客户端与服务端运行几秒之后,服务端和客户端程序直接卡死

  • ②再测试服务端与客户端不属于同一系统中的测试,结果如下:
    • 左侧为服务端(ubuntu系统),右侧为客户端(Windows系统)
    • 测试时,客户端与服务端运行几秒之后,服务端程序卡死,客户端抛出异常

外网测试结果

  • 此处我个人有一个远程云服务器(IP为111.229.177.161),测试结果如下:
    • 左侧为服务端(ubuntu系统),右侧为客户端(Windows系统)
    • 测试时,客户端与服务端运行几秒之后,服务端终止,客户端抛出异常

三、网络缓冲区的概念

  • 在不同的操作系统中,系统为网络程序设置了数据的接收缓冲区和发送缓冲区,用来存储接收的网络数据/发送的网络数据
  • 双方进行通信的基本流程为:发送端将发送的数据先发送到发送缓冲区中,之后数据通过网络传输层传输到接收端的接收缓冲区中,之后接收端从接收缓冲区中接收数据。这就是通信双薪进行数据传输的基本流程

  • 注意事项:
    • 这个缓冲区是由操作系统底层的网络通信栈所决定的,程序一般不能更改,但是可以通过一些方法(套接字选项、设置配置文件等)来更改
    • 不同操作系统间的缓冲区大小可能都不相同
  • 有了缓冲区的概念之后,我们来解析上面的程序所表现的情况的原因:
    • 在交互少量数据(8kb)时:
      • 内网测试:交互少量的数据,发送端的发送速率虽然过快,但是数据量比较少,发送端的发送缓冲区和接收端的接受缓冲区都足够用,数据没有发生溢出的情况,因此程序运行都正确
      • 外网测试:因为与外网测试,可能由于网络的原因,发送端的发送速率过快,但是接收端的接受速率慢于发送端的发送速率,导致发送端的发送缓冲区和接收端的接收缓冲区溢出,程序无法正确执行,因此发生错误
    • 在交互大量数据(1032kb):
      • 内网测试:交互大量数据,原理与上面的外网测试一样,发送端的发送缓冲区和接收端的接收缓冲区溢出,程序无法正确执行,因此发生错误
      • 外网测试:同上,发送端的发送缓冲区和接收端的接收缓冲区溢出

四、自定义程序缓冲区的概念

  • 网络缓冲区我们一般不去设置,我们通过在程序中自定义一个缓冲区,不论网络中有多少数据发送过来,我们直接使用recv()函数将网络缓冲区中的数据接收到程序所设置的自定义缓冲区中,这样就不会导致接收缓冲区的溢出了,程序也就可以发送正常

五、交互大量数据:为客户端设置程序接收缓冲区,但服务端不设置(程序卡死)

服务端与客户端代码

  • 需要更改一下MessageHeader.hpp头文件中LogoutResult报文的大小,更改如下,为其添加了一个1024kb的data成员
struct LoginResult :public DataHeader
{
	LoginResult() :result(0) {
		cmd = CMD_LOGIN_RESULT;
		dataLength = sizeof(LoginResult);
	}
	int result;
	char data[1024];  //为了测试发送大量数据而添加的
};
  • 服务端的代码不变,没有为其设置接收缓冲区
  • 客户端的代码如下:
  • 然后客户端在主程序中向服务端不断的发送Login类型的报文,之后服务端会给客户端回送LoginResult类型的报文(1032字节的)
#include "EasyTcpClient.hpp"

int main()
{
	EasyTcpClient client1;
	client1.ConnectServer("111.229.177.161", 4567);

	Login login;
	strcpy(login.userName, "dongshao");
	strcpy(login.PassWord, "123456");

	while (client1.isRun())
	{
		client1.Onrun();
		client1.SendData(&login); //一直发送Login类型的报文,之后服务端会给自己回送LoginResult类型的报文
	}

	client1.CloseSocket();
	std::cout << "客户端停止工作!" << std::endl;
	getchar();  //防止程序一闪而过
	return 0;
}
  • 另外,在接收数据时,我们在应用层为客户端设置一个接收缓冲区,大小为409600KB。然后重新更改RecvData()函数,之前的RecvData()函数是先接收头部部分大小的数据,再去接收报文实体部分的数据,这样肯定会导致缓冲区的溢出,现在我们让其每次接收数据时,直接从网络接收缓冲区中最大可以接收409600KB大小的数据
//其余省略同之前一样
class EasyTcpClient
{
private:
	#define RECV_BUFF_SIZE 409600
	char _recvBuff[RECV_BUFF_SIZE ]; //接收缓冲区
}

int EasyTcpClient::RecvData()
{
	//直接最大可以接收409600字节,并且打印每次从网络缓冲区中接收的数据大小
	int _nLen = recv(_sock, _recvBuff, 409600, 0);
	if (_nLen < 0) {
		std::cout << "<Socket=" << _sock << ">:recv函数出错!" << std::endl;
		return -1;
	}
	else if (_nLen == 0) {
		std::cout << "<Socket=" << _sock << ">:接收数据失败,服务端已关闭!" << std::endl;
		return -1;
	}
	std::cout << "_nLen=" << _nLen << std::endl;
	return 0;
}

内网测试

  • ①先测试服务端和客户端都属于同一系统中的测试,结果如下:
    • 左侧为服务端(Windows系统),右侧为客户端(Windows系统)
    • 测试时,客户端与服务端运行几秒之后,服务端和客户端程序直接卡死

  • ②再测试服务端与客户端不属于同一系统中的测试,结果如下:
    • 左侧为服务端(ubuntu系统),右侧为客户端(Windows系统)。此处显得比较特殊,可能是ubuntu系统的网络接收缓冲区比较大(此处运行两个客户端仍然可以工作正常,随着客户端数量的增加,接收与发送速率变慢)

  • 但是当我们退出一个客户端之后,服务器直接崩溃退出。这里我猜想的原因是:
    • 一开始有两个客户端与服务端进行交互,服务端的网络接收缓冲区与网络发送缓冲区没有溢出,工作都正常
    • 当退出一个客户端之后,服务端将回送给客户端的数据先放入网络发送缓冲区时,网络发送缓冲区溢出了(因为客户端减少了,从服务端网络发送缓冲区中接收的数据变少了),因此导致程序崩溃,服务端退出

外网测试

  • 此处我个人有一个远程云服务器(IP为111.229.177.161),测试结果如下:
    • 左侧为服务端(ubuntu系统),右侧为客户端(Windows系统),与上面一样,服务端缓冲区溢出,程序崩溃

  • 总结上面的原因,可以用下面的一张图表示:
    • 客户端设置了程序接收缓冲区(409600KB),但是服务端没有设置程序接收缓冲区。导致服务端的网络接收缓冲区发送溢出(接收的数据满了),网络发生阻塞,程序出错

  • 但是有一个例外:
    • 上面的内网测试中,服务端为ubuntu系统时,虽然没有为其设置程序接收缓冲区,但是其网络接收缓冲区足够大,可以接收客户端的数据
    • 当然,这种现象中的网络缓冲区的大小根据系统而定的,可能到别的系统上又不足够用了

六、交互大量数据:为客户端和服务端都设置程序接收缓冲区(数据交互正常)

服务端与客户端代码

  • 客户端代码与“五”中的一样,不需要更改
  • 服务端的代码EasyTcpServer.hpp需要更改一下,为其添加一个接受数据缓冲区。并且更改RecvData()函数,之前的RecvData()函数是先接收头部部分大小的数据,再去接收报文实体部分的数据,这样肯定会导致缓冲区的溢出,现在我们让其每次接收数据时,直接从网络接收缓冲区中最大可以接收409600KB大小的数据。并且由于测试中客户端只发送Login类型的数据,因此在接收数据之后,直接使用SendData()函数给客户端回送一个LoginResult类型的报文
class EasyTcpServer
{
private:
	#define RECV_BUFF_SIZE 409600
	char _recvBuff[RECV_BUFF_SIZE ]; //接收缓冲区
};

int EasyTcpServer::RecvData(SOCKET _cSock)
{
	int _nLen = recv(_cSock, _recvBuff, 409600, 0);
	if (_nLen < 0) {
		std::cout << "recv函数出错!" << std::endl;
		return -1;
	}
	else if (_nLen == 0) {
		std::cout << "客户端<Socket=" << _cSock << ">:已退出!" << std::endl;
		return -1;
	}
	std::cout << "_nLen=" << _nLen << std::endl;
	LoginResult ret;
	SendData(_cSock, &ret);

	return 0;
}

内网测试

  • ①先测试服务端和客户端都属于同一系统中的测试,结果如下:
    • 左侧为服务端(Windows系统),右侧为客户端(Windows系统),测试显示正常

  • 查看资源监视器可以看到两个客户端与服务端之间交互的数据量的大小,并且可以看到两个客户端与服务端交互时,网络带宽接近于300Mbps

  • ②再测试服务端与客户端不属于同一系统中的测试,结果如下:
    • 左侧为服务端(ubuntu系统),右侧为两个客户端(Windows系统),测试显示正常

  • 并且也没有出现“五”中一个客户端退出之后,服务端由于发送缓冲区溢出而导致的程序崩溃了。下面显示一个客户端退出之后,服务端仍然可以正常运行

外网测试

  • 此处我个人有一个远程云服务器(IP为111.229.177.161),测试结果如下:
    • 左侧为服务端(ubuntu系统),右侧为两个客户端(Windows系统),测试显示正常

  • 总结上面的原因,可以用下面的一张图表示:
    • 客户端和服务端都设置了程序接收缓冲区(409600KB),因此服务端和客户端之间的数据交互都能正常工作,且各自网络发送缓冲区和网络接收缓冲区都不会发生溢出

七、粘包现象

  • 在“六”中我们为客户端与服务端各自在程序中设置了足够大的接收数据缓冲区,但是会有一个问题:
    • 程序从接收缓冲区中每次都接收足够多的数据(最大不超过409600),因此每次接收时会从网络接收缓冲区中收到很多粘在一起的数据包
    • 这些数据包的数量不固定,因此我们的程序需要处理这种现象,因此我们需要拆分数据包,对单个数据包进行处理。在后面的文章中我们会介绍如何处理粘包现象
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 游动-白 设计师: 上身试试
应支付0元
点击重新获取
扫码支付

支付成功即可阅读