diff --git a/Assets/Mirror/Runtime/NetworkConnection.cs b/Assets/Mirror/Runtime/NetworkConnection.cs index 76b1e9e11..2e3ae79f5 100644 --- a/Assets/Mirror/Runtime/NetworkConnection.cs +++ b/Assets/Mirror/Runtime/NetworkConnection.cs @@ -214,18 +214,24 @@ internal void TransportReceive(ArraySegment buffer) // unpack message using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(buffer)) { - if (MessagePacker.UnpackMessage(networkReader, out int msgType)) + // the other end might batch multiple messages into one packet. + // we need to try to unpack multiple times. + while (networkReader.Remaining > 0) { - // logging - // Debug.Log("ConnectionRecv " + this + " msgType:" + msgType + " content:" + BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count)); + if (MessagePacker.UnpackMessage(networkReader, out int msgType)) + { + // logging + // Debug.Log("ConnectionRecv " + this + " msgType:" + msgType + " content:" + BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count)); - // try to invoke the handler for that message - InvokeHandler(msgType, networkReader); - } - else - { - Debug.LogError("Closed connection: " + this + ". Invalid message header."); - Disconnect(); + // try to invoke the handler for that message + InvokeHandler(msgType, networkReader); + } + else + { + Debug.LogError("Closed connection: " + this + ". Invalid message header."); + Disconnect(); + break; + } } } } diff --git a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs b/Assets/Mirror/Runtime/NetworkConnectionToClient.cs index 3af1c3ae3..88dd83b81 100644 --- a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs +++ b/Assets/Mirror/Runtime/NetworkConnectionToClient.cs @@ -4,10 +4,40 @@ namespace Mirror { public class NetworkConnectionToClient : NetworkConnection { - public NetworkConnectionToClient(int networkConnectionId) : base(networkConnectionId) { } + // batching from server to client. + // fewer transport calls give us significantly better performance/scale. + // + // for a 64KB max message transport and 64 bytes/message on average, we + // reduce transport calls by a factor of 1000. + // + // depending on the transport, this can give 10x performance. + NetworkWriter batch = new NetworkWriter(); + + // batch interval is 0 by default, meaning that we send immediately. + // (useful to run tests without waiting for intervals too) + float batchInterval; + double batchSendTime; + + public NetworkConnectionToClient(int networkConnectionId, float batchInterval) : base(networkConnectionId) + { + this.batchInterval = batchInterval; + } public override string address => Transport.activeTransport.ServerGetClientAddress(connectionId); + void SendBatch(int channelId) + { + // send batch + Transport.activeTransport.ServerSend(connectionId, channelId, batch.ToArraySegment()); + + // clear batch + batch.Position = 0; + + // reset send time + // (use NetworkTime for maximum precision over days) + batchSendTime = NetworkTime.time; + } + internal override bool Send(ArraySegment segment, int channelId = Channels.DefaultReliable) { //Debug.Log("ConnectionSend " + this + " bytes:" + BitConverter.ToString(segment.Array, segment.Offset, segment.Count)); @@ -15,11 +45,45 @@ internal override bool Send(ArraySegment segment, int channelId = Channels // validate packet size first. if (ValidatePacketSize(segment, channelId)) { - return Transport.activeTransport.ServerSend(connectionId, channelId, segment); + // batching? + if (batchInterval > 0) + { + // if batch would become bigger than MaxPacketSize then send + // out the previous batch first + // TODO respect channelId + int max = Transport.activeTransport.GetMaxPacketSize(Channels.DefaultReliable); + if (batch.Position + segment.Count > max) + { + // TODO respect channelId + //Debug.LogWarning($"sending batch {batch.Position} / {max} after full for segment={segment.Count} for connectionId={connectionId}"); + SendBatch(Channels.DefaultReliable); + } + + // now add segment to batch + batch.WriteBytes(segment.Array, segment.Offset, segment.Count); + return true; + } + // otherwise send directly (for tests etc.) + else return Transport.activeTransport.ServerSend(connectionId, channelId, segment); } return false; } + // flush batched messages every batchInterval to make sure that they are + // sent out every now and then, even if the batch isn't full yet. + // (avoids 30s latency if batches would only get full every 30s) + internal void Update() + { + // enough time elapsed and anything batched? + if (NetworkTime.time > batchSendTime + batchInterval && + batch.Position > 0) + { + // TODO respect channelId + //Debug.LogWarning($"sending batch of {batch.Position} bytes after time for connId= {connectionId}"); + SendBatch(Channels.DefaultReliable); + } + } + /// /// Disconnects this connection. /// diff --git a/Assets/Mirror/Runtime/NetworkManager.cs b/Assets/Mirror/Runtime/NetworkManager.cs index d7ca03143..62ac69ee6 100644 --- a/Assets/Mirror/Runtime/NetworkManager.cs +++ b/Assets/Mirror/Runtime/NetworkManager.cs @@ -62,6 +62,13 @@ public class NetworkManager : MonoBehaviour [Tooltip("Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] public int serverTickRate = 30; + // batching from server to client. + // fewer transport calls give us significantly better performance/scale. + // batch interval is 0 by default, meaning that we send immediately. + // (useful to run tests without waiting for intervals too) + [Tooltip("Server can batch messages up to Transport.GetMaxPacketSize to significantly reduce transport calls and improve performance/scale./nNote that this increases latency./n0 means send immediately.")] + public float serverBatchInterval = 0.010f; + // transport layer [Header("Network Info")] [Tooltip("Transport component attached to this object that server and client will use to connect")] @@ -261,6 +268,8 @@ void SetupServer() authenticator.OnServerAuthenticated.AddListener(OnServerAuthenticated); } + NetworkServer.batchInterval = serverBatchInterval; + ConfigureServerFrameRate(); // start listening to network connections diff --git a/Assets/Mirror/Runtime/NetworkServer.cs b/Assets/Mirror/Runtime/NetworkServer.cs index 24adfe512..11ca37745 100644 --- a/Assets/Mirror/Runtime/NetworkServer.cs +++ b/Assets/Mirror/Runtime/NetworkServer.cs @@ -46,6 +46,12 @@ public static class NetworkServer /// public static bool active { get; internal set; } + // batching from server to client. + // fewer transport calls give us significantly better performance/scale. + // batch interval is 0 by default, meaning that we send immediately. + // (useful to run tests without waiting for intervals too) + public static float batchInterval = 0; + /// /// This shuts down the server and disconnects all clients. /// @@ -382,6 +388,12 @@ public static void Update() Debug.LogWarning("Found 'null' entry in spawned list for netId=" + kvp.Key + ". Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy."); } } + + // update all connections to send out batched messages in interval + foreach (NetworkConnectionToClient conn in connections.Values) + { + conn.Update(); + } } static void OnConnected(int connectionId) @@ -412,7 +424,7 @@ static void OnConnected(int connectionId) if (connections.Count < maxConnections) { // add connection - NetworkConnectionToClient conn = new NetworkConnectionToClient(connectionId); + NetworkConnectionToClient conn = new NetworkConnectionToClient(connectionId, batchInterval); OnConnected(conn); } else diff --git a/Assets/Mirror/Tests/Common/FakeNetworkConnection.cs b/Assets/Mirror/Tests/Common/FakeNetworkConnection.cs index ac4f6a436..0aae22963 100644 --- a/Assets/Mirror/Tests/Common/FakeNetworkConnection.cs +++ b/Assets/Mirror/Tests/Common/FakeNetworkConnection.cs @@ -4,7 +4,7 @@ namespace Mirror.Tests { public class FakeNetworkConnection : NetworkConnectionToClient { - public FakeNetworkConnection() : base(1) + public FakeNetworkConnection() : base(1, 0) { } diff --git a/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs b/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs index fed8e97cd..042515378 100644 --- a/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs +++ b/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs @@ -865,9 +865,9 @@ public void Reset() // creates .observers and generates a netId identity.OnStartServer(); uint netId = identity.netId; - identity.connectionToClient = new NetworkConnectionToClient(1); + identity.connectionToClient = new NetworkConnectionToClient(1, 0); identity.connectionToServer = new NetworkConnectionToServer(); - identity.observers.Add(new NetworkConnectionToClient(2)); + identity.observers.Add(new NetworkConnectionToClient(2, 0)); // mark for reset and reset identity.Reset(); @@ -882,7 +882,7 @@ public void HandleCommand() { // add component CommandTestNetworkBehaviour comp0 = gameObject.AddComponent(); - NetworkConnectionToClient connection = new NetworkConnectionToClient(1); + NetworkConnectionToClient connection = new NetworkConnectionToClient(1, 0); Assert.That(comp0.called, Is.EqualTo(0)); Assert.That(comp0.senderConnectionInCall, Is.Null); diff --git a/Assets/Mirror/Tests/Editor/NetworkServerTest.cs b/Assets/Mirror/Tests/Editor/NetworkServerTest.cs index bf72d0725..b50a2f113 100644 --- a/Assets/Mirror/Tests/Editor/NetworkServerTest.cs +++ b/Assets/Mirror/Tests/Editor/NetworkServerTest.cs @@ -256,7 +256,7 @@ public void AddConnectionTest() Assert.That(NetworkServer.connections.Count, Is.EqualTo(0)); // add first connection - NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42); + NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42, 0); bool result42 = NetworkServer.AddConnection(conn42); Assert.That(result42, Is.True); Assert.That(NetworkServer.connections.Count, Is.EqualTo(1)); @@ -264,7 +264,7 @@ public void AddConnectionTest() Assert.That(NetworkServer.connections[42], Is.EqualTo(conn42)); // add second connection - NetworkConnectionToClient conn43 = new NetworkConnectionToClient(43); + NetworkConnectionToClient conn43 = new NetworkConnectionToClient(43, 0); bool result43 = NetworkServer.AddConnection(conn43); Assert.That(result43, Is.True); Assert.That(NetworkServer.connections.Count, Is.EqualTo(2)); @@ -274,7 +274,7 @@ public void AddConnectionTest() Assert.That(NetworkServer.connections[43], Is.EqualTo(conn43)); // add duplicate connectionId - NetworkConnectionToClient connDup = new NetworkConnectionToClient(42); + NetworkConnectionToClient connDup = new NetworkConnectionToClient(42, 0); bool resultDup = NetworkServer.AddConnection(connDup); Assert.That(resultDup, Is.False); Assert.That(NetworkServer.connections.Count, Is.EqualTo(2)); @@ -297,7 +297,7 @@ public void RemoveConnectionTest() Assert.That(NetworkServer.connections.Count, Is.EqualTo(0)); // add connection - NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42); + NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42, 0); bool result42 = NetworkServer.AddConnection(conn42); Assert.That(result42, Is.True); Assert.That(NetworkServer.connections.Count, Is.EqualTo(1)); @@ -323,7 +323,7 @@ public void DisconnectAllConnectionsTest() Assert.That(NetworkServer.connections.Count, Is.EqualTo(0)); // add connection - NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42); + NetworkConnectionToClient conn42 = new NetworkConnectionToClient(42, 0); NetworkServer.AddConnection(conn42); Assert.That(NetworkServer.connections.Count, Is.EqualTo(1)); @@ -356,7 +356,7 @@ public void OnDataReceivedTest() Assert.That(NetworkServer.connections.Count, Is.EqualTo(0)); // add a connection - NetworkConnectionToClient connection = new NetworkConnectionToClient(42); + NetworkConnectionToClient connection = new NetworkConnectionToClient(42, 0); NetworkServer.AddConnection(connection); Assert.That(NetworkServer.connections.Count, Is.EqualTo(1)); @@ -440,7 +440,7 @@ public void RegisterUnregisterClearHandlerTest() Assert.That(NetworkServer.connections.Count, Is.EqualTo(0)); // add a connection - NetworkConnectionToClient connection = new NetworkConnectionToClient(42); + NetworkConnectionToClient connection = new NetworkConnectionToClient(42, 0); NetworkServer.AddConnection(connection); Assert.That(NetworkServer.connections.Count, Is.EqualTo(1)); diff --git a/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs b/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs index f661e5df5..34c83127f 100644 --- a/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs +++ b/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs @@ -36,7 +36,7 @@ public void TearDown() public IEnumerator DestroyPlayerForConnectionTest() { GameObject player = new GameObject("testPlayer", typeof(NetworkIdentity)); - NetworkConnectionToClient conn = new NetworkConnectionToClient(1); + NetworkConnectionToClient conn = new NetworkConnectionToClient(1, 0); NetworkServer.AddPlayerForConnection(conn, player); @@ -55,7 +55,7 @@ public IEnumerator DestroyPlayerForConnectionTest() public IEnumerator RemovePlayerForConnectionTest() { GameObject player = new GameObject("testPlayer", typeof(NetworkIdentity)); - NetworkConnectionToClient conn = new NetworkConnectionToClient(1); + NetworkConnectionToClient conn = new NetworkConnectionToClient(1, 0); NetworkServer.AddPlayerForConnection(conn, player);