├── .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 | }
--------------------------------------------------------------------------------