|
文/罗培羽
开发Unity网络模块时,一般会有异步(多线程同理)、多路复用两种方法。它们分别是什么?以及孰优孰劣呢?
在《Unity3D网络游戏实战(第2版)》中,客户端使用了异步,服务端使用了多路复用。有读者问到为什么这么做,为什么不在客户端使用多路复用呢?这个问题很多人会遇到,决定写一篇文章说明这个问题。
我们先简单看看异步和多路复用是什么样子。
异步
异步程序的写法如下,会调用.net网络编程的API,使用BeginXXX和EndXXX这样的语法。实际上,程序内部会开启另外的线程去接收数据。
- public class Echo : MonoBehaviour {
- Socket socket;
- //接收缓冲区
- byte[] readBuff = new byte[1024];
- public Start()
- {
- //Socket
- socket = new Socket(AddressFamily.InterNetwork,
- SocketType.Stream, ProtocolType.Tcp);
- //Connect
- socket.Connect("127.0.0.1", 8888);
- //BeginReceive
- socket.BeginReceive( readBuff, 0, 1024, 0, ReceiveCallback, socket);
- }
- //Receive回调
- public void ReceiveCallback(IAsyncResult ar){
- Socket socket = (Socket) ar.AsyncState;
- int count = socket.EndReceive(ar);
- string s = System.Text.Encoding.Default.GetString(readBuff, 0, count);
- socket.BeginReceive( readBuff, 0, 1024, 0,ReceiveCallback, socket);
- }
- }
复制代码
图示如下,调用BeginReceive后,程序就开启了一条新的线程,在新的线程里阻塞等待。等有消息回来时,才往下执行。
多路复用
异步程序写起来比较麻烦,而且代码量多,其实有一种更简便的处理方法,那就是使用poll或select。使用poll的代码如下。
- //省略各种using
- public class Echo : MonoBehaviour {
- //定义套接字
- Socket socket;
- public void Start()
- {
- //Socket
- socket = new Socket(AddressFamily.InterNetwork,
- SocketType.Stream, ProtocolType.Tcp);
- //Connect
- socket.Connect("127.0.0.1", 8888);
- }
- public void Update(){
- if(socket.Poll(0, SelectMode.SelectRead)){
- byte[] readBuff = new byte[1024];
- int count = socket.Receive(readBuff);
- string recvStr =
- System.Text.Encoding.Default.GetString(readBuff, 0, count);
- }
- }
- }
复制代码
无论如何,这段代码比异步要少一些。它的原理是使用socket.Poll,但socket有可读数据时,该方法返回true,如果没有返回false。那么程序只要在Update中不断去检测socket的状态,有数据的时候才去读取,也可以实现功能。
客户端为什么使用异步
我见过的大多数游戏程序使用异步或者多线程去处理网络模块,这就产生了个疑问,多路复用代码量少写起来更简单,但为什么不使用呢?在小型项目中其实使用哪种方式都没有太大区别,但当我们要考虑网络性能的时候,就要仔细斟酌了,客户端不使用多路复用出于以下两个原因。
1、不断遍历
由上述程序可知,poll模式中,程序要在Update不断检测,可能每秒要检测30到60次,增加了计算量。而异步就没有这个问题,在网络消息到达的时候,线程被唤起,不需要遍历。
2、对主线程的影响
当接收到网络数据时,例子中使用了Encoding.Default.GetString把字节流转换成字符串,在实际游戏中,可能使用protobuf或者json协议,把字节流解析成协议对象有一定的计算量。在下图中,异步程序可以在异步线程中做解码,使得程序不会因为解码而卡住主线程。而Poll程序就做不到这一点,它在主线程中解码。Unity的脚本逻辑(Awake、Start、Update、碰撞、cpu渲染部分)都依赖于主线程,网络模块对主线程的影响越小,性能就越好。
服务端为什么使用多路复用
客户端和服务端多面临的情况不同,客户端一般只需要维持一个连接,而服务需要维持所有客户端的连接。多路复用为何取名叫“多路”,其核心就是要解决“多个连接”的问题。
因为服务端要处理各个玩家的逻辑,玩家之间可能还有交互,比如下图中,玩家1和玩家2都在一个房间内。
如果使用了多线程(异步),那么玩家1和玩家2的操作就有可能出现线程冲突,在处理逻辑时需要给房间对象加锁,《Unity3D网络游戏实战(第1版)》使用的就是这种方式。如果避开多线程,加锁的问题也就不复存在了,逻辑会更加明了,出Bug的可能性也会减少。多线程处理并不是一个简单的事情,需要很多经验积累才能处理好,《Unity3D网络游戏实战(第2版)》使用多路复用也就避开了这个问题。
|
|