Unity Mirror 网络消息详解(2)

·  阅读 60

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第41天,点击查看活动详情

网络消息底层机制

消息的打包和发送机制

Mirror并不是在connection上调用Send就直接通过网络发送一个消息包。Mirror会对消息进行打包,然后在一帧的结束统一发送。

Batch队列

在Send方法内部,调用GetBatchForChannelId(channelId).AddMessage(segment, NetworkTime.localTime);将消息添加到batch中。在Batcher类中,有一个队列:

Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
复制代码

这就是Batch队列,这儿的batch是NetworkWriterPooled对象。

AddMessage

在AddMessage中,会向当前操作的batch写入数据:

batch.WriteBytes(message.Array, message.Offset, message.Count); 如果当前batch容器不足以添加这个消息,则将它插入到batches队列中,并从池中获取一个新的batc进行操作。

if (batch != null &&
    batch.Position + message.Count > threshold)
{
    batches.Enqueue(batch);
    batch = null;
}

// initialize a new batch if necessary
if (batch == null)
{
    // borrow from pool. we return it in GetBatch.
    batch = NetworkWriterPool.Get();

    // write timestamp first.
    // -> double precision for accuracy over long periods of time
    // -> batches are per-frame, it doesn't matter which message's
    //    timestamp we use.
    batch.WriteDouble(timeStamp);
}
复制代码

消息发送

NetworkConnectionUpdate方法中,会遍历batches队列,调用SendToTransport将消息发送出去。

using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{    
    while (batcher.GetBatch(writer))
    {        
        ArraySegment<byte> segment = writer.ToArraySegment();
        if (ValidatePacketSize(segment, kvp.Key))
        {
            // send to transport
            SendToTransport(segment, kvp.Key);
            //UnityEngine.Debug.Log($"sending batch of {writer.Position} bytes for channel={kvp.Key} connId={connectionId}");

            // reset writer for each new batch
            writer.Position = 0;
        }
    }
}
复制代码

Mirror的消息打包和批量发送机制可以减少消息数量,由于网络消息是高层消息,为了发送它们会将它们包装到底层的网络包上,比如UDP包,如果每个UDP只携带很少的信息无疑是一种浪费。Mirror中每个消息设置为不超过1500字节,这是以太网的MTU,当然这儿的计算不是特别精确,因为UDP以及上层协议的包头都会占用一部分MTU,但总的来说,1500字节或者更大一些的UDP或TCP包是一个比较稳的容量。而游戏消息本身一般都比较小,所以一帧一般各种消息打包到一起也就能发送一个包。由于Mirror的这个机制,你可以自由的定义消息,消息体可以很小,因为你知道消息会被合并,不会浪费MTU。

自定义消息

再研究完Mirror的网络消息机制后,自己实现自定义消息是非常简单的。这儿我们实现一个从服务器发送给客户端的世界时间消息。虽然Mirror提供了NetworkTime,可以在客户端获取到RTT修正后的服务器时间。但是某些游戏类型,比如模拟来的游戏,会有一个逻辑上的世界时间,这个时间的流逝速度和真实时间成一个比例,并且有可能根据游戏玩法进行加速减速暂停等。因此我们不能直接使用服务器时间。为此我们在服务器上计算这个时间,并且发送网络消息。

消息定义

public struct WorldTimeMessage : NetworkMessage
{
    public float WorldCurrentTime;
}
复制代码

非常简单,只包含一个float。float的精度对于我们已经足够了,因为并不是在客户端计算的,完全是从服务器每帧同步,所以不会有累积误差,唯一有的误差就是RTT,不过可以根据游戏需求是否要把RTT考虑在内,这儿我们就忽略RTT的影响了,只是简单同步服务器上的世界时间。

客户端注册消息Handler

[Client]
private void OnClientConnected()
{      
    NetworkClient.RegisterHandler<WorldTimeMessage>(OnWorldTimeMessage);                       
}
复制代码

当客户端连接时,即注册消息Handler,如果注册晚了,当世界时间开始更新,发送WorldTimeMessage过来,如果找不到Handler,就会有警告说找不到Handler。

消息发送

[ServerCallback]
void ServerUpdate()
{
    if(worldTimer.isRunning())
    {
        worldTimer.UpdateTime();
        if(NetworkServer.connections.Count > 0)
        {                    
            NetworkServer.SendToAll(new WorldTimeMessage() { WorldCurrentTime = worldTimer.CurrentTime }, Channels.Unreliable, true);
        }
    }
    
}
复制代码

当世界时钟在走时,我们在服务器更新时间时钟,并且如果有客户端连接存在,则使用SendToAll发送WorldTimeMessageworldTimer是服务器上的世界时钟对象,负责世界时间的更新和计算。SendToAll使用了Channels.Unreliable信道,因为这个消息是否丢失不是很重要,毕竟每帧都在发送,所以使用该信道。

Handler实现

[ClientCallback]            
public void OnWorldTimeMessage(WorldTimeMessage msg)
{
    worldTimer.SetCurrentTime(msg.WorldCurrentTime);
    TODSystem.Update();
}
复制代码

这儿的worldTimer是客户端的世界时钟对象,该时钟不会自己更新,只能在这儿从消息中获取时间设置给自己。然后我们的客户端TOD系统也在这儿更新,TOD系统内部会使用worldTimer中的时间。

总结

Mirror的Network Message机制其实还是很好用的,虽然是底层机制,但是Mirror替我们实现了很多细节,包括消息的打包和批量发送,另外这儿没有说的是默认的序列化机制,Mirror支持的类型可以自动生成序列化代码,将消息体序列化然后进行打包。

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改