博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
用 Unity 做个游戏(七) - TCP Socket 客户端
阅读量:6271 次
发布时间:2019-06-22

本文共 6318 字,大约阅读时间需要 21 分钟。

本文首发自

前言

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

写到这里已经第七篇了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<T>(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);复制代码

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

测试程序

创建一个这样的UI

0701
点击连接服务器的按钮,尝试连接服务器:
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.1request: {
"pid":1,"uid":"abc","loginOrOut":1}复制代码

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

0703

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

0704

完整代码

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

转载于:https://juejin.im/post/58e3b6ab61ff4b00617cfab5

你可能感兴趣的文章
合并两个排序的链表
查看>>
rtf格式的一些说明,转载的
查看>>
REST Security with JWT using Java and Spring Security
查看>>
echarts学习总结(二):一个页面存在多个echarts图形,图形自适应窗口大小
查看>>
IIS7显示ASP的详细错误信息到浏览器
查看>>
使用fiddler对手机APP进行抓包
查看>>
exit和_exit的区别
查看>>
Javascript、Jquery获取浏览器和屏幕各种高度宽度(单位都为px)
查看>>
php不重新编译,安装未安装过的扩展,如curl扩展
查看>>
JavaScript编码encode和decode escape和unescape
查看>>
ppp点对点协议
查看>>
html5游戏开发-简单tiger机
查看>>
Codeforces 712C Memory and De-Evolution
查看>>
编写的windows程序,崩溃时产生crash dump文件的办法
查看>>
Ural2110 : Remove or Maximize
查看>>
Django REST framework 的TokenAuth认证及外键Serializer基本实现
查看>>
《ArcGIS Runtime SDK for Android开发笔记》——问题集:如何解决ArcGIS Runtime SDK for Android中文标注无法显示的问题(转载)...
查看>>
Spring Boot日志管理
查看>>
动态注册HttpModule管道,实现global.asax功能
查看>>
使用 ES2015 编写 Gulp 构建
查看>>