paint-brush
Unity Realtime Multiplayer, partie 2 : TCP, UDP, protocoles WebSocketpar@dmitrii
3,194 lectures
3,194 lectures

Unity Realtime Multiplayer, partie 2 : TCP, UDP, protocoles WebSocket

par Dmitrii Ivashchenko16m2023/08/06
Read on Terminal Reader

Trop long; Pour lire

La série Unity Networking Landscape in 2023 se poursuit en mettant l'accent sur les protocoles de transmission de données dans les jeux multijoueurs en temps réel. La couche de transport du modèle OSI avec TCP et UDP est explorée pour un échange de données optimal entre les joueurs. TCP assure une livraison fiable des données mais peut entraîner des retards, tandis que UDP offre une transmission plus rapide avec une perte de données potentielle. WebSocket, un protocole au niveau de l'application basé sur TCP, permet une communication bidirectionnelle persistante et convient aux jeux multijoueurs en temps réel. Des exemples de code pour les clients et serveurs TCP et UDP, ainsi que pour le client et le serveur WebSocket, illustrent les approches de mise en œuvre. Le choix du protocole dépend des exigences du jeu - TCP pour la fiabilité, UDP pour la vitesse et WebSocket pour la communication bidirectionnelle. Le prochain article se penchera sur l'organisation d'une transmission de données fiable à l'aide d'UDP.
featured image - Unity Realtime Multiplayer, partie 2 : TCP, UDP, protocoles WebSocket
Dmitrii Ivashchenko HackerNoon profile picture
0-item
1-item


Ma série d'articles sur le paysage du réseau Unity en 2023 continue ! L'article d'aujourd'hui couvrira les protocoles de transmission de données utilisés dans les jeux multijoueurs en temps réel.


Salutations à tous ! Je suis Dmitrii Ivashchenko, ingénieur logiciel principal chez MY.GAMES. Nous commencerons par un bref aperçu des protocoles qui existent à différents niveaux d'interaction réseau.


Aperçu du contenu

  • Niveaux OSI
  • Protocole de contrôle de transmission
    • Client TCP
    • Serveur TCP
  • Protocole de datagramme utilisateur
    • Client UDP
    • Serveur UDP
  • Websocket
    • Client WebSocket
    • Serveur Websocket
  • Conclusion


Niveaux OSI

Le modèle OSI (Open Systems Interconnection) est un modèle conceptuel qui caractérise et normalise les fonctions d'un système de communication en termes de niveaux d'abstraction. Ce modèle a été développé par l'Organisation internationale de normalisation (ISO) en 1978.

Il se compose de sept couches :


  1. Couche physique : Cette couche traite de la transmission et de la réception des bits bruts sur le canal. Les protocoles de cette couche décrivent l'interface physique et les caractéristiques du support, y compris les représentations binaires, les débits de transmission, les câbles physiques, les cartes et les conceptions de connecteurs.


  2. Couche de liaison de données : cette couche assure le transfert de données sur le support physique et gère les erreurs pouvant survenir au niveau physique.


  3. Couche réseau : Cette couche détermine le chemin (routage) pour la transmission des données entre les réseaux.


  4. Couche Transport : Cette couche gère la livraison des messages entre les points et assure la fiabilité si nécessaire.


  5. Couche de session : cette couche gère l'établissement, la maintenance et l'arrêt des sessions entre les utilisateurs et les applications.


  6. Couche de présentation : cette couche garantit l'indépendance des données vis-à-vis des différences de représentation des données (codage) côté émetteur et côté récepteur.


  7. Couche d'application : cette couche comprend des protocoles qui ont une relation directe avec les applications avec lesquelles l'utilisateur interagit.



Il convient de noter que chaque couche s'appuie sur la précédente. Les couches au-dessus de la couche Transport (Session, Présentation et Application) sont hautement spécialisées et ne peuvent pas nous aider à organiser le multijoueur en temps réel. Il faut donc s'arrêter à la couche transport et utiliser ses protocoles TCP et UDP pour un échange de données optimal entre les joueurs.


Protocole de contrôle de transmission

TCP est un protocole orienté connexion, ce qui signifie que la communication se produit entre deux appareils qui établissent une connexion pour échanger des données. Ce protocole assure la fiabilité car il garantit que toutes les données transmises atteindront leur destination dans le bon ordre. Si des données sont perdues pendant la transmission, TCP réessayera automatiquement la demande jusqu'à ce que toutes les données soient transmises avec succès.


L'établissement d'une connexion implique les étapes suivantes :


  • Le client et le serveur créent des sockets pour échanger des données via TCP.
  • Le client envoie un segment SYN (synchronisation) au serveur avec un port de destination spécifié.
  • Le serveur accepte le segment SYN, crée son propre socket et envoie un segment SYN-ACK (acquittement de synchronisation) au client.
  • Le client accepte le segment SYN-ACK et envoie un segment ACK (accusé de réception) au serveur pour terminer le processus d'établissement de la connexion. Une connexion bidirectionnelle fiable est maintenant établie.





Client TCP

L'exemple ci-dessous montre une implémentation de base d'un client TCP et peut être étendu pour fonctionner avec des données spécifiques et une logique de jeu.


Le code se connecte à un serveur avec une adresse IP et un port spécifiés, puis envoie et reçoit des données via la connexion. Un flux réseau est utilisé pour la réception de données asynchrones depuis le serveur.


 using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class TCPClient : MonoBehaviour { private TcpClient tcpClient; private NetworkStream networkStream; private byte[] receiveBuffer; private void Start() { // Example: Connect to server with IP address 127.0.0.1 (localhost) and port 5555 ConnectToServer("127.0.0.1", 5555); } private void ConnectToServer(string ipAddress, int port) { tcpClient = new TcpClient(); tcpClient.Connect(IPAddress.Parse(ipAddress), port); networkStream = tcpClient.GetStream(); // Start asynchronous operation to receive data from the server receiveBuffer = new byte[tcpClient.ReceiveBufferSize]; networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); } private void ReceiveData(IAsyncResult result) { int bytesRead = networkStream.EndRead(result); byte[] receivedBytes = new byte[bytesRead]; Array.Copy(receiveBuffer, receivedBytes, bytesRead); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from server: " + receivedMessage); // Continue the asynchronous operation to receive data networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); } private void SendData(string message) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); networkStream.Write(sendBytes, 0, sendBytes.Length); networkStream.Flush(); } }


La méthode ConnectToServer(string ipAddress, int port) établit une connexion au serveur à l'adresse IP et au port spécifiés. La réception des données depuis le serveur s'effectue dans la méthode ReceiveData(IAsyncResult result) , tandis que la transmission des données s'effectue dans la méthode SendData(string message) . Les données reçues sont envoyées à la console à l'aide Debug.Log .


Serveur TCP

L'exemple de code ci-dessous représente un simple serveur TCP dans Unity. Le code initialise le serveur, écoute le port spécifié et accepte les connexions entrantes des clients. Après s'être connecté à un client, le serveur envoie et reçoit des données via le flux réseau.


La méthode StartServer(int port) initialise le serveur sur le port spécifié et commence à écouter les connexions entrantes. Lorsqu'une connexion client est établie, la méthode HandleIncomingConnection(IAsyncResult result) est exécutée, qui reçoit les données du client et démarre une opération asynchrone pour recevoir les données.


Les données reçues sont traitées dans la méthode ReceiveData(IAsyncResult result) . Après avoir reçu les données du client, le serveur peut effectuer le traitement nécessaire ou renvoyer les données au client.


La méthode SendData(string message) envoie des données au client via le flux réseau. Les données sont converties en un tableau d'octets et envoyées au client.


 using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class TCPServer : MonoBehaviour { private TcpListener tcpListener; private TcpClient connectedClient; private NetworkStream networkStream; private byte[] receiveBuffer; private void Start() { // Example: Start the server on port 5555 StartServer(5555); } private void StartServer(int port) { tcpListener = new TcpListener(IPAddress.Any, port); tcpListener.Start(); Debug.Log("Server started. Waiting for connections..."); // Start accepting client connections asynchronously tcpListener.BeginAcceptTcpClient(HandleIncomingConnection, null); } }


La méthode HandleIncomingConnection est utilisée pour gérer une connexion client entrante. Après avoir accepté une connexion, il obtient un flux pour l'échange de données avec le client et lance une opération asynchrone pour recevoir des données du client.


Ensuite, le procédé crée une mémoire tampon pour recevoir des données, sur la base de la taille de la mémoire tampon reçue du client connecté. Une opération asynchrone est lancée pour lire les données du flux à l'aide du tampon créé. Une fois l'opération de lecture terminée, les données seront transmises à la méthode ReceiveData pour un traitement ultérieur.


Aussi, le procédé lance une autre opération asynchrone pour accepter les connexions entrantes pour la possibilité d'accepter le client suivant.


Lorsqu'un client se connecte au serveur, cette méthode sera appelée pour gérer la connexion et le serveur sera prêt à accepter de manière asynchrone les clients suivants.


 private void HandleIncomingConnection(IAsyncResult result) { connectedClient = tcpListener.EndAcceptTcpClient(result); networkStream = connectedClient.GetStream(); Debug.Log("Client connected: " + connectedClient.Client.RemoteEndPoint); // Start asynchronous operation to receive data from the client receiveBuffer = new byte[connectedClient.ReceiveBufferSize]; networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); // Accept next client connection asynchronously tcpListener.BeginAcceptTcpClient(HandleIncomingConnection, null); }


La méthode ReceiveData est utilisée pour traiter les données reçues du client. Une fois l'opération de lecture des données terminée, le procédé vérifie le nombre d'octets lus. Si le nombre d'octets est inférieur ou égal à zéro, cela signifie que le client s'est déconnecté. Dans ce cas, la méthode ferme la connexion avec le client et termine l'exécution.


Si le nombre d'octets lus est supérieur à zéro, le procédé crée un tableau d'octets pour les données reçues et copie les données lues dans ce tableau. Ensuite, la méthode convertit les octets reçus en une chaîne à l'aide du codage UTF-8 et envoie le message reçu à la console.


 private void ReceiveData(IAsyncResult result) { int bytesRead = networkStream.EndRead(result); if (bytesRead <= 0) { Debug.Log("Client disconnected: " + connectedClient.Client.RemoteEndPoint); connectedClient.Close(); return; } byte[] receivedBytes = new byte[bytesRead]; Array.Copy(receiveBuffer, receivedBytes, bytesRead); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from client: " + receivedMessage); // Process received data // Continue the asynchronous operation to receive data networkStream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveData, null); } private void SendData(string message) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); networkStream.Write(sendBytes, 0, sendBytes.Length); networkStream.Flush(); Debug.Log("Sent to client: " + message); }


La méthode SendData est utilisée pour envoyer des données au client. Il convertit la chaîne de message en un tableau d'octets à l'aide du codage UTF-8 et écrit ce tableau dans le flux réseau. Après avoir envoyé les données, la méthode efface le flux et envoie le message envoyé à la console.

Bien que la fiabilité puisse sembler être un gros plus, cette fonctionnalité TCP peut créer des problèmes dans les jeux multijoueurs en temps réel. La transmission de données dans TCP peut être ralentie par le mécanisme de retransmission et de contrôle de flux, ce qui peut entraîner des retards ou des "décalages".


Protocole de datagramme utilisateur

UDP est un protocole plus simple qui ne garantit pas la livraison ou l'ordre des paquets. Cela le rend beaucoup plus rapide que TCP, car il ne perd pas de temps à établir une connexion ou à retransmettre les paquets perdus. En raison de sa vitesse et de sa simplicité, UDP est souvent utilisé dans les jeux en réseau et d'autres applications nécessitant une transmission de données en temps réel.



Cependant, l'utilisation d'UDP oblige les développeurs à gérer plus soigneusement la transmission des données. Étant donné qu'UDP ne garantit pas la livraison, vous devrez peut-être implémenter vos propres mécanismes pour gérer les paquets perdus ou les paquets arrivés dans le désordre.


Client UDP

Ce code illustre une implémentation de base d'un client UDP dans Unity. La méthode StartUDPClient initialise le client UDP et le connecte au serveur distant spécifié par l'adresse IP et le port. Le client commence à recevoir des données de manière asynchrone à l'aide de la méthode BeginReceive et envoie un message au serveur à l'aide de la méthode SendData .


 using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class UDPExample : MonoBehaviour { private UdpClient udpClient; private IPEndPoint remoteEndPoint; private void Start() { // Example: Start the UDP client and connect to the remote server StartUDPClient("127.0.0.1", 5555); } private void StartUDPClient(string ipAddress, int port) { udpClient = new UdpClient(); remoteEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port); // Start receiving data asynchronously udpClient.BeginReceive(ReceiveData, null); // Send a message to the server SendData("Hello, server!"); } private void ReceiveData(IAsyncResult result) { byte[] receivedBytes = udpClient.EndReceive(result, ref remoteEndPoint); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from server: " + receivedMessage); // Continue receiving data asynchronously udpClient.BeginReceive(ReceiveData, null); } private void SendData(string message) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); // Send the message to the server udpClient.Send(sendBytes, sendBytes.Length, remoteEndPoint); Debug.Log("Sent to server: " + message); } }

Lorsque des données sont reçues du serveur, la méthode ReceiveData est appelée, qui traite les octets reçus et les convertit en une chaîne. Le message reçu est ensuite enregistré dans la console. Le client continue de recevoir des données de manière asynchrone en appelant à nouveau BeginReceive .

La méthode SendData convertit le message en octets et l'envoie au serveur à l'aide de la méthode Send du client UDP.


Serveur UDP

Ce code illustre une implémentation de base d'un serveur UDP dans Unity. La méthode StartUDPServer initialise le serveur UDP sur le port spécifié et commence à recevoir des données de manière asynchrone à l'aide de la méthode BeginReceive .


Lorsque des données sont reçues d'un client, la méthode ReceiveData est appelée, qui traite les octets reçus et les convertit en une chaîne. Le message reçu est ensuite enregistré dans la console. Le serveur continue de recevoir des données de manière asynchrone en appelant à nouveau BeginReceive .


 using System; using System.Net; using System.Net.Sockets; using UnityEngine; public class UDPServer : MonoBehaviour { private UdpClient udpServer; private IPEndPoint remoteEndPoint; private void Start() { // Example: Start the UDP server on port 5555 StartUDPServer(5555); } private void StartUDPServer(int port) { udpServer = new UdpClient(port); remoteEndPoint = new IPEndPoint(IPAddress.Any, port); Debug.Log("Server started. Waiting for messages..."); // Start receiving data asynchronously udpServer.BeginReceive(ReceiveData, null); } private void ReceiveData(IAsyncResult result) { byte[] receivedBytes = udpServer.EndReceive(result, ref remoteEndPoint); string receivedMessage = System.Text.Encoding.UTF8.GetString(receivedBytes); Debug.Log("Received from client: " + receivedMessage); // Process the received data // Continue receiving data asynchronously udpServer.BeginReceive(ReceiveData, null); } private void SendData(string message, IPEndPoint endPoint) { byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(message); // Send the message to the client udpServer.Send(sendBytes, sendBytes.Length, endPoint); Debug.Log("Sent to client: " + message); } }


La méthode SendData prend un message et un IPEndPoint représentant l'adresse et le port du client. Il convertit le message en octets et l'envoie au client en utilisant la méthode Send du serveur UDP.


Dans le cadre du développement de jeux, le choix entre TCP et UDP dépend en grande partie du type de votre jeu. Si votre jeu nécessite une livraison de données fiable et que le temps de réponse n'est pas un facteur critique (par exemple, dans les jeux de stratégie en temps réel ou au tour par tour), alors TCP peut être un choix approprié. D'autre part, si votre jeu nécessite une transmission de données rapide et peut gérer certaines pertes de données (par exemple, dans les jeux de tir à la première personne ou les jeux de course), alors UDP peut être le meilleur choix.


WebSocket

WebSocket est un protocole de communication qui permet l'établissement d'une connexion persistante entre un navigateur et un serveur. La principale différence avec le protocole HTTP classique est qu'il permet une communication bidirectionnelle ; le serveur est capable, non seulement de répondre aux requêtes des clients, il peut aussi lui envoyer des messages.


WebSocket est un protocole de niveau application basé sur TCP. Il prend en charge les messages, pas les flux, ce qui le distingue du TCP normal. WebSocket inclut des fonctionnalités supplémentaires qui peuvent augmenter les performances.


Voici comment cela fonctionne, étape par étape :


  • Le client envoie une requête HTTP spéciale appelée "Demande de mise à niveau". Cette requête informe le serveur que le client souhaite basculer vers WebSocket.


  • Si le serveur prend en charge WebSocket et accepte de basculer, il répond par une réponse HTTP spéciale qui confirme la mise à niveau vers WebSocket.


  • Après l'échange de ces messages, une connexion bidirectionnelle persistante est établie entre le client et le serveur. Les deux parties peuvent envoyer des messages à tout moment, pas seulement en réponse aux demandes de l'autre partie.


  • Désormais, le client et le serveur peuvent envoyer et recevoir des messages à tout moment. Chaque message WebSocket est enveloppé dans des "cadres" qui indiquent quand le message commence et se termine. Cela permet au navigateur et au serveur d'interpréter correctement les messages, même s'ils arrivent dans un ordre mélangé ou sont divisés en plusieurs parties en raison de problèmes de réseau.




L'un ou l'autre côté peut fermer la connexion à tout moment en envoyant une trame spéciale "fermer la connexion". L'autre partie peut répondre par une confirmation de la fermeture, et après cela, les deux parties doivent immédiatement cesser d'envoyer d'autres données. Les codes d'état peuvent également être utilisés pour indiquer la raison de la fermeture.

Client WebSocket

Le code ci-dessous fournit un exemple d'implémentation d'un client WebSocket dans Unity à l'aide du langage C# et de la bibliothèque WebSocketSharp .


Dans la méthode Start() , qui est appelée lors de l'initialisation de l'objet, une nouvelle instance de WebSocket est créée, initialisée avec l'adresse de votre serveur WebSocket.


Après cela, les gestionnaires d'événements OnOpen et OnMessage sont configurés.


 using UnityEngine; using WebSocketSharp; public class WebSocketClient : MonoBehaviour { private WebSocket ws; void Start() { ws = new WebSocket("ws://your-websocket-server-url/Auth"); ws.OnOpen += OnOpenHandler; ws.OnMessage += OnMessageHandler; ws.ConnectAsync(); } private void OnOpenHandler(object sender, System.EventArgs e) { var data = "Player1"; ws.Send(data) } private void OnMessageHandler(object sender, MessageEventArgs e) { Debug.Log("WebSocket server said: " + e.Data); } }


OnOpen est déclenché lorsqu'une connexion est établie avec le serveur. Dans cet exemple, lorsque la connexion est établie, un message avec le texte "Player1" est envoyé au serveur.


OnMessage est déclenché lorsqu'un message est reçu du serveur. Ici, à la réception d'un message, son contenu est affiché dans la console.


Ensuite, la méthode ConnectAsync() est appelée, qui se connecte de manière asynchrone au serveur.


Serveur WebSocket

Le code ci-dessous est un exemple de création d'un serveur WebSocket.


Dans la méthode Start() , appelée lors de l'initialisation de l'objet, une nouvelle instance de WebSocketServer est créée, initialisée avec l'adresse de votre serveur WebSocket. Ensuite, le service AuthBehaviour WebSocket est ajouté au serveur, disponible au chemin /Auth . Après cela, le serveur est démarré à l'aide de la méthode Start() .


 using UnityEngine; using WebSocketSharp; using WebSocketSharp.Server; public class WebSocketServer : MonoBehaviour { void Start() { var socket = new WebSocketServer("ws://your-websocket-server-url"); socket.AddWebSocketService<AuthBehaviour>("/Auth"); socket.Start(); } } public class AuthBehaviour : WebSocketBehavior { protected override void OnMessage (MessageEventArgs e) { var playerName = e.Data; Debug.Log("WebSocket client connected: " + playerName); Send("Auth Complete: " + playerName); } }


AuthBehaviour est une classe dérivée de WebSocketBehavior qui définit le comportement du serveur lors de la réception de messages de clients. Ici, la méthode OnMessage() est remplacée, qui est appelée lorsque le serveur reçoit un message d'un client.


Dans cette méthode, le texte du message est d'abord extrait, puis un message est envoyé à la console indiquant quel client s'est connecté, en utilisant le nom passé dans le message. Ensuite, le serveur renvoie un message au client contenant des informations sur l'achèvement de l'authentification.


Conclusion

Nous avons expliqué comment créer des connexions pour les jeux Unity à l'aide de TCP, UDP et WebSockets, ainsi que les avantages et les inconvénients de chacun. Malgré la nature légère d'UDP et la fiabilité de TCP, aucun n'est un bon choix pour organiser le multijoueur en temps réel dans les jeux. UDP perdra certaines données et TCP ne fournira pas la vitesse de transmission nécessaire. Dans le prochain article, nous verrons comment organiser une transmission de données fiable à l'aide d'UDP.