《Unity 3D游戏客户端基础框架》多线程异步 Socket 框架构建
引言:
之前写过一个 demo 案例大致讲解了 Socket 通信的过程,并和自建的服务器完成连接和简单的数据通信,详细的内容可以查看 Unity3D —— Socket通信(C#)。但是在实际项目应用的过程中,这个 demo 的实现方式显得异常简陋,而且对应多个业务同时发起 Socket 通信请求的处理能力也是有限,总不能每个请求都创建一个线程去监听返回结果,所以有必要进一步优化一番,例如加入线程池管理已经用一个队列来管理同时发起的请求,让 Socket 请求和接收异步执行,基本的思路就是引入 多线程
和 异步
这两个概念。
设计思路:
正如上面提及的,我们优化的思路主要两点:多线程 和 异步 Socket
1.多线程:
考虑到并发处理能力,假如是服务端,我们需要对每个连接上来的 Socket 客户端创建管理线程,通常会用线程池来管理。而在客户端,我们可以选择创建三个核心的线程,根据功能分为:
发送数据线程 SenderThread:负责将数据发送给服务端;
接收数据线程 ReceiverThread:负责接收服务端的数据;
其他业务线程 MainThread:负责收发数据之外的操作,例如:压缩数据、加密解密和数据打包解包等。
由于在 .NET 中已经提供了 System.Threading.Thread
这个线程的基础类,所以我们可以通过使用这个类来创建我们自己的线程管理类,添加一些附加属性。
大致步骤:
- 先创建一个
Socket
对象,然后通过对应的 API 去连接服务器 IP 端口,可以有两种方式:阻塞式
的Connect
接口;非阻塞式
的BeginConnect
接口; - 连接完成后创建
发送线程
和接收线程
,并将套接字 Socket 对象传入它们的操作方法中; - 发送线程的发送方法是被调用才触发的,接收线程则需要一个轮休等待网络数据的方法。
2.异步 Socket:
在这一点上,主要做法就是使用队列的概念,即不管要发送消息还是收到消息,都先把消息放到队列(发送队列、接收队列)中,然后依次从队列中中取出消息来执行。
队列:在 .NET 中,提供了一个 Queue
的结构实现队列的功能,队列里面可以存放任何可以转化为 object
的对象或数据,所以自然也能存放我们的消息。
- 发送队列:
当我们要发送一条网络协议的时候,不是直接将消息丢给丢给 Socket 通道进行传输,而是先通过Queue.Enqueue
接口压入队列,然后在一个循环执行的逻辑(轮询)中不断地从队列中通过Queue.Dequeue
接口取出队列中的消息然后再传递给 Socket 进行真实的网络传输。 - 接收队列:
接收网络数据也是相似的过程,从缓冲区取到字节流数据,解析成功后,先存在接收队列中,然后再循环遍历操作中从队列里取出消息进行处理。
当然,为了解决队列在多线程中的并发问题,还需要了解一下 lock
方法,这是解决问题的方法之一,但不是唯一办法,这里我的原则是在不严重影响效率的情况下怎么简单怎么来,使用大致如下:
//线程安全的访问队列的方式
lock (mQueue)
{
...
}
3.内存流缓存池:
在做消息数据的读写和格式转换操作时,都会用到 MemoryStream
,这是一个流结构,其后备存储是内存,也就是每次创建一个这样的对象都会占用一定的内存空间,假如每一次操作都创建一个 MemoryStream
对象,用完又不做主动回收,显然效率较低。
内存流池化思想:我们可以在池中使用一个
List<MemoryStream>
列表来保存已创建的流对象,再使用Dictionary<MemoryStream,bool>
字典来存储每一个对象是否可用的状态,每次需要创建流对象的步骤:- 通过判断列表中 MemoryStream 个数是否大于0,假如大于0则取出第一个,并删除列表中第一个 item (防止重复引用),在字典中记录此对象为不可用状态;
- 假如列表中已经没有可用的 MemoryStream,则创建新的 MemoryStream 对象并在字典中记录状态,省去了删除列表 item 的步骤;
- 使用完毕后,将 MemoryStream 对象的属性重置,并放回管理列表,字典中该对象的状态置为可用状态。
多个内存池:为了避免多线程的并发问题,所以一般一个内存池只允许一个线程来访问,根据上述我们创建三个核心线程的思想,所以这里我们也要对应地创建三个池,分别为:发送线程使用 MemoryStream 池、接收线程使用 MemoryStream 池 和 业务线程线程使用 MemoryStream 池。
所以最终每个池需要提供至少两个接口:
New
用于从池中获取一个可用的 MemoryStream 对象;Delete
用于将一个使用完后的 MemoryStream 对象回收到池中。
4.收发包线程管理类:
这个类主要完成连接的建立和断开的管理,以及在连接成功后创建读写包的线程,并将网络读写操作的对象传递给收发包的线程以便后续网络数据包的接收和发送操作:
建立连接:
在 .Net 中提供了两种创建 Socket 连接的方式(用于客户端):- 使用
System.Net.Sockets.TcpClient
创建一个TcpClient
对象,然后使用TcpClient.BeginConnect
接口去连接指定 IP 端口,使用这种方式创建连接后; - 使用
System.Net.Sockets.Socket
创建一个Socket
对象,然后通过Socket.Connect
去连接指定 IP 端口。
- 使用
数据收发:
TcpClient
通过TcpClient.GetStream()
接口得到一个NetworkStream
对象,然后分别使用NetworkStream.Write
和NetworkStream.Read
完成网络数据的发送和接收;Socket
连接完成后,直接使用最开始创建的 Socket 套接字对象调用Socket.Send
和Scoket.Receive
来完成网络数据的发送和接收。
断开连接:
TcpClient
需要执行两步操作:一是关闭NetworkStream
,二是关闭TcpClient
,都是调用对应的关闭方法:TcpClient.GetStream().Close()
/TcpClient.Close()
;Socket
直接关闭即可Socket.Close()
。
不了解
TcpClient
的可以查看一下 《C# Tcp协议收发数据(TCPClient发,Socket收)》
不了解Socket
的可以查看一下 《.net网络编程之一:Socket编程》
5.调用异步的 API:
出于灵活性的考虑,我还是选择使用 Socket
来收发数据而不使用封装好的 TcpClient
中获取的 NetworkStream
对象来实现,主要考虑两点:
- 其一,我打算使用2个字节的
ushort
类型数据来表示数据包体的大小,将每次的数据包控制在 65535 byte 之内; - 其二,传输数据类型没有限制。
6.NetworkStream.Write 和 Socket.Send 对比:
这两个都是 TCP 通信中发送数据的接口,但是,NetworkStream
在使用 Write(byte[] buffer, int offset, int size)
传输数据时,会在数据的头部加上一个 int
类型的数据,即传入的 size
参数,用于表示 buffer
的大小,但假如定制网络协议的时候规定只允许在头部使用2个字节表示数据的长度,即一个 short
类型,那此时 NetworkStream
就无法满足需求了,只能使用 Socket
来完成。
参考 官方介绍,C# 中的
ushort
取值范围是0 ~ 65535
,即2个字节表示的包头,一次最大传输数据大小是 65535 byte 大概看为是 64kb。
7.异步收发接口:
- 发送:
使用Socket.BeginSend
和Socket.EnSend
这对 API 来实现异步的数据接收操作; - 接收:
使用Socket.BeginReceive
和``Socket.EnReceive
这对 API 来实现异步的数据接收操作。
案例:
网上也有使用类似的思路对 C# 的 Socket 进行封装的,这里简单列举几个:
参考资料:
《Unity 3D游戏客户端基础框架》多线程异步 Socket 框架构建的更多相关文章
- 《Unity 3D游戏客户端基础框架》概述
框架概述: 做了那么久的业务开发,也做了一年多的核心战斗开发,最近想着自己倒腾一套游戏框架,当然暂不涉及核心玩法类型和战斗框架,核心战斗的设计要根据具体的游戏类型而定制,这里只是一些通用的基础系统的框 ...
- 《Unity 3D游戏客户端基础框架》系统设计
引言 最近到看一个 <贪吃蛇大战开发实例>,其中 贪吃蛇大作战游戏开发实战(3):系统构架设计 提供的系统架构的设计思路我觉得还是值得学习一下的,接下来的内容是我看完视频后的一点笔记. 架 ...
- 《Unity 3D游戏客户端基础框架》消息系统
功能分析: 首先,我们必须先明确一个消息系统的核心功能: 一个通用的事件监听器 管理各个业务监听的事件类型(注册和解绑事件监听器) 全局广播事件 广播事件所传参数数量和数据类型都是可变的(数量可以是 ...
- 可扩展多线程异步Socket服务器框架EMTASS 2.0 (转自:http://blog.csdn.net/hulihui)
可扩展多线程异步Socket服务器框架EMTASS 2.0 (转自:http://blog.csdn.net/hulihui) 0 前言 >>[前言].[第1节].[第2节].[第3节]. ...
- C# 实现的多线程异步Socket数据包接收器框架
转载自Csdn : http://blog.csdn.net/jubao_liang/article/details/4005438 几天前在博问中看到一个C# Socket问题,就想到笔者2004年 ...
- Unity 3D游戏开发学习路线(方法篇)
Unity 3D本来是由德国的一些苹果粉丝开发的一款游戏引擎,一直只能用于Mac平台,所以一直不被业外人士所知晓.但是后来也推出了2.5版,同时发布了PC版本,并将其发布方向拓展到手持移动设备.Uni ...
- 可扩展多线程异步Socket服务器框架EMTASS 2.0 续
转载自Csdn:http://blog.csdn.net/hulihui/article/details/3158613 (原创文章,转载请注明来源:http://blog.csdn.net/huli ...
- 可扩展多线程异步Socket服务器框架EMTASS 2.0
0 前言 >>[前言].[第1节].[第2节].[第3节].[第4节].[第5节].[第6节] 在程序设计与实际应用中,Socket数据包接收服务器够得上一个经典问题了:需要计算机与网络编 ...
- Unity 3D游戏开发引擎:最火的插件推荐
摘要:为了帮助使用Unity引擎的开发人员制作更完美的游戏.我们精心挑选了十款相关开发插件和工具.它们是:2D Toolkit.NGUI.Playmaker.EasyTouch & EasyJ ...
随机推荐
- centos上yum安装nodeJS
更新node.js各版本yum源 Node.js v8.x安装命令 curl --silent --location https://rpm.nodesource.com/setup_8.x | ba ...
- tp基础补充
ThinkPHP php框架 真实项目开发步骤: 多人同时开发项目,协作开发项目.分工合理.效率有提高(代码风格不一样.分工不好) 测试阶段 上线运行 对项目进行维护.修改.升级(单个人维护项目,十分 ...
- MD5—加密,加盐
MD5的参考盐值:String salt = "212*)()()**()^&UYGbakdkj " ; MD5—加密工具类 package com.demo.tools; ...
- js动态创建Form表单并提交
javascript动态创建Form表单和表单项,然后提交表单请求,最后删除表单,代码片段如下(Firefox测试通过): var dlform = document.createElement('f ...
- Redis 资料整理
Redis is an open source, BSD licensed, advanced key-value store. Redis is often referred to as a dat ...
- Python笔记 #13# Pandas: Viewing Data
感觉很详细:数据分析:pandas 基础 import pandas as pd import numpy as np import matplotlib.pyplot as plt dates = ...
- mongodb-的副本集
复制的重要性不再多说,其主要就是提供数据保护,数据高可用和灾难恢复. 复制是跨多个mongodb服务器分布和维护的方法.mongodb可以把数据从一个节点复制到其他节点并在修改时进行同步. mongo ...
- bzoj1008 / P3197 [HNOI2008]越狱
P3197 [HNOI2008]越狱 考虑所有状况:显然是$m^{n}$ 考虑所有不合法状况: 显然相邻两个数不相等 那么后面$n-1$个数就有$(m-1)^{n-1}$种取法 第一个数前面没有相邻的 ...
- P3327/bzoj3994 [SDOI2015]约数个数和(莫比乌斯反演)
P3327 [SDOI2015]约数个数和 神犇题解(转) 无话可补 #include<iostream> #include<cstdio> #include<cstri ...
- 20145201李子璇 《网络对抗》MSF基础应用
实践报告 MS08_067漏洞渗透攻击 ms11_050漏洞攻击 Adobe漏洞攻击 辅助模块应用(auxiliary/scanner/portscan/tcp漏洞) MS08_067漏洞渗透攻击实践 ...