前言

  • 本文进行send()函数的测试,查看在不同的情况下send()函数每秒大概可以执行多少次
  • 因为是对send()函数测试,所以我们用客户端给服务器send()发送数据,不接收数据服务端只使用recv()函数接收数据,但是不给客户端发送数据
  • 我们使用client给server发送大量数据,但是将client中的send()函数参数3改为1,因此每次只发送1个字节的数据,但是因为单个数据包为100字节,所以需要循环调用100次send才能将一个完整的数据包发送出去这样就更加可以测试出recv()函数的执行极限了

一、代码改造

服务端代码修改(EasyTcpServer.hpp)

  • 将CellServer::RecvData()中recv()函数的参数3改为RECV_BUFF_SIZE,其余代码不变,代码过长就不复制粘贴了,代码参阅:https://blog.csdn.net/qq_41453285/article/details/105700236

服务端测试程序(server.cpp)

#include "EasyTcpServer.hpp"
#include "MessageHeader.hpp"int main()
{EasyTcpServer server;server.Bind("192.168.0.105", 4567);server.Listen(5);server.Start(4);while (server.isRun()){server.Onrun();}server.CloseSocket();std::cout << "服务端停止工作!" << std::endl;getchar();  //防止程序一闪而过return 0;
}

客户端测试程序(EasyTcpClient.hpp)

  • 只要修改SendData()函数即可:将send()的参数1改为1,这样每次send()只发送1个字节的数据,但是因为单个数据包为100字节,所以需要循环调用100次send才能将一个完整的数据包发送出去
