EasyTCP 服务器消息接收与发送分离

在EasyTCP服务器发送数据的时候,不是直接调用send函数发送,而是设计一个发送缓冲区,每次从这个发送缓冲区中发送

	//发送数据
	int SendData(DataHeader* header)
	{
		int ret = SOCKET_ERROR;
		//要发送的数据长度
		int nSendLen = header->dataLength;
		//要发送的数据
		const char* pSendData = (const char*)header;

		while (true)
		{
			if (_lastSendPos + nSendLen >= SEND_BUFF_SZIE)
			{
				//计算可拷贝的数据长度
				int nCopyLen = SEND_BUFF_SZIE - _lastSendPos;
				//拷贝数据
				memcpy(_szSendBuf + _lastSendPos, pSendData, nCopyLen);
				//计算剩余数据位置
				pSendData += nCopyLen;
				//计算剩余数据长度
				nSendLen -= nSendLen;
				//发送数据
				ret = send(_sockfd, _szSendBuf, SEND_BUFF_SZIE, 0);
				//数据尾部位置清零
				_lastSendPos = 0;
				//发送错误
				if (SOCKET_ERROR == ret)
				{
					return ret;
				}
			}else {
				//将要发送的数据 拷贝到发送缓冲区尾部
				memcpy(_szSendBuf + _lastSendPos, pSendData, nSendLen);
				//计算数据尾部位置
				_lastSendPos += nSendLen;
				break;
			}
		}
		return ret;
	}

服务器接收数据也是先通过缓冲区接收,在拷贝到消息缓冲区

	//接收数据 处理粘包 拆分包
	int RecvData(ClientSocket* pClient)
	{

		//接收客户端数据
		char* szRecv = pClient->msgBuf() + pClient->getLastPos();
		int nLen = (int)recv(pClient->sockfd(), szRecv, (RECV_BUFF_SZIE)- pClient->getLastPos(), 0);
		_pNetEvent->OnNetRecv(pClient);
		//printf("nLen=%d\n", nLen);
		if (nLen <= 0)
		{
			//printf("客户端<Socket=%d>已退出,任务结束。\n", pClient->sockfd());
			return -1;
		}
		//将收取到的数据拷贝到消息缓冲区
		//memcpy(pClient->msgBuf() + pClient->getLastPos(), _szRecv, nLen);
		//消息缓冲区的数据尾部位置后移
		pClient->setLastPos(pClient->getLastPos() + nLen);

		//判断消息缓冲区的数据长度大于消息头DataHeader长度
		while (pClient->getLastPos() >= sizeof(DataHeader))
		{
			//这时就可以知道当前消息的长度
			DataHeader* header = (DataHeader*)pClient->msgBuf();
			//判断消息缓冲区的数据长度大于消息长度
			if (pClient->getLastPos() >= header->dataLength)
			{
				//消息缓冲区剩余未处理数据的长度
				int nSize = pClient->getLastPos() - header->dataLength;
				//处理网络消息
				OnNetMsg(pClient, header);
				//将消息缓冲区剩余未处理数据前移
				memcpy(pClient->msgBuf(), pClient->msgBuf() + header->dataLength, nSize);
				//消息缓冲区的数据尾部位置前移
				pClient->setLastPos(nSize);
			}
			else {
				//消息缓冲区剩余数据不够一条完整消息
				break;
			}
		}
		return 0;
	}

发送任务类

//网络消息发送任务
class CellSendMsg2ClientTask:public CellTask
{
	ClientSocket* _pClient;
	DataHeader* _pHeader;
public:
	CellSendMsg2ClientTask(ClientSocket* pClient, DataHeader* header)
	{
		_pClient = pClient;
		_pHeader = header;
	}

	//执行任务
	void doTask()
	{
		_pClient->SendData(_pHeader);
		delete _pHeader;
	}
};

再来梳理一下主函数

