UE4 Socket多线程非阻塞通信【2】

7 篇文章 3 订阅

昨日不可追, 今日尤可为.勤奋,炽诚,不忘初心


 

 

紧接着第一部分,别说话,勿打断我游离的思绪我们走我们走...

 

二.消息结构&收发队列

先不急着往下走,先捋一捋,不知道自己要干什么地走下去是一件很可怕的事情.

首先,我们需要一个通讯接口,即socket.通讯有两种模式,一种是阻塞通讯,另一种是非阻塞通讯.

 

阻塞通讯:一直卡在那儿,直到处理完了再返回.比如我要发10000个字节的消息,那么该线程就会一直卡在那儿,直到发完了才返回.

非阻塞通讯:一次性处理不完,下次接着处理,每次处理一点.不会产生线程卡在那儿的情况.比如我要发10000个字节的消息,我这次发50,下一次发100,直到发完10000为止.

 

因为我们采用的是非阻塞通讯,Socket默认是阻塞模式的,如果想要非阻塞模式,只要这样设置就行:

粉红色的好,显得娘炮.

 

 

首先,我们先简单的定义一个消息结构体,如:

struct Message

{

     int m_ID; //消息ID,根据ID识别不同的用途

     float m_Float[4]; //自定义浮点数据

 

     //复制

     void Copy(const Message* msg)

     {  

             m_ID = msg->m_ID;

             for(int i = 0; i<4; i++)

             {

                    m_Float[i] = msg->m_Float[i];

             }

      }
 

     //生成数据流

     char* DataStream()

     {

            int offset = 0;//偏移

            char* p = new char[sizeof(Message)];//new内存

            memcpy(p + offset, &m_ID, sizeof(int)); offset += sizeof(int);

            for (int i = 0; i < 4; i++)
           {
                   memcpy(p + offset, &m_Float[i], sizeof(float));

                   offset += sizeof(float);
           }

     }

}


正因为我们采用的是非阻塞模式通讯,所以我们不知道一条消息发了多少,有没有发完.另外,我门非阻塞线程接收消息的话,也需要要解决分包,粘包的问题.

所以,我们可以弄一个接收消息队列和一个发送消息队列,数据过来了非阻塞模式下慢慢读,慢慢发:

发送消息队列的原理:把要发送给服务器的消息(Message结构体)压入发送消息队列,然后线程从发送队列中取出一条消息,一直发一直发,直到这条消息发完了,再从发送队列中取出下一条消息...

接收消息队列的原理:把从服务器上获取到的数据保存起来,有一个消息的长度了就压入接收消息队列,若不足一个消息长度,那么等下次发来的和这次的组装.打个比方:一个消息长度假设为100字节,已经收到了50字节了,这次又收到了100字节,那么,拆包,前50个字节和已经收到的那50字节组装,剩下的50字节不足100,为半包,先存起来,等下次收到再处理.

 

另外值得注意的是,因为是多线程,所以存在太多不可控性未知性和并发性,比如发送队列中一个线程在读,而另一个线程在往里面写数据...这将导致了数据结构被破坏!!!

解决办法是加互斥锁,唉一下子多了这么多专业术语,困扰纳闷了我好多好多天...我是接受不了短时间内这么多的要点...老了...配置跟不上了...

#pragma once

#include <stdint.h>
#include <string>
#include <memory>
#include <queue>
#include <mutex>
using namespace std;

//发送消息队列
class SendMessageQueue
{
public:
	SendMessageQueue();
	~SendMessageQueue();

	//将要发送的消息压入队列的尾部(待发送)
	void Push(const Message* msg)
	{
		//把数据复制进New的消息结构体,压入队列
		shared_ptr<Message> p(new Message());
		p->Copy(msg);
		//互斥锁
		lock_guard<recursive_mutex> mg(m_Mutex);
		m_Queue.push(p);
	}

