tcp unity 图片_用 Unity 做个游戏(七) - TCP Socket 客户端

前言

这真的是最后一篇有关基础框架的文章了!

写到这里已经第七篇了orz之前的其实还是挺枯燥的,都是些基础方面的东西,并看不到什么有趣的内容

可能是我把事情想的太复杂了吧,所有东西都想做到能力范围内的最好,尤其是这些底层框架层次的东西

不过这些东西真的很重要,小游戏的话可能不会明显,Unity的一大优势便在于可以快速地产出游戏原型来,我这个项目整了这么久就一个TestView,里面居然只有两个按钮!233

我这些东西也是考虑了许多生产环境中遇到过的问题,不敢说是最优,我也还是在学习嘛XD

嘛,等把网络框架也搭起来,我们就能正式开始写游戏相关的逻辑啦~

网络通信

我们这游戏是个多人在线实时对战的游戏,之前的坑里就是网络这块给搞崩了,重新来设计

网络这块使用原生TCP Socket进行通讯,自定协议。这里主要先介绍客户端,先把协议定下来,这样之后介绍服务端的时候就不会和客户端有太大耦合了。当然一开始的话还是先弄一个最简单的服务端,本项目计划使用Node.js开发

最简单的服务端

在某个端口上创建一个TCP服务器,接收客户端传来的消息,拼接一个字符串后返回。

代码如下:

const net = require("net");

net.createServer(function(socket){

console.log("有新的连接:" + socket.remoteAddress);

socket.on("data", function(data){

console.log("request: " + data);

socket.write('{"pid":1,"retCode":0}');

});

socket.on("end", function(data){

console.log("socket end");

});

socket.on("close", function(data){

console.log("连接已断开");

});

socket.write("Hello!");

}).listen(19621);复制代码

客户端设计

用两个类,一个相对底层的SFTcpClient,用户不直接使用这个类,而是通过SFNetworkManager加一层封装,这是个单例类,可以在游戏运行过程中随时访问网络,还有就是为了以后可能不仅仅使用一个SfTcpClient,封装之后可以更优雅地管理多个TCP客户端。

SFNetworkManager

先看SFNetworkManager,管理着若干个SFTcpClient,通过后者的以下接口:

|方法|说明|

|--|--|

|void init(string, int, SFClientCallback, SFSocketStateCallback)|根据指定的IP地址,端口以及相关回调初始化|

|void uninit()|关闭TCP客户端|

|void sendData(string)|往服务器发送数据|

|bool isReady|服务器是否就绪|

首先是作为一个单例类应该有的内容:私有的构造函数,唯一的实例,获取实例的方法:

private SFNetworkManager(){}

private static sm_instance = null;

public static SFNetworkManager getInstance(){

if (null == sm_instance)

{

sm_instance = new SFNetworkManager();

}

return sm_instance;

}复制代码

然后是连接初始化

public void init(){

m_client = new SFTcpClient();

m_client.init("127.0.0.1", 19621, onRecvMsg, ret =>

{

dispatcher.dispatchEvent(SFEvent.EVENT_NETWORK_READY, new SFSimpleEventData(ret));

});

// 向上传递连接断开的事件

m_client.dispatcher.addEventListener(SFEvent.EVENT_NETWORK_INTERRUPTED, e =>

{

dispatcher.dispatchEvent(e);

});

}复制代码

void onRecvMsg(string)是处理服务端推送消息的回调函数

void onRecvMsg(string msg){

SFUtils.log("收到了" + msg);

}复制代码

现在问题来了,因为消息回调函数是在Socket子线程里调用的,Unity里不允许在子线程中对场景中的物体进行修改,所以要稍加改造,让这些消息在主线程中处理。

用一个队列,子线程中收到的消息全加入到这个队列,把内容存在内存里,然后主线程通过update函数定期检查队列中是否还有未处理的信息,有的话就全部取出来处理。

void onRecvMsg(string msg){

m_recvQueue.Enqueue(msg);

}

void update(){

while (m_recvQueue.Count > 0)

{

string data = m_recvQueue.Dequeue();

SFUtils.log("收到了" + data);

}

}复制代码

SFTcpClient

使用C# TCP Socket的异步实现。数据收发的子线程由系统管理。

所有的方法都有对应的一对BeginXX和EndXX,以接收数据为例:

try

