paint-brush
Unity 实时多人游戏,第 2 部分:TCP、UDP、WebSocket 协议经过@dmitrii
3,185 讀數
3,185 讀數

Unity 实时多人游戏,第 2 部分:TCP、UDP、WebSocket 协议

经过 Dmitrii Ivashchenko16m2023/08/06
Read on Terminal Reader

太長; 讀書

2023 年 Unity 网络前景系列继续关注实时多人游戏中的数据传输协议。 OSI 模型的 TCP 和 UDP 传输层旨在实现玩家之间的最佳数据交换。 TCP 可确保可靠的数据传输,但可能会导致延迟,而 UDP 提供更快的传输速度,但可能会导致数据丢失。 WebSocket是一种基于TCP的应用层协议,能够实现持久的双向通信,适用于实时多人游戏。 TCP 和 UDP 客户端和服务器以及 WebSocket 客户端和服务器的代码示例说明了实现方法。协议的选择取决于游戏要求 - TCP 用于可靠性,UDP 用于速度,WebSocket 用于双向通信。下一篇文章将深入探讨使用 UDP 组织可靠的数据传输。
featured image - Unity 实时多人游戏,第 2 部分:TCP、UDP、WebSocket 协议
Dmitrii Ivashchenko HackerNoon profile picture
0-item
1-item


我关于 2023 年 Unity 网络前景的系列文章仍在继续!今天的文章将介绍实时多人游戏中使用的数据传输协议。


大家好!我是 Dmitrii Ivashchenko,MY.GAMES 的首席软件工程师。我们将首先简要概述不同级别的网络交互中存在哪些协议。


内容概述

  • OSI 级别
  • 传输控制协议
    • TCP客户端
    • TCP服务器
  • 用户数据报协议
    • UDP客户端
    • UDP服务器
  • 网络套接字
    • Websocket客户端
    • Websocket服务器
  • 结论


OSI 级别

OSI(开放系统互连)模型是一种概念模型,它在抽象级别上描述和标准化通信系统的功能。该模型由国际标准化组织(ISO)于 1978 年制定。

它由七层组成:


  1. 物理层:该层处理通道上原始比特的传输和接收。这一层的协议描述了物理接口和介质的特性,包括位表示、传输速率、物理电缆、卡和连接器设计。


  2. 数据链路层:该层提供跨物理介质的数据传输并处理物理层可能发生的错误。


  3. 网络层:该层决定网络之间数据传输的路径(路由)。


  4. 传输层:该层管理点之间的消息传递,并在必要时提供可靠性。


  5. 会话层:该层管理用户和应用程序之间的会话建立、维护和终止。


  6. 表示层:该层确保数据独立于发送方和接收方数据表示(编码)的差异。


  7. 应用层:该层包括与用户交互的应用程序有直接关系的协议。



值得注意的是,每一层都建立在前一层的基础上。传输层之上的层(会话、表示和应用程序)是高度专业化的,无法帮助我们组织实时多人游戏。因此,我们必须停留在传输层,并使用它的 TCP 和 UDP 协议来实现玩家之间的最佳数据交换。


传输控制协议

TCP是面向连接的协议,这意味着通信发生在建立连接以交换数据的两个设备之间。该协议确保了可靠性,因为它保证所有传输的数据都将以正确的顺序到达目的地。如果传输过程中丢失任何数据,TCP将自动重试请求,直到所有数据传输成功。


建立连接涉及以下步骤:


  • 客户端和服务器创建套接字以通过 TCP 交换数据。
  • 客户端向指定目的端口的服务器发送SYN(同步)报文段。
  • 服务器接受 SYN 段,创建自己的套接字,并向客户端发送 SYN-ACK(同步确认)段。
  • 客户端接受SYN-ACK报文段并向服务器发送ACK(确认)报文段,完成连接建立过程。可靠的双向连接现已建立。





TCP客户端

下面的示例显示了 TCP 客户端的基本实现,并且可以扩展以使用特定的数据和游戏逻辑。


