NetworkMessages.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Runtime.CompilerServices;
  4. using System.Text;
  5. using UnityEngine;
  6. namespace Mirror
  7. {
  8. // for performance, we (ab)use c# generics to cache the message id in a static field
  9. // this is significantly faster than doing the computation at runtime or looking up cached results via Dictionary
  10. // generic classes have separate static fields per type specification
  11. public static class NetworkMessageId<T> where T : struct, NetworkMessage
  12. {
  13. // automated message id from type hash.
  14. // platform independent via stable hashcode.
  15. // => convenient so we don't need to track messageIds across projects
  16. // => addons can work with each other without knowing their ids before
  17. // => 2 bytes is enough to avoid collisions.
  18. // registering a messageId twice will log a warning anyway.
  19. public static readonly ushort Id = CalculateId();
  20. // Gets the 32bit fnv1a hash
  21. // To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort
  22. // Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits
  23. // This will create a more uniform 16bit hash, the method is described in:
  24. // http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding"
  25. static ushort CalculateId() => typeof(T).FullName.GetStableHashCode16();
  26. }
  27. // message packing all in one place, instead of constructing headers in all
  28. // kinds of different places
  29. //
  30. // MsgType (2 bytes)
  31. // Content (ContentSize bytes)
  32. public static class NetworkMessages
  33. {
  34. // size of message id header in bytes
  35. public const int IdSize = sizeof(ushort);
  36. // Id <> Type lookup for debugging, profiler, etc.
  37. // important when debugging messageId errors!
  38. public static readonly Dictionary<ushort, Type> Lookup =
  39. new Dictionary<ushort, Type>();
  40. // dump all types for debugging
  41. public static void LogTypes()
  42. {
  43. StringBuilder builder = new StringBuilder();
  44. builder.AppendLine("NetworkMessageIds:");
  45. foreach (KeyValuePair<ushort, Type> kvp in Lookup)
  46. {
  47. builder.AppendLine($" Id={kvp.Key} = {kvp.Value}");
  48. }
  49. Debug.Log(builder.ToString());
  50. }
  51. // max message content size (without header) calculation for convenience
  52. // -> Transport.GetMaxPacketSize is the raw maximum
  53. // -> Every message gets serialized into <<id, content>>
  54. // -> Every serialized message get put into a batch with one timestamp per batch
  55. // -> Every message in a batch has a varuint size header.
  56. // use the worst case VarUInt size for the largest possible
  57. // message size = int.max.
  58. public static int MaxContentSize(int channelId)
  59. {
  60. // calculate the max possible size that can fit in a batch
  61. int transportMax = Transport.active.GetMaxPacketSize(channelId);
  62. return transportMax - IdSize - Batcher.MaxMessageOverhead(transportMax);
  63. }
  64. // max message size which includes header + content.
  65. public static int MaxMessageSize(int channelId) =>
  66. MaxContentSize(channelId) + IdSize;
  67. // automated message id from type hash.
  68. // platform independent via stable hashcode.
  69. // => convenient so we don't need to track messageIds across projects
  70. // => addons can work with each other without knowing their ids before
  71. // => 2 bytes is enough to avoid collisions.
  72. // registering a messageId twice will log a warning anyway.
  73. // keep this for convenience. easier to use than NetworkMessageId<T>.Id.
  74. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  75. public static ushort GetId<T>() where T : struct, NetworkMessage =>
  76. NetworkMessageId<T>.Id;
  77. // pack message before sending
  78. // -> NetworkWriter passed as arg so that we can use .ToArraySegment
  79. // and do an allocation free send before recycling it.
  80. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  81. public static void Pack<T>(T message, NetworkWriter writer)
  82. where T : struct, NetworkMessage
  83. {
  84. writer.WriteUShort(NetworkMessageId<T>.Id);
  85. writer.Write(message);
  86. }
  87. // read only the message id.
  88. // common function in case we ever change the header size.
  89. public static bool UnpackId(NetworkReader reader, out ushort messageId)
  90. {
  91. // read message type
  92. try
  93. {
  94. messageId = reader.ReadUShort();
  95. return true;
  96. }
  97. catch (System.IO.EndOfStreamException)
  98. {
  99. messageId = 0;
  100. return false;
  101. }
  102. }
  103. // version for handlers with channelId
  104. // inline! only exists for 20-30 messages and they call it all the time.
  105. internal static NetworkMessageDelegate WrapHandler<T, C>(Action<C, T, int> handler, bool requireAuthentication, bool exceptionsDisconnect)
  106. where T : struct, NetworkMessage
  107. where C : NetworkConnection
  108. => (conn, reader, channelId) =>
  109. {
  110. // protect against DOS attacks if attackers try to send invalid
  111. // data packets to crash the server/client. there are a thousand
  112. // ways to cause an exception in data handling:
  113. // - invalid headers
  114. // - invalid message ids
  115. // - invalid data causing exceptions
  116. // - negative ReadBytesAndSize prefixes
  117. // - invalid utf8 strings
  118. // - etc.
  119. //
  120. // let's catch them all and then disconnect that connection to avoid
  121. // further attacks.
  122. T message = default;
  123. // record start position for NetworkDiagnostics because reader might contain multiple messages if using batching
  124. int startPos = reader.Position;
  125. try
  126. {
  127. if (requireAuthentication && !conn.isAuthenticated)
  128. {
  129. // message requires authentication, but the connection was not authenticated
  130. Debug.LogWarning($"Disconnecting connection: {conn}. Received message {typeof(T)} that required authentication, but the user has not authenticated yet");
  131. conn.Disconnect();
  132. return;
  133. }
  134. //Debug.Log($"ConnectionRecv {conn} msgType:{typeof(T)} content:{BitConverter.ToString(reader.buffer.Array, reader.buffer.Offset, reader.buffer.Count)}");
  135. // if it is a value type, just use default(T)
  136. // otherwise allocate a new instance
  137. message = reader.Read<T>();
  138. }
  139. catch (Exception exception)
  140. {
  141. // should we disconnect on exceptions?
  142. if (exceptionsDisconnect)
  143. {
  144. 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}");
  145. conn.Disconnect();
  146. return;
  147. }
  148. // otherwise log it but allow the connection to keep playing
  149. else
  150. {
  151. Debug.LogError($"Caught an Exception when reading a message from: {conn} of type {typeof(T)}. Reason: {exception}");
  152. return;
  153. }
  154. }
  155. finally
  156. {
  157. int endPos = reader.Position;
  158. // TODO: Figure out the correct channel
  159. NetworkDiagnostics.OnReceive(message, channelId, endPos - startPos);
  160. }
  161. // user handler exception should not stop the whole server
  162. try
  163. {
  164. // user implemented handler
  165. handler((C)conn, message, channelId);
  166. }
  167. catch (Exception exception)
  168. {
  169. // should we disconnect on exceptions?
  170. if (exceptionsDisconnect)
  171. {
  172. 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}");
  173. conn.Disconnect();
  174. }
  175. // otherwise log it but allow the connection to keep playing
  176. else
  177. {
  178. Debug.LogError($"Caught an Exception when handling a message from: {conn} of type {typeof(T)}. Reason: {exception}");
  179. }
  180. }
  181. };
  182. // version for handlers without channelId
  183. // TODO obsolete this some day to always use the channelId version.
  184. // all handlers in this version are wrapped with 1 extra action.
  185. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  186. internal static NetworkMessageDelegate WrapHandler<T, C>(Action<C, T> handler, bool requireAuthentication, bool exceptionsDisconnect)
  187. where T : struct, NetworkMessage
  188. where C : NetworkConnection
  189. {
  190. // wrap action as channelId version, call original
  191. void Wrapped(C conn, T msg, int _) => handler(conn, msg);
  192. return WrapHandler((Action<C, T, int>)Wrapped, requireAuthentication, exceptionsDisconnect);
  193. }
  194. }
  195. }