{

if (!m_socket.Connected)

{

throw new Exception("Socket is not connected");

}

byte[] data = new byte[1024]; // 以1024字节为单位接收数据

m_socket.BeginReceive(data, 0, data.Length, SocketFlags.None, result =>

{

int length = m_socket.EndReceive(result); // length为实际接收到的数据长度

if (length > 0)

{

m_callback(Encoding.UTF8.GetString(data)); // 转换为字符串并调用回调

}

else

{

// length为0说明网络已断开

m_socket.close();

SFUtils.logWarning("网络连接中断");

dispatcher.dispatchEvent(SFEvent.EVENT_NETWORK_INTERRUPTED);

}

}, null);

}

catch (Exception e)

{

SFUtils.logWarning("网络连接中断:" + e.Message);

}复制代码

其他像是连接,发送都大同小异,具体的完整代码可以查看文章末尾的完整代码链接。

自定协议

数据的首发暂时就先这样(当然有很多坑,比如因为我使用原生TCP Socket来传输数据包,数据多的时候必然会产生粘包的情况,所以必须手动分包,这个之后再说,和接下来的内容关系不大,要加的话直接在SFTcpClient里的sendData()方法和socketRecv()方法里修改就是了)

网络中传输的数据使用JSON字符串,发送和接收的时候客户端和服务端分别各自进行序列化和反序列化,这里先只讨论客户端的实现。

Unity提供了一个JsonUtility类,有了这个类我们就能方便地进行对象和JSON之间的序列化和反序列化了。主要使用的是两个方法:

|方法名|作用|

|--|--|

|string JsonUtility.ToJson(object)|把一个对象转化成JSON字符串|

|T JsonUtility.FromJson(string)|把一个JSON字符串转化成指定类型的对象,如果出错则抛出异常|

请求

请求类型均继承自基类SFBaseRequestMessage,举一个例子:

// 基类

public class SFBaseRequestMessage

{

public int pid; // 协议号

public string uid; // 用户唯一ID

};

// 用户登陆登出

[Serializable]

public class SFRequestMsgUnitLogin : SFBaseRequestMessage

{

public SFRequestMsgUnitLogin(){ pid = 1; }

public int loginOrOut;

};复制代码

响应

响应类型是类似的,每个请求类型一定对应一个响应类型,但反过来却不一定,即一个协议拥有请求类型是拥有响应类型的必要非充分条件。

同样是上面那个登陆的协议:

// 基类

public class SFBaseResponseMessage : ISFEventData // 为了让响应结果也可以方便地作为事件数据传递

{

public int pid; // 协议号

public int retCode; // 错误代码,0表示成功

};

// 用户登陆登出

[Serializable]

public class SFResponseMsgUnitLogin : SFBaseResponseMessage

{

public const string pName = "socket_1"; // pName作为SFEvent的事件名称

public SFReponseMsgUnitLogin(){ pid = 1; }

};复制代码

发送和接收

发送非常简单,创建一个sendMessage()方法,接收参数类型为请求基类SFBaseRequestMessage,先序列化然后直接丢给TCP Client来处理发送即可。

public void sendMessage(SFBaseRequestMessage req){

string data = JsonUtility.ToJson(req);

m_client.sendData(data);

}复制代码

接收稍微复杂点儿,分两步,首先把原始字符串转成SFBaseResponseMessage,获取其协议号pid,然后根据不同的pid再转成具体的响应类型。

SFBaseResponse obj = null;

obj = JsonUtility.FromJson(data);

if (obj == null)

{

SFUtils.logWarning("不能解析的信息格式:\n" + data);

}

else

{

int pid = obj.pid;

string pName = string.Format("socket_{0}", pid);

if (pid == 1)

{

obj = JsonUtility.FromJson(data)

}

// else if 更多协议

else

{

SFUtils.logWarning("不能识别的协议号: {0}", 0, pid);

obj = null;

}

if (obj != null)

{

dispatcher.dispatchEvent(pName, obj);

}

}复制代码

然后在其他地方添加相应协议的监听即可:

SFNetworkManager.getInstance().dispatcher.addEventListener(SFResponseMsgUnitLogin.pName, onRecvMsg);复制代码

回调函数一定在主线程中被调用,所以可以在里面放心地修改游戏场景。

测试程序

创建一个这样的UI0701

点击连接服务器的按钮,尝试连接服务器:

m_mgr = SFNetworkManager.getInstance();

m_mgr.init();

m_mgr.dispatcher.addEventListener(SFEvent.EVENT_NETWORK_READY, result =>

{

SFSimpleEventData retCode = result.data as SFSimpleEventData;

if (retCode.intVal == 0)

{

m_infoMsg = "服务器连接成功";

}

else

{

m_infoMsg = "服务器连接失败";

}

});

