123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- using System;
- using System.Collections.Generic;
- using System.Runtime.CompilerServices;
- using System.Text;
- using UnityEngine;
- namespace Mirror
- {
- // for performance, we (ab)use c# generics to cache the message id in a static field
- // this is significantly faster than doing the computation at runtime or looking up cached results via Dictionary
- // generic classes have separate static fields per type specification
- public static class NetworkMessageId<T> where T : struct, NetworkMessage
- {
- // automated message id from type hash.
- // platform independent via stable hashcode.
- // => convenient so we don't need to track messageIds across projects
- // => addons can work with each other without knowing their ids before
- // => 2 bytes is enough to avoid collisions.
- // registering a messageId twice will log a warning anyway.
- public static readonly ushort Id = CalculateId();
- // Gets the 32bit fnv1a hash
- // To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort
- // Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits
- // This will create a more uniform 16bit hash, the method is described in:
- // http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding"
- static ushort CalculateId() => typeof(T).FullName.GetStableHashCode16();
- }
- // message packing all in one place, instead of constructing headers in all
- // kinds of different places
- //
- // MsgType (2 bytes)
- // Content (ContentSize bytes)
- public static class NetworkMessages
- {
- // size of message id header in bytes
- public const int IdSize = sizeof(ushort);
- // Id <> Type lookup for debugging, profiler, etc.
- // important when debugging messageId errors!
- public static readonly Dictionary<ushort, Type> Lookup =
- new Dictionary<ushort, Type>();
- // dump all types for debugging
- public static void LogTypes()
- {
- StringBuilder builder = new StringBuilder();
- builder.AppendLine("NetworkMessageIds:");
- foreach (KeyValuePair<ushort, Type> kvp in Lookup)
- {
- builder.AppendLine($" Id={kvp.Key} = {kvp.Value}");
- }
- Debug.Log(builder.ToString());
- }
- // max message content size (without header) calculation for convenience
- // -> Transport.GetMaxPacketSize is the raw maximum
- // -> Every message gets serialized into <<id, content>>
- // -> Every serialized message get put into a batch with one timestamp per batch
- // -> Every message in a batch has a varuint size header.
- // use the worst case VarUInt size for the largest possible
- // message size = int.max.
- public static int MaxContentSize(int channelId)
- {
- // calculate the max possible size that can fit in a batch
- int transportMax = Transport.active.GetMaxPacketSize(channelId);
- return transportMax - IdSize - Batcher.MaxMessageOverhead(transportMax);
- }
- // max message size which includes header + content.
- public static int MaxMessageSize(int channelId) =>
- MaxContentSize(channelId) + IdSize;
- // automated message id from type hash.
- // platform independent via stable hashcode.
- // => convenient so we don't need to track messageIds across projects
- // => addons can work with each other without knowing their ids before
- // => 2 bytes is enough to avoid collisions.
- // registering a messageId twice will log a warning anyway.
- // keep this for convenience. easier to use than NetworkMessageId<T>.Id.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static ushort GetId<T>() where T : struct, NetworkMessage =>
- NetworkMessageId<T>.Id;
- // pack message before sending
- // -> NetworkWriter passed as arg so that we can use .ToArraySegment
- // and do an allocation free send before recycling it.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void Pack<T>(T message, NetworkWriter writer)
- where T : struct, NetworkMessage
- {
- writer.WriteUShort(NetworkMessageId<T>.Id);
- writer.Write(message);
- }
- // read only the message id.
- // common function in case we ever change the header size.
- public static bool UnpackId(NetworkReader reader, out ushort messageId)
- {
- // read message type
- try
- {
- messageId = reader.ReadUShort();
- return true;
- }
- catch (System.IO.EndOfStreamException)
- {
- messageId = 0;
- return false;
- }
- }
- // version for handlers with channelId
- // inline! only exists for 20-30 messages and they call it all the time.
- internal static NetworkMessageDelegate WrapHandler<T, C>(Action<C, T, int> handler, bool requireAuthentication, bool exceptionsDisconnect)
- where T : struct, NetworkMessage
- where C : NetworkConnection
- => (conn, reader, channelId) =>
- {
- // protect against DOS attacks if attackers try to send invalid
- // data packets to crash the server/client. there are a thousand
- // ways to cause an exception in data handling:
- // - invalid headers
- // - invalid message ids
- // - invalid data causing exceptions
- // - negative ReadBytesAndSize prefixes
- // - invalid utf8 strings
- // - etc.
- //
- // let's catch them all and then disconnect that connection to avoid
- // further attacks.
- T message = default;
- // record start position for NetworkDiagnostics because reader might contain multiple messages if using batching
- int startPos = reader.Position;
- try
- {
- if (requireAuthentication && !conn.isAuthenticated)
- {
- // message requires authentication, but the connection was not authenticated
- Debug.LogWarning($"Disconnecting connection: {conn}. Received message {typeof(T)} that required authentication, but the user has not authenticated yet");
- conn.Disconnect();
- return;
- }
- //Debug.Log($"ConnectionRecv {conn} msgType:{typeof(T)} content:{BitConverter.ToString(reader.buffer.Array, reader.buffer.Offset, reader.buffer.Count)}");
- // if it is a value type, just use default(T)
- // otherwise allocate a new instance
- message = reader.Read<T>();
- }
- catch (Exception exception)
- {
- // should we disconnect on exceptions?
- if (exceptionsDisconnect)
- {
- Debug.LogError($"Disconnecting connection: {conn} because reading a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}");
- conn.Disconnect();
- return;
- }
- // otherwise log it but allow the connection to keep playing
- else
- {
- Debug.LogError($"Caught an Exception when reading a message from: {conn} of type {typeof(T)}. Reason: {exception}");
- return;
- }
- }
- finally
- {
- int endPos = reader.Position;
- // TODO: Figure out the correct channel
- NetworkDiagnostics.OnReceive(message, channelId, endPos - startPos);
- }
- // user handler exception should not stop the whole server
- try
- {
- // user implemented handler
- handler((C)conn, message, channelId);
- }
- catch (Exception exception)
- {
- // should we disconnect on exceptions?
- if (exceptionsDisconnect)
- {
- Debug.LogError($"Disconnecting connection: {conn} because handling a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}");
- conn.Disconnect();
- }
- // otherwise log it but allow the connection to keep playing
- else
- {
- Debug.LogError($"Caught an Exception when handling a message from: {conn} of type {typeof(T)}. Reason: {exception}");
- }
- }
- };
- // version for handlers without channelId
- // TODO obsolete this some day to always use the channelId version.
- // all handlers in this version are wrapped with 1 extra action.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static NetworkMessageDelegate WrapHandler<T, C>(Action<C, T> handler, bool requireAuthentication, bool exceptionsDisconnect)
- where T : struct, NetworkMessage
- where C : NetworkConnection
- {
- // wrap action as channelId version, call original
- void Wrapped(C conn, T msg, int _) => handler(conn, msg);
- return WrapHandler((Action<C, T, int>)Wrapped, requireAuthentication, exceptionsDisconnect);
- }
- }
- }
|