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

568 阅读4分钟

文/罗培羽

开发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版)》使用多路复用也就避开了这个问题。

最后依然还是放个广告,笔者近期出版的《Unity3D网络游戏实战(第2版)》详细介绍网络游戏的开发的全过程,比起第1版,更加注重网络编程的知识。看完本书,能够亲手从零开始制作一款有一定规模的网络游戏。