设计Unity网络模块,该用异步还是多路复用?

2019-01-15 5.5k
开发Unity网络模块时,一般会有异步(多线程同理)、多路复用两种方法。它们分别是什么?以及孰优孰劣呢?

在《Unity3D网络游戏实战(第2版)》中,客户端使用了异步,服务端使用了多路复用。有读者问到为什么这么做,为什么不在客户端使用多路复用呢?这个问题很多人会遇到,决定写一篇文章说明这个问题。

我们先简单看看异步和多路复用是什么样子。

异步

异步程序的写法如下,会调用.net网络编程的API,使用BeginXXX和EndXXX这样的语法。实际上,程序内部会开启另外的线程去接收数据。

  1. public class Echo : MonoBehaviour {

  2.     Socket socket;

  3.     //接收缓冲区
  4.     byte[] readBuff = new byte[1024];

  5.     public Start()
  6.     {
  7.         //Socket
  8.         socket = new Socket(AddressFamily.InterNetwork,
  9.             SocketType.Stream, ProtocolType.Tcp);
  10.         //Connect
  11.         socket.Connect("127.0.0.1", 8888);
  12.         //BeginReceive
  13.         socket.BeginReceive( readBuff, 0, 1024, 0, ReceiveCallback, socket);
  14.     }

  15.     //Receive回调
  16.     public void ReceiveCallback(IAsyncResult ar){
  17.         Socket socket = (Socket) ar.AsyncState;
  18.         int count = socket.EndReceive(ar);
  19.         string s = System.Text.Encoding.Default.GetString(readBuff, 0, count);
  20.         socket.BeginReceive( readBuff, 0, 1024, 0,ReceiveCallback, socket);
  21.     }
  22. }
复制代码

图示如下,调用BeginReceive后,程序就开启了一条新的线程,在新的线程里阻塞等待。等有消息回来时,才往下执行。



多路复用

异步程序写起来比较麻烦,而且代码量多,其实有一种更简便的处理方法,那就是使用poll或select。使用poll的代码如下。

  1. //省略各种using
  2. public class Echo : MonoBehaviour {

  3.     //定义套接字
  4.     Socket socket;

  5.     public void Start()
  6.     {
  7.         //Socket
  8.         socket = new Socket(AddressFamily.InterNetwork,
  9.             SocketType.Stream, ProtocolType.Tcp);
  10.         //Connect
  11.         socket.Connect("127.0.0.1", 8888);
  12.     }

  13.     public void Update(){
  14.         if(socket.Poll(0, SelectMode.SelectRead)){
  15.             byte[] readBuff = new byte[1024];
  16.             int count = socket.Receive(readBuff);
  17.             string recvStr =
  18.                           System.Text.Encoding.Default.GetString(readBuff, 0, count);
  19.         }
  20.     }
  21. }
复制代码

无论如何,这段代码比异步要少一些。它的原理是使用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版)》使用多路复用也就避开了这个问题。

相关推荐

最新评论
暂无评论
参与评论

商务合作 查看更多

独立游戏 查看更多