转自: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[], 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 = ); //发送消息(in_Socket,in_数据,in_长度,in_标志位(默认0));返回实际发出去的消息长度
int SendMSG(void* Socket, char* Data, int Num, int Flags = ); //关闭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[], int Htons, void*& Socket)
{
WSADATA Data;
SOCKADDR_IN DestSockAddress;
unsigned long DestAddress; //创建套接字(采用流式套接字)
WSAStartup(MAKEWORD(, ), &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, ); //连接
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 = ; //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[]; //自定义浮点数据 //复制
void Copy(const Message* msg)
{
m_ID = msg->m_ID;
for(int i = ; i<; i++)
{
m_Float[i] = msg->m_Float[i];
}
} //生成数据流
char* DataStream()
{
int offset = ;//偏移
char* p = new char[sizeof(Message)];//new内存
memcpy(p + offset, &m_ID, sizeof(int)); offset += sizeof(int);
for (int i = ; i < ; 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 = ;
//删除已发送的那条消息
delete[] m_AMSGBuffer;
m_AMSGBuffer = nullptr;//注意设置为NULL!!!
//互斥锁
lock_guard<recursive_mutex> mg(m_Mutex);
if (!m_Queue.empty())//但队列里有消息
{
//初始化m_Offset
m_Offset = ;
//队列中取出一条消息
shared_ptr<Message> pMSG = m_Queue.front();
m_Queue.pop();
//把消息生成数据流
m_AMSGBuffer = pMSG->DataStream();
//消息长度
dataLength = sizeof(Message);
return m_AMSGBuffer;
}
else//但队列里没消息了
{
dataLength = ;
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 = ;
//队列中取出一条消息
shared_ptr<Message> pMSG = m_Queue.front();
m_Queue.pop();
//把消息生成数据流
m_AMSGBuffer = pMSG->DataStream();
//消息长度
dataLength = sizeof(Message);
return m_AMSGBuffer;
}
else//队列里也没有消息
{
dataLength = ;
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() > )//线程计数器控制
{
if (UMyGameInstance::GameOnOff)
{
//接收
char data[];
int RcvNum = YJ::ReceiveMSG(m_Client->m_Socket, data, , );
if (RcvNum > )
{
m_Client->m_RecvingQueue.Push(data, RcvNum);
}
}
}
return ;
} 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()>)
{
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 > )
{
m_Client->m_SendedLen = YJ::SendMSG(m_Client->m_Socket, m_Client->m_SendingData, m_Client->m_SendingMsgLen, );
}
}
}
return ;
} 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 = -;
m_SendingMsgLen = ;
m_SendingData = nullptr;
m_SendedLen = ;
}
~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 == )
{
//设置Socket为非阻塞模式
INT32 result2 = YJ::Ioctlsocket(m_Socket);
if (result2 != )
{
//连接服务器成功,非阻塞模式失败
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 来发送或者接收消息了.

UE4 Socket多线程非阻塞通信的更多相关文章

  1. Java NIO Socket 非阻塞通信

    相对于非阻塞通信的复杂性,通常客户端并不需要使用非阻塞通信以提高性能,故这里只有服务端使用非阻塞通信方式实现 客户端: package com.test.client; import java.io. ...

  2. 基于MFC的socket编程(异步非阻塞通信)

       对于许多初学者来说,网络通信程序的开发,普遍的一个现象就是觉得难以入手.许多概念,诸如:同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)等,初学者往往迷惑不清, ...

  3. Python编写基于socket的非阻塞多人聊天室程序(单线程&多线程)

    前置知识:socket非阻塞函数(socket.setblocking(False)),简单多线程编程 代码示例: 1. 单线程非阻塞版本: 服务端: #!/usr/bin/env python # ...

  4. 用Java实现非阻塞通信

    用ServerSocket和Socket来编写服务器程序和客户程序,是Java网络编程的最基本的方式.这些服务器程序或客户程序在运行过程中常常会阻塞.例如当一个线程执行ServerSocket的acc ...

  5. [Python]再学 socket 之非阻塞 Server

    再学 socket 之非阻塞 Server 本文是基于 python2.7 实现,运行于 Mac 系统下 本篇文章是上一篇初探 socket 的续集, 上一篇文章介绍了:如何建立起一个基本的 sock ...

  6. 利用Python中SocketServer 实现客户端与服务器间非阻塞通信

    利用SocketServer模块来实现网络客户端与服务器并发连接非阻塞通信 版权声明 本文转自:http://blog.csdn.net/cnmilan/article/details/9664823 ...

  7. 【python】网络编程-SocketServer 实现客户端与服务器间非阻塞通信

    利用SocketServer模块来实现网络客户端与服务器并发连接非阻塞通信.首先,先了解下SocketServer模块中可供使用的类:BaseServer:包含服务器的核心功能与混合(mix-in)类 ...

  8. 斐讯面试记录—阻塞Socket和非阻塞Socket

    文章出自:http://blog.csdn.net/VCSockets/ 1.TCP中的阻塞Socket和非阻塞Socket 阻塞与非阻塞是对一个文件描述符指定的文件或设备的两种工作方式. 阻塞的意思 ...

  9. 【MPI学习4】MPI并行程序设计模式:非阻塞通信MPI程序设计

    这一章讲了MPI非阻塞通信的原理和一些函数接口,最后再用非阻塞通信方式实现Jacobi迭代,记录学习中的一些知识. (1)阻塞通信与非阻塞通信 阻塞通信调用时,整个程序只能执行通信相关的内容,而无法执 ...

随机推荐

  1. 统计cpu相关信息

    我的cpu为i3310m 适用类型:笔记本 CPU系列:酷睿i3 3代系列 CPU主频:2.4GHz 三级缓存:3MB 插槽类型:FCBGA1023,FCPGA988 封装大小:37.5×37.5mm ...

  2. caller

    caller返回调用了当前函数的那个对象(谁call了当前函数,即当前函数的caller) 对于函数来说,caller 属性只有在函数执行时才有定义 假如函数是由顶层(window)调用的,那么 ca ...

  3. SimpleEntity

    项目地址 :  https://github.com/kelin-xycs/SimpleEntity SimpleEntity 一个 用 C# 实现的 简单的 持久层 Entity 实现 . 这是一个 ...

  4. ML(5)——神经网络2(BP反向传播)

    上一章的神经网络实际上是前馈神经网络(feedforward neural network),也叫多层感知机(multilayer perceptron,MLP).具体来说,每层神经元与下一层神经元全 ...

  5. jstat查看JVM统计信息

    可以查看以下信息 [jdk基于1.8] 1.类装载 2.垃圾收集 3.JIT编译 一.类装载 返回参数的含义 二.垃圾收集 -gc的输出结果含义 JVM的内存结构 三.查看JIT编译 -compile ...

  6. Hystrix 详细说明

    在 Hystrix 入门中,使用 Hystrix 时创建命令并给予执行,实际上 Hystrix 有一套较为复杂的执行逻辑,简单来说明以下运作流程: 在命令开始执行时,会做一些准备工作,例如为命令创建响 ...

  7. Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(

    application.class要放在根目录下,否则会发生以下错误

  8. Uedit编辑器编辑Velocity

    1.切换到源代码模式 UM.getEditor('editor').execCommand("source"); 2.给编辑器插入Velocity源码 $(".edui- ...

  9. WPF Demo18 路由事件

    using System.Windows; namespace 路由事件2 { public class Student { ////声明并定义路由事件 //public static readonl ...

  10. WebGL和ThreeJs学习6--射线法确定3D空间中所选物体

    一.在 threejs 中如何确定下图3D空间中鼠标点击位置的 object 对象? 二.射线法确定步骤及代码 //Three.js提供一个射线类Raycaster来拾取场景里面的物体.更方便的使用鼠 ...