int EasyTcpClient::SendData(DataHeader* header,int nLen)
{int ret = SOCKET_ERROR;if (isRun() && header){//每次发送1字节,因为单个数据包为100字节,所以需要调用100次send才能将一个完整的数据包发送出去for (int n = 0; n < 100; n++){//每次发送1字节ret = send(_sock, ((const char*)header + n), 1, 0);if (ret == SOCKET_ERROR) {CloseSocket();printf("Client:socket<%d>发送数据错误,关闭客户端连接\n", static_cast<int>(_sock));}}}return ret;
}
  • 总代码如下:
#ifndef _EasyTcpClient_hpp_
#define _EasyTcpClient_hpp_#ifdef _WIN32#define WIN32_LEAN_AND_MEAN#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()#define _CRT_SECURE_NO_WARNINGS#include <windows.h>#include <WinSock2.h>#pragma comment(lib, "ws2_32.lib")
#else#include <unistd.h>#include <sys/socket.h>#include <sys/types.h>#include <arpa/inet.h>#include <netinet/in.h>#include <sys/select.h>//在Unix下没有这些宏,为了兼容,自己定义#define SOCKET int#define INVALID_SOCKET  (SOCKET)(~0)#define SOCKET_ERROR            (-1)
#endif//接收缓冲区的大小
#ifndef RECV_BUFF_SIZE
#define RECV_BUFF_SIZE 10240
#endif // !RECV_BUFF_SIZE#include <iostream>
#include <string.h>
#include <stdio.h>
#include "MessageHeader.hpp"using namespace std;class EasyTcpClient
{
public:EasyTcpClient() :_sock(INVALID_SOCKET), _isConnect(false), _lastPos(0) {memset(_recvBuff, 0, sizeof(_recvBuff));memset(_recvMsgBuff, 0, sizeof(_recvMsgBuff));}virtual ~EasyTcpClient() { CloseSocket(); }
public:void InitSocket();  //初始化socketvoid CloseSocket(); //关闭socketbool Onrun();       //处理网络消息bool isRun() { return ((_sock != INVALID_SOCKET) && _isConnect); }       //判断当前客户端是否在运行int  ConnectServer(const char* ip, unsigned int port); //连接服务器//使用RecvData接收任何类型的数据,然后将消息的头部字段传递给OnNetMessage()函数中,让其响应不同类型的消息int  RecvData();                                //接收数据virtual void OnNetMessage(DataHeader* header);  //响应网络消息int  SendData(DataHeader* header, int nLen)   ; //发送数据
private:SOCKET _sock;bool   _isConnect;                       //当前是否连接char   _recvBuff[RECV_BUFF_SIZE];        //第一缓冲区(接收缓冲区),用来存储从网络缓冲区中接收的数据char   _recvMsgBuff[RECV_BUFF_SIZE * 5]; //第二缓冲区(消息缓冲区),将第一缓冲区中的数据存储在这个缓冲区中,并在这个缓冲区中对数据进行处理(粘包拆包处理)int    _lastPos;                         //用来标识当前消息缓冲区中数据的结尾位置
};void EasyTcpClient::InitSocket()
{//如果之前有连接了,关闭旧连接,开启新连接if (isRun()){std::cout << "<Socket=" << (int)_sock << ">:关闭旧连接,建立了新连接" << std::endl;CloseSocket();}#ifdef _WIN32WORD ver = MAKEWORD(2, 2);WSADATA dat;WSAStartup(ver, &dat);
#endif_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (INVALID_SOCKET == _sock) {std::cout << "ERROR:建立socket失败!" << std::endl;}else {//std::cout << "<Socket=" << (int)_sock << ">:建立socket成功!" << std::endl;}
}int EasyTcpClient::ConnectServer(const char* ip, unsigned int port)
{if (!isRun()){InitSocket();}//声明要连接的服务端地址(注意,不同平台的服务端IP地址也不同)struct sockaddr_in _sin = {};
#ifdef _WIN32_sin.sin_addr.S_un.S_addr = inet_addr(ip);
#else_sin.sin_addr.s_addr = inet_addr(ip);
#endif_sin.sin_family = AF_INET;_sin.sin_port = htons(port);//连接服务端int ret = connect(_sock, (struct sockaddr*)&_sin, sizeof(_sin));if (SOCKET_ERROR == ret) {std::cout << "<Socket=" << (int)_sock << ">:连接服务端(" << ip << "," << port << ")失败!" << std::endl;}else {_isConnect = true;//std::cout << "<Socket=" << (int)_sock << ">:连接服务端(" << ip << "," << port << ")成功!" << std::endl;}return ret;
}void EasyTcpClient::CloseSocket()
{if (_sock != INVALID_SOCKET){
#ifdef _WIN32closesocket(_sock);WSACleanup();
#elseclose(_sock);
#endif_sock = INVALID_SOCKET;_isConnect = false;}
}bool EasyTcpClient::Onrun()
{if (isRun()){fd_set fdRead;FD_ZERO(&fdRead);FD_SET(_sock, &fdRead);struct timeval t = { 0,0 };int ret = select(_sock + 1, &fdRead, NULL, NULL, &t);if (ret < 0){std::cout << "<Socket=" << _sock << ">:select出错!" << std::endl;return false;}if (FD_ISSET(_sock, &fdRead)) //如果服务端有数据发送过来,接收显示数据{FD_CLR(_sock, &fdRead);if (-1 == RecvData()){std::cout << "<Socket=" << _sock << ">:数据接收失败,或服务端已断开!" << std::endl;CloseSocket();return false;}}return true;}return false;
}int EasyTcpClient::RecvData()
{int _nLen = recv(_sock, _recvBuff, RECV_BUFF_SIZE, 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;//将获取的数据拷贝到消息缓冲区memcpy(_recvMsgBuff + _lastPos, _recvBuff, _nLen);_lastPos += _nLen;//如果_recvMsgBuff中的数据长度大于等于DataHeaderwhile (_lastPos >= sizeof(DataHeader)){DataHeader* header = (DataHeader*)_recvMsgBuff;//如果_lastPos的位置大于等于一个数据包的长度,那么就会这个数据包进行处理if (_lastPos >= header->dataLength){//剩余未处理消息缓冲区的长度int nSize = _lastPos - header->dataLength;//处理网络消息OnNetMessage(header);//处理完成之后,将_recvMsgBuff中剩余未处理部分的数据前移memcpy(_recvMsgBuff, _recvMsgBuff + header->dataLength, nSize);_lastPos = nSize;}else {//消息缓冲区剩余数据不够一条完整消息break;}}return 0;
}void EasyTcpClient::OnNetMessage(DataHeader* header)
{switch (header->cmd){case CMD_LOGIN_RESULT:   //如果返回的是登录的结果{LoginResult* loginResult = (LoginResult*)header;//std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_LOGIN_RESULT,数据长度:" << loginResult->dataLength << ",结果为:" << loginResult->result << std::endl;}break;case CMD_LOGOUT_RESULT:  //如果是退出的结果{LogoutResult* logoutResult = (LogoutResult*)header;//std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_LOGOUT_RESULT,数据长度:" << logoutResult->dataLength << ",结果为:" << logoutResult->result << std::endl;}break;case CMD_NEW_USER_JOIN:  //有新用户加入{NewUserJoin* newUserJoin = (NewUserJoin*)header;//std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_NEW_USER_JOIN,数据长度:" << newUserJoin->dataLength << ",新用户Socket为:" << newUserJoin->sock << std::endl;}break;case CMD_ERROR:  //错误消息{//错误消息的类型就是DataHeader的,因此直接使用header即可//std::cout << "<Socket=" << _sock << ">,收到服务端数据:CMD_ERROR,数据长度:" << header->dataLength << std::endl;}break;default:{//std::cout << "<Socket=" << _sock << ">,收到服务端数据:未知类型的消息,数据长度:" << header->dataLength << std::endl;}}
}int EasyTcpClient::SendData(DataHeader* header,int nLen)
{int ret = SOCKET_ERROR;if (isRun() && header){//每次发送1字节,因为单个数据包为100字节,所以需要调用100次send才能将一个完整的数据包发送出去for (int n = 0; n < 100; n++){//每次发送1字节ret = send(_sock, ((const char*)header + n), 1, 0);if (ret == SOCKET_ERROR) {CloseSocket();printf("Client:socket<%d>发送数据错误,关闭客户端连接\n", static_cast<int>(_sock));}}}return ret;
}#endif // !_EasyTcpClient_hpp_

客户端测试程序(client.cpp)

  • 其实没有什么变化,只是在每次调用SendData()函数之后将sendCount+=100,因为每执行一次SendData(),send()就会执行100次
#include "EasyTcpClient.hpp"
#include "CELLTimestamp.hpp"
#include <thread>
#include <atomic>bool g_bRun = false;
const int cCount = 400;        //客户端的数量
const int tCount = 4;          //线程的数量
std::atomic_int sendCount = 0; //send()函数执行的次数
std::atomic_int readyCount = 0;//代表已经准备就绪的线程数量
EasyTcpClient* client[cCount]; //客户端的数组void cmdThread();
void sendThread(int id);int main()
{g_bRun = true;//UI线程,可以输入命令std::thread t(cmdThread);t.detach();//启动发送线程for (int n = 0; n < tCount; ++n){std::thread t(sendThread, n + 1);t.detach();}//每1秒中打印一次信息(其中包括send()函数的执行次数)CELLTimestamp tTime;while (true){auto t = tTime.getElapsedSecond();if (t >= 1.0){printf("time<%lf>,thread numer<%d>,client number<%d>,sendCount<%d>\n",t, tCount, cCount, static_cast<int>(sendCount / t));sendCount = 0;tTime.update();}Sleep(1);}return 0;
}void cmdThread()
{char cmdBuf[256] = {};while (true){std::cin >> cmdBuf;if (0 == strcmp(cmdBuf, "exit")){g_bRun = false;break;}else {std::cout << "命令不识别,请重新输入" << std::endl;}}
}void sendThread(int id)
{/*下面这几个变量是为了平均每个线程创建的客户端的数量:例如,本次测试时客户端数量为1000,线程数量为4,那么每个线程应该创建250个客户端线程1:c=250,begin=0,end=250线程2:c=250,begin=250,end=500线程3:c=250,begin=500,end=750线程4:c=250,begin=750,end=1000*/int c = cCount / tCount;int begin = (id - 1)*c;int end = id*c;for (int n = begin; n < end; ++n) //创建客户端{client[n] = new EasyTcpClient;}for (int n = begin; n < end; ++n) //让每个客户端连接服务器{client[n]->ConnectServer("192.168.0.105", 4567);}printf("Thread<%d>,Connect=<begin=%d, end=%d>\n", id, (begin + 1), end);//将readyCount,然后判断readyCount是否达到了tCount//如果没有,说明所有的线程还没有准备好,那么就等待所有线程都准备好一起返回发送数据readyCount++;while (readyCount < tCount){std::chrono::microseconds t(10);std::this_thread::sleep_for(t);}//这里定义为数组,可以随根据需求修改客户端单次发送给服务端的数据包数量const int nNum = 1;Login login[nNum];for (int n = 0; n < nNum; ++n){strcpy(login[n].userName, "dongshao");strcpy(login[n].PassWord, "123456");}//在外面定义nLen,就不用每次在for循环中SendData时还要去sizeof计算一下login的大小int nLen = sizeof(login);//循环向服务端发送消息while (g_bRun){for (int n = begin; n < end; ++n){if (client[n]->SendData(login, nLen) != SOCKET_ERROR){sendCount += 100;}//client[n]->Onrun();}}//关闭客户端for (int n = begin; n < end; ++n){client[n]->CloseSocket();delete client[n];}printf("thread:all clients close the connection!\n");
}

二、测试1

  • 客户端:修改client.cpp,使其执行4个线程,4个客户端连接,然后给服务端发送数据,send()函数每次只发送一个字节

  • 服务端:server.cpp不变,每次Start 4个线程(也就是EasyTcpServer中有4个CellServer子服务器接收数据)接收数据,接收客户端的数据

测试结果

  • 先开启一个服务端(左侧),再开启一个客户端(右侧):

    • 左侧为服务端:服务端每秒大概接收5000个数据包(每个数据包100字节),recv()函数每秒执行的次数为5万多次(因为客户端send()函数每次只发送1字节数据,因此服务端的recv()函数执行的次数比较多,因为只要客户端有数据,服务端就调用recv()接收)
    • 右侧为客户端:客户端每秒钟大概执行50万次send()函数

  • 现在我们保持服务端不变(左侧所示),然后逐个开启多个客户端(右侧所示):

    • 左侧为服务端:其情况基本不变
    • 右侧为客户端:当客户端数量增加的时候,可以看到单个客户端每秒钟执行的send()函数的次数由50万左右降低到25万左右,也就是所send()的次数被平摊了(如果继续增加客户端的数量,那么单个客户端的send()次数还会减少,并且数量也是被平摊的)

三、测试2

  • 客户端:修改client.cpp,使其执行4个线程,400个客户端连接,其余不变同上

  • 服务端:不变,同上

测试结果

  • 先开启一个服务端(左侧),再开启一个客户端(右侧):

    • 左侧为服务端:服务端每秒大概接收5000个数据包(每个数据包100字节),recv()函数每秒执行的次数为5万多次(因为客户端send()函数每次只发送1字节数据,因此服务端的recv()函数执行的次数比较多,因为只要客户端有数据,服务端就调用recv()接收)
    • 右侧为客户端:客户端每秒钟大概执行50万次send()函数

  • 现在我们保持服务端不变(左侧所示),然后逐个开启多个客户端(右侧所示):

    • 左侧为服务端:其情况基本不变
    • 右侧为客户端:当客户端数量增加的时候,可以看到单个客户端每秒钟执行的send()函数的次数由50万左右降低到25万左右,也就是所send()的次数被平摊了,情况与上面的实验相似

四、总结

  • 上面的测试用到的线程数目比较固定,当然如果更改线程数目,或者在线程数不同的CPU的系统上运行,效果会不一致
  • 至于为什么当客户端数量增加之后,每个客户端的send()次数会减少并且平摊,因为其发送缓冲区已经达到上限了

项目(百万并发网络通信架构)10.3---send()函数的极限测试相关推荐

  1. C++ 百万并发网络通信引擎架构与实现视频课程

    下载地址:百度网盘 课程目录         第1章第1章 搭建多平台下C++开发环境7小时16分钟22节                 1-1课程介绍 32:124 s1 E7 {* l. P&q ...

  2. 最新C++ 百万并发网络通信引擎架构与实现视频课程完整分享

    课程目录 下载地址:百度网盘         第1章第1章 搭建多平台下C++开发环境7小时16分钟22节                 1-1课程介绍 32:124                 ...

  3. 多游课堂C++ 百万并发网络通信引擎架构与实现学习笔记

    一般来说技术团队的金字塔顶尖往往是技术最牛的人做底层架构师(或高级工程师).所以底层架构师在广大码农中的占比大概平均不到 20%. 然而80%码农干上许多年都是重复以下内容,所以做不了架构师,正在辛苦 ...

  4. 10.幂指函数的极限

  5. 设计撑百万并发的数据库架构

    设计撑百万并发的数据库架构 https://www.toutiao.com/a6742034135486824973/ 前言 作为一个全球人数最多的国家,一个再怎么凄惨的行业,都能找出很多的人为之付出 ...

  6. 阿里年薪百万架构师分享「亿级并发系统架构设计」全彩版技术手册,只能说其实高并发不难

    什么是高并发? 并发是操作系统领域的一个概念,指的是一段时间内多任务流交替执行的现象 高并发用来指大流量.高请求的业务情景,比如春运抢票,电商双十一,秒杀大促等场景. 高并发的指标有 响应时间:系统对 ...

  7. 高并发网站架构与正态分布的前生今世

    高并发网站架构 什么是服务器? 不就是提供"付费"."免费"服务的高档电脑嘛! 你提到服务? 存储一个图片,读取一篇文字,观看一个动作片,计算一个账户存款,- ...

  8. Java实现百万并发(整理)

    关于服务器百万并发的一些整理 1 初始架构图 2 反向代理 2.1 反向代理概念 2.2 正向代理 2.2.1 正向代理介绍 2.2.2 关于代理总结 2.3 Nginx 2.3.1 Nginx服务器 ...

  9. 面试被问到如何设计微信钉钉后端高并发IM架构?懵了.....

    金九银十来临,前几天朋友出去面试-- 回来抱怨说面试时各种分布式.高并发.底层源码连环套一样打过来,幸亏基础够扎实,不然扛不住. 现在还感叹金九银十果然不是吹的,面试难度都比其他月份要高. 正值招聘求 ...

  10. 即时通讯源码-即时通讯集群服务免费-通讯百万并发技术-Openfire 的安装配置教程手册-哇谷即时通讯集群方案-哇谷云-哇谷即时通讯源码

    即时通讯源码-即时通讯集群服务免费-通讯百万并发技术-Openfire 的安装配置教程手册-哇谷即时通讯集群方案-哇谷云 1,openfire开发环境配置 很久没有写点东西了.最近很烦心,领导不给力. ...

最新文章

  1. js --- for in 和 for of
  2. OpenCASCADE:形状愈合之形状自定义
  3. mongodb 入门笔记
  4. Android Studio 3.3 Beta提供了新的Android代码压缩器R8
  5. 背水一战 Windows 10 (15) - 动画: 缓动动画
  6. PyTorch基础(part4)
  7. 广东省计算机媒体大赛,广东省大学生计算机设计大赛
  8. c语言程序设计--图书管理系统
  9. 在Eclipse中高效运行HTTP / REST集成测试
  10. Linux命令行配置静态IP地址
  11. 人工智能对数据分析师的影响
  12. 【第008问 Unity中什么是UV?】
  13. 吕 思 伟 ---- 潘 爱 民 :: ATL 介 绍( 一) (转)
  14. 计算机没网络怎么更新网卡驱动,电脑网卡驱动更新 怎么更新网卡驱动(图文)...
  15. 阿里云--短信服务---开通步骤
  16. Druid基本概念及架构介绍
  17. 中式客厅装修的特点 亦古亦今的惊艳每一家
  18. 价值千万的职业操盘手教程
  19. Swagger2生成在线接口文档并导出pdf文件
  20. 7-3 求最大值 (10 分)

热门文章

  1. linux自动备份系统快照,我的 Linux 时光机——Snapper系统级自动备份工具
  2. 快手打开后不显示画面_快手打开不显示图像怎么办
  3. 电商后台系统:管理后台之账号管理(一)
  4. Facebook:Novi数字钱包“已经准备好进入市场”
  5. php 正则获取邮箱后缀名,php中邮箱地址正则表达式实现与详解
  6. 关于vue组件引用外部Js数值和方法出现的问题
  7. python根据时间序列画折线图_时间序列模型的python实现
  8. python 安装Cython
  9. 【设计模式】Unity3D 观察者模式
  10. unity 观察者模式