引言:

之前写过一个 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)
{
    ...
}

网上还有另一种方案 Unity 异步网络方案 IOCP Socket + ThreadSafe Queue

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.WriteNetworkStream.Read 完成网络数据的发送和接收;
    • Socket 连接完成后,直接使用最开始创建的 Socket 套接字对象调用 Socket.SendScoket.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.BeginSendSocket.EnSend 这对 API 来实现异步的数据接收操作;
  • 接收:

    使用 Socket.BeginReceive``Socket.EnReceive 这对 API 来实现异步的数据接收操作。

案例:

网上也有使用类似的思路对 C# 的 Socket 进行封装的,这里简单列举几个:

参考资料:

《Unity 3D游戏客户端基础框架》多线程异步 Socket 框架构建的更多相关文章

  1. 《Unity 3D游戏客户端基础框架》概述

    框架概述: 做了那么久的业务开发,也做了一年多的核心战斗开发,最近想着自己倒腾一套游戏框架,当然暂不涉及核心玩法类型和战斗框架,核心战斗的设计要根据具体的游戏类型而定制,这里只是一些通用的基础系统的框 ...

  2. 《Unity 3D游戏客户端基础框架》系统设计

    引言 最近到看一个 <贪吃蛇大战开发实例>,其中 贪吃蛇大作战游戏开发实战(3):系统构架设计 提供的系统架构的设计思路我觉得还是值得学习一下的,接下来的内容是我看完视频后的一点笔记. 架 ...

  3. 《Unity 3D游戏客户端基础框架》消息系统

    功能分析: 首先,我们必须先明确一个消息系统的核心功能: 一个通用的事件监听器 管理各个业务监听的事件类型(注册和解绑事件监听器) 全局广播事件 广播事件所传参数数量和数据类型都是可变的(数量可以是 ...

  4. 可扩展多线程异步Socket服务器框架EMTASS 2.0 (转自:http://blog.csdn.net/hulihui)

    可扩展多线程异步Socket服务器框架EMTASS 2.0 (转自:http://blog.csdn.net/hulihui) 0 前言 >>[前言].[第1节].[第2节].[第3节]. ...

  5. C# 实现的多线程异步Socket数据包接收器框架

    转载自Csdn : http://blog.csdn.net/jubao_liang/article/details/4005438 几天前在博问中看到一个C# Socket问题,就想到笔者2004年 ...

  6. Unity 3D游戏开发学习路线(方法篇)

    Unity 3D本来是由德国的一些苹果粉丝开发的一款游戏引擎,一直只能用于Mac平台,所以一直不被业外人士所知晓.但是后来也推出了2.5版,同时发布了PC版本,并将其发布方向拓展到手持移动设备.Uni ...

  7. 可扩展多线程异步Socket服务器框架EMTASS 2.0 续

    转载自Csdn:http://blog.csdn.net/hulihui/article/details/3158613 (原创文章,转载请注明来源:http://blog.csdn.net/huli ...

  8. 可扩展多线程异步Socket服务器框架EMTASS 2.0

    0 前言 >>[前言].[第1节].[第2节].[第3节].[第4节].[第5节].[第6节] 在程序设计与实际应用中,Socket数据包接收服务器够得上一个经典问题了:需要计算机与网络编 ...

  9. Unity 3D游戏开发引擎:最火的插件推荐

    摘要:为了帮助使用Unity引擎的开发人员制作更完美的游戏.我们精心挑选了十款相关开发插件和工具.它们是:2D Toolkit.NGUI.Playmaker.EasyTouch & EasyJ ...

随机推荐

  1. CSLA.Net学习(3)INotifyPropertyChanged和IDataErrorInfo

    今天晕晕糊糊的看CSLA.net,希望能找到验证数据正确性的方法,还是摸索出了INotifyPropertyChanged, IDataErrorInfo接口的使用方法,通过INotifyProper ...

  2. MySQL多个相同结构的表查询并把结果合并放在一起的语句(union all)

    union all select *,'1' as category from table1001 where price > 10 union all select *,'2' as cate ...

  3. 上传文件小的oK,大一点的传不了,显示 (failed) net::ERR_CONNECTION_RESET

    我很确定已经修改了php.ini中的文件上传限制,文件权限可写. 修改php.ini file_uploads = on ;是否允许通过HTTP上传文件的开关.默认为ON即是开 upload_tmp_ ...

  4. [C#]解决程序Vista/Win7下因UAC导致的读写错误

    在微软的操作系统中,vista和win7加入了UAC的功能,UAC(User Account Control,用户帐户控制)是微软为提高系统安全而在Windows Vista中引入的新技术,它要求用户 ...

  5. mysql慢日志

    mysql慢日志是用来记录执行时间比较长的sql工具(超过long_query_time的sql),这样对于跟踪有问题的sql很有帮助. 查看是否启用慢日志和相关信息 上面截图其中: log_slow ...

  6. SecureCRT 下载,安装,绝佳配色,实用配置,上传下载配置合集

    SecureCRT 下载,安装,绝佳配色,实用配置,上传下载配置合集 chocoball 发布于 2年前,共有 3 条评论 SecureCRT 是一款支持 SSH2.SSH1.Telnet.Telne ...

  7. hdu 5111 树链剖分加函数式线段树

    这题说的是给了两棵树,各有100000 个节点,然后Q个操作Q<=50000; 每个操作L1 R1 L2 R2.因为对于每棵树都有一个与本棵树其他点与众不同的值, 最后问 在树上从L1到R1这条 ...

  8. Python中的MySQL接口:PyMySQL & MySQLdb

    MySQLdb模块只支持MySQL-3.23到5.5之间的版本,只支持Python-2.4到2.7之间的版本 PyMySQL支持 Python3.0以后的版本 PyMySQL https://pypi ...

  9. python基础学习十 logging模块详细使用【转载】

    很多程序都有记录日志的需求,并且日志中包含的信息既有正常的程序访问日志,还可能有错误.警告等信息输出,python的logging模块提供了标准的日志接口,你可以通过它存储各种格式的日志,主要用于输出 ...

  10. 前端学习笔记之HTML/CSS 速写神器 Emmet

    HTML/CSS 速写神器:Emmet 在前端开发的过程中,一个最繁琐的工作就是写 HTML.CSS 代码.数量繁多的标签.属性.尖括号.标签闭合等,让前端们甚是苦恼.于是,我向大家推荐 Emmet, ...