• UE4 Socket多线程非阻塞通信


    转自:https://blog.csdn.net/lunweiwangxi3/article/details/50468593

    ue4自带的Fsocket用起来依旧不是那么的顺手,感觉超出了我的理解范围了.另外我也不想让我近一个礼拜研究的C++ Socket无用武之地,毅然决然的决定使用自己的C++通讯库.再美再豪华的别墅真不如自己亲手搭建的草庐来的舒畅.这就好比我表弟,要花200块钱玩一个游戏,我说,我有一个1000巅峰的大神号,我不玩了,送你吧,你不要买了.他说:不!我就要自己的号! 他梦幻没钱充点卡了,我说,我的号给你玩吧,满修满猎满技能,锦衣祥瑞无级别...第二天,他依然开着自己的号在东海湾抓大海龟...

    ,不说了,都是泪.

    一.创建C++Socket通讯库

    不要问我为什么执意要封装成库,我至今都没能摆脱那 DWORD的噩梦.

    打开VS,新建空项目,新建SocketLibrary.h和SocketLibrary.cpp这两个文件,还有一个Source.def文件.

    SocketLibrary.h的内容如下:

    #pragma once  
      
    namespace YJ  
    {  
        //创建并且连接到服务器(in_IP地址,in_端口号,out_Socket)  
        int CreateAndConnect(char IPAddress[256], int Htons, void*& Socket);  
      
        //设置Socket为非阻塞模式;返回0:成功  
        int Ioctlsocket(void* socket);  
      
        //接收消息(in_Socket,in_数据,in_长度,in_标志位(默认0));返回实际接收到的消息长度  
        int ReceiveMSG(void* Socket, char* Data, int Num, int Flags = 0);  
      
        //发送消息(in_Socket,in_数据,in_长度,in_标志位(默认0));返回实际发出去的消息长度  
        int SendMSG(void* Socket, char* Data, int Num, int Flags = 0);  
      
        //关闭Socket  
        int CloseSocket(void* Socket);  
    }  

    SocketLibrary.cpp

    include "SocketLibrary.h"  
    #include <iostream>  
    #include <winsock.h>  
    #pragma comment(lib,"ws2_32.lib")  
    using namespace std;  
      
    namespace YJ  
    {  
        //创建并且连接到服务器(in_IP地址,in_端口号,out_Socket)  
        int CreateAndConnect(char IPAddress[256], int Htons, void*& Socket)  
        {  
            WSADATA         Data;  
            SOCKADDR_IN     DestSockAddress;  
            unsigned long   DestAddress;  
      
            //创建套接字(采用流式套接字)  
            WSAStartup(MAKEWORD(1, 1), &Data);  
      
            DestAddress = inet_addr(IPAddress);  
      
            memcpy(&DestSockAddress.sin_addr, &DestAddress, sizeof(DestAddress));  
            DestSockAddress.sin_port = htons(Htons);  
            DestSockAddress.sin_family = AF_INET;   //指定地址协议族  
      
            //构造socket(服务器端:构造监听流式SOCKET;客户端:构造通讯流式SOCKET)  
            Socket = (void*)socket(AF_INET, SOCK_STREAM, 0);  
          
            //连接  
            return connect((SOCKET)Socket, (LPSOCKADDR)&DestSockAddress, sizeof(DestSockAddress));  
        }  
      
        //设置Socket为非阻塞模式;返回0:成功  
        int ReceiveMSG(void* Socket, char* Data, int Num, int Flags /*= 0*/)  
        {  
            return recv((SOCKET)Socket, Data, Num, Flags);  
        }  
      
        //接收消息(in_Socket,in_数据,in_长度,in_标志位(默认0));返回实际接收到的消息长度  
        int SendMSG(void* Socket, char* Data, int Num, int Flags /*= 0*/)  
        {  
            return send((SOCKET)Socket, Data, Num, Flags);  
        }  
      
        //发送消息(in_Socket,in_数据,in_长度,in_标志位(默认0));返回实际发出去的消息长度  
        int CloseSocket(void* Socket)  
        {  
            return closesocket((SOCKET)Socket);  
        }  
      
        //关闭Socket  
        int Ioctlsocket(void* socket)  
        {  
            int iMode = 1; //0:阻塞  
            return ioctlsocket((SOCKET)socket, FIONBIO, (u_long FAR*)&iMode);  
        }  
      
    }  

    Source.def的内容如下:

    LIBRARY SocketLibrary  
    EXPORTS   
    CreateAndConnect  
    Ioctlsocket  
    ReceiveMSG  
    SendMSG  
    CloseSocket  

    配置好,这里我虚幻是准备打包64位的,所以我的库就要编译64位的:

    OK,把SocketLibrary.dll,SocketLibrary.lib,SocketLibrary.h这三个文件拿出来放到虚幻中.(每次修改这个通讯库,都要重新编译和替换这三个文件)

    --SocketLibrary.dll放到虚幻项目的Binaries>Win64里面(.dll文件要和EXE文件放在一起)

    --在虚幻项目根目录下新建一个SocketLib文件夹:

    --打开SocketLib这个文件夹,再新建两个文件夹:Include,Lib. 头文件.h和库文件.lib对号入座拷贝到相应目录下:

    然后打开虚幻项目 : 项目名.sln; 打开:项目名.Build.cs文件:

    Fill out your copyright notice in the Description page of Project Settings.  
    using UnrealBuildTool;  
    using System.IO;        //1.添加引用文件  
      
    public class Text_work_10_27 : ModuleRules  
    {  
        private string ModulePath  
        {  
            get { return Path.GetDirectoryName(RulesCompiler.GetModuleFilename(this.GetType().Name)); }  
        }  
      
        private string ThirdPartyPath  
        {  
            get { return Path.GetFullPath(Path.Combine(ModulePath, "../../SocketLib/")); }  
        }  
      
        public Text_work_10_27(TargetInfo Target)  
        {  
            PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });  
            PrivateDependencyModuleNames.AddRange(new string[] {  });  
      
            //2.添加Socket通讯库头文件目录和库目录  
            PublicIncludePaths.Add(Path.Combine(ThirdPartyPath, "Include"));  
            PublicAdditionalLibraries.Add(Path.Combine(ThirdPartyPath, "Lib", "SocketLibrary.lib"));       
        }  
    }  

    这样,就能使用通讯库了.

    二.消息结构&收发队列

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

    首先,我们需要一个通讯接口,即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;  
    };  

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

    /** 
     * 与服务器连接 : 客户端 
     */  
    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 来发送或者接收消息了.

  • 相关阅读:
    Java泛型方法
    HashMap,LinkedHashMap和TreeMap的区别
    HashSet,TreeSet和LinkedHashSet的区别
    Java中Arrays.sort()和Collections.sort()
    Java基础面试集合
    面试:字符串中的相关试题
    Hadoop相关知识整理系列之一:HBase基本架构及原理
    Ubuntu 15.10环境下安装Hive
    机器学习相关知识整理系列之三:Boosting算法原理,GBDT&XGBoost
    机器学习相关知识整理系列之二:Bagging及随机森林
  • 原文地址:https://www.cnblogs.com/sevenyuan/p/8670443.html
Copyright © 2020-2023  润新知