int main()
{

	MyServer server;
	server.InitSocket();
	server.Bind(nullptr, 4567);
	server.Listen(5);
	server.Start(4);

	//启动UI线程
	std::thread t1(cmdThread);
	t1.detach();

	while (g_bRun)
	{
		server.OnRun();
		//printf("空闲时间处理其它业务..\n");
	}
	server.Close();
	printf("已退出。\n");
	getchar();
	return 0;
}

看server.Start方法

	void Start(int nCellServer)
	{
		for (int n = 0; n < nCellServer; n++)
		{
			auto ser = new CellServer(_sock);
			_cellServers.push_back(ser);
			//注册网络事件接受对象
			ser->setEventObj(this);
			//启动消息处理线程
			ser->Start();
		}
	}

这里Start开启了nCellServer个CellServer,所以要继续看CellServer的Start函数,其中涉及到客户端的断开处理。

	void Start()
	{
		_thread = std::thread(std::mem_fn(&CellServer::OnRun), this);
		_taskServer.Start();
	}

这里启动了线程执行CellServer::OnRun函数
这里OnRun从任务队列中,取出任务,在使用select IO多路复用,单独处理客户端

	void OnRun()
	{
		_clients_change = true;
		while (isRun())
		{
			if (!_clientsBuff.empty())
			{//从缓冲队列里取出客户数据
				std::lock_guard<std::mutex> lock(_mutex);
				for (auto pClient : _clientsBuff)
				{
					_clients[pClient->sockfd()] = pClient;
				}
				_clientsBuff.clear();
				_clients_change = true;
			}

			//如果没有需要处理的客户端,就跳过
			if (_clients.empty())
			{
				std::chrono::milliseconds t(1);
				std::this_thread::sleep_for(t);
				continue;
			}

			//伯克利套接字 BSD socket
			fd_set fdRead;//描述符(socket) 集合
			//清理集合
			FD_ZERO(&fdRead);
			if (_clients_change)
			{
				_clients_change = false;
				//将描述符(socket)加入集合
				_maxSock = _clients.begin()->second->sockfd();
				for (auto iter : _clients)
				{
					FD_SET(iter.second->sockfd(), &fdRead);
					if (_maxSock < iter.second->sockfd())
					{
						_maxSock = iter.second->sockfd();
					}
				}
				memcpy(&_fdRead_bak, &fdRead, sizeof(fd_set));
			}
			else {
				memcpy(&fdRead, &_fdRead_bak, sizeof(fd_set));
			}

			///nfds 是一个整数值 是指fd_set集合中所有描述符(socket)的范围,而不是数量
			///既是所有文件描述符最大值+1 在Windows中这个参数可以写0
			int ret = select(_maxSock + 1, &fdRead, nullptr, nullptr, nullptr);
			if (ret < 0)
			{
				printf("select任务结束。\n");
				Close();
				return;
			}
			else if (ret == 0)
			{
				continue;
			}
			
#ifdef _WIN32
			for (int n = 0; n < fdRead.fd_count; n++)
			{
				auto iter  = _clients.find(fdRead.fd_array[n]);
				if (iter != _clients.end())
				{
					if (-1 == RecvData(iter->second))
					{
						if (_pNetEvent)
							_pNetEvent->OnNetLeave(iter->second);
						_clients_change = true;
						_clients.erase(iter->first);
					}
				}else {
					printf("error. if (iter != _clients.end())\n");
				}

			}
#else
			std::vector<ClientSocket*> temp;
			for (auto iter : _clients)
			{
				if (FD_ISSET(iter.second->sockfd(), &fdRead))
				{
					if (-1 == RecvData(iter.second))
					{
						if (_pNetEvent)
							_pNetEvent->OnNetLeave(iter.second);
						_clients_change = false;
						temp.push_back(iter.second);
					}
				}
			}
			for (auto pClient : temp)
			{
				_clients.erase(pClient->sockfd());
				delete pClient;
			}
#endif
		}
	}