	//从队列的头部取出一条消息,直到发完再取下一条(out_要发送的字节长度, in_已发送的字节数)
	char* Pop(int& dataLength, int size)
	{
		if (m_AMSGBuffer)//消息缓存有消息
		{
			if (m_Offset >= sizeof(Message))//如果一条消息发完了
			{
				//初始化m_Offset
				m_Offset = 0;
				//删除已发送的那条消息
				delete[] m_AMSGBuffer;
				m_AMSGBuffer = nullptr;//注意设置为NULL!!!
				//互斥锁
				lock_guard<recursive_mutex> mg(m_Mutex);
				if (!m_Queue.empty())//但队列里有消息
				{
					//初始化m_Offset
					m_Offset = 0;
					//队列中取出一条消息
					shared_ptr<Message> pMSG = m_Queue.front();
					m_Queue.pop();
					//把消息生成数据流
					m_AMSGBuffer = pMSG->DataStream();
					//消息长度
					dataLength = sizeof(Message);
					return m_AMSGBuffer;
				}
				else//但队列里没消息了
				{
					dataLength = 0;
					return nullptr;
				}
			}
			else//一条消息还没发完
			{
				m_Offset += size;
				dataLength = sizeof(Message)-m_Offset;
				return m_AMSGBuffer + m_Offset;
			}
		}
		else//消息缓存里没有消息
		{
			//互斥锁
			lock_guard<recursive_mutex> mg(m_Mutex);
			if (!m_Queue.empty())//但队列里有消息
			{
				//初始化m_Offset
				m_Offset = 0;
				//队列中取出一条消息
				shared_ptr<Message> pMSG = m_Queue.front();
				m_Queue.pop();
				//把消息生成数据流
				m_AMSGBuffer = pMSG->DataStream();
				//消息长度
				dataLength = sizeof(Message);
				return m_AMSGBuffer;
			}
			else//队列里也没有消息
			{
				dataLength = 0;
				return nullptr;
			}
		}
	}

private:
	recursive_mutex				m_Mutex;			//互斥锁(我们要保护的是m_Queue这个变量,所以我们每次在变动m_Queue之前加上互斥锁)
	char*						m_AMSGBuffer;		//一条要发送消息的缓存区
	int							m_Offset;			//缓冲区中的偏移
	queue<shared_ptr<Message>>	m_Queue;		//数据包队列
};

 

同理,再写个接收消息队列,要处理粘包和分包的问题.代码就不写了.大致是这样的:

 

	//数据包队列,自动处理粘包问题。
	class RecvMessageQueue
	{
	public:
		//将新的数据包放入队列尾部
		void		Push(const void* data, uint32_t size);
		//获取并删除队列头部的数据
		shared_ptr<Message> Pop();
		//获取数据包的数量
		uint32_t	GetSize();
		//清空队列
		void		Clear();
	private:
		recursive_mutex		m_Mutex;	//互斥锁
		char*				m_Buffer;	//已经写入的数据,用于处理粘包
		int					m_Offset;	//缓冲区中的偏移
		int					m_RestSize;	//剩余数据,用于处理粘包
		queue<shared_ptr<Message>>	m_Queue;	//数据包队列
	public:
		RecvMessageQueue();
		~RecvMessageQueue();
	};

 

 

 

 


三.虚幻4创建新线程

收数据线程调用类,线程变量在客户端类中创建:

#include "Runtime/Core/Public/HAL/ThreadingBase.h"
/**
* 收数据线程
*/
class TEXT_WORK_10_27_API FRecvThread : public FRunnable
{
public:
	FRecvThread(FClientSide* client) :m_Client(client){}

	~FRecvThread(){}

	//初始化成功则返回True,否则失败
	virtual bool Init() override
	{
		m_StopTaskCounter.Increment();//线程计数器+1
		return true;
	}

	virtual uint32 Run() override
	{
		//接收数据包
		while (m_StopTaskCounter.GetValue() > 0)//线程计数器控制
		{
			if (UMyGameInstance::GameOnOff)
			{
				//接收
				char data[1024];
				int RcvNum = YJ::ReceiveMSG(m_Client->m_Socket, data, 1024, 0);
				if (RcvNum > 0)
				{
					m_Client->m_RecvingQueue.Push(data, RcvNum);
				}
			}
		}
		return 1;
	}

	virtual void Stop() override
	{
		m_StopTaskCounter.Decrement();//计数器-1
	}

private:
	FClientSide*		m_Client;
	FThreadSafeCounter	m_StopTaskCounter;//线程引用计数器
};


发数据线程调用类,线程变量在客户端类中创建:

 