该代码连接到具有指定IP地址和端口的服务器,然后通过该连接发送和接收数据。网络流用于从服务器异步接收数据。


 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(); } }


ConnectToServer(string ipAddress, int port)方法建立与指定 IP 地址和端口的服务器的连接。来自服务器的数据接收是在方法ReceiveData(IAsyncResult result)中执行的,而数据传输是在方法SendData(string message)中完成的。使用Debug.Log将接收到的数据输出到控制台。


TCP服务器

下面的代码示例代表了 Unity 中的一个简单 TCP 服务器。该代码初始化服务器,侦听指定端口,并接受来自客户端的传入连接。连接到客户端后,服务器通过网络流发送和接收数据。


StartServer(int port)方法在指定端口上初始化服务器并开始侦听传入连接。当客户端连接建立后,会执行HandleIncomingConnection(IAsyncResult result)方法,该方法从客户端接收数据,并启动异步操作来接收数据。


接收到的数据在ReceiveData(IAsyncResult result)方法中进行处理。服务器接收到客户端发送的数据后,可以进行必要的处理或将数据发送回客户端。


SendData(string message)方法通过网络流向客户端发送数据。数据被转换为字节数组并发送给客户端。


 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); } }


HandleIncomingConnection方法用于处理传入的客户端连接。接受连接后,获取与客户端进行数据交换的流,并发起异步操作从客户端接收数据。


然后,该方法根据从连接的客户端接收到的缓冲区大小创建用于接收数据的缓冲区。启动异步操作以使用创建的缓冲区从流中读取数据。读取操作完成后,数据将传递给ReceiveData方法进行进一步处理。


此外,该方法还启动另一个异步操作来接受传入连接,以便可以接受下一个客户端。


当客户端连接到服务器时,将调用此方法来处理连接,并且服务器将准备好异步接受下一个客户端。


 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); }


ReceiveData方法用于处理从客户端接收到的数据。数据读取操作完成后,该方法检查读取的字节数。如果字节数小于或等于0,则表示客户端已断开连接。在这种情况下,该方法关闭与客户端的连接并终止执行。


如果读取的字节数大于零,则该方法为接收到的数据创建一个字节数组,并将读取的数据复制到该数组中。然后,该方法使用 UTF-8 编码将接收到的字节转换为字符串,并将接收到的消息输出到控制台。


 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); }


SendData方法用于向客户端发送数据。它使用 UTF-8 编码将消息字符串转换为字节数组,并将该数组写入网络流。发送数据后,该方法清除流并将发送的消息输出到控制台。

尽管可靠性听起来像是一个很大的优势,但这种 TCP 功能可能会在实时多人游戏中产生问题。 TCP 中的数据传输可以通过重传和流量控制机制减慢,这可能导致延迟或“滞后”。


用户数据报协议

UDP 是一种更简单的协议,不保证传送或数据包顺序。这使得它比 TCP 快得多,因为它不需要花时间建立连接或重新传输丢失的数据包。由于其速度快且简单,UDP经常用于网络游戏和其他需要实时数据传输的应用中。



然而,使用UDP需要开发人员更加仔细地管理数据传输。由于 UDP 不保证传送,因此您可能需要实现自己的机制来处理丢失的数据包或无序到达的数据包。


UDP客户端

此代码演示了 Unity 中 UDP 客户端的基本实现。 StartUDPClient方法初始化 UDP 客户端并将其连接到由 IP 地址和端口指定的远程服务器。客户端使用BeginReceive方法开始异步接收数据,并使用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); } }

当从服务器接收到数据时,将调用ReceiveData方法,该方法处理接收到的字节并将其转换为字符串。然后将收到的消息记录到控制台。客户端再次调用BeginReceive继续异步接收数据。

SendData方法将消息转换为字节,并使用 UDP 客户端的Send方法将其发送到服务器。


UDP服务器

此代码演示了 Unity 中 UDP 服务器的基本实现。 StartUDPServer方法初始化指定端口上的 UDP 服务器,并使用BeginReceive方法开始异步接收数据。