m_mgr.dispatcher.addEventListener(SFEvent.EVENT_NETWORK_INTERRUPTED, onInterrupt);

m_mgr.dispatcher.addEventListener(SFResponseMsgUnitLogin.pName, onRecvMsg);复制代码

在此之前启动服务端程序的话就会成功连接至服务器。0702

同时会收到来自服务端的消息"Hello!",当然这个不符合我们的协议,console面板可以看到程序无法解析这个字符串,并忽略。

然后点击发送消息按钮,客户端程序会发送一个测试协议给服务端,服务端就会收到:

$ node ./

started

有新的连接:::ffff:127.0.0.1

request: {"pid":1,"uid":"abc","loginOrOut":1}复制代码

此时服务端返回字符串'{"pid":1,"retCode":0}',这就是一个标准的协议信息了,程序解析后发现这是一个登陆成功的响应,做出处理:0703

然后按Ctrl+C强制关闭服务端程序进程,网络中断,客户端也有对应的处理0704

完整代码

上面贴出的代码片段由于篇幅限制只保留了关键部分,完整的代码可在我的github上找到

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306362.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习——文件的读取

机器学习——文件的读取(一).txt文件的读取(二)excel文件读取操作(一).txt文件的读取 txt的链接 链接:https://pan.baidu.com/s/1fIAUdCDTpR7TiqLHZtx1yg 提取码:0929 python strip() 函数和 split() 函数的详解及实例 一直以来都分不清楚strip和split…

理解C#中的闭包

1、 闭包的含义首先闭包并不是针对某一特定语言的概念,而是一个通用的概念。除了在各个支持函数式编程的语言中,我们会接触到它。一些不支持函数式编程的语言中也能支持闭包(如java8之前的匿名内部类)。在看过的对于闭包的定义中&…

linux select读取节点数据失败_MySQL中覆盖索引查询和select*查询执行结果案例分析...

索引优化建议在MySQL中要尽可能使用覆盖索引进行检索,只访问索引的查询(索引列和查询列一致),减少select * 可提高查询效率覆盖索引(Covering Index)理解方式一:就是select的数据列只用从索引中就能够取得,不必读取数据行,MySQL可…

机器学习之乳腺癌预测

机器学习之乳腺癌预测 (一)问题分析1.问题背景2.问题分析3.题目所需的代码及数据(二).导入数据1. 认识数据集2.导入数据3.数据概述(三)EDA 数据探索性分析1.描述性统计分析A.查看数据维度(行列数)B.数据统计描述(列名对应的信息)C.查看数据信息(统计学信息)D.缺失处理2.数据可视…

使用 Azure WAF 羞辱黑客的智商

点击上方蓝字关注“汪宇杰博客”导语还记得之前给大家介绍过的《使用 Azure Web 应用防火墙拦截黑客攻击》吗?今天我又带来了一个有趣的 Azure WAF 小技巧,可以让你爽一把。好奇的黑客今天 Azure Application Insights 上发现了一段集中时间的404错误&am…

实现option上下移动_用jQuery实现lt;selectgt;选项上下移动 - 不要哀求 学会争取 若是如此 终有所获 - ITeye博客...