#include "Runtime/Core/Public/HAL/ThreadingBase.h"
/*
* 发数据线程
*/
class TEXT_WORK_10_27_API FSendThread : public FRunnable
{
public:
	FSendThread(FClientSide* client):m_Client(client){}

	~FSendThread(){}

	//初始化成功则返回True,否则失败
	virtual bool Init() override
	{
		m_StopTaskCounter.Increment();//线程计数器+1
		return true;
	}
	
	virtual uint32 Run() override
	{
		while (m_StopTaskCounter.GetValue()>0)
		{
			if (UMyGameInstance::GameOnOff)
			{
				//发送	
				m_Client->m_SendingData = m_Client->m_SendingQueue.Pop(m_Client->m_SendingMsgLen, m_Client->m_SendedLen);
				if (m_Client->m_SendingData && m_Client->m_SendingMsgLen > 0)
				{
					m_Client->m_SendedLen = YJ::SendMSG(m_Client->m_Socket, m_Client->m_SendingData, m_Client->m_SendingMsgLen, 0);
				}
			}
		}
		return 1;
	}

	virtual void Stop() override
	{
		m_StopTaskCounter.Decrement();
	}
private:
	FClientSide*		m_Client;
	FThreadSafeCounter	m_StopTaskCounter;
};

 

 

 

 

 

 

 

 

 

消息有了,发送和接收消息队列有了,线程也有了,下面就是 客户端类,比如:

 

</pre><p><pre class="cpp" name="code">/**
 * 与服务器连接 : 客户端
 */
class TEXT_WORK_10_27_API FClientSide
{
public:
	FClientSide()
	{
		//指针在构造函数里不初始化的话一定要设置为NULL,不然打包错误找都找不到!!!
		//另外释放内存,指针也要设置为NULL.不然就指向的地方是一堆烂数据了!!!
		m_SendThread = nullptr;
		m_RecvThread = nullptr;
		
		m_Socket = nullptr;
		m_ServeIP = nullptr;
		m_ServeHtons = -1;
		m_SendingMsgLen = 0;
		m_SendingData = nullptr;
		m_SendedLen = 0;
	}
	~FClientSide()
	{
		//释放内存
		if (m_SendThread)
		{
			delete m_SendThread;
			m_SendThread = nullptr;
		}
		
	
		if (m_RecvThread)
		{
			delete m_RecvThread;
			m_RecvThread = nullptr;
		}
	}

	//成员函数:初始化客户端;(IP,端口号);返回true:初始化成功,false:失败
	bool	Initialize(char* serveIP, INT32	htons)
	{
		//创建:发线程
		m_SendThread = FRunnableThread::Create(new FSendThread(this), TEXT("SedThread"));
		//创建:收线程
		m_RecvThread = FRunnableThread::Create(new FRecvThread(this), TEXT("RecvThread"));

		//连接
		int result = YJ::CreateAndConnect(m_ServeIP, m_ServeHtons, m_Socket);
		if (result == 0)
		{
			//设置Socket为非阻塞模式
			INT32 result2 = YJ::Ioctlsocket(m_Socket);
			if (result2 != 0)
			{
				//连接服务器成功,非阻塞模式失败
				return false;
			}
			else
			{
				//连接服务器成功,非阻塞模式成功
				return true;
			}
		}
		else
		{
			//连接服务器失败
			return false;
		}
	}

	//成员函数:发送消息
	void	Send(const Message* oneSendMessage)
	{
		m_SendingQueue.Push(oneSendMessage);
	}

	//成员函数:获取收到服务器的一条消息
	Message* Pop()
	{
		return m_RecvingQueue.Pop().get();
	}

public:
	void*									m_Socket;					//Socket(采用非阻塞通信)
	
	//---发送相关
	SendMessageQueue						m_SendingQueue;				//发送消息队列	
	FRunnableThread*						m_SendThread;				//发送线程
	int										m_SendingMsgLen;			//要发送数据的长度
	char*									m_SendingData;				//要发送的数据流
	int										m_SendedLen;				//已经发送数据的长度

	//---接收相关
	RecvMessageQueue						m_RecvingQueue;				//接收消息队列
	FRunnableThread*						m_RecvThread;				//接收线程
};

 

 

 

 

 

 

 

这样我们就可以实例化一个客户端类, 客户端.Send;客户端.Recv 来发送或者接收消息了.

 

 

 







  • 1
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值