当从客户端接收到数据时,将调用ReceiveData方法,该方法处理接收到的字节并将其转换为字符串。然后将收到的消息记录到控制台。服务器再次调用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); } }


SendData方法采用一条消息和一个表示客户端地址和端口的IPEndPoint 。它将消息转换为字节并使用UDP服务器的Send方法将其发送到客户端。


在游戏开发中,TCP 和 UDP 之间的选择很大程度上取决于游戏的类型。如果您的游戏需要可靠的数据传输并且响应时间不是关键因素(例如,在实时策略或回合制游戏中),那么 TCP 可能是合适的选择。另一方面,如果您的游戏需要快速数据传输并且可以处理一些数据丢失(例如,在第一人称射击游戏或赛车游戏中),那么 UDP 可能是最佳选择。


WebSocket

WebSocket是一种通信协议,允许在浏览器和服务器之间建立持久连接。与常规 HTTP 的主要区别在于它支持双向通信;服务器不仅能够响应客户端请求,还可以向客户端发送消息。


WebSocket是一种基于TCP的应用层协议。它支持消息,而不是流,这与常规 TCP 不同。 WebSocket 包含可能会增加一些性能开销的附加功能。


其工作原理如下:


  • 客户端发送一个称为“升级请求”的特殊 HTTP 请求。该请求通知服务器客户端想要切换到 WebSocket。


  • 如果服务器支持 WebSocket 并同意切换,它会响应一个特殊的 HTTP 响应,确认升级到 WebSocket。


  • 交换这些消息后,客户端和服务器之间建立持久的双向连接。双方可以随时发送消息,而不仅仅是响应对方的请求。


  • 现在客户端和服务器可以随时发送和接收消息。每个 WebSocket 消息都包装在“帧”中,指示消息何时开始和结束。这使得浏览器和服务器能够正确解释消息,即使它们以混合顺序到达或由于网络问题被分成几部分。




任何一方都可以通过发送特殊的“关闭连接”帧随时关闭连接。另一方可以响应关闭确认,之后双方必须立即停止发送任何其他数据。状态代码也可用于指示关闭的原因。

WebSocket客户端

下面的代码提供了使用 C# 语言和WebSocketSharp库在 Unity 中实现 WebSocket 客户端的示例。


在对象初始化时调用的Start()方法中,将创建一个新的WebSocket实例,并使用 WebSocket 服务器的地址进行初始化。


之后,设置事件处理程序OnOpenOnMessage


 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在与服务器建立连接时触发。在此示例中,建立连接后,将向服务器发送一条包含文本“Player1”的消息。


OnMessage当从服务器收到消息时触发。这里,收到消息后,其内容显示在控制台中。


然后,调用ConnectAsync()方法,该方法异步连接到服务器。


WebSocket服务器

下面的代码是创建 WebSocket 服务器的示例。


在初始化对象时调用的Start()方法中,将创建一个新的WebSocketServer实例,并使用 WebSocket 服务器的地址进行初始化。然后, AuthBehaviour WebSocket 服务将添加到服务器,可在/Auth路径中找到。之后,使用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是从WebSocketBehavior派生的类,定义从客户端接收消息时服务器的行为。这里, OnMessage()方法被重写,当服务器收到来自客户端的消息时调用该方法。


在此方法中,首先提取消息文本,然后使用消息中传递的名称将消息输出到控制台,指示哪个客户端已连接。然后,服务器将一条消息发送回客户端,其中包含有关身份验证完成的信息。


结论

我们讨论了如何使用 TCP、UDP 和 WebSockets 为 Unity 游戏创建连接,以及各自的优点和缺点。尽管 UDP 具有轻量级特性并且 TCP 具有可靠性,但对于在游戏中组织实时多人游戏来说,这两者都不是一个好的选择。 UDP会丢失一些数据,而TCP将无法提供必要的传输速度。在下一篇文章中,我们将讨论如何使用UDP组织可靠的数据传输。