嘖嘖嘖,短短兩個API串接: $opt.next().after($opt)就做出了向下移動的效果。記得以往用純Javascript寫,還得判斷是否為最後一個,若是就不能下移;然後上下位置交換得用options[index]搞半天。不得不要再次讚嘆jQuery的神奇!$(funct…

机器学习之乳腺癌问题(SVM)

机器学习之乳腺癌问题SVM题目所需的代码及数据利用SVM建模SVM调参题目所需的代码及数据 链接:https://pan.baidu.com/s/1bS7Ku_PUfcimiVkmLz9Fzw 提取码:0929 利用SVM建模 import matplotlib.pyplot as plt import pandas as pd import numpy as npfro…

数据科学与python语言实验——NumPy数值计算基础

NumPy数值计算基础实验数据: 链接:https://pan.baidu.com/s/1-E2ShVTdI0X5lwDtMLFFsQ 提取码:0929 代码实现: 之前不会的地方: 1.读取文件 使用numpy内置的loadtxt()函数以及这个函数的参数frame&#x…

如何将日志记录到 Windows事件日志 中

每当出现一些未捕获异常时,操作系统都会将异常信息写入到 Windows 事件日志 中,可以通过 Windows 事件查看器 查看,如下图:这篇文章将会讨论如何使用编程的方式将日志记录到 Windows 事件日志 中。安装 EventLog 要想在 .NET Core…

linux磁盘写保护怎么修改_mount: /dev/vdb 写保护,将以只读方式挂载

今天再mount磁盘的时候遇到一个问题:[rootoracle1 /]# mount /dev/vdb /oradatamount: /dev/vdb 写保护,将以只读方式挂载mount: 未知的文件系统类型“(null)”这个问题重新格式化磁盘后就好了mkfs.ext4 /dev/vdb让这个盘在系统启动的时候自动挂载[oracl…

【Java】springboot

文章目录 Spingboot1、起步依赖2、构建springboot工程jar包3、springboot配置文件4、多环境配置5、maven和boot多环境兼容问题6、配置文件分类7、springboot整合mybatis Spingboot springboot用来简化spring的初始搭建以及开发过程。 比方说,创建一个springmvc程序…

数据科学与python语言——Matplotlib数据可视化基础

Matplotlib数据可视化基础一.读取数据与数据处理阶段1.提取指定行中的数据2.得到>指定数值的数据3.得到指定值得数据4.整体的数据处理:二.画图函数1.plt.subplots()2.plt.subplots_adjust()3.设置x轴y轴的刻度和标签4.使用中文标题在作图时三.画折线图(plot)四.画…

2021年,Azure云遇到. NET5,注定开启高光时刻,微软的心,真大!

云开发诞生的市场背景云开发是一个已经存在了很多年的概念,但在过去未能真正成为主流。然而,由于云和软件即服务的宏观趋势的结合,以及技术的进步,如容器技术 Docker 和 Kubernetes,云开发现在有机会最终成为基于云的应…

fpga开发教程 labview_LabVIEW项目样例 - NI VST FPGA软件编程使用步骤与实例教程

3. LabVIEW项目样例NI VST仪器设计库的能力强大,但是并不能单枪匹马地完全满足软件设计仪器实现默认功能的要求,也不能提供大多数VSA和VSG仪器编程人员所熟悉的简单主机接口。LabVIEW 2012提供了一个新功能,这种方便的机制不仅能够分配附加代…

.net5+nacos+ocelot 配置中心和服务发现实现

相关文章:手动造轮子——为Ocelot集成Nacos注册中心出处:https://www.cnblogs.com/buruainiaaaa/p/14121176.html作者:唐 最近一段时间 因公司业务需要,需要使用.net5做一套微服务的接口,使用nacos 做注册中心和配置中…

数据科学与python语言——Pandas统计分析基础(时间转换+聚合)

Pandas统计分析基础(时间转换聚合)实验要求一实验二要求全部代码实验要求一 #M表的时间戳类型转为datetime data_Mete[TIMESTAMP]pd.to_datetime(data_Mete[TIMESTAMP],format%Y%m%d%H%M%S)data_VI[Date]pd.to_datetime(data_VI[Date],format%Y/%m/%d) p…

用keil怎么擦除_分享STM32 FLASH 擦除(以及防止误擦除程序代码)、写入

编译环境:我用的是(Keil)MDK4.7.2stm32库版本:我用的是3.5.0一、本文不对FLASH的基础知识做详细的介绍,不懂得地方请查阅有关资料。对STM32 内部FLASH进行编程操作,需要遵循以下流程:FLASH解锁清除相关标志位擦除FLASH…

如何在 ASP.NET Core 中使用 Quartz.NET 执行任务调度

当我们在web开发中,常常会遇到这么一个需求,在后台执行某一项具体的任务,具体的说就是这些任务必须在后台定时执行。Quartz.NET 是一个开源的 JAVA 移植版,它有着悠久的历史并且提供了强大的 Cron 表达式,这篇我们就来…

数据科学与python——Pandas统计分析基础(数据堆叠+数据清洗)

Pandas统计分析基础数据堆叠数据清洗一.合并数据:获取完整的数据集。1.读取数据2.将两个csv文件按照mete.csv文件的日期对齐3.纵向合并数据data1与data24.使用drop_duplicates()函数去除重复值二.异常值处理:去除data3中GPP中的异常点1.根据3σ原则检测异…

python 为什么动态语言图片_聊聊动态语言那些事(Python)

动态编程语言是高级程序设计语言的一个类别,在计算机科学领域已被广泛应用。它是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。动态语言目前非常具有活力&#xff0…