实际上这里相当于分出了两个线程,一个接收,一个发送。

	void Start()
	{
		//线程
		std::thread t(std::mem_fn(&CellTaskServer::OnRun),this);
		t.detach();
	}
	//工作函数
	void OnRun()
	{
		while (true)
		{
			//从缓冲区取出数据
			if (!_tasksBuf.empty())
			{
				std::lock_guard<std::mutex> lock(_mutex);
				for (auto pTask : _tasksBuf)
				{
					_tasks.push_back(pTask);
				}
				_tasksBuf.clear();
			}
			//如果没有任务
			if (_tasks.empty())
			{
				std::chrono::milliseconds t(1);
				std::this_thread::sleep_for(t);
				continue;
			}
			//处理任务
			for (auto pTask : _tasks)
			{
				pTask->doTask();
				delete pTask;
			}
			//清空任务
			_tasks.clear();
		}

	}
};

这里任务又是一个虚函数,所以继续看下面

//网络消息发送任务
class CellSendMsg2ClientTask:public CellTask
{
	ClientSocket* _pClient;
	DataHeader* _pHeader;
public:
	CellSendMsg2ClientTask(ClientSocket* pClient, DataHeader* header)
	{
		_pClient = pClient;
		_pHeader = header;
	}

	//执行任务
	void doTask()
	{
		_pClient->SendData(_pHeader);
		delete _pHeader;
	}
};

 

这个MyServer类是继承自EasyTcpServer类,只重写了几个虚函数,这里暂时不管

那就看EasyTcpServer的OnRun函数

	//处理网络消息
	bool OnRun()
	{
		if (isRun())
		{
			time4msg();
			//伯克利套接字 BSD socket
			fd_set fdRead;//描述符(socket) 集合
			//清理集合
			FD_ZERO(&fdRead);
			//将描述符(socket)加入集合
			FD_SET(_sock, &fdRead);
			///nfds 是一个整数值 是指fd_set集合中所有描述符(socket)的范围,而不是数量
			///既是所有文件描述符最大值+1 在Windows中这个参数可以写0
			timeval t = { 0,10};
			int ret = select(_sock + 1, &fdRead, 0, 0, &t); //
			if (ret < 0)
			{
				printf("Accept Select任务结束。\n");
				Close();
				return false;
			}
			//判断描述符(socket)是否在集合中
			if (FD_ISSET(_sock, &fdRead))
			{
				FD_CLR(_sock, &fdRead);
				Accept();
				return true;
			}
			return true;
		}
		return false;
	}

可以发现,OnRun函数用了select模型,调用了Accept函数

	//接受客户端连接
	SOCKET Accept()
	{
		// 4 accept 等待接受客户端连接
		sockaddr_in clientAddr = {};
		int nAddrLen = sizeof(sockaddr_in);
		SOCKET cSock = INVALID_SOCKET;
#ifdef _WIN32
		cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
#else
		cSock = accept(_sock, (sockaddr*)&clientAddr, (socklen_t *)&nAddrLen);
#endif
		if (INVALID_SOCKET == cSock)
		{
			printf("socket=<%d>错误,接受到无效客户端SOCKET...\n", (int)_sock);
		}
		else
		{
			//将新客户端分配给客户数量最少的cellServer
			addClientToCellServer(new ClientSocket(cSock));
			//获取IP地址 inet_ntoa(clientAddr.sin_addr)
		}
		return cSock;
	}

Accept函数将新客户端分配给客户数量最少的CellServer

	void addClientToCellServer(ClientSocket* pClient)
	{
		//查找客户数量最少的CellServer消息处理对象
		auto pMinServer = _cellServers[0];
		for(auto pCellServer : _cellServers)
		{
			if (pMinServer->getClientCount() > pCellServer->getClientCount())
			{
				pMinServer = pCellServer;
			}
		}
		pMinServer->addClient(pClient);
		OnNetJoin(pClient);
	}

CellServer使用了OnNetJoin函数处理新客户端的连接,注意这里OnNetJoin是一个虚函数,调用的是这个结果

	virtual void OnNetJoin(ClientSocket* pClient)
	{
		EasyTcpServer::OnNetJoin(pClient);
	}
	//只会被一个线程触发 安全
	virtual void OnNetJoin(ClientSocket* pClient)
	{
		_clientCount++;
		//printf("client<%d> join\n", pClient->sockfd());
	}

 

简单来说, 这个服务器的架构这这样的。

首先开启4个工作线程,工作线程又分程两个线程,接收和发送,接收和发送是分开的,接收到数据又可能回调发送数据,接收从客户端数组中使用IO多路复用的select函数监听。发送/任务处理从任务队列中处理

服务器主循环中,只负责监听客户端连接,然后将客户端加入到客户端队列中。

整个程序大致就是这样。

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 易语言中的JScript可以使用EasyX支持库。EasyX是易语言的一款图形界面开发库,它提供了丰富的绘图函数和控制台窗口功能,可用于开发各种图形界面应用程序。 EasyX支持库中包含了许多绘图函数,如画线、画矩形、画圆等,开发者可以使用这些函数来绘制各种图形元素。同时,EasyX还提供了控制台窗口相关的函数,如获取键盘输入、清除屏幕、设置光标位置等,方便开发者进行控制台程序的开发。 除了绘图函数和控制台窗口函数,EasyX还提供了一些其他功能,如声音播放、图片处理、鼠标输入等。这些功能可以帮助开发者实现更加丰富和复杂的应用程序。 EasyX支持库的使用相对简单,易语言开发者可以通过引入EasyX.h头文件来使用其中的函数。在使用EasyX函数之前,还需要初始化图形界面和控制台窗口,然后就可以调用相应的函数来实现所需的功能。 总之,EasyX是易语言中用于支持JScript的一款开发库,提供了丰富的绘图函数和控制台窗口功能,可以帮助开发者实现各种图形界面应用程序。 ### 回答2: 易语言jscript可以使用一些支持库来扩展其功能和提高开发效率。以下是一些常用的支持库: 1. 界面支持库:提供了创建窗口、按钮、文本框等常见用户界面元素的功能,如易框架(eui)和ET跨平台GUI库(ETGui)等。 2. 文件操作支持库:用于进行文件读写和管理,常用的有易通用文件操作库(Yi-File)、易文件访问库(Yfrm)、方舟文件操作扩展库(FZFileEx)等。 3. 数据库支持库:用于与数据库进行交互,如易Mysql操作支持库(EasyMysql)和易数据库访问库(EasyDb)等,可以方便地进行数据库的连接、查询和操作。 4. 网络通讯支持库:用于进行网络通讯,如易UDP支持库(EasyUDP)和易TCP通信支持库(EasyTcp)等,可以实现网络编程相关功能。 5. 图像处理支持库:用于进行图像的处理和操作,比如易高级图形支持库(EGraphics)和易图片处理支持库(EasyImage)等,可以进行图像的加载、保存、编辑等操作。 6. 字符串处理支持库:用于进行字符串的处理和操作,如易字符串操作库(YString)和易正则表达式支持库(YRegExp)等,可以方便地进行字符串的查找、替换、截取等操作。 这些支持库可以通过官方网站、开发者论坛等渠道获取和学习,使用它们可以提高易语言jscript的开发效率和功能扩展能力。 ### 回答3: 易语言JScript可以使用EasyX库,这是一个专门为易语言开发者设计的图形界面库。EasyX库不仅提供了丰富的绘图函数,方便开发者绘制各种图形和动画效果,还支持各种输入输出操作,包括键盘输入、鼠标输入、文件读写等。另外,EasyX库还有自己的文本编辑器,可以直接在其中编写代码,并且可以进行调试和运行,方便开发者进行开发和调试。 除了EasyX库,易语言JScript还可以使用其他一些第三方支持库,比如WinApi库和Borland库。WinApi库提供了一系列用于Windows系统编程的函数接口,方便开发者直接调用系统API进行系统级编程。Borland库是易语言中常用的库之一,提供了大量的函数和类,方便开发者进行字符串处理、文件操作、网络通信等常用功能的开发。 总的来说,易语言JScript使用EasyX库是比较常见的选择,因为EasyX库提供了强大的图形界面和输入输出功能,可以满足大部分的开发需求。但如果有特殊需求,开发者还可以选择其他第三方支持库进行开发。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值