├── .gitignore ├── Assets └── FishNet │ └── Plugins │ ├── FishyUnityTransport │ ├── UnityTransport.cs.meta │ ├── BatchedQueue.meta │ ├── NetworkEvent.cs.meta │ ├── FishyUnityTransport.asmdef.meta │ ├── BatchedQueue │ │ ├── BatchedSendQueue.cs.meta │ │ ├── BatchedReceiveQueue.cs.meta │ │ ├── BatchedReceiveQueue.cs │ │ └── BatchedSendQueue.cs │ ├── LocalConnectionStateExtensions.cs.meta │ ├── LICENSE.meta │ ├── README.md.meta │ ├── package.json.meta │ ├── package.json │ ├── LocalConnectionStateExtensions.cs │ ├── FishyUnityTransport.asmdef │ ├── NetworkEvent.cs │ ├── LICENSE │ ├── README.md │ └── UnityTransport.cs │ └── FishyUnityTransport.meta ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !Assets/FishNet/Plugins/FishyUnityTransport/** 3 | !.gitignore 4 | !FishyUnityTransport.unitypackage -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/UnityTransport.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1cdcee4396754c63a1574492499cffc8 -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/BatchedQueue.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8f86fc2e3b8d4cd9ba3392fe8ae4297c 3 | timeCreated: 1671296224 -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/NetworkEvent.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5f508713bcb34ded89a2c5d87274caac 3 | timeCreated: 1743192782 -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/FishyUnityTransport.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c240c7a8153142ad92a23143a68f0c23 3 | timeCreated: 1670311561 -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/BatchedQueue/BatchedSendQueue.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a0bd2c83a8914bceb6099d6e8968785c 3 | timeCreated: 1670682346 -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/LocalConnectionStateExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bd7897550cf54e65adb11d05c0e4d46a 3 | timeCreated: 1711894689 -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/BatchedQueue/BatchedReceiveQueue.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 32770745bece44bebe28c99e07bbf579 3 | timeCreated: 1670682346 -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c7b445bb52e3c7e4eb7ae9b3f1522751 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 85488fd1577ab4348be6e7fd3b78be0a 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7dbd539f8c5d87c468c7ffe8a03fd5e6 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fac6ee1d634c7e94e892394f3574a8e8 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.alven.fishnet.unitytransport", 3 | "displayName": "FishyUnityTransport", 4 | "description": "UnityTransport for FishNet", 5 | "version": "2.0.0-pre.2", 6 | "unity": "2020.3", 7 | "author": "Alven (https://github.com/ooonush)", 8 | "homepage": "https://github.com/ooonush/FishyUnityTransport", 9 | "dependencies": { 10 | "com.unity.transport": "1.3.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/LocalConnectionStateExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace FishNet.Transporting.UTP 2 | { 3 | internal static class LocalConnectionStateExtensions 4 | { 5 | public static bool IsStartingOrStarted(this LocalConnectionState state) 6 | { 7 | return state == LocalConnectionState.Starting || state == LocalConnectionState.Started; 8 | } 9 | 10 | public static bool IsStoppingOrStopped(this LocalConnectionState state) 11 | { 12 | return state == LocalConnectionState.Stopping || state == LocalConnectionState.Stopped; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/FishyUnityTransport.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FishyUnityTransport", 3 | "rootNamespace": "FishNet.Transporting.UTP", 4 | "references": [ 5 | "GUID:7c88a4a7926ee5145ad2dfa06f454c67", 6 | "GUID:e0cd26848372d4e5c891c569017e11f1", 7 | "GUID:f2d49d9fa7e7eb3418e39723a7d3b92f", 8 | "GUID:2665a8d13d1b3f18800f46e256720795" 9 | ], 10 | "allowUnsafeCode": true, 11 | "versionDefines": [ 12 | { 13 | "name": "com.unity.transport", 14 | "expression": "2.0.0-exp", 15 | "define": "UTP_TRANSPORT_2_0_ABOVE" 16 | } 17 | ], 18 | "noEngineReferences": false 19 | } -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/NetworkEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Unity.Netcode 2 | { 3 | /// 4 | /// Represents a netEvent when polling 5 | /// 6 | public enum NetworkEvent 7 | { 8 | /// 9 | /// New data is received 10 | /// 11 | Data, 12 | 13 | /// 14 | /// A client is connected, or client connected to server 15 | /// 16 | Connect, 17 | 18 | /// 19 | /// A client disconnected, or client disconnected from server 20 | /// 21 | Disconnect, 22 | 23 | /// 24 | /// Transport has encountered an unrecoverable failure 25 | /// 26 | TransportFailure, 27 | 28 | /// 29 | /// No new event 30 | /// 31 | Nothing 32 | } 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alven 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alven 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/BatchedQueue/BatchedReceiveQueue.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Unity Technologies 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all copies 11 | // or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | // 21 | // License: https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/blob/develop/com.unity.netcode.gameobjects/LICENSE.md 22 | 23 | using System; 24 | using Unity.Networking.Transport; 25 | #if UTP_TRANSPORT_2_0_ABOVE 26 | using Unity.Collections; 27 | using Unity.Collections.LowLevel.Unsafe; 28 | #endif 29 | 30 | namespace FishNet.Transporting.UTP 31 | { 32 | /// Queue for batched messages received through UTP. 33 | /// This is meant as a companion to . 34 | internal class BatchedReceiveQueue 35 | { 36 | private byte[] m_Data; 37 | private int m_Offset; 38 | private int m_Length; 39 | 40 | public bool IsEmpty => m_Length <= 0; 41 | 42 | /// 43 | /// Construct a new receive queue from a returned by 44 | /// when popping a data event. 45 | /// 46 | /// The to construct from. 47 | public BatchedReceiveQueue(DataStreamReader reader) 48 | { 49 | m_Data = new byte[reader.Length]; 50 | unsafe 51 | { 52 | fixed (byte* dataPtr = m_Data) 53 | { 54 | #if UTP_TRANSPORT_2_0_ABOVE 55 | reader.ReadBytesUnsafe(dataPtr, reader.Length); 56 | #else 57 | reader.ReadBytes(dataPtr, reader.Length); 58 | #endif 59 | } 60 | } 61 | 62 | m_Offset = 0; 63 | m_Length = reader.Length; 64 | } 65 | 66 | /// 67 | /// Push the entire data from a (as returned by popping an 68 | /// event from a ) to the queue. 69 | /// 70 | /// The to push the data of. 71 | public void PushReader(DataStreamReader reader) 72 | { 73 | // Resize the array and copy the existing data to the beginning if there's not enough 74 | // room to copy the reader's data at the end of the existing data. 75 | var available = m_Data.Length - (m_Offset + m_Length); 76 | if (available < reader.Length) 77 | { 78 | if (m_Length > 0) 79 | { 80 | Array.Copy(m_Data, m_Offset, m_Data, 0, m_Length); 81 | } 82 | 83 | m_Offset = 0; 84 | 85 | while (m_Data.Length - m_Length < reader.Length) 86 | { 87 | Array.Resize(ref m_Data, m_Data.Length * 2); 88 | } 89 | } 90 | 91 | unsafe 92 | { 93 | fixed (byte* dataPtr = m_Data) 94 | { 95 | #if UTP_TRANSPORT_2_0_ABOVE 96 | reader.ReadBytesUnsafe(dataPtr + m_Offset + m_Length, reader.Length); 97 | #else 98 | reader.ReadBytes(dataPtr + m_Offset + m_Length, reader.Length); 99 | #endif 100 | } 101 | } 102 | 103 | m_Length += reader.Length; 104 | } 105 | 106 | /// Pop the next full message in the queue. 107 | /// The message, or the default value if no more full messages. 108 | public ArraySegment PopMessage() 109 | { 110 | if (m_Length < sizeof(int)) 111 | { 112 | return default; 113 | } 114 | 115 | var messageLength = BitConverter.ToInt32(m_Data, m_Offset); 116 | 117 | if (m_Length - sizeof(int) < messageLength) 118 | { 119 | return default; 120 | } 121 | 122 | var data = new ArraySegment(m_Data, m_Offset + sizeof(int), messageLength); 123 | 124 | m_Offset += sizeof(int) + messageLength; 125 | m_Length -= sizeof(int) + messageLength; 126 | 127 | return data; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FishyUnityTransport 2 | 3 | A UnityTransport implementation for Fish-Net. 4 | 5 | If you have further questions, come find me as `ooonush` in the **[FirstGearGames Discord](https://discord.gg/Ta9HgDh4Hj)**! 6 | 7 | The FishyUnityTransport library API is close to **[UnityTransport for NGO](https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/tree/develop/com.unity.netcode.gameobjects/Runtime/Transports/UTP)** and uses some of its code. 8 | 9 | ## Setting Up 10 | 1. Install Fish-Net from the **[official repo](https://github.com/FirstGearGames/FishNet/releases)** or **[Asset Store](https://assetstore.unity.com/packages/tools/network/fish-net-networking-evolved-207815)**. 11 | 2. Install **[UnityTransport](https://docs-multiplayer.unity3d.com/transport/current/install)** package (1.3.1 or newer). 12 | 3. Install `FishyUnityTransport` **[unitypackage](https://github.com/ooonush/FishyUnityTransport/releases)** from the release section or using the Git URL: 13 | ``` 14 | https://github.com/ooonush/FishyUnityTransport.git?path=Assets/FishNet/Plugins/FishyUnityTransport 15 | ``` 16 | 4. Add `UnityTransport` component to `NetworkManager` GameObject. 17 | 5. Set `Transport` variable on the `TransportManager` component to `UnityTransport`. 18 | 19 | ## Relay 20 | 21 | With `FishyUnityTransport`, you can use an IP address and port to connect to a `Dedicated Server`. 22 | However, if you want to use a player device as a server (host), using an IP address to establish a connection does not always work. 23 | For example, the device's firewall may prohibit these kinds of connections because they are not secure. 24 | Instead, use Unity Relay to successfully initiate a connection between multiple clients and a host. 25 | 26 | Many factors impact how you connect to the remote host. To connect to a remote host, use one of the following methods: 27 | - Perform a [NAT punchthrough](https://docs-multiplayer.unity3d.com/netcode/current/learn/listen-server-host-architecture/#option-c-nat-punchthrough): This advanced technique directly connects to the host computer, even if it's on another network. 28 | - Use a [Relay server](https://docs.unity.com/relay/en/manual/relay-servers): A Relay server exists on the internet with a public-facing IP that you and the host can access. 29 | After the client and the host connect to a relay server, they can send data to each other through the Relay server. 30 | 31 | FishNet doesn't offer tools to help you punch through a NAT. 32 | However, you can use the `Relay service` provided by `Unity Services`. 33 | 34 | ## Unity Relay support 35 | 36 | `FishyUnityTransport` supports [Unity Relay](https://docs.unity.com/ugs/en-us/manual/relay/manual/introduction), and since its API is similar to the `UnityTransport` for [NGO](https://docs-multiplayer.unity3d.com/netcode/current/about/index.html) API, 37 | 38 | ### Installation and configuration 39 | 40 | 1. In the Unity Editor's Package Manager, select Unity Registry. 41 | 2. Search for the following package: 42 | - For Unity 2022 LTS and later: `com.unity.services.multiplayer` 43 | - For earlier versions of Unity: `com.unity.services.relay` 44 | 3. Select the package, then Install. Refer to Package Manager. 45 | 46 | Note: For most users, the unified [Multiplayer Services package](https://docs.unity.com/ugs/en-us/manual/mps-sdk/manual) replaces the Relay standalone package, which is deprecated in Unity 6. Consider migrating to the unified package to facilitate a smooth transition. Visit the [migration guide](https://docs.unity.com/ugs/en-us/manual/mps-sdk/manual/migration-path) for a step-by-step transition process. 47 | 48 | You must first configure your Unity project for Unity before using Relay with FishNet. Check out [Get started with Relay](https://docs.unity.com/ugs/en-us/manual/relay/manual/get-started) to learn how to link your project in the project settings. 49 | 50 | ## How to use Relay 51 | 52 | To access a Relay server, do the following: 53 | - As the host, request an allocation on the relay server. 54 | - As a client, use the [join code](https://docs.unity.com/relay/en/manual/join-codes) that the host creates to connect to the relay server. This code allows the host and clients to communicate through the Relay server without disclosing their IP addresses and ports directly. 55 | 56 | ## Create allocation on a Relay server 57 | 58 | To create an [allocation](https://docs.unity.com/relay/en/manual/allocations) on a Relay server, make an authenticated call to the Unity backend using the SDK. 59 | To do this, call the `CreateAllocationAsync` method on the host with the maximum number of expected peers. For example, a host that requests a maximum of three peer connections reserves four slots for a four player game. This function can throw exceptions, which you can catch to learn about the error that caused them. 60 | 61 | ```csharp 62 | //Ask Unity Services to allocate a Relay server that will handle up to eight players: seven peers and the host. 63 | Allocation allocation = await Unity.Services.Relay.RelayService.Instance.CreateAllocationAsync(7); 64 | ``` 65 | 66 | The `Allocation` class represents all the necessary data for a [host player](https://docs.unity.com/relay/manual/players#Host) to start hosting using the specific Relay server allocated. You don't need to understand each part of this allocation; you only need to feed them to your chosen transport that handles the Relay communication on its own. For the more curious (and for reference), here's a simple overview of those parameters: 67 | 68 | - A `RelayServer` class containing the IP and port of your allocation's server. 69 | - The allocation ID in a Base64 form and GUID form referred to as `AllocationIdBytes` and `AllocationId`. 70 | - A blob of encrypted bytes representing the [connection data](https://docs.unity.com/relay/en/manual/connection-data) (known as `ConnectionData`) allows users to connect to this host. 71 | - A Base64 encoded `Key` for message signature. 72 | 73 | Each allocation creates a unique [join code](https://docs.unity.com/relay/en/manual/join-codes) suitable for sharing over instant messages or other means. 74 | This join code allows your clients to join your game. You can retrieve it by calling the Relay SDK like so: 75 | 76 | ```csharp 77 | string joinCode = await Unity.Services.Relay.RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId); 78 | ``` 79 | 80 | With those two calls, you now have your Relay allocation ready and the associated join code. Pass the allocation parameters to your host transport and send the join code (a simple string) over the Internet by the mean of your choice to your clients. 81 | 82 | Remember to [authenticate](https://docs.unity.com/relay/en/manual/authentication) your users before using SDK methods. 83 | The easiest way is the anonymous one (shown in the following code snippet), but you can use more advanced techniques. 84 | 85 | ```csharp 86 | //Initialize the Unity Services engine 87 | await UnityServices.InitializeAsync(); 88 | if (!AuthenticationService.Instance.IsSignedIn) 89 | { 90 | //If not already logged, log the user in 91 | await AuthenticationService.Instance.SignInAnonymouslyAsync(); 92 | } 93 | ``` 94 | 95 | ## Join an allocation on a Relay server 96 | 97 | To join an allocation on a Relay server, the following must be true: 98 | 99 | - The host of the game has created a Relay allocation. 100 | - The client has received a join code. 101 | 102 | To join a relay, a client requests all the allocation parameters from the join code to join the game. 103 | To do this, use the join code to call the `JoinAllocationAsync` method as the following code snippet demonstrates: 104 | 105 | ```csharp 106 | //Ask Unity Services to join a Relay allocation based on our join code 107 | JoinAllocation allocation = await Unity.Services.Relay.RelayService.Instance.JoinAllocationAsync(joinCode); 108 | ``` 109 | 110 | For more information about the join code connection process, refer to [Connection flow](https://docs.unity.com/relay/manual/connection-flow#4). 111 | 112 | ## Pass allocation data to a transport component 113 | 114 | When an allocation exists, you need to make all traffic that comes from FishNet go through the Relay. 115 | To do this, perform the following actions to pass the allocation parameters to UnityTransport: 116 | 117 | 1. Retrieve UnityTransport from your `NetworkManager`: 118 | ```csharp 119 | //Retrieve the UnityTransport used by the NetworkManager 120 | UnityTransport transport = NetworkManager.TransportManager.GetTransport(); 121 | ``` 122 | 123 | 2. Call the `SetRelayServerData` method on the retrieved transport by passing the allocation data that you retrieved, as well as the connection type (here set to [dtls](https://docs.unity.com/relay/en/manual/dtls-encryption)). For example: 124 | ```csharp 125 | transport.SetRelayServerData(new RelayServerData(allocation, connectionType:"dtls")); 126 | ``` 127 | 128 | 3. Call `NetworkManager.ServerManager.StartConnection()` to start server or `NetworkManager.ClientManager.StartConnection()` to start client. 129 | 130 | # Code samples 131 | 132 | Use the following code to work with the [Relay server](https://docs.unity.com/relay/en/manual/relay-servers). 133 | For more information, refer to [Unity Relay](https://docs.unity.com/ugs/en-us/manual/relay/manual/introduction). 134 | 135 | ## Host player 136 | 137 | The `StartHostWithRelay` function shows how to create a Relay allocation, and request a join code. This function requires the maximum number of connections the allocation is expecting and the host player connection type. 138 | 139 | The connection type must be one of the following options: 140 | * udp 141 | * dtls 142 | * wss 143 | 144 | Refer to [DTLS](https://docs.unity.com/ugs/en-us/manual/relay/manual/dtls-encryption) to learn more about DTLS encryption, and to [Web Platform Support](https://docs.unity.com/ugs/en-us/manual/relay/manual/relay-and-ngo#Note-about-Unity-Web-platform-support) to learn about using wss. 145 | 146 | ### Using the Multiplayer Services SDK (com.unity.services.multiplayer) 147 | 148 | ```csharp 149 | [SerializeField] private NetworkManager _networkManager; 150 | 151 | public async Task StartHostWithRelay(int maxConnections, string connectionType) 152 | { 153 | await UnityServices.InitializeAsync(); 154 | if (!AuthenticationService.Instance.IsSignedIn) 155 | { 156 | await AuthenticationService.Instance.SignInAnonymouslyAsync(); 157 | } 158 | 159 | // Request allocation and join code 160 | Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxConnections); 161 | var joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId); 162 | // Configure transport 163 | var unityTransport = _networkManager.TransportManager.GetTransport(); 164 | unityTransport.SetRelayServerData(AllocationUtils.ToRelayServerData(allocation, connectionType)); 165 | 166 | // Start host 167 | if (_networkManager.ServerManager.StartConnection()) // Server is successfully started. 168 | { 169 | _networkManager.ClientManager.StartConnection(); // You can choose not to call this method. Then only the server will start. 170 | return joinCode; 171 | } 172 | return null; 173 | } 174 | ``` 175 | 176 | This code should be adapted to your needs to use a different authentication mechanism, a different error handling, or to use a different connection type. 177 | Similarly, you can start only the server instead of the host, to start a server instead of a host. 178 | 179 | ### Using the Relay standalone SDK (com.unity.services.relay) 180 | 181 | ```csharp 182 | [SerializeField] private NetworkManager _networkManager; 183 | 184 | public async Task StartHostWithRelay(int maxConnections, string connectionType) 185 | { 186 | await UnityServices.InitializeAsync(); 187 | if (!AuthenticationService.Instance.IsSignedIn) 188 | { 189 | await AuthenticationService.Instance.SignInAnonymouslyAsync(); 190 | } 191 | 192 | // Request allocation and join code 193 | Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxConnections); 194 | var joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId); 195 | // Configure transport 196 | var unityTransport = _networkManager.TransportManager.GetTransport(); 197 | unityTransport.SetRelayServerData(new RelayServerData(allocation, connectionType)); 198 | 199 | // Start host 200 | if (_networkManager.ServerManager.StartConnection()) // Server is successfully started. 201 | { 202 | _networkManager.ClientManager.StartConnection(); // You can choose not to call this method. Then only the server will start. 203 | return joinCode; 204 | } 205 | return null; 206 | } 207 | ``` 208 | 209 | ## Joining player 210 | 211 | When your game client functions as a joining player, the relay join code, retrieved in the previous step when the host created the allocation, must be passed to find the allocation. The following code samples show how to join an allocation with a join code and configure the connection type. 212 | 213 | The connection type must be one of the following options: 214 | 215 | * udp 216 | * dtls 217 | * wss 218 | 219 | Refer to [DTLS](https://docs.unity.com/ugs/en-us/manual/relay/manual/dtls-encryption) to learn more about DTLS encryption, and to [Web Platform Support](https://docs.unity.com/ugs/en-us/manual/relay/manual/relay-and-ngo#Note-about-Unity-Web-platform-support) to learn about using wss. 220 | 221 | Using the Multiplayer Services SDK (com.unity.services.multiplayer) 222 | 223 | ### Using the Multiplayer Services SDK (com.unity.services.multiplayer) 224 | 225 | ```csharp 226 | public async Task StartClientWithRelay(string joinCode, string connectionType) 227 | { 228 | await UnityServices.InitializeAsync(); 229 | if (!AuthenticationService.Instance.IsSignedIn) 230 | { 231 | await AuthenticationService.Instance.SignInAnonymouslyAsync(); 232 | } 233 | 234 | var allocation = await RelayService.Instance.JoinAllocationAsync(joinCode: joinCode); 235 | NetworkManager.Singleton.GetComponent().SetRelayServerData(AllocationUtils.ToRelayServerData(allocation, connectionType)); 236 | return !string.IsNullOrEmpty(joinCode) && NetworkManager.Singleton.StartClient(); 237 | } 238 | ``` 239 | 240 | ### Using the Relay standalone SDK (com.unity.services.relay) 241 | 242 | ```csharp 243 | public async Task StartClientWithRelay(string joinCode, string connectionType) 244 | { 245 | await UnityServices.InitializeAsync(); 246 | if (!AuthenticationService.Instance.IsSignedIn) 247 | { 248 | await AuthenticationService.Instance.SignInAnonymouslyAsync(); 249 | } 250 | 251 | var allocation = await RelayService.Instance.JoinAllocationAsync(joinCode); 252 | NetworkManager.Singleton.GetComponent().SetRelayServerData(new RelayServerData(allocation, connectionType)); 253 | return !string.IsNullOrEmpty(joinCode) && NetworkManager.Singleton.StartClient(); 254 | } 255 | ``` 256 | 257 | ## Note about WebGL support 258 | 259 | To use Relay with FishNet in `WebGL`, you must upgrade the Unity Transport Package to 2.0.0 or later and configure the `FishyUnityTransport` component to use `Web Sockets`. 260 | 261 | Using the above code snippets, pass **wss** as connectionType and use the following to SetRelayServerData: 262 | 263 | ### Using the Multiplayer Services SDK (com.unity.services.multiplayer) 264 | 265 | ```csharp 266 | var unityTransport = _networkManager.TransportManager.GetTransport(); 267 | unityTransport.SetRelayServerData(new RelayServerData(allocation, "wss")); 268 | unityTransport.UseWebSockets = true; 269 | ``` 270 | 271 | ### Using the Multiplayer Services SDK (com.unity.services.multiplayer) 272 | 273 | ```csharp 274 | var unityTransport = _networkManager.TransportManager.GetTransport(); 275 | unityTransport.SetRelayServerData(AllocationUtils.ToRelayServerData(allocation, "wss")); 276 | unityTransport.UseWebSockets = true; 277 | ``` 278 | 279 | # Other Unity Services support 280 | 281 | In addition, `FishyUnityTransport` supports other Unity Services. You can explore them yourself and use them in your project: 282 | - [Unity Lobby](https://docs.unity.com/ugs/en-us/manual/lobby/manual/unity-lobby-service) 283 | - [Game Server Hosting (Multiplay)](https://docs.unity.com/ugs/en-us/manual/game-server-hosting/manual/welcome) -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/README.md: -------------------------------------------------------------------------------- 1 | # FishyUnityTransport 2 | 3 | A UnityTransport implementation for Fish-Net. 4 | 5 | If you have further questions, come find me as `ooonush` in the **[FirstGearGames Discord](https://discord.gg/Ta9HgDh4Hj)**! 6 | 7 | The FishyUnityTransport library API is close to **[UnityTransport for NGO](https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/tree/develop/com.unity.netcode.gameobjects/Runtime/Transports/UTP)** and uses some of its code. 8 | 9 | ## Setting Up 10 | 1. Install Fish-Net from the **[official repo](https://github.com/FirstGearGames/FishNet/releases)** or **[Asset Store](https://assetstore.unity.com/packages/tools/network/fish-net-networking-evolved-207815)**. 11 | 2. Install **[UnityTransport](https://docs-multiplayer.unity3d.com/transport/current/install)** package (1.3.1 or newer). 12 | 3. Install `FishyUnityTransport` **[unitypackage](https://github.com/ooonush/FishyUnityTransport/releases)** from the release section or using the Git URL: 13 | ``` 14 | https://github.com/ooonush/FishyUnityTransport.git?path=Assets/FishNet/Plugins/FishyUnityTransport 15 | ``` 16 | 4. Add `UnityTransport` component to `NetworkManager` GameObject. 17 | 5. Set `Transport` variable on the `TransportManager` component to `UnityTransport`. 18 | 19 | ## Relay 20 | 21 | With `FishyUnityTransport`, you can use an IP address and port to connect to a `Dedicated Server`. 22 | However, if you want to use a player device as a server (host), using an IP address to establish a connection does not always work. 23 | For example, the device's firewall may prohibit these kinds of connections because they are not secure. 24 | Instead, use Unity Relay to successfully initiate a connection between multiple clients and a host. 25 | 26 | Many factors impact how you connect to the remote host. To connect to a remote host, use one of the following methods: 27 | - Perform a [NAT punchthrough](https://docs-multiplayer.unity3d.com/netcode/current/learn/listen-server-host-architecture/#option-c-nat-punchthrough): This advanced technique directly connects to the host computer, even if it's on another network. 28 | - Use a [Relay server](https://docs.unity.com/relay/en/manual/relay-servers): A Relay server exists on the internet with a public-facing IP that you and the host can access. 29 | After the client and the host connect to a relay server, they can send data to each other through the Relay server. 30 | 31 | FishNet doesn't offer tools to help you punch through a NAT. 32 | However, you can use the `Relay service` provided by `Unity Services`. 33 | 34 | ## Unity Relay support 35 | 36 | `FishyUnityTransport` supports [Unity Relay](https://docs.unity.com/ugs/en-us/manual/relay/manual/introduction), and since its API is similar to the `UnityTransport` for [NGO](https://docs-multiplayer.unity3d.com/netcode/current/about/index.html) API, 37 | 38 | ### Installation and configuration 39 | 40 | 1. In the Unity Editor's Package Manager, select Unity Registry. 41 | 2. Search for the following package: 42 | - For Unity 2022 LTS and later: `com.unity.services.multiplayer` 43 | - For earlier versions of Unity: `com.unity.services.relay` 44 | 3. Select the package, then Install. Refer to Package Manager. 45 | 46 | Note: For most users, the unified [Multiplayer Services package](https://docs.unity.com/ugs/en-us/manual/mps-sdk/manual) replaces the Relay standalone package, which is deprecated in Unity 6. Consider migrating to the unified package to facilitate a smooth transition. Visit the [migration guide](https://docs.unity.com/ugs/en-us/manual/mps-sdk/manual/migration-path) for a step-by-step transition process. 47 | 48 | You must first configure your Unity project for Unity before using Relay with FishNet. Check out [Get started with Relay](https://docs.unity.com/ugs/en-us/manual/relay/manual/get-started) to learn how to link your project in the project settings. 49 | 50 | ## How to use Relay 51 | 52 | To access a Relay server, do the following: 53 | - As the host, request an allocation on the relay server. 54 | - As a client, use the [join code](https://docs.unity.com/relay/en/manual/join-codes) that the host creates to connect to the relay server. This code allows the host and clients to communicate through the Relay server without disclosing their IP addresses and ports directly. 55 | 56 | ## Create allocation on a Relay server 57 | 58 | To create an [allocation](https://docs.unity.com/relay/en/manual/allocations) on a Relay server, make an authenticated call to the Unity backend using the SDK. 59 | To do this, call the `CreateAllocationAsync` method on the host with the maximum number of expected peers. For example, a host that requests a maximum of three peer connections reserves four slots for a four player game. This function can throw exceptions, which you can catch to learn about the error that caused them. 60 | 61 | ```csharp 62 | //Ask Unity Services to allocate a Relay server that will handle up to eight players: seven peers and the host. 63 | Allocation allocation = await Unity.Services.Relay.RelayService.Instance.CreateAllocationAsync(7); 64 | ``` 65 | 66 | The `Allocation` class represents all the necessary data for a [host player](https://docs.unity.com/relay/manual/players#Host) to start hosting using the specific Relay server allocated. You don't need to understand each part of this allocation; you only need to feed them to your chosen transport that handles the Relay communication on its own. For the more curious (and for reference), here's a simple overview of those parameters: 67 | 68 | - A `RelayServer` class containing the IP and port of your allocation's server. 69 | - The allocation ID in a Base64 form and GUID form referred to as `AllocationIdBytes` and `AllocationId`. 70 | - A blob of encrypted bytes representing the [connection data](https://docs.unity.com/relay/en/manual/connection-data) (known as `ConnectionData`) allows users to connect to this host. 71 | - A Base64 encoded `Key` for message signature. 72 | 73 | Each allocation creates a unique [join code](https://docs.unity.com/relay/en/manual/join-codes) suitable for sharing over instant messages or other means. 74 | This join code allows your clients to join your game. You can retrieve it by calling the Relay SDK like so: 75 | 76 | ```csharp 77 | string joinCode = await Unity.Services.Relay.RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId); 78 | ``` 79 | 80 | With those two calls, you now have your Relay allocation ready and the associated join code. Pass the allocation parameters to your host transport and send the join code (a simple string) over the Internet by the mean of your choice to your clients. 81 | 82 | Remember to [authenticate](https://docs.unity.com/relay/en/manual/authentication) your users before using SDK methods. 83 | The easiest way is the anonymous one (shown in the following code snippet), but you can use more advanced techniques. 84 | 85 | ```csharp 86 | //Initialize the Unity Services engine 87 | await UnityServices.InitializeAsync(); 88 | if (!AuthenticationService.Instance.IsSignedIn) 89 | { 90 | //If not already logged, log the user in 91 | await AuthenticationService.Instance.SignInAnonymouslyAsync(); 92 | } 93 | ``` 94 | 95 | ## Join an allocation on a Relay server 96 | 97 | To join an allocation on a Relay server, the following must be true: 98 | 99 | - The host of the game has created a Relay allocation. 100 | - The client has received a join code. 101 | 102 | To join a relay, a client requests all the allocation parameters from the join code to join the game. 103 | To do this, use the join code to call the `JoinAllocationAsync` method as the following code snippet demonstrates: 104 | 105 | ```csharp 106 | //Ask Unity Services to join a Relay allocation based on our join code 107 | JoinAllocation allocation = await Unity.Services.Relay.RelayService.Instance.JoinAllocationAsync(joinCode); 108 | ``` 109 | 110 | For more information about the join code connection process, refer to [Connection flow](https://docs.unity.com/relay/manual/connection-flow#4). 111 | 112 | ## Pass allocation data to a transport component 113 | 114 | When an allocation exists, you need to make all traffic that comes from FishNet go through the Relay. 115 | To do this, perform the following actions to pass the allocation parameters to UnityTransport: 116 | 117 | 1. Retrieve UnityTransport from your `NetworkManager`: 118 | ```csharp 119 | //Retrieve the UnityTransport used by the NetworkManager 120 | UnityTransport transport = NetworkManager.TransportManager.GetTransport(); 121 | ``` 122 | 123 | 2. Call the `SetRelayServerData` method on the retrieved transport by passing the allocation data that you retrieved, as well as the connection type (here set to [dtls](https://docs.unity.com/relay/en/manual/dtls-encryption)). For example: 124 | ```csharp 125 | transport.SetRelayServerData(new RelayServerData(allocation, connectionType:"dtls")); 126 | ``` 127 | 128 | 3. Call `NetworkManager.ServerManager.StartConnection()` to start server or `NetworkManager.ClientManager.StartConnection()` to start client. 129 | 130 | # Code samples 131 | 132 | Use the following code to work with the [Relay server](https://docs.unity.com/relay/en/manual/relay-servers). 133 | For more information, refer to [Unity Relay](https://docs.unity.com/ugs/en-us/manual/relay/manual/introduction). 134 | 135 | ## Host player 136 | 137 | The `StartHostWithRelay` function shows how to create a Relay allocation, and request a join code. This function requires the maximum number of connections the allocation is expecting and the host player connection type. 138 | 139 | The connection type must be one of the following options: 140 | * udp 141 | * dtls 142 | * wss 143 | 144 | Refer to [DTLS](https://docs.unity.com/ugs/en-us/manual/relay/manual/dtls-encryption) to learn more about DTLS encryption, and to [Web Platform Support](https://docs.unity.com/ugs/en-us/manual/relay/manual/relay-and-ngo#Note-about-Unity-Web-platform-support) to learn about using wss. 145 | 146 | ### Using the Multiplayer Services SDK (com.unity.services.multiplayer) 147 | 148 | ```csharp 149 | [SerializeField] private NetworkManager _networkManager; 150 | 151 | public async Task StartHostWithRelay(int maxConnections, string connectionType) 152 | { 153 | await UnityServices.InitializeAsync(); 154 | if (!AuthenticationService.Instance.IsSignedIn) 155 | { 156 | await AuthenticationService.Instance.SignInAnonymouslyAsync(); 157 | } 158 | 159 | // Request allocation and join code 160 | Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxConnections); 161 | var joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId); 162 | // Configure transport 163 | var unityTransport = _networkManager.TransportManager.GetTransport(); 164 | unityTransport.SetRelayServerData(AllocationUtils.ToRelayServerData(allocation, connectionType)); 165 | 166 | // Start host 167 | if (_networkManager.ServerManager.StartConnection()) // Server is successfully started. 168 | { 169 | _networkManager.ClientManager.StartConnection(); // You can choose not to call this method. Then only the server will start. 170 | return joinCode; 171 | } 172 | return null; 173 | } 174 | ``` 175 | 176 | This code should be adapted to your needs to use a different authentication mechanism, a different error handling, or to use a different connection type. 177 | Similarly, you can start only the server instead of the host, to start a server instead of a host. 178 | 179 | ### Using the Relay standalone SDK (com.unity.services.relay) 180 | 181 | ```csharp 182 | [SerializeField] private NetworkManager _networkManager; 183 | 184 | public async Task StartHostWithRelay(int maxConnections, string connectionType) 185 | { 186 | await UnityServices.InitializeAsync(); 187 | if (!AuthenticationService.Instance.IsSignedIn) 188 | { 189 | await AuthenticationService.Instance.SignInAnonymouslyAsync(); 190 | } 191 | 192 | // Request allocation and join code 193 | Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxConnections); 194 | var joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId); 195 | // Configure transport 196 | var unityTransport = _networkManager.TransportManager.GetTransport(); 197 | unityTransport.SetRelayServerData(new RelayServerData(allocation, connectionType)); 198 | 199 | // Start host 200 | if (_networkManager.ServerManager.StartConnection()) // Server is successfully started. 201 | { 202 | _networkManager.ClientManager.StartConnection(); // You can choose not to call this method. Then only the server will start. 203 | return joinCode; 204 | } 205 | return null; 206 | } 207 | ``` 208 | 209 | ## Joining player 210 | 211 | When your game client functions as a joining player, the relay join code, retrieved in the previous step when the host created the allocation, must be passed to find the allocation. The following code samples show how to join an allocation with a join code and configure the connection type. 212 | 213 | The connection type must be one of the following options: 214 | 215 | * udp 216 | * dtls 217 | * wss 218 | 219 | Refer to [DTLS](https://docs.unity.com/ugs/en-us/manual/relay/manual/dtls-encryption) to learn more about DTLS encryption, and to [Web Platform Support](https://docs.unity.com/ugs/en-us/manual/relay/manual/relay-and-ngo#Note-about-Unity-Web-platform-support) to learn about using wss. 220 | 221 | Using the Multiplayer Services SDK (com.unity.services.multiplayer) 222 | 223 | ### Using the Multiplayer Services SDK (com.unity.services.multiplayer) 224 | 225 | ```csharp 226 | public async Task StartClientWithRelay(string joinCode, string connectionType) 227 | { 228 | await UnityServices.InitializeAsync(); 229 | if (!AuthenticationService.Instance.IsSignedIn) 230 | { 231 | await AuthenticationService.Instance.SignInAnonymouslyAsync(); 232 | } 233 | 234 | var allocation = await RelayService.Instance.JoinAllocationAsync(joinCode: joinCode); 235 | NetworkManager.Singleton.GetComponent().SetRelayServerData(AllocationUtils.ToRelayServerData(allocation, connectionType)); 236 | return !string.IsNullOrEmpty(joinCode) && NetworkManager.Singleton.StartClient(); 237 | } 238 | ``` 239 | 240 | ### Using the Relay standalone SDK (com.unity.services.relay) 241 | 242 | ```csharp 243 | public async Task StartClientWithRelay(string joinCode, string connectionType) 244 | { 245 | await UnityServices.InitializeAsync(); 246 | if (!AuthenticationService.Instance.IsSignedIn) 247 | { 248 | await AuthenticationService.Instance.SignInAnonymouslyAsync(); 249 | } 250 | 251 | var allocation = await RelayService.Instance.JoinAllocationAsync(joinCode); 252 | NetworkManager.Singleton.GetComponent().SetRelayServerData(new RelayServerData(allocation, connectionType)); 253 | return !string.IsNullOrEmpty(joinCode) && NetworkManager.Singleton.StartClient(); 254 | } 255 | ``` 256 | 257 | ## Note about WebGL support 258 | 259 | To use Relay with FishNet in `WebGL`, you must upgrade the Unity Transport Package to 2.0.0 or later and configure the `FishyUnityTransport` component to use `Web Sockets`. 260 | 261 | Using the above code snippets, pass **wss** as connectionType and use the following to SetRelayServerData: 262 | 263 | ### Using the Multiplayer Services SDK (com.unity.services.multiplayer) 264 | 265 | ```csharp 266 | var unityTransport = _networkManager.TransportManager.GetTransport(); 267 | unityTransport.SetRelayServerData(new RelayServerData(allocation, "wss")); 268 | unityTransport.UseWebSockets = true; 269 | ``` 270 | 271 | ### Using the Multiplayer Services SDK (com.unity.services.multiplayer) 272 | 273 | ```csharp 274 | var unityTransport = _networkManager.TransportManager.GetTransport(); 275 | unityTransport.SetRelayServerData(AllocationUtils.ToRelayServerData(allocation, "wss")); 276 | unityTransport.UseWebSockets = true; 277 | ``` 278 | 279 | # Other Unity Services support 280 | 281 | In addition, `FishyUnityTransport` supports other Unity Services. You can explore them yourself and use them in your project: 282 | - [Unity Lobby](https://docs.unity.com/ugs/en-us/manual/lobby/manual/unity-lobby-service) 283 | - [Game Server Hosting (Multiplay)](https://docs.unity.com/ugs/en-us/manual/game-server-hosting/manual/welcome) -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/BatchedQueue/BatchedSendQueue.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Unity Technologies 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all copies 11 | // or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | // 21 | // License: https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/blob/develop/com.unity.netcode.gameobjects/LICENSE.md 22 | 23 | 24 | using System; 25 | using Unity.Collections; 26 | using Unity.Collections.LowLevel.Unsafe; 27 | using Unity.Networking.Transport; 28 | 29 | namespace FishNet.Transporting.UTP 30 | { 31 | /// Queue for batched messages meant to be sent through UTP. 32 | /// 33 | /// Messages should be pushed on the queue with . To send batched 34 | /// messages, call or 35 | /// with the obtained from . 36 | /// This will fill the writer with as many messages/bytes as possible. If the send is 37 | /// successful, call to remove the data from the queue. 38 | /// 39 | /// This is meant as a companion to , which should be used to 40 | /// read messages sent with this queue. 41 | /// 42 | internal struct BatchedSendQueue : IDisposable 43 | { 44 | // Note that we're using NativeList basically like a growable NativeArray, where the length 45 | // of the list is the capacity of our array. (We can't use the capacity of the list as our 46 | // queue capacity because NativeList may elect to set it higher than what we'd set it to 47 | // with SetCapacity, which breaks the logic of our code.) 48 | private NativeList m_Data; 49 | private NativeArray m_HeadTailIndices; 50 | private int m_MaximumCapacity; 51 | private int m_MinimumCapacity; 52 | 53 | /// Overhead that is added to each message in the queue. 54 | public const int PerMessageOverhead = sizeof(int); 55 | 56 | internal const int MinimumMinimumCapacity = 4096; 57 | 58 | // Indices into m_HeadTailIndicies. 59 | private const int k_HeadInternalIndex = 0; 60 | private const int k_TailInternalIndex = 1; 61 | 62 | /// Index of the first byte of the oldest data in the queue. 63 | private int HeadIndex 64 | { 65 | get { return m_HeadTailIndices[k_HeadInternalIndex]; } 66 | set { m_HeadTailIndices[k_HeadInternalIndex] = value; } 67 | } 68 | 69 | /// Index one past the last byte of the most recent data in the queue. 70 | private int TailIndex 71 | { 72 | get { return m_HeadTailIndices[k_TailInternalIndex]; } 73 | set { m_HeadTailIndices[k_TailInternalIndex] = value; } 74 | } 75 | 76 | public int Length => TailIndex - HeadIndex; 77 | public int Capacity => m_Data.Length; 78 | public bool IsEmpty => HeadIndex == TailIndex; 79 | public bool IsCreated => m_Data.IsCreated; 80 | 81 | /// Construct a new empty send queue. 82 | /// Maximum capacity of the send queue. 83 | public BatchedSendQueue(int capacity) 84 | { 85 | // Make sure the maximum capacity will be even. 86 | m_MaximumCapacity = capacity + (capacity & 1); 87 | 88 | // We pick the minimum capacity such that if we keep doubling it, we'll eventually hit 89 | // the maximum capacity exactly. The alternative would be to use capacities that are 90 | // powers of 2, but this can lead to over-allocating quite a bit of memory (especially 91 | // since we expect maximum capacities to be in the megabytes range). The approach taken 92 | // here avoids this issue, at the cost of not having allocations of nice round sizes. 93 | m_MinimumCapacity = m_MaximumCapacity; 94 | while (m_MinimumCapacity / 2 >= MinimumMinimumCapacity) 95 | { 96 | m_MinimumCapacity /= 2; 97 | } 98 | 99 | m_Data = new NativeList(m_MinimumCapacity, Allocator.Persistent); 100 | m_HeadTailIndices = new NativeArray(2, Allocator.Persistent); 101 | 102 | m_Data.ResizeUninitialized(m_MinimumCapacity); 103 | 104 | HeadIndex = 0; 105 | TailIndex = 0; 106 | } 107 | 108 | public void Dispose() 109 | { 110 | if (IsCreated) 111 | { 112 | m_Data.Dispose(); 113 | m_HeadTailIndices.Dispose(); 114 | } 115 | } 116 | 117 | /// Write a raw buffer to a DataStreamWriter. 118 | private unsafe void WriteBytes(ref DataStreamWriter writer, byte* data, int length) 119 | { 120 | #if UTP_TRANSPORT_2_0_ABOVE 121 | writer.WriteBytesUnsafe(data, length); 122 | #else 123 | writer.WriteBytes(data, length); 124 | #endif 125 | } 126 | 127 | /// Append data at the tail of the queue. No safety checks. 128 | private void AppendDataAtTail(ArraySegment data) 129 | { 130 | unsafe 131 | { 132 | #if UTP_TRANSPORT_2_0_ABOVE 133 | var writer = new DataStreamWriter(m_Data.GetUnsafePtr() + TailIndex, Capacity - TailIndex); 134 | #else 135 | var writer = new DataStreamWriter((byte*)m_Data.GetUnsafePtr() + TailIndex, Capacity - TailIndex); 136 | #endif 137 | 138 | writer.WriteInt(data.Count); 139 | 140 | fixed (byte* dataPtr = data.Array) 141 | { 142 | WriteBytes(ref writer, dataPtr + data.Offset, data.Count); 143 | } 144 | } 145 | 146 | TailIndex += sizeof(int) + data.Count; 147 | } 148 | 149 | /// Append a new message to the queue. 150 | /// Message to append to the queue. 151 | /// 152 | /// Whether the message was appended successfully. The only way it can fail is if there's 153 | /// no more room in the queue. On failure, nothing is written to the queue. 154 | /// 155 | public bool PushMessage(ArraySegment message) 156 | { 157 | if (!IsCreated) 158 | { 159 | return false; 160 | } 161 | 162 | // Check if there's enough room after the current tail index. 163 | if (Capacity - TailIndex >= sizeof(int) + message.Count) 164 | { 165 | AppendDataAtTail(message); 166 | return true; 167 | } 168 | 169 | // Move the data at the beginning of of m_Data. Either it will leave enough space for 170 | // the message, or we'll grow m_Data and will want the data at the beginning anyway. 171 | if (HeadIndex > 0 && Length > 0) 172 | { 173 | unsafe 174 | { 175 | #if UTP_TRANSPORT_2_0_ABOVE 176 | UnsafeUtility.MemMove(m_Data.GetUnsafePtr(), m_Data.GetUnsafePtr() + HeadIndex, Length); 177 | #else 178 | UnsafeUtility.MemMove(m_Data.GetUnsafePtr(), (byte*)m_Data.GetUnsafePtr() + HeadIndex, Length); 179 | #endif 180 | } 181 | 182 | TailIndex = Length; 183 | HeadIndex = 0; 184 | } 185 | 186 | // If there's enough space left at the end for the message, now is a good time to trim 187 | // the capacity of m_Data if it got very large. We define "very large" here as having 188 | // more than 75% of m_Data unused after adding the new message. 189 | if (Capacity - TailIndex >= sizeof(int) + message.Count) 190 | { 191 | AppendDataAtTail(message); 192 | 193 | while (TailIndex < Capacity / 4 && Capacity > m_MinimumCapacity) 194 | { 195 | m_Data.ResizeUninitialized(Capacity / 2); 196 | } 197 | 198 | return true; 199 | } 200 | 201 | // If we get here we need to grow m_Data until the data fits (or it's too large). 202 | while (Capacity - TailIndex < sizeof(int) + message.Count) 203 | { 204 | // Can't grow m_Data anymore. Message simply won't fit. 205 | if (Capacity * 2 > m_MaximumCapacity) 206 | { 207 | return false; 208 | } 209 | 210 | m_Data.ResizeUninitialized(Capacity * 2); 211 | } 212 | 213 | // If we get here we know there's now enough room for the message. 214 | AppendDataAtTail(message); 215 | return true; 216 | } 217 | 218 | /// 219 | /// Fill as much of a as possible with data from the head of 220 | /// the queue. Only full messages (and their length) are written to the writer. 221 | /// 222 | /// 223 | /// This does NOT actually consume anything from the queue. That is, calling this method 224 | /// does not reduce the length of the queue. Callers are expected to call 225 | /// with the value returned by this method afterwards if the data can 226 | /// be safely removed from the queue (e.g. if it was sent successfully). 227 | /// 228 | /// This method should not be used together with since this 229 | /// could lead to a corrupted queue. 230 | /// 231 | /// The to write to. 232 | /// 233 | /// Maximum number of bytes to copy (0 means writer capacity). This is a soft limit only. 234 | /// If a message is larger than that but fits in the writer, it will be written. In effect, 235 | /// this parameter is the maximum size that small messages can be coalesced together. 236 | /// 237 | /// How many bytes were written to the writer. 238 | public int FillWriterWithMessages(ref DataStreamWriter writer, int softMaxBytes = 0) 239 | { 240 | if (!IsCreated || Length == 0) 241 | { 242 | return 0; 243 | } 244 | 245 | softMaxBytes = softMaxBytes == 0 ? writer.Capacity : Math.Min(softMaxBytes, writer.Capacity); 246 | 247 | unsafe 248 | { 249 | var reader = new DataStreamReader(m_Data.AsArray()); 250 | var readerOffset = HeadIndex; 251 | 252 | reader.SeekSet(readerOffset); 253 | var messageLength = reader.ReadInt(); 254 | var bytesToWrite = messageLength + sizeof(int); 255 | 256 | // Our behavior here depends on the size of the first message in the queue. If it's 257 | // larger than the soft limit, then add only that message to the writer (we want 258 | // large payloads to be fragmented on their own). Otherwise coalesce all small 259 | // messages until we hit the soft limit (which presumably means they won't be 260 | // fragmented, which is the desired behavior for smaller messages). 261 | 262 | if (bytesToWrite > softMaxBytes && bytesToWrite <= writer.Capacity) 263 | { 264 | writer.WriteInt(messageLength); 265 | #if UTP_TRANSPORT_2_0_ABOVE 266 | WriteBytes(ref writer, m_Data.GetUnsafePtr() + reader.GetBytesRead(), messageLength); 267 | #else 268 | WriteBytes(ref writer, (byte*)m_Data.GetUnsafePtr() + reader.GetBytesRead(), messageLength); 269 | #endif 270 | 271 | return bytesToWrite; 272 | } 273 | else 274 | { 275 | var bytesWritten = 0; 276 | 277 | while (readerOffset < TailIndex) 278 | { 279 | reader.SeekSet(readerOffset); 280 | messageLength = reader.ReadInt(); 281 | bytesToWrite = messageLength + sizeof(int); 282 | 283 | if (bytesWritten + bytesToWrite <= softMaxBytes) 284 | { 285 | writer.WriteInt(messageLength); 286 | #if UTP_TRANSPORT_2_0_ABOVE 287 | WriteBytes(ref writer, m_Data.GetUnsafePtr() + reader.GetBytesRead(), messageLength); 288 | #else 289 | WriteBytes(ref writer, (byte*)m_Data.GetUnsafePtr() + reader.GetBytesRead(), messageLength); 290 | #endif 291 | 292 | readerOffset += bytesToWrite; 293 | bytesWritten += bytesToWrite; 294 | } 295 | else 296 | { 297 | break; 298 | } 299 | } 300 | 301 | return bytesWritten; 302 | } 303 | } 304 | } 305 | 306 | /// 307 | /// Fill the given with as many bytes from the queue as 308 | /// possible, disregarding message boundaries. 309 | /// 310 | /// 311 | /// This does NOT actually consume anything from the queue. That is, calling this method 312 | /// does not reduce the length of the queue. Callers are expected to call 313 | /// with the value returned by this method afterwards if the data can 314 | /// be safely removed from the queue (e.g. if it was sent successfully). 315 | /// 316 | /// This method should not be used together with since 317 | /// this could lead to reading messages from a corrupted queue. 318 | /// 319 | /// The to write to. 320 | /// Max number of bytes to copy (0 means writer capacity). 321 | /// How many bytes were written to the writer. 322 | public int FillWriterWithBytes(ref DataStreamWriter writer, int maxBytes = 0) 323 | { 324 | if (!IsCreated || Length == 0) 325 | { 326 | return 0; 327 | } 328 | 329 | var maxLength = maxBytes == 0 ? writer.Capacity : Math.Min(maxBytes, writer.Capacity); 330 | var copyLength = Math.Min(maxLength, Length); 331 | 332 | unsafe 333 | { 334 | #if UTP_TRANSPORT_2_0_ABOVE 335 | WriteBytes(ref writer, m_Data.GetUnsafePtr() + HeadIndex, copyLength); 336 | #else 337 | WriteBytes(ref writer, (byte*)m_Data.GetUnsafePtr() + HeadIndex, copyLength); 338 | #endif 339 | } 340 | 341 | return copyLength; 342 | } 343 | 344 | /// Consume a number of bytes from the head of the queue. 345 | /// 346 | /// This should only be called with a size that matches the last value returned by 347 | /// . Anything else will result in a corrupted queue. 348 | /// 349 | /// Number of bytes to consume from the queue. 350 | public void Consume(int size) 351 | { 352 | // Adjust the head/tail indices such that we consume the given size. 353 | if (size >= Length) 354 | { 355 | HeadIndex = 0; 356 | TailIndex = 0; 357 | 358 | // This is a no-op if m_Data is already at minimum capacity. 359 | m_Data.ResizeUninitialized(m_MinimumCapacity); 360 | } 361 | else 362 | { 363 | HeadIndex += size; 364 | } 365 | } 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /Assets/FishNet/Plugins/FishyUnityTransport/UnityTransport.cs: -------------------------------------------------------------------------------- 1 | using FishNet.Managing; 2 | using Networking = Unity.Networking; 3 | using System.Linq; 4 | using System.Net; 5 | using System; 6 | using System.Collections.Generic; 7 | using UnityEngine; 8 | using TransportNetworkEvent = Unity.Networking.Transport.NetworkEvent; 9 | using Unity.Burst; 10 | using Unity.Collections.LowLevel.Unsafe; 11 | using Unity.Collections; 12 | using Unity.Jobs; 13 | using Unity.Networking.Transport; 14 | using Unity.Networking.Transport.Relay; 15 | using Unity.Networking.Transport.Utilities; 16 | #if UTP_TRANSPORT_2_0_ABOVE 17 | using Unity.Networking.Transport.TLS; 18 | #endif 19 | 20 | #if !UTP_TRANSPORT_2_0_ABOVE 21 | using NetworkEndpoint = Unity.Networking.Transport.NetworkEndPoint; 22 | #endif 23 | 24 | namespace FishNet.Transporting.UTP 25 | { 26 | /// 27 | /// Provides an interface that overrides the ability to create your own drivers and pipelines 28 | /// 29 | public interface INetworkStreamDriverConstructor 30 | { 31 | /// 32 | /// Creates the internal NetworkDriver 33 | /// 34 | /// The owner transport 35 | /// The driver 36 | /// The UnreliableFragmented NetworkPipeline 37 | /// The UnreliableSequencedFragmented NetworkPipeline 38 | /// The ReliableSequenced NetworkPipeline 39 | void CreateDriver( 40 | UnityTransport transport, 41 | out NetworkDriver driver, 42 | out NetworkPipeline unreliableFragmentedPipeline, 43 | out NetworkPipeline unreliableSequencedFragmentedPipeline, 44 | out NetworkPipeline reliableSequencedPipeline); 45 | } 46 | 47 | /// 48 | /// Helper utility class to convert error codes to human readable error messages. 49 | /// 50 | public static class ErrorUtilities 51 | { 52 | private static readonly FixedString128Bytes k_NetworkSuccess = "Success"; 53 | private static readonly FixedString128Bytes k_NetworkIdMismatch = "Invalid connection ID {0}."; 54 | private static readonly FixedString128Bytes k_NetworkVersionMismatch = "Connection ID is invalid. Likely caused by sending on stale connection {0}."; 55 | private static readonly FixedString128Bytes k_NetworkStateMismatch = "Connection state is invalid. Likely caused by sending on connection {0} which is stale or still connecting."; 56 | private static readonly FixedString128Bytes k_NetworkPacketOverflow = "Packet is too large to be allocated by the transport."; 57 | private static readonly FixedString128Bytes k_NetworkSendQueueFull = "Unable to queue packet in the transport. Likely caused by send queue size ('Max Send Queue Size') being too small."; 58 | 59 | /// 60 | /// Convert a UTP error code to human-readable error message. 61 | /// 62 | /// UTP error code. 63 | /// ID of the connection on which the error occurred. 64 | /// Human-readable error message. 65 | public static string ErrorToString(Networking.Transport.Error.StatusCode error, ulong connectionId) 66 | { 67 | return ErrorToString((int)error, connectionId); 68 | } 69 | 70 | internal static string ErrorToString(int error, ulong connectionId) 71 | { 72 | return ErrorToFixedString(error, connectionId).ToString(); 73 | } 74 | 75 | internal static FixedString128Bytes ErrorToFixedString(int error, ulong connectionId) 76 | { 77 | switch ((Networking.Transport.Error.StatusCode)error) 78 | { 79 | case Networking.Transport.Error.StatusCode.Success: 80 | return k_NetworkSuccess; 81 | case Networking.Transport.Error.StatusCode.NetworkIdMismatch: 82 | return FixedString.Format(k_NetworkIdMismatch, connectionId); 83 | case Networking.Transport.Error.StatusCode.NetworkVersionMismatch: 84 | return FixedString.Format(k_NetworkVersionMismatch, connectionId); 85 | case Networking.Transport.Error.StatusCode.NetworkStateMismatch: 86 | return FixedString.Format(k_NetworkStateMismatch, connectionId); 87 | case Networking.Transport.Error.StatusCode.NetworkPacketOverflow: 88 | return k_NetworkPacketOverflow; 89 | case Networking.Transport.Error.StatusCode.NetworkSendQueueFull: 90 | return k_NetworkSendQueueFull; 91 | default: 92 | return FixedString.Format("Unknown error code {0}.", error); 93 | } 94 | } 95 | } 96 | 97 | 98 | [DisallowMultipleComponent] 99 | [AddComponentMenu("FishNet/Transport/Unity Transport")] 100 | public class UnityTransport : Transport, INetworkStreamDriverConstructor 101 | { 102 | /// 103 | /// Enum type stating the type of protocol 104 | /// 105 | public enum ProtocolType 106 | { 107 | /// 108 | /// Unity Transport Protocol 109 | /// 110 | UnityTransport, 111 | /// 112 | /// Unity Transport Protocol over Relay 113 | /// 114 | RelayUnityTransport, 115 | } 116 | 117 | private enum State 118 | { 119 | Disconnected, 120 | Listening, 121 | Connected, 122 | } 123 | 124 | /// 125 | /// The default maximum (receive) packet queue size 126 | /// 127 | public const int InitialMaxPacketQueueSize = 128; 128 | 129 | /// 130 | /// The default maximum payload size 131 | /// 132 | public const int InitialMaxPayloadSize = 6 * 1024; 133 | 134 | // Maximum reliable throughput, assuming the full reliable window can be sent on every 135 | // frame at 60 FPS. This will be a large over-estimation in any realistic scenario. 136 | private const int k_MaxReliableThroughput = (NetworkParameterConstants.MTU * 64 * 60) / 1000; // bytes per millisecond 137 | 138 | private static ConnectionAddressData s_DefaultConnectionAddressData = new ConnectionAddressData { Address = "127.0.0.1", Port = 7777, ServerListenAddress = string.Empty }; 139 | 140 | /// 141 | /// The global implementation 142 | /// 143 | private INetworkStreamDriverConstructor m_DriverConstructor; 144 | 145 | /// 146 | /// Returns either the global implementation or the current instance 147 | /// 148 | public INetworkStreamDriverConstructor DriverConstructor 149 | { 150 | get => m_DriverConstructor ?? this; 151 | set => m_DriverConstructor = value; 152 | } 153 | 154 | [Tooltip("Which protocol should be selected (Relay/Non-Relay).")] 155 | [SerializeField] 156 | private ProtocolType m_ProtocolType; 157 | 158 | #if UTP_TRANSPORT_2_0_ABOVE 159 | [Tooltip("Per default the client/server will communicate over UDP. Set to true to communicate with WebSocket.")] 160 | [SerializeField] 161 | private bool m_UseWebSockets = false; 162 | 163 | public bool UseWebSockets 164 | { 165 | get => m_UseWebSockets; 166 | set => m_UseWebSockets = value; 167 | } 168 | 169 | /// 170 | /// Per default the client/server communication will not be encrypted. Select true to enable DTLS for UDP and TLS for Websocket. 171 | /// 172 | [Tooltip("Per default the client/server communication will not be encrypted. Select true to enable DTLS for UDP and TLS for Websocket.")] 173 | [SerializeField] 174 | private bool m_UseEncryption = false; 175 | public bool UseEncryption 176 | { 177 | get => m_UseEncryption; 178 | set => m_UseEncryption = value; 179 | } 180 | #endif 181 | 182 | [Tooltip("The maximum amount of packets that can be in the internal send/receive queues. Basically this is how many packets can be sent/received in a single update/frame.")] 183 | [SerializeField] 184 | private int m_MaxPacketQueueSize = InitialMaxPacketQueueSize; 185 | 186 | /// The maximum amount of packets that can be in the internal send/receive queues. 187 | /// Basically this is how many packets can be sent/received in a single update/frame. 188 | public int MaxPacketQueueSize 189 | { 190 | get => m_MaxPacketQueueSize; 191 | set => m_MaxPacketQueueSize = value; 192 | } 193 | 194 | [Tooltip("The maximum size of an unreliable payload that can be handled by the transport.")] 195 | [SerializeField] 196 | private int m_MaxPayloadSize = InitialMaxPayloadSize; 197 | 198 | /// The maximum size of an unreliable payload that can be handled by the transport. 199 | public int MaxPayloadSize 200 | { 201 | get => m_MaxPayloadSize; 202 | set => m_MaxPayloadSize = value; 203 | } 204 | 205 | private int m_MaxSendQueueSize = 0; 206 | 207 | /// The maximum size in bytes of the transport send queue. 208 | /// 209 | /// The send queue accumulates messages for batching and stores messages when other internal 210 | /// send queues are full. Note that there should not be any need to set this value manually 211 | /// since the send queue size is dynamically sized based on need. 212 | /// 213 | /// This value should only be set if you have particular requirements (e.g. if you want to 214 | /// limit the memory usage of the send queues). Note however that setting this value too low 215 | /// can easily lead to disconnections under heavy traffic. 216 | /// 217 | public int MaxSendQueueSize 218 | { 219 | get => m_MaxSendQueueSize; 220 | set => m_MaxSendQueueSize = value; 221 | } 222 | 223 | [Tooltip("Timeout in milliseconds after which a heartbeat is sent if there is no activity.")] 224 | [SerializeField] 225 | private int m_HeartbeatTimeoutMS = NetworkParameterConstants.HeartbeatTimeoutMS; 226 | 227 | /// Timeout in milliseconds after which a heartbeat is sent if there is no activity. 228 | public int HeartbeatTimeoutMS 229 | { 230 | get => m_HeartbeatTimeoutMS; 231 | set => m_HeartbeatTimeoutMS = value; 232 | } 233 | 234 | [Tooltip("Timeout in milliseconds indicating how long we will wait until we send a new connection attempt.")] 235 | [SerializeField] 236 | private int m_ConnectTimeoutMS = NetworkParameterConstants.ConnectTimeoutMS; 237 | 238 | /// 239 | /// Timeout in milliseconds indicating how long we will wait until we send a new connection attempt. 240 | /// 241 | public int ConnectTimeoutMS 242 | { 243 | get => m_ConnectTimeoutMS; 244 | set => m_ConnectTimeoutMS = value; 245 | } 246 | 247 | [Tooltip("The maximum amount of connection attempts we will try before disconnecting.")] 248 | [SerializeField] 249 | private int m_MaxConnectAttempts = NetworkParameterConstants.MaxConnectAttempts; 250 | 251 | /// The maximum amount of connection attempts we will try before disconnecting. 252 | public int MaxConnectAttempts 253 | { 254 | get => m_MaxConnectAttempts; 255 | set => m_MaxConnectAttempts = value; 256 | } 257 | 258 | [Tooltip("Inactivity timeout after which a connection will be disconnected. The connection needs to receive data from the connected endpoint within this timeout. Note that with heartbeats enabled, simply not sending any data will not be enough to trigger this timeout (since heartbeats count as connection events).")] 259 | [SerializeField] 260 | private int m_DisconnectTimeoutMS = NetworkParameterConstants.DisconnectTimeoutMS; 261 | 262 | /// Inactivity timeout after which a connection will be disconnected. 263 | /// 264 | /// The connection needs to receive data from the connected endpoint within this timeout. 265 | /// Note that with heartbeats enabled, simply not sending any data will not be enough to 266 | /// trigger this timeout (since heartbeats count as connection events). 267 | /// 268 | public int DisconnectTimeoutMS 269 | { 270 | get => m_DisconnectTimeoutMS; 271 | set => m_DisconnectTimeoutMS = value; 272 | } 273 | 274 | /// 275 | /// Structure to store the address to connect to 276 | /// 277 | [Serializable] 278 | public struct ConnectionAddressData 279 | { 280 | /// 281 | /// IP address of the server (address to which clients will connect to). 282 | /// 283 | [Tooltip("IP address of the server (address to which clients will connect to).")] 284 | [SerializeField] 285 | public string Address; 286 | 287 | /// 288 | /// UDP port of the server. 289 | /// 290 | [Tooltip("UDP port of the server.")] 291 | [SerializeField] 292 | public ushort Port; 293 | 294 | /// 295 | /// IP address the server will listen on. If not provided, will use localhost. 296 | /// 297 | [Tooltip("IP address the server will listen on. If not provided, will use localhost.")] 298 | [SerializeField] 299 | public string ServerListenAddress; 300 | 301 | private static NetworkEndpoint ParseNetworkEndpoint(string ip, ushort port, bool silent = false) 302 | { 303 | NetworkEndpoint endpoint = default; 304 | 305 | if (!NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv4) && 306 | !NetworkEndpoint.TryParse(ip, port, out endpoint, NetworkFamily.Ipv6)) 307 | { 308 | IPAddress[] ipList = Dns.GetHostAddresses(ip); 309 | if (ipList.Length > 0) 310 | { 311 | endpoint = ParseNetworkEndpoint(ipList[0].ToString(), port, true); 312 | } 313 | } 314 | 315 | if (endpoint == default && !silent) 316 | { 317 | Debug.LogError($"Invalid network endpoint: {ip}:{port}."); 318 | } 319 | 320 | return endpoint; 321 | } 322 | 323 | /// 324 | /// Endpoint (IP address and port) clients will connect to. 325 | /// 326 | public NetworkEndpoint ServerEndPoint => ParseNetworkEndpoint(Address, Port); 327 | 328 | /// 329 | /// Endpoint (IP address and port) server will listen/bind on. 330 | /// 331 | public NetworkEndpoint ListenEndPoint 332 | { 333 | get 334 | { 335 | if (string.IsNullOrEmpty(ServerListenAddress)) 336 | { 337 | var ep = NetworkEndpoint.LoopbackIpv4; 338 | 339 | // If an address was entered and it's IPv6, switch to using ::1 as the 340 | // default listen address. (Otherwise we always assume IPv4.) 341 | if (!string.IsNullOrEmpty(Address) && ServerEndPoint.Family == NetworkFamily.Ipv6) 342 | { 343 | ep = NetworkEndpoint.LoopbackIpv6; 344 | } 345 | 346 | return ep.WithPort(Port); 347 | } 348 | else 349 | { 350 | return ParseNetworkEndpoint(ServerListenAddress, Port); 351 | } 352 | } 353 | } 354 | 355 | public bool IsIpv6 => !string.IsNullOrEmpty(Address) && ParseNetworkEndpoint(Address, Port, true).Family == NetworkFamily.Ipv6; 356 | } 357 | 358 | 359 | /// 360 | /// The connection (address) data for this instance. 361 | /// This is where you can change IP Address, Port, or server's listen address. 362 | /// 363 | /// 364 | public ConnectionAddressData ConnectionData = s_DefaultConnectionAddressData; 365 | 366 | /// 367 | /// Parameters for the Network Simulator 368 | /// 369 | [Serializable] 370 | public struct SimulatorParameters 371 | { 372 | /// 373 | /// Delay to add to every send and received packet (in milliseconds). Only applies in the editor and in development builds. The value is ignored in production builds. 374 | /// 375 | [Tooltip("Delay to add to every send and received packet (in milliseconds). Only applies in the editor and in development builds. The value is ignored in production builds.")] 376 | [SerializeField] 377 | public int PacketDelayMS; 378 | 379 | /// 380 | /// Jitter (random variation) to add/substract to the packet delay (in milliseconds). Only applies in the editor and in development builds. The value is ignored in production builds. 381 | /// 382 | [Tooltip("Jitter (random variation) to add/substract to the packet delay (in milliseconds). Only applies in the editor and in development builds. The value is ignored in production builds.")] 383 | [SerializeField] 384 | public int PacketJitterMS; 385 | 386 | /// 387 | /// Percentage of sent and received packets to drop. Only applies in the editor and in the editor and in developments builds. 388 | /// 389 | [Tooltip("Percentage of sent and received packets to drop. Only applies in the editor and in the editor and in developments builds.")] 390 | [SerializeField] 391 | public int PacketDropRate; 392 | } 393 | 394 | /// 395 | /// Can be used to simulate poor network conditions such as: 396 | /// - packet delay/latency 397 | /// - packet jitter (variances in latency, see: https://en.wikipedia.org/wiki/Jitter) 398 | /// - packet drop rate (packet loss) 399 | /// 400 | 401 | #if UTP_TRANSPORT_2_0_ABOVE 402 | [Obsolete("DebugSimulator is no longer supported and has no effect. Use Network Simulator from the Multiplayer Tools package.", false)] 403 | [HideInInspector] 404 | #endif 405 | public SimulatorParameters DebugSimulator = new SimulatorParameters 406 | { 407 | PacketDelayMS = 0, 408 | PacketJitterMS = 0, 409 | PacketDropRate = 0 410 | }; 411 | 412 | protected NetworkDriver m_Driver; 413 | private State m_State = State.Disconnected; 414 | private NetworkSettings m_NetworkSettings; 415 | private ulong m_ServerClientId; 416 | 417 | private NetworkPipeline m_UnreliableFragmentedPipeline; 418 | private NetworkPipeline m_UnreliableSequencedFragmentedPipeline; 419 | private NetworkPipeline m_ReliableSequencedPipeline; 420 | 421 | /// 422 | /// The client id used to represent the server. 423 | /// 424 | private ulong ServerClientId => m_ServerClientId; 425 | 426 | /// 427 | /// The current ProtocolType used by the transport 428 | /// 429 | public ProtocolType Protocol => m_ProtocolType; 430 | 431 | private RelayServerData m_RelayServerData; 432 | 433 | /// 434 | /// SendQueue dictionary is used to batch events instead of sending them immediately. 435 | /// 436 | private readonly Dictionary m_SendQueue = new Dictionary(); 437 | 438 | // Since reliable messages may be spread out over multiple transport payloads, it's possible 439 | // to receive only parts of a message in an update. We thus keep the reliable receive queues 440 | // around to avoid losing partial messages. 441 | private readonly Dictionary m_ReliableReceiveQueues = new Dictionary(); 442 | 443 | private void InitDriver() 444 | { 445 | DriverConstructor.CreateDriver( 446 | this, 447 | out m_Driver, 448 | out m_UnreliableFragmentedPipeline, 449 | out m_UnreliableSequencedFragmentedPipeline, 450 | out m_ReliableSequencedPipeline); 451 | } 452 | 453 | private void DisposeInternals() 454 | { 455 | if (m_Driver.IsCreated) 456 | { 457 | m_Driver.Dispose(); 458 | } 459 | 460 | m_NetworkSettings.Dispose(); 461 | 462 | foreach (var queue in m_SendQueue.Values) 463 | { 464 | queue.Dispose(); 465 | } 466 | 467 | m_SendQueue.Clear(); 468 | DisposeClientHost(); 469 | } 470 | 471 | private NetworkPipeline SelectSendPipeline(Channel channel) 472 | { 473 | switch (channel) 474 | { 475 | case Channel.Unreliable: 476 | return m_UnreliableFragmentedPipeline; 477 | case Channel.Reliable: 478 | return m_ReliableSequencedPipeline; 479 | default: 480 | Debug.LogError($"Unknown {nameof(Channel)} value: {channel}"); 481 | return NetworkPipeline.Null; 482 | } 483 | } 484 | 485 | private bool ClientBindAndConnect() 486 | { 487 | var serverEndpoint = default(NetworkEndpoint); 488 | 489 | if (m_ProtocolType == ProtocolType.RelayUnityTransport) 490 | { 491 | //This comparison is currently slow since RelayServerData does not implement a custom comparison operator that doesn't use 492 | //reflection, but this does not live in the context of a performance-critical loop, it runs once at initial connection time. 493 | if (m_RelayServerData.Equals(default(RelayServerData))) 494 | { 495 | Debug.LogError("You must call SetRelayServerData() at least once before calling StartClient."); 496 | return false; 497 | } 498 | 499 | m_NetworkSettings.WithRelayParameters(ref m_RelayServerData, m_HeartbeatTimeoutMS); 500 | serverEndpoint = m_RelayServerData.Endpoint; 501 | } 502 | else 503 | { 504 | serverEndpoint = ConnectionData.ServerEndPoint; 505 | } 506 | 507 | // Verify the endpoint is valid before proceeding 508 | if (serverEndpoint.Family == NetworkFamily.Invalid) 509 | { 510 | Debug.LogError($"Target server network address ({ConnectionData.Address}) is {nameof(NetworkFamily.Invalid)}!"); 511 | return false; 512 | } 513 | 514 | InitDriver(); 515 | 516 | var bindEndpoint = serverEndpoint.Family == NetworkFamily.Ipv6 ? NetworkEndpoint.AnyIpv6 : NetworkEndpoint.AnyIpv4; 517 | int result = m_Driver.Bind(bindEndpoint); 518 | if (result != 0) 519 | { 520 | Debug.LogError("Client failed to bind"); 521 | return false; 522 | } 523 | 524 | var serverConnection = m_Driver.Connect(serverEndpoint); 525 | m_ServerClientId = ParseClientId(serverConnection); 526 | 527 | return true; 528 | } 529 | 530 | private bool ServerBindAndListen(NetworkEndpoint endPoint) 531 | { 532 | // Verify the endpoint is valid before proceeding 533 | if (endPoint.Family == NetworkFamily.Invalid) 534 | { 535 | Debug.LogError($"Network listen address ({ConnectionData.Address}) is {nameof(NetworkFamily.Invalid)}!"); 536 | return false; 537 | } 538 | 539 | InitDriver(); 540 | 541 | int result = m_Driver.Bind(endPoint); 542 | if (result != 0) 543 | { 544 | Debug.LogError("Server failed to bind. This is usually caused by another process being bound to the same port."); 545 | return false; 546 | } 547 | 548 | result = m_Driver.Listen(); 549 | if (result != 0) 550 | { 551 | Debug.LogError("Server failed to listen."); 552 | return false; 553 | } 554 | 555 | m_State = State.Listening; 556 | return true; 557 | } 558 | 559 | private void SetProtocol(ProtocolType inProtocol) 560 | { 561 | m_ProtocolType = inProtocol; 562 | } 563 | 564 | /// Set the relay server data for the server. 565 | /// IP address or hostname of the relay server. 566 | /// UDP port of the relay server. 567 | /// Allocation ID as a byte array. 568 | /// Allocation key as a byte array. 569 | /// Connection data as a byte array. 570 | /// The HostConnectionData as a byte array. 571 | /// Whether the connection is secure (uses DTLS). 572 | public void SetRelayServerData(string ipv4Address, ushort port, byte[] allocationIdBytes, byte[] keyBytes, byte[] connectionDataBytes, byte[] hostConnectionDataBytes = null, bool isSecure = false) 573 | { 574 | var hostConnectionData = hostConnectionDataBytes ?? connectionDataBytes; 575 | m_RelayServerData = new RelayServerData(ipv4Address, port, allocationIdBytes, connectionDataBytes, hostConnectionData, keyBytes, isSecure); 576 | SetProtocol(ProtocolType.RelayUnityTransport); 577 | } 578 | 579 | /// Set the relay server data (using the lower-level Unity Transport data structure). 580 | /// Data for the Relay server to use. 581 | public void SetRelayServerData(RelayServerData serverData) 582 | { 583 | if (m_ServerState.IsStartingOrStarted()) 584 | { 585 | NetworkManager.LogWarning("It looks like you are trying to connect as a host to the " + 586 | "Relay server. Since the local server is already running, " + 587 | "calling SetRelayServerData() for the client is unnecessary. " + 588 | "It doesn't cause errors, you can ignore it if you're sure " + 589 | "you're doing it right."); 590 | } 591 | 592 | m_RelayServerData = serverData; 593 | SetProtocol(ProtocolType.RelayUnityTransport); 594 | } 595 | 596 | /// Set the relay server data for the host. 597 | /// IP address or hostname of the relay server. 598 | /// UDP port of the relay server. 599 | /// Allocation ID as a byte array. 600 | /// Allocation key as a byte array. 601 | /// Connection data as a byte array. 602 | /// Whether the connection is secure (uses DTLS). 603 | public void SetHostRelayData(string ipAddress, ushort port, byte[] allocationId, byte[] key, byte[] connectionData, bool isSecure = false) 604 | { 605 | SetRelayServerData(ipAddress, port, allocationId, key, connectionData, null, isSecure); 606 | } 607 | 608 | /// Set the relay server data for the host. 609 | /// IP address or hostname of the relay server. 610 | /// UDP port of the relay server. 611 | /// Allocation ID as a byte array. 612 | /// Allocation key as a byte array. 613 | /// Connection data as a byte array. 614 | /// Host's connection data as a byte array. 615 | /// Whether the connection is secure (uses DTLS). 616 | public void SetClientRelayData(string ipAddress, ushort port, byte[] allocationId, byte[] key, byte[] connectionData, byte[] hostConnectionData, bool isSecure = false) 617 | { 618 | SetRelayServerData(ipAddress, port, allocationId, key, connectionData, hostConnectionData, isSecure); 619 | } 620 | 621 | /// 622 | /// Sets IP and Port information. This will be ignored if using the Unity Relay and you should call 623 | /// 624 | /// The remote IP address (despite the name, can be an IPv6 address) 625 | /// The remote port 626 | /// The local listen address 627 | public void SetConnectionData(string ipv4Address, ushort port, string listenAddress = null) 628 | { 629 | ConnectionData = new ConnectionAddressData 630 | { 631 | Address = ipv4Address, 632 | Port = port, 633 | ServerListenAddress = listenAddress ?? ipv4Address 634 | }; 635 | 636 | SetProtocol(ProtocolType.UnityTransport); 637 | } 638 | 639 | /// 640 | /// Sets IP and Port information. This will be ignored if using the Unity Relay and you should call 641 | /// 642 | /// The remote end point 643 | /// The local listen endpoint 644 | public void SetConnectionData(NetworkEndpoint endPoint, NetworkEndpoint listenEndPoint = default) 645 | { 646 | string serverAddress = endPoint.Address.Split(':')[0]; 647 | 648 | string listenAddress = string.Empty; 649 | if (listenEndPoint != default) 650 | { 651 | listenAddress = listenEndPoint.Address.Split(':')[0]; 652 | if (endPoint.Port != listenEndPoint.Port) 653 | { 654 | Debug.LogError($"Port mismatch between server and listen endpoints ({endPoint.Port} vs {listenEndPoint.Port})."); 655 | } 656 | } 657 | 658 | SetConnectionData(serverAddress, endPoint.Port, listenAddress); 659 | } 660 | 661 | /// Set the parameters for the debug simulator. 662 | /// Packet delay in milliseconds. 663 | /// Packet jitter in milliseconds. 664 | /// Packet drop percentage. 665 | 666 | #if UTP_TRANSPORT_2_0_ABOVE 667 | [Obsolete("SetDebugSimulatorParameters is no longer supported and has no effect. Use Network Simulator from the Multiplayer Tools package.", false)] 668 | #endif 669 | 670 | public void SetDebugSimulatorParameters(int packetDelay, int packetJitter, int dropRate) 671 | { 672 | if (m_Driver.IsCreated) 673 | { 674 | Debug.LogError("SetDebugSimulatorParameters() must be called before StartClient() or StartServer()."); 675 | return; 676 | } 677 | 678 | DebugSimulator = new SimulatorParameters 679 | { 680 | PacketDelayMS = packetDelay, 681 | PacketJitterMS = packetJitter, 682 | PacketDropRate = dropRate 683 | }; 684 | } 685 | 686 | private bool StartRelayServer() 687 | { 688 | //This comparison is currently slow since RelayServerData does not implement a custom comparison operator that doesn't use 689 | //reflection, but this does not live in the context of a performance-critical loop, it runs once at initial connection time. 690 | if (m_RelayServerData.Equals(default(RelayServerData))) 691 | { 692 | Debug.LogError("You must call SetRelayServerData() at least once before calling StartServer."); 693 | return false; 694 | } 695 | else 696 | { 697 | m_NetworkSettings.WithRelayParameters(ref m_RelayServerData, m_HeartbeatTimeoutMS); 698 | return ServerBindAndListen(NetworkEndpoint.AnyIpv4); 699 | } 700 | } 701 | 702 | [BurstCompile] 703 | private struct SendBatchedMessagesJob : IJob 704 | { 705 | public NetworkDriver.Concurrent Driver; 706 | public SendTarget Target; 707 | public BatchedSendQueue Queue; 708 | public NetworkPipeline ReliablePipeline; 709 | 710 | public void Execute() 711 | { 712 | var clientId = Target.ClientId; 713 | var connection = ParseClientId(clientId); 714 | var pipeline = Target.NetworkPipeline; 715 | 716 | while (!Queue.IsEmpty) 717 | { 718 | var result = Driver.BeginSend(pipeline, connection, out var writer); 719 | if (result != (int)Networking.Transport.Error.StatusCode.Success) 720 | { 721 | Debug.LogError($"Error sending message: {ErrorUtilities.ErrorToFixedString(result, clientId)}"); 722 | return; 723 | } 724 | 725 | // We don't attempt to send entire payloads over the reliable pipeline. Instead we 726 | // fragment it manually. This is safe and easy to do since the reliable pipeline 727 | // basically implements a stream, so as long as we separate the different messages 728 | // in the stream (the send queue does that automatically) we are sure they'll be 729 | // reassembled properly at the other end. This allows us to lift the limit of ~44KB 730 | // on reliable payloads (because of the reliable window size). 731 | var written = pipeline == ReliablePipeline ? Queue.FillWriterWithBytes(ref writer) : Queue.FillWriterWithMessages(ref writer); 732 | 733 | result = Driver.EndSend(writer); 734 | if (result == written) 735 | { 736 | // Batched message was sent successfully. Remove it from the queue. 737 | Queue.Consume(written); 738 | } 739 | else 740 | { 741 | // Some error occured. If it's just the UTP queue being full, then don't log 742 | // anything since that's okay (the unsent message(s) are still in the queue 743 | // and we'll retry sending them later). Otherwise log the error and remove the 744 | // message from the queue (we don't want to resend it again since we'll likely 745 | // just get the same error again). 746 | if (result != (int)Networking.Transport.Error.StatusCode.NetworkSendQueueFull) 747 | { 748 | Debug.LogError($"Error sending the message: {ErrorUtilities.ErrorToFixedString(result, clientId)}"); 749 | Queue.Consume(written); 750 | } 751 | 752 | return; 753 | } 754 | } 755 | } 756 | } 757 | 758 | // Send as many batched messages from the queue as possible. 759 | private void SendBatchedMessages(SendTarget sendTarget, BatchedSendQueue queue) 760 | { 761 | if (!m_Driver.IsCreated) 762 | { 763 | return; 764 | } 765 | new SendBatchedMessagesJob 766 | { 767 | Driver = m_Driver.ToConcurrent(), 768 | Target = sendTarget, 769 | Queue = queue, 770 | ReliablePipeline = m_ReliableSequencedPipeline 771 | }.Run(); 772 | } 773 | 774 | private bool AcceptConnection() 775 | { 776 | var connection = m_Driver.Accept(); 777 | 778 | if (connection == default) 779 | { 780 | return false; 781 | } 782 | 783 | HandleRemoteConnectionState(RemoteConnectionState.Started, ParseClientId(connection)); 784 | return true; 785 | } 786 | 787 | private void ReceiveMessages(ulong clientId, NetworkPipeline pipeline, DataStreamReader dataReader) 788 | { 789 | BatchedReceiveQueue queue; 790 | if (pipeline == m_ReliableSequencedPipeline) 791 | { 792 | if (m_ReliableReceiveQueues.TryGetValue(clientId, out queue)) 793 | { 794 | queue.PushReader(dataReader); 795 | } 796 | else 797 | { 798 | queue = new BatchedReceiveQueue(dataReader); 799 | m_ReliableReceiveQueues[clientId] = queue; 800 | } 801 | } 802 | else 803 | { 804 | queue = new BatchedReceiveQueue(dataReader); 805 | } 806 | 807 | while (!queue.IsEmpty) 808 | { 809 | var message = queue.PopMessage(); 810 | if (message == default) 811 | { 812 | // Only happens if there's only a partial message in the queue (rare). 813 | break; 814 | } 815 | 816 | HandleTransportDataEvent(clientId, message, pipeline); 817 | } 818 | } 819 | 820 | private bool ProcessEvent() 821 | { 822 | var eventType = m_Driver.PopEvent(out var networkConnection, out var reader, out var pipeline); 823 | var clientId = ParseClientId(networkConnection); 824 | 825 | switch (eventType) 826 | { 827 | case TransportNetworkEvent.Type.Connect: 828 | { 829 | HandleTransportConnectEvent(clientId); 830 | 831 | m_State = State.Connected; 832 | return true; 833 | } 834 | case TransportNetworkEvent.Type.Disconnect: 835 | { 836 | // Handle cases where we're a client receiving a Disconnect event. The 837 | // meaning of the event depends on our current state. If we were connected 838 | // then it means we got disconnected. If we were disconnected means that our 839 | // connection attempt has failed. 840 | if (m_State == State.Connected) 841 | { 842 | m_State = State.Disconnected; 843 | m_ServerClientId = default; 844 | } 845 | else if (m_State == State.Disconnected) 846 | { 847 | Debug.LogError("Failed to connect to server."); 848 | m_ServerClientId = default; 849 | } 850 | 851 | m_ReliableReceiveQueues.Remove(clientId); 852 | ClearSendQueuesForClientId(clientId); 853 | 854 | HandleTransportDisconnectEvent(clientId); 855 | 856 | return true; 857 | } 858 | case TransportNetworkEvent.Type.Data: 859 | { 860 | ReceiveMessages(clientId, pipeline, reader); 861 | return true; 862 | } 863 | } 864 | 865 | return false; 866 | } 867 | 868 | /// 869 | /// Handles accepting new connections and processing transport events. 870 | /// 871 | private void IterateIncoming() 872 | { 873 | if (m_Driver.IsCreated) 874 | { 875 | if (m_ProtocolType == ProtocolType.RelayUnityTransport && m_Driver.GetRelayConnectionStatus() == RelayConnectionStatus.AllocationInvalid) 876 | { 877 | Debug.LogError("Transport failure! Relay allocation needs to be recreated, and NetworkManager restarted. " + 878 | "Use NetworkManager.OnTransportFailure to be notified of such events programmatically."); 879 | 880 | HandleTransportFailureEvent(); 881 | return; 882 | } 883 | 884 | m_Driver.ScheduleUpdate().Complete(); 885 | 886 | // Process any new connections 887 | while (AcceptConnection() && m_Driver.IsCreated) 888 | { 889 | ; 890 | } 891 | 892 | // Process any transport events (i.e. connect, disconnect, data, etc) 893 | while (ProcessEvent() && m_Driver.IsCreated) 894 | { 895 | ; 896 | } 897 | } 898 | } 899 | 900 | /// 901 | /// Handles sending any queued batched messages. 902 | /// 903 | public void IterateOutgoing() 904 | { 905 | if (m_Driver.IsCreated) 906 | { 907 | foreach (var kvp in m_SendQueue) 908 | { 909 | SendBatchedMessages(kvp.Key, kvp.Value); 910 | } 911 | 912 | // Schedule a flush send as the last transport action for the 913 | // current frame. 914 | m_Driver.ScheduleFlushSend(default).Complete(); 915 | } 916 | } 917 | 918 | private void OnDestroy() 919 | { 920 | Shutdown(); 921 | } 922 | 923 | private int ExtractRtt(NetworkConnection networkConnection) 924 | { 925 | if (m_Driver.GetConnectionState(networkConnection) != NetworkConnection.State.Connected) 926 | { 927 | return 0; 928 | } 929 | 930 | m_Driver.GetPipelineBuffers(m_ReliableSequencedPipeline, 931 | #if UTP_TRANSPORT_2_0_ABOVE 932 | NetworkPipelineStageId.Get(), 933 | #else 934 | NetworkPipelineStageCollection.GetStageId(typeof(ReliableSequencedPipelineStage)), 935 | #endif 936 | networkConnection, 937 | out _, 938 | out _, 939 | out var sharedBuffer); 940 | 941 | unsafe 942 | { 943 | var sharedContext = (ReliableUtility.SharedContext*)sharedBuffer.GetUnsafePtr(); 944 | 945 | return sharedContext->RttInfo.LastRtt; 946 | } 947 | } 948 | 949 | private static unsafe ulong ParseClientId(NetworkConnection connection) 950 | { 951 | return *(ulong*)&connection; 952 | } 953 | 954 | private static unsafe NetworkConnection ParseClientId(ulong clientId) 955 | { 956 | return *(NetworkConnection*)&clientId; 957 | } 958 | 959 | private void ClearSendQueuesForClientId(ulong clientId) 960 | { 961 | // NativeList and manual foreach avoids any allocations. 962 | using var keys = new NativeList(16, Allocator.Temp); 963 | foreach (var key in m_SendQueue.Keys) 964 | { 965 | if (key.ClientId == clientId) 966 | { 967 | keys.Add(key); 968 | } 969 | } 970 | 971 | foreach (var target in keys) 972 | { 973 | m_SendQueue[target].Dispose(); 974 | m_SendQueue.Remove(target); 975 | } 976 | } 977 | 978 | private void FlushSendQueuesForClientId(ulong clientId) 979 | { 980 | foreach (var kvp in m_SendQueue) 981 | { 982 | if (kvp.Key.ClientId == clientId) 983 | { 984 | SendBatchedMessages(kvp.Key, kvp.Value); 985 | } 986 | } 987 | } 988 | 989 | /// 990 | /// Disconnects the local client from the remote 991 | /// 992 | private void DisconnectLocalClient() 993 | { 994 | SetClientConnectionState(LocalConnectionState.Stopping); 995 | 996 | if (m_State == State.Connected) 997 | { 998 | FlushSendQueuesForClientId(m_ServerClientId); 999 | 1000 | if (m_Driver.Disconnect(ParseClientId(m_ServerClientId)) == 0) 1001 | { 1002 | m_State = State.Disconnected; 1003 | 1004 | m_ReliableReceiveQueues.Remove(m_ServerClientId); 1005 | ClearSendQueuesForClientId(m_ServerClientId); 1006 | 1007 | ShutdownInternals(); 1008 | } 1009 | } 1010 | 1011 | SetClientConnectionState(LocalConnectionState.Stopped); 1012 | } 1013 | 1014 | /// 1015 | /// Disconnects a remote client from the server 1016 | /// 1017 | /// The client to disconnect 1018 | private bool DisconnectRemoteClient(ulong clientId) 1019 | { 1020 | Debug.Assert(m_State == State.Listening, "DisconnectRemoteClient should be called on a listening server"); 1021 | 1022 | if (m_State == State.Listening) 1023 | { 1024 | FlushSendQueuesForClientId(clientId); 1025 | 1026 | m_ReliableReceiveQueues.Remove(clientId); 1027 | ClearSendQueuesForClientId(clientId); 1028 | 1029 | var connection = ParseClientId(clientId); 1030 | if (m_Driver.GetConnectionState(connection) != NetworkConnection.State.Disconnected) 1031 | { 1032 | m_Driver.Disconnect(connection); 1033 | 1034 | // Disconnect doesn't notify SDK of disconnection. 1035 | HandleTransportDisconnectEvent(clientId); 1036 | return true; 1037 | } 1038 | } 1039 | 1040 | return false; 1041 | } 1042 | 1043 | /// 1044 | /// Gets the current RTT for a specific client 1045 | /// 1046 | /// The client RTT to get 1047 | /// The RTT 1048 | public ulong GetCurrentRtt(int clientId) 1049 | { 1050 | if (m_TransportIdToClientIdMap.TryGetValue(clientId, out ulong transportId)) 1051 | { 1052 | return (ulong)ExtractRtt(ParseClientId(transportId)); 1053 | } 1054 | NetworkManager.LogWarning($"Connection with id {clientId} is disconnected. Unable to get the current Rtt."); 1055 | return 0; 1056 | } 1057 | 1058 | private void InitializeNetworkSettings() 1059 | { 1060 | m_NetworkSettings = new NetworkSettings(Allocator.Persistent); 1061 | 1062 | // If the user sends a message of exactly m_MaxPayloadSize in length, we need to 1063 | // account for the overhead of its length when we store it in the send queue. 1064 | var fragmentationCapacity = m_MaxPayloadSize + BatchedSendQueue.PerMessageOverhead; 1065 | m_NetworkSettings.WithFragmentationStageParameters(payloadCapacity: fragmentationCapacity); 1066 | 1067 | m_NetworkSettings.WithReliableStageParameters( 1068 | windowSize: 64 1069 | #if UTP_TRANSPORT_2_0_ABOVE 1070 | , 1071 | maximumResendTime: m_ProtocolType == ProtocolType.RelayUnityTransport ? 750 : 500 1072 | #endif 1073 | ); 1074 | 1075 | #if !UTP_TRANSPORT_2_0_ABOVE && !UNITY_WEBGL 1076 | m_NetworkSettings.WithBaselibNetworkInterfaceParameters( 1077 | receiveQueueCapacity: m_MaxPacketQueueSize, 1078 | sendQueueCapacity: m_MaxPacketQueueSize); 1079 | #endif 1080 | } 1081 | 1082 | /// 1083 | /// Send a payload to the specified clientId, data and networkDelivery. 1084 | /// 1085 | /// The clientId to send to 1086 | /// The data to send 1087 | /// 1088 | private void Send(ulong clientId, ArraySegment payload, Channel channel) 1089 | { 1090 | var pipeline = SelectSendPipeline(channel); 1091 | 1092 | if (pipeline != m_ReliableSequencedPipeline && payload.Count > m_MaxPayloadSize) 1093 | { 1094 | Debug.LogError($"Unreliable payload of size {payload.Count} larger than configured 'Max Payload Size' ({m_MaxPayloadSize})."); 1095 | return; 1096 | } 1097 | 1098 | var sendTarget = new SendTarget(clientId, pipeline); 1099 | if (!m_SendQueue.TryGetValue(sendTarget, out var queue)) 1100 | { 1101 | // The maximum size of a send queue is determined according to the disconnection 1102 | // timeout. The idea being that if the send queue contains enough reliable data that 1103 | // sending it all out would take longer than the disconnection timeout, then there's 1104 | // no point storing even more in the queue (it would be like having a ping higher 1105 | // than the disconnection timeout, which is far into the realm of unplayability). 1106 | // 1107 | // The throughput used to determine what consists the maximum send queue size is 1108 | // the maximum theoritical throughput of the reliable pipeline assuming we only send 1109 | // on each update at 60 FPS, which turns out to be around 2.688 MB/s. 1110 | // 1111 | // Note that we only care about reliable throughput for send queues because that's 1112 | // the only case where a full send queue causes a connection loss. Full unreliable 1113 | // send queues are dealt with by flushing it out to the network or simply dropping 1114 | // new messages if that fails. 1115 | var maxCapacity = m_MaxSendQueueSize > 0 ? m_MaxSendQueueSize : m_DisconnectTimeoutMS * k_MaxReliableThroughput; 1116 | 1117 | queue = new BatchedSendQueue(Math.Max(maxCapacity, m_MaxPayloadSize)); 1118 | m_SendQueue.Add(sendTarget, queue); 1119 | } 1120 | 1121 | if (!queue.PushMessage(payload)) 1122 | { 1123 | if (pipeline == m_ReliableSequencedPipeline) 1124 | { 1125 | // If the message is sent reliably, then we're over capacity and we can't 1126 | // provide any reliability guarantees anymore. Disconnect the client since at 1127 | // this point they're bound to become desynchronized. 1128 | 1129 | Debug.LogError($"Couldn't add payload of size {payload.Count} to reliable send queue. " + 1130 | $"Closing connection {TransportIdToClientId(clientId)} as reliability guarantees can't be maintained."); 1131 | 1132 | if (clientId == m_ServerClientId) 1133 | { 1134 | DisconnectLocalClient(); 1135 | } 1136 | else 1137 | { 1138 | DisconnectRemoteClient(clientId); 1139 | } 1140 | } 1141 | else 1142 | { 1143 | // If the message is sent unreliably, we can always just flush everything out 1144 | // to make space in the send queue. This is an expensive operation, but a user 1145 | // would need to send A LOT of unreliable traffic in one update to get here. 1146 | 1147 | m_Driver.ScheduleFlushSend(default).Complete(); 1148 | SendBatchedMessages(sendTarget, queue); 1149 | 1150 | // Don't check for failure. If it still doesn't work, there's nothing we can do 1151 | // at this point and the message is lost (it was sent unreliable anyway). 1152 | queue.PushMessage(payload); 1153 | } 1154 | } 1155 | } 1156 | 1157 | /// 1158 | /// Connects client to the server 1159 | /// Note: 1160 | /// When this method returns false it could mean: 1161 | /// - You are trying to start a client that is already started 1162 | /// - It failed during the initial port binding when attempting to begin to connect 1163 | /// 1164 | /// true if the client was started and false if it failed to start the client 1165 | private bool StartClient() 1166 | { 1167 | if (m_ClientState != LocalConnectionState.Stopped) 1168 | { 1169 | return false; 1170 | } 1171 | 1172 | SetClientConnectionState(LocalConnectionState.Starting); 1173 | 1174 | if (m_ServerState == LocalConnectionState.Starting) 1175 | { 1176 | return true; 1177 | } 1178 | if (m_ServerState == LocalConnectionState.Started) 1179 | { 1180 | if (m_TransportIdToClientIdMap.Count >= GetMaximumClients()) 1181 | { 1182 | SetClientConnectionState(LocalConnectionState.Stopping); 1183 | NetworkManager.LogWarning("Connection limit reached. Server cannot accept new connections."); 1184 | SetClientConnectionState(LocalConnectionState.Stopped); 1185 | return false; 1186 | } 1187 | m_ServerClientId = k_ClientHostId; 1188 | HandleRemoteConnectionState(RemoteConnectionState.Started, m_ServerClientId); 1189 | SetClientConnectionState(LocalConnectionState.Started); 1190 | return true; 1191 | } 1192 | 1193 | if (m_Driver.IsCreated) 1194 | { 1195 | return false; 1196 | } 1197 | 1198 | InitializeNetworkSettings(); 1199 | 1200 | bool succeeded = ClientBindAndConnect(); 1201 | if (!succeeded) 1202 | { 1203 | SetClientConnectionState(LocalConnectionState.Stopping); 1204 | ShutdownInternals(); 1205 | SetClientConnectionState(LocalConnectionState.Stopped); 1206 | } 1207 | return succeeded; 1208 | } 1209 | 1210 | /// 1211 | /// Starts to listening for incoming clients 1212 | /// Note: 1213 | /// When this method returns false it could mean: 1214 | /// - You are trying to start a client that is already started 1215 | /// - It failed during the initial port binding when attempting to begin to connect 1216 | /// 1217 | /// true if the server was started and false if it failed to start the server 1218 | private bool StartServer() 1219 | { 1220 | if (m_Driver.IsCreated) 1221 | { 1222 | return false; 1223 | } 1224 | 1225 | SetServerConnectionState(LocalConnectionState.Starting); 1226 | 1227 | InitializeNetworkSettings(); 1228 | 1229 | bool succeeded = Protocol switch 1230 | { 1231 | ProtocolType.UnityTransport => ServerBindAndListen(ConnectionData.ListenEndPoint), 1232 | ProtocolType.RelayUnityTransport => StartRelayServer(), 1233 | _ => false 1234 | }; 1235 | 1236 | if (succeeded) 1237 | { 1238 | SetServerConnectionState(LocalConnectionState.Started); 1239 | if (m_ClientState == LocalConnectionState.Starting) 1240 | { 1241 | // Success client host starting 1242 | HandleRemoteConnectionState(RemoteConnectionState.Started, m_ServerClientId); 1243 | SetClientConnectionState(LocalConnectionState.Started); 1244 | } 1245 | } 1246 | else 1247 | { 1248 | SetServerConnectionState(LocalConnectionState.Stopping); 1249 | StopClientHost(); // Fail client host if starting 1250 | DisposeInternals(); 1251 | SetServerConnectionState(LocalConnectionState.Stopped); 1252 | } 1253 | 1254 | return succeeded; 1255 | } 1256 | 1257 | /// 1258 | /// Shuts down the transport 1259 | /// 1260 | private void ShutdownInternals() 1261 | { 1262 | if (m_Driver.IsCreated) 1263 | { 1264 | while (ProcessEvent() && m_Driver.IsCreated) 1265 | { 1266 | ; 1267 | } 1268 | 1269 | // Flush all send queues to the network. NGO can be configured to flush its message 1270 | // queue on shutdown. But this only calls the Send() method, which doesn't actually 1271 | // get anything to the network. 1272 | foreach (var kvp in m_SendQueue) 1273 | { 1274 | SendBatchedMessages(kvp.Key, kvp.Value); 1275 | } 1276 | 1277 | // The above flush only puts the message in UTP internal buffers, need an update to 1278 | // actually get the messages on the wire. (Normally a flush send would be sufficient, 1279 | // but there might be disconnect messages and those require an update call.) 1280 | m_Driver.ScheduleUpdate().Complete(); 1281 | } 1282 | 1283 | DisposeInternals(); 1284 | 1285 | m_ReliableReceiveQueues.Clear(); 1286 | m_State = State.Disconnected; 1287 | 1288 | // We must reset this to zero because UTP actually re-uses clientIds if there is a clean disconnect 1289 | m_ServerClientId = 0; 1290 | } 1291 | 1292 | #if UTP_TRANSPORT_2_0_ABOVE 1293 | private void ConfigureSimulatorForUtp2() 1294 | { 1295 | // As DebugSimulator is deprecated, the 'packetDelayMs', 'packetJitterMs' and 'packetDropPercentage' 1296 | // parameters are set to the default and are supposed to be changed using Network Simulator tool instead. 1297 | m_NetworkSettings.WithSimulatorStageParameters( 1298 | maxPacketCount: 300, // TODO Is there any way to compute a better value? 1299 | maxPacketSize: NetworkParameterConstants.MTU, 1300 | packetDelayMs: 0, 1301 | packetJitterMs: 0, 1302 | packetDropPercentage: 0, 1303 | randomSeed: (uint)System.Diagnostics.Stopwatch.GetTimestamp() 1304 | , mode: ApplyMode.AllPackets 1305 | ); 1306 | 1307 | m_NetworkSettings.WithNetworkSimulatorParameters(); 1308 | } 1309 | #else 1310 | private void ConfigureSimulatorForUtp1() 1311 | { 1312 | m_NetworkSettings.WithSimulatorStageParameters( 1313 | maxPacketCount: 300, // TODO Is there any way to compute a better value? 1314 | maxPacketSize: NetworkParameterConstants.MTU, 1315 | packetDelayMs: DebugSimulator.PacketDelayMS, 1316 | packetJitterMs: DebugSimulator.PacketJitterMS, 1317 | packetDropPercentage: DebugSimulator.PacketDropRate, 1318 | randomSeed: (uint)System.Diagnostics.Stopwatch.GetTimestamp() 1319 | ); 1320 | } 1321 | #endif 1322 | 1323 | #if UTP_TRANSPORT_2_0_ABOVE 1324 | private string m_ServerPrivateKey; 1325 | private string m_ServerCertificate; 1326 | 1327 | private string m_ServerCommonName; 1328 | private string m_ClientCaCertificate; 1329 | 1330 | /// Set the server parameters for encryption. 1331 | /// 1332 | /// The public certificate and private key are expected to be in the PEM format, including 1333 | /// the begin/end markers like -----BEGIN CERTIFICATE-----. 1334 | /// 1335 | /// Public certificate for the server (PEM format). 1336 | /// Private key for the server (PEM format). 1337 | public void SetServerSecrets(string serverCertificate, string serverPrivateKey) 1338 | { 1339 | m_ServerPrivateKey = serverPrivateKey; 1340 | m_ServerCertificate = serverCertificate; 1341 | } 1342 | 1343 | /// Set the client parameters for encryption. 1344 | /// 1345 | /// 1346 | /// If the CA certificate is not provided, validation will be done against the OS/browser 1347 | /// certificate store. This is what you'd want if using certificates from a known provider. 1348 | /// For self-signed certificates, the CA certificate needs to be provided. 1349 | /// 1350 | /// 1351 | /// The CA certificate (if provided) is expected to be in the PEM format, including the 1352 | /// begin/end markers like -----BEGIN CERTIFICATE-----. 1353 | /// 1354 | /// 1355 | /// Common name of the server (typically hostname). 1356 | /// CA certificate used to validate the server's authenticity. 1357 | public void SetClientSecrets(string serverCommonName, string caCertificate = null) 1358 | { 1359 | m_ServerCommonName = serverCommonName; 1360 | m_ClientCaCertificate = caCertificate; 1361 | } 1362 | #endif 1363 | 1364 | /// 1365 | /// Creates the internal NetworkDriver 1366 | /// 1367 | /// The owner transport 1368 | /// The driver 1369 | /// The UnreliableFragmented NetworkPipeline 1370 | /// The UnreliableSequencedFragmented NetworkPipeline 1371 | /// The ReliableSequenced NetworkPipeline 1372 | public void CreateDriver(UnityTransport transport, out NetworkDriver driver, 1373 | out NetworkPipeline unreliableFragmentedPipeline, 1374 | out NetworkPipeline unreliableSequencedFragmentedPipeline, 1375 | out NetworkPipeline reliableSequencedPipeline) 1376 | { 1377 | #if !UTP_TRANSPORT_2_0_ABOVE && (UNITY_EDITOR || DEVELOPMENT_BUILD) 1378 | ConfigureSimulatorForUtp1(); 1379 | #endif 1380 | bool asServer = m_ServerState == LocalConnectionState.Starting; 1381 | 1382 | m_NetworkSettings.WithNetworkConfigParameters( 1383 | maxConnectAttempts: transport.m_MaxConnectAttempts, 1384 | connectTimeoutMS: transport.m_ConnectTimeoutMS, 1385 | disconnectTimeoutMS: transport.m_DisconnectTimeoutMS, 1386 | #if UTP_TRANSPORT_2_0_ABOVE 1387 | sendQueueCapacity: m_MaxPacketQueueSize, 1388 | receiveQueueCapacity: m_MaxPacketQueueSize, 1389 | #endif 1390 | heartbeatTimeoutMS: transport.m_HeartbeatTimeoutMS); 1391 | 1392 | #if UNITY_WEBGL && !UNITY_EDITOR 1393 | if (asServer && m_ProtocolType != ProtocolType.RelayUnityTransport) 1394 | { 1395 | throw new Exception("WebGL as a server is not supported by Unity Transport, outside the Editor."); 1396 | } 1397 | #endif 1398 | 1399 | #if UTP_TRANSPORT_2_0_ABOVE 1400 | if (m_UseEncryption) 1401 | { 1402 | if (m_ProtocolType == ProtocolType.RelayUnityTransport) 1403 | { 1404 | if (m_RelayServerData.IsSecure == 0) 1405 | { 1406 | // log an error because we have mismatched configuration 1407 | NetworkManager.LogError("Mismatched security configuration, between Relay and local UnityTransport settings"); 1408 | } 1409 | 1410 | // No need to to anything else if using Relay because UTP will handle the 1411 | // configuration of the security parameters on its own. 1412 | } 1413 | else 1414 | { 1415 | if (asServer) 1416 | { 1417 | if (string.IsNullOrEmpty(m_ServerCertificate) || string.IsNullOrEmpty(m_ServerPrivateKey)) 1418 | { 1419 | throw new Exception("In order to use encrypted communications, when hosting, you must set the server certificate and key."); 1420 | } 1421 | 1422 | m_NetworkSettings.WithSecureServerParameters(m_ServerCertificate, m_ServerPrivateKey); 1423 | } 1424 | else 1425 | { 1426 | if (string.IsNullOrEmpty(m_ServerCommonName)) 1427 | { 1428 | throw new Exception("In order to use encrypted communications, clients must set the server common name."); 1429 | } 1430 | else if (string.IsNullOrEmpty(m_ClientCaCertificate)) 1431 | { 1432 | m_NetworkSettings.WithSecureClientParameters(m_ServerCommonName); 1433 | } 1434 | else 1435 | { 1436 | m_NetworkSettings.WithSecureClientParameters(m_ClientCaCertificate, m_ServerCommonName); 1437 | } 1438 | } 1439 | } 1440 | } 1441 | #endif 1442 | 1443 | #if UTP_TRANSPORT_2_1_ABOVE 1444 | if (m_ProtocolType == ProtocolType.RelayUnityTransport) 1445 | { 1446 | if (m_UseWebSockets && m_RelayServerData.IsWebSocket == 0) 1447 | { 1448 | Debug.LogError("Transport is configured to use WebSockets, but Relay server data isn't. Be sure to use \"wss\" as the connection type when creating the server data (instead of \"dtls\" or \"udp\")."); 1449 | } 1450 | 1451 | if (!m_UseWebSockets && m_RelayServerData.IsWebSocket != 0) 1452 | { 1453 | Debug.LogError("Relay server data indicates usage of WebSockets, but \"Use WebSockets\" checkbox isn't checked under \"Unity Transport\" component."); 1454 | } 1455 | } 1456 | #endif 1457 | 1458 | #if UTP_TRANSPORT_2_0_ABOVE 1459 | if (m_UseWebSockets) 1460 | { 1461 | driver = NetworkDriver.Create(new WebSocketNetworkInterface(), m_NetworkSettings); 1462 | } 1463 | else 1464 | { 1465 | #if UNITY_WEBGL && !UNITY_EDITOR 1466 | Debug.LogWarning($"WebSockets were used even though they're not selected in NetworkManager. You should check {nameof(UseWebSockets)}', on the Unity Transport component, to silence this warning."); 1467 | driver = NetworkDriver.Create(new WebSocketNetworkInterface(), m_NetworkSettings); 1468 | #else 1469 | driver = NetworkDriver.Create(new UDPNetworkInterface(), m_NetworkSettings); 1470 | #endif 1471 | } 1472 | #else 1473 | driver = NetworkDriver.Create(m_NetworkSettings); 1474 | #endif 1475 | 1476 | #if !UTP_TRANSPORT_2_0_ABOVE 1477 | SetupPipelinesForUtp1(driver, 1478 | out unreliableFragmentedPipeline, 1479 | out unreliableSequencedFragmentedPipeline, 1480 | out reliableSequencedPipeline); 1481 | #else 1482 | SetupPipelinesForUtp2(driver, 1483 | out unreliableFragmentedPipeline, 1484 | out unreliableSequencedFragmentedPipeline, 1485 | out reliableSequencedPipeline); 1486 | #endif 1487 | } 1488 | 1489 | #if !UTP_TRANSPORT_2_0_ABOVE 1490 | private void SetupPipelinesForUtp1(NetworkDriver driver, 1491 | out NetworkPipeline unreliableFragmentedPipeline, 1492 | out NetworkPipeline unreliableSequencedFragmentedPipeline, 1493 | out NetworkPipeline reliableSequencedPipeline) 1494 | { 1495 | #if UNITY_EDITOR || DEVELOPMENT_BUILD 1496 | if (DebugSimulator.PacketDelayMS > 0 || DebugSimulator.PacketDropRate > 0) 1497 | { 1498 | unreliableFragmentedPipeline = driver.CreatePipeline( 1499 | typeof(FragmentationPipelineStage), 1500 | typeof(SimulatorPipelineStage), 1501 | typeof(SimulatorPipelineStageInSend) 1502 | ); 1503 | unreliableSequencedFragmentedPipeline = driver.CreatePipeline( 1504 | typeof(FragmentationPipelineStage), 1505 | typeof(UnreliableSequencedPipelineStage), 1506 | typeof(SimulatorPipelineStage), 1507 | typeof(SimulatorPipelineStageInSend) 1508 | ); 1509 | reliableSequencedPipeline = driver.CreatePipeline( 1510 | typeof(ReliableSequencedPipelineStage), 1511 | typeof(SimulatorPipelineStage), 1512 | typeof(SimulatorPipelineStageInSend) 1513 | ); 1514 | } 1515 | else 1516 | #endif 1517 | { 1518 | unreliableFragmentedPipeline = driver.CreatePipeline( 1519 | typeof(FragmentationPipelineStage) 1520 | ); 1521 | unreliableSequencedFragmentedPipeline = driver.CreatePipeline( 1522 | typeof(FragmentationPipelineStage), 1523 | typeof(UnreliableSequencedPipelineStage) 1524 | ); 1525 | reliableSequencedPipeline = driver.CreatePipeline( 1526 | typeof(ReliableSequencedPipelineStage) 1527 | ); 1528 | } 1529 | } 1530 | #else 1531 | private void SetupPipelinesForUtp2(NetworkDriver driver, 1532 | out NetworkPipeline unreliableFragmentedPipeline, 1533 | out NetworkPipeline unreliableSequencedFragmentedPipeline, 1534 | out NetworkPipeline reliableSequencedPipeline) 1535 | { 1536 | 1537 | unreliableFragmentedPipeline = driver.CreatePipeline( 1538 | typeof(FragmentationPipelineStage) 1539 | ); 1540 | 1541 | unreliableSequencedFragmentedPipeline = driver.CreatePipeline( 1542 | typeof(FragmentationPipelineStage), 1543 | typeof(UnreliableSequencedPipelineStage) 1544 | ); 1545 | 1546 | reliableSequencedPipeline = driver.CreatePipeline( 1547 | typeof(ReliableSequencedPipelineStage) 1548 | ); 1549 | } 1550 | #endif 1551 | // -------------- Utility Types ------------------------------------------------------------------------------- 1552 | 1553 | 1554 | /// 1555 | /// Cached information about reliability mode with a certain client 1556 | /// 1557 | private struct SendTarget : IEquatable 1558 | { 1559 | public readonly ulong ClientId; 1560 | public readonly NetworkPipeline NetworkPipeline; 1561 | 1562 | public SendTarget(ulong clientId, NetworkPipeline networkPipeline) 1563 | { 1564 | ClientId = clientId; 1565 | NetworkPipeline = networkPipeline; 1566 | } 1567 | 1568 | public bool Equals(SendTarget other) 1569 | { 1570 | return ClientId == other.ClientId && NetworkPipeline.Equals(other.NetworkPipeline); 1571 | } 1572 | 1573 | public override bool Equals(object obj) 1574 | { 1575 | return obj is SendTarget other && Equals(other); 1576 | } 1577 | 1578 | public override int GetHashCode() 1579 | { 1580 | unchecked 1581 | { 1582 | return (ClientId.GetHashCode() * 397) ^ NetworkPipeline.GetHashCode(); 1583 | } 1584 | } 1585 | } 1586 | 1587 | private void HandleTransportConnectEvent(ulong clientId) 1588 | { 1589 | if (m_ServerState == LocalConnectionState.Started) 1590 | { 1591 | HandleRemoteConnectionState(RemoteConnectionState.Started, clientId); 1592 | } 1593 | else 1594 | { 1595 | SetClientConnectionState(LocalConnectionState.Started); 1596 | } 1597 | } 1598 | 1599 | private void HandleTransportDisconnectEvent(ulong clientId) 1600 | { 1601 | if (m_ServerState == LocalConnectionState.Started) 1602 | { 1603 | HandleRemoteConnectionState(RemoteConnectionState.Stopped, clientId); 1604 | } 1605 | else if (m_ClientState == LocalConnectionState.Started) 1606 | { 1607 | SetClientConnectionState(LocalConnectionState.Stopping); 1608 | ShutdownInternals(); 1609 | SetClientConnectionState(LocalConnectionState.Stopped); 1610 | } 1611 | } 1612 | 1613 | private void HandleTransportDataEvent(ulong clientId, ArraySegment data, NetworkPipeline pipeline) 1614 | { 1615 | Channel channel = SelectSendChannel(pipeline); 1616 | switch (m_State) 1617 | { 1618 | case State.Listening: 1619 | int connectionId = TransportIdToClientId(clientId); 1620 | HandleServerReceivedDataArgs(new ServerReceivedDataArgs(data, channel, connectionId, Index)); 1621 | break; 1622 | case State.Connected: 1623 | HandleClientReceivedDataArgs(new ClientReceivedDataArgs(data, channel, Index)); 1624 | break; 1625 | default: 1626 | throw new ArgumentOutOfRangeException(); 1627 | } 1628 | } 1629 | 1630 | private void HandleTransportFailureEvent() 1631 | { 1632 | var clientSeverOrHost = m_ServerState.IsStartingOrStarted() ? m_ClientState.IsStartingOrStarted() ? "Host" : "Server" : "Client"; 1633 | bool duringStart = m_ServerState == LocalConnectionState.Starting || m_ClientState == LocalConnectionState.Starting; 1634 | var whenFailed = duringStart ? "start failure" : "failure"; 1635 | NetworkManager.LogError($"{clientSeverOrHost} is shutting down due to network transport {whenFailed} of {nameof(UnityTransport)}!"); 1636 | Shutdown(); 1637 | } 1638 | 1639 | #region FishNet 1640 | 1641 | [Range(1, 4095)] 1642 | [SerializeField] private int m_MaximumClients = 4095; 1643 | 1644 | public override event Action OnClientConnectionState; 1645 | public override event Action OnServerConnectionState; 1646 | public override event Action OnRemoteConnectionState; 1647 | 1648 | public override string GetConnectionAddress(int connectionId) 1649 | { 1650 | bool isServer = m_ServerState == LocalConnectionState.Started; 1651 | bool isClient = m_ClientState == LocalConnectionState.Started; 1652 | 1653 | if (isServer) 1654 | { 1655 | if (!m_TransportIdToClientIdMap.TryGetValue(connectionId, out ulong transportId)) 1656 | { 1657 | NetworkManager.LogWarning($"Connection with id {connectionId} is disconnected. Unable to get connection address."); 1658 | return string.Empty; 1659 | } 1660 | if (isClient && transportId == k_ClientHostId) 1661 | { 1662 | return GetLocalEndPoint().Address; 1663 | } 1664 | NetworkConnection connection = ParseClientId(transportId); 1665 | return connection.GetState(m_Driver) == NetworkConnection.State.Disconnected 1666 | ? string.Empty 1667 | #if UTP_TRANSPORT_2_0_ABOVE 1668 | : m_Driver.GetRemoteEndpoint(connection).Address; 1669 | #else 1670 | : m_Driver.RemoteEndPoint(connection).Address; 1671 | #endif 1672 | } 1673 | 1674 | if (isClient && NetworkManager.ClientManager.Connection.ClientId == connectionId) 1675 | { 1676 | return GetLocalEndPoint().Address; 1677 | } 1678 | return string.Empty; 1679 | 1680 | NetworkEndpoint GetLocalEndPoint() 1681 | { 1682 | #if UTP_TRANSPORT_2_0_ABOVE 1683 | return m_Driver.GetLocalEndpoint(); 1684 | #else 1685 | return m_Driver.LocalEndPoint(); 1686 | #endif 1687 | } 1688 | } 1689 | 1690 | public override LocalConnectionState GetConnectionState(bool server) 1691 | { 1692 | return server ? m_ServerState : m_ClientState; 1693 | } 1694 | 1695 | public override RemoteConnectionState GetConnectionState(int connectionId) 1696 | { 1697 | if (m_TransportIdToClientIdMap.TryGetValue(connectionId, out ulong transportId)) 1698 | { 1699 | return ParseClientId(transportId).GetState(m_Driver) == NetworkConnection.State.Connected 1700 | ? RemoteConnectionState.Started 1701 | : RemoteConnectionState.Stopped; 1702 | } 1703 | return RemoteConnectionState.Stopped; 1704 | } 1705 | 1706 | public override void HandleClientConnectionState(ClientConnectionStateArgs connectionStateArgs) 1707 | { 1708 | OnClientConnectionState?.Invoke(connectionStateArgs); 1709 | } 1710 | 1711 | public override void HandleServerConnectionState(ServerConnectionStateArgs connectionStateArgs) 1712 | { 1713 | OnServerConnectionState?.Invoke(connectionStateArgs); 1714 | } 1715 | 1716 | public override void HandleRemoteConnectionState(RemoteConnectionStateArgs connectionStateArgs) 1717 | { 1718 | OnRemoteConnectionState?.Invoke(connectionStateArgs); 1719 | } 1720 | 1721 | public override void IterateIncoming(bool server) 1722 | { 1723 | if (m_ClientState == LocalConnectionState.Started && m_ServerState == LocalConnectionState.Started) 1724 | { 1725 | IterateClientHost(server); 1726 | } 1727 | 1728 | IterateIncoming(); 1729 | } 1730 | 1731 | public override void IterateOutgoing(bool server) 1732 | { 1733 | if (server || m_ServerState != LocalConnectionState.Started) 1734 | { 1735 | IterateOutgoing(); 1736 | } 1737 | } 1738 | 1739 | public override event Action OnClientReceivedData; 1740 | 1741 | public override void HandleClientReceivedDataArgs(ClientReceivedDataArgs receivedDataArgs) 1742 | { 1743 | OnClientReceivedData?.Invoke(receivedDataArgs); 1744 | } 1745 | 1746 | public override event Action OnServerReceivedData; 1747 | 1748 | public override void HandleServerReceivedDataArgs(ServerReceivedDataArgs receivedDataArgs) 1749 | { 1750 | OnServerReceivedData?.Invoke(receivedDataArgs); 1751 | } 1752 | 1753 | public override void SendToServer(byte channelId, ArraySegment segment) 1754 | { 1755 | if (m_ClientState != LocalConnectionState.Started) return; 1756 | 1757 | if (m_ServerState == LocalConnectionState.Started) 1758 | { 1759 | ClientHostSendToServer(channelId, segment); 1760 | } 1761 | else 1762 | { 1763 | Send(m_ServerClientId, segment, (Channel)channelId); 1764 | } 1765 | } 1766 | 1767 | public override void SendToClient(byte channelId, ArraySegment segment, int connectionId) 1768 | { 1769 | if (m_ServerState != LocalConnectionState.Started) return; 1770 | 1771 | if (!m_TransportIdToClientIdMap.TryGetValue(connectionId, out ulong transportId)) 1772 | { 1773 | return; 1774 | } 1775 | 1776 | if (m_ClientState == LocalConnectionState.Started && transportId == m_ServerClientId) 1777 | { 1778 | SendToClientHost(channelId, segment); 1779 | } 1780 | else 1781 | { 1782 | Send(transportId, segment, (Channel)channelId); 1783 | } 1784 | } 1785 | 1786 | public override int GetMaximumClients() => m_MaximumClients; 1787 | 1788 | public override void SetMaximumClients(int value) 1789 | { 1790 | if (m_ServerState.IsStartingOrStarted()) 1791 | { 1792 | NetworkManager.LogWarning("Cannot set maximum clients when server is running."); 1793 | } 1794 | else 1795 | { 1796 | m_MaximumClients = value; 1797 | } 1798 | } 1799 | 1800 | public override void SetClientAddress(string address) 1801 | { 1802 | ConnectionData.Address = address; 1803 | } 1804 | 1805 | public override string GetClientAddress() 1806 | { 1807 | return ConnectionData.Address; 1808 | } 1809 | 1810 | public override void SetPort(ushort port) 1811 | { 1812 | ConnectionData.Port = port; 1813 | } 1814 | 1815 | public override ushort GetPort() 1816 | { 1817 | return ConnectionData.Port; 1818 | } 1819 | 1820 | public override void SetServerBindAddress(string address, IPAddressType addressType) 1821 | { 1822 | ConnectionData.ServerListenAddress = address; 1823 | } 1824 | 1825 | public override string GetServerBindAddress(IPAddressType addressType) 1826 | { 1827 | return ConnectionData.ServerListenAddress; 1828 | } 1829 | 1830 | public override bool StartConnection(bool server) 1831 | { 1832 | return server ? StartServer() : StartClient(); 1833 | } 1834 | 1835 | public override bool StopConnection(bool server) 1836 | { 1837 | return server ? StopServer() : StopClient(); 1838 | } 1839 | 1840 | public override bool StopConnection(int connectionId, bool immediately) 1841 | { 1842 | if (!m_TransportIdToClientIdMap.TryGetValue(connectionId, out ulong transportId)) 1843 | { 1844 | return false; 1845 | } 1846 | return transportId == m_ServerClientId ? ServerRequestedStopClientHost() : DisconnectRemoteClient(transportId); 1847 | } 1848 | 1849 | public override void Shutdown() 1850 | { 1851 | StopConnection(false); 1852 | StopConnection(true); 1853 | } 1854 | 1855 | public override int GetMTU(byte channelId) 1856 | { 1857 | return NetworkParameterConstants.MTU; 1858 | } 1859 | 1860 | private Channel SelectSendChannel(NetworkPipeline pipeline) 1861 | { 1862 | return pipeline == m_ReliableSequencedPipeline ? Channel.Reliable : Channel.Unreliable; 1863 | } 1864 | 1865 | #endregion 1866 | 1867 | #region Server 1868 | 1869 | private LocalConnectionState m_ServerState = LocalConnectionState.Stopped; 1870 | private int m_NextClientId = 1; 1871 | private readonly Dictionary m_TransportIdToClientIdMap = new Dictionary(); 1872 | private readonly Dictionary m_ClientIdToTransportIdMap = new Dictionary(); 1873 | 1874 | private int TransportIdToClientId(ulong connection) => m_ClientIdToTransportIdMap[connection]; 1875 | 1876 | private ulong ClientIdToTransportId(int transportId) => m_TransportIdToClientIdMap[transportId]; 1877 | 1878 | private void HandleRemoteConnectionState(RemoteConnectionState state, ulong clientId) 1879 | { 1880 | int transportId; 1881 | switch (state) 1882 | { 1883 | case RemoteConnectionState.Started: 1884 | if (m_TransportIdToClientIdMap.Count >= GetMaximumClients()) 1885 | { 1886 | Debug.LogWarning("Connection limit reached. Server cannot accept new connections."); 1887 | // The server can disconnect the client at any time, even during ProcessEvent(), which can cause errors because the client still has Network Events. 1888 | // This workaround simply clears the Event queue for the client. 1889 | NetworkConnection connection = ParseClientId(clientId); 1890 | if (m_Driver.GetConnectionState(connection) != NetworkConnection.State.Disconnected) 1891 | { 1892 | m_Driver.Disconnect(connection); 1893 | while (m_Driver.PopEventForConnection(connection, out var _) != NetworkEvent.Type.Empty) { } 1894 | } 1895 | } 1896 | else 1897 | { 1898 | transportId = m_NextClientId++; 1899 | m_TransportIdToClientIdMap[transportId] = clientId; 1900 | m_ClientIdToTransportIdMap[clientId] = transportId; 1901 | HandleRemoteConnectionState(new RemoteConnectionStateArgs(state, transportId, Index)); 1902 | } 1903 | break; 1904 | case RemoteConnectionState.Stopped: 1905 | transportId = m_ClientIdToTransportIdMap[clientId]; 1906 | HandleRemoteConnectionState(new RemoteConnectionStateArgs(state, transportId, Index)); 1907 | m_TransportIdToClientIdMap.Remove(transportId); 1908 | m_ClientIdToTransportIdMap.Remove(clientId); 1909 | break; 1910 | default: 1911 | throw new ArgumentOutOfRangeException(nameof(state), state, null); 1912 | } 1913 | } 1914 | 1915 | private void SetServerConnectionState(LocalConnectionState state) 1916 | { 1917 | if (m_ServerState == state) 1918 | { 1919 | return; 1920 | } 1921 | m_ServerState = state; 1922 | HandleServerConnectionState(new ServerConnectionStateArgs(state, Index)); 1923 | } 1924 | 1925 | private bool StopServer() 1926 | { 1927 | if (m_ServerState.IsStoppingOrStopped()) 1928 | { 1929 | return false; 1930 | } 1931 | 1932 | if (m_ClientState.IsStartingOrStarted()) 1933 | { 1934 | ServerRequestedStopClientHost(); 1935 | } 1936 | 1937 | ulong[] connectedClients = m_ClientIdToTransportIdMap.Keys.ToArray(); 1938 | 1939 | foreach (ulong clientId in connectedClients) 1940 | { 1941 | DisconnectRemoteClient(clientId); 1942 | } 1943 | 1944 | SetServerConnectionState(LocalConnectionState.Stopping); 1945 | ShutdownInternals(); 1946 | m_NextClientId = 1; 1947 | m_TransportIdToClientIdMap.Clear(); 1948 | m_ClientIdToTransportIdMap.Clear(); 1949 | SetServerConnectionState(LocalConnectionState.Stopped); 1950 | 1951 | return true; 1952 | } 1953 | 1954 | #endregion 1955 | 1956 | #region Client 1957 | 1958 | private LocalConnectionState m_ClientState = LocalConnectionState.Stopped; 1959 | 1960 | private void SetClientConnectionState(LocalConnectionState state) 1961 | { 1962 | if (m_ClientState == state) 1963 | { 1964 | return; 1965 | } 1966 | m_ClientState = state; 1967 | HandleClientConnectionState(new ClientConnectionStateArgs(state, Index)); 1968 | } 1969 | 1970 | private bool StopClient() 1971 | { 1972 | if (m_ClientState.IsStoppingOrStopped()) 1973 | { 1974 | return false; 1975 | } 1976 | 1977 | if (m_ServerState.IsStartingOrStarted()) 1978 | { 1979 | return StopClientHost(); 1980 | } 1981 | 1982 | DisconnectLocalClient(); 1983 | 1984 | return true; 1985 | } 1986 | 1987 | private readonly struct ClientHostSendData 1988 | { 1989 | public readonly Channel Channel; 1990 | public readonly ArraySegment Segment; 1991 | 1992 | public ClientHostSendData(Channel channel, ArraySegment data) 1993 | { 1994 | if (data.Array == null) throw new InvalidOperationException(); 1995 | 1996 | Channel = channel; 1997 | 1998 | var array = new byte[data.Count]; 1999 | Buffer.BlockCopy(data.Array, data.Offset, array, 0, data.Count); 2000 | Segment = new ArraySegment(array, 0, data.Count); 2001 | } 2002 | } 2003 | 2004 | private const ulong k_ClientHostId = 0; 2005 | private readonly Queue m_ClientHostSendQueue = new Queue(); 2006 | private readonly Queue m_ClientHostReceiveQueue = new Queue(); 2007 | 2008 | private bool StopClientHost() 2009 | { 2010 | if (m_ClientState.IsStoppingOrStopped()) 2011 | { 2012 | return false; 2013 | } 2014 | 2015 | SetClientConnectionState(LocalConnectionState.Stopping); 2016 | DisposeClientHost(); 2017 | SetClientConnectionState(LocalConnectionState.Stopped); 2018 | if (m_ServerState == LocalConnectionState.Started) 2019 | { 2020 | HandleRemoteConnectionState(RemoteConnectionState.Stopped, m_ServerClientId); 2021 | } 2022 | 2023 | return true; 2024 | } 2025 | 2026 | private bool ServerRequestedStopClientHost() 2027 | { 2028 | if (m_ClientState.IsStoppingOrStopped()) 2029 | { 2030 | return false; 2031 | } 2032 | 2033 | if (m_ServerState == LocalConnectionState.Started) 2034 | { 2035 | HandleRemoteConnectionState(RemoteConnectionState.Stopped, m_ServerClientId); 2036 | SetClientConnectionState(LocalConnectionState.Stopping); 2037 | DisposeClientHost(); 2038 | SetClientConnectionState(LocalConnectionState.Stopped); 2039 | } 2040 | 2041 | return true; 2042 | } 2043 | 2044 | private void DisposeClientHost() 2045 | { 2046 | m_ServerClientId = default; 2047 | m_ClientHostSendQueue.Clear(); 2048 | m_ClientHostReceiveQueue.Clear(); 2049 | } 2050 | 2051 | private void IterateClientHost(bool asServer) 2052 | { 2053 | if (asServer) 2054 | { 2055 | while (m_ClientHostSendQueue != null && m_ClientHostSendQueue.Count > 0) 2056 | { 2057 | ClientHostSendData packet = m_ClientHostSendQueue.Dequeue(); 2058 | int connectionId = TransportIdToClientId(k_ClientHostId); 2059 | HandleServerReceivedDataArgs(new ServerReceivedDataArgs(packet.Segment, packet.Channel, connectionId, Index)); 2060 | } 2061 | } 2062 | else 2063 | { 2064 | while (m_ClientHostReceiveQueue != null && m_ClientHostReceiveQueue.Count > 0) 2065 | { 2066 | ClientHostSendData packet = m_ClientHostReceiveQueue.Dequeue(); 2067 | HandleClientReceivedDataArgs(new ClientReceivedDataArgs(packet.Segment, packet.Channel, Index)); 2068 | } 2069 | } 2070 | } 2071 | 2072 | private void SendToClientHost(int channelId, ArraySegment payload) 2073 | { 2074 | m_ClientHostReceiveQueue.Enqueue(new ClientHostSendData((Channel)channelId, payload)); 2075 | } 2076 | 2077 | private void ClientHostSendToServer(int channelId, ArraySegment payload) 2078 | { 2079 | m_ClientHostSendQueue.Enqueue(new ClientHostSendData((Channel)channelId, payload)); 2080 | } 2081 | 2082 | #endregion 2083 | } 2084 | } --------------------------------------------------------------------------------