của tôi về Bối cảnh mạng thống nhất vào năm 2023 sẽ tiếp tục! Bài đăng hôm nay sẽ đề cập đến các giao thức truyền dữ liệu được sử dụng trong các trò chơi nhiều người chơi trong thời gian thực. Loạt bài viết Xin chào tất cả mọi người! Tôi là Dmitrii Ivashchenko, Kỹ sư phần mềm hàng đầu tại MY.GAMES. Chúng ta sẽ bắt đầu với một cái nhìn tổng quan ngắn gọn về những giao thức tồn tại ở các cấp độ tương tác mạng khác nhau. Tổng quan nội dung Cấp độ OSI Giao thức điều khiển truyền dẫn Máy khách TCP Máy chủ TCP Giao thức gói dữ liệu người dùng ứng dụng khách UDP Máy chủ UDP ổ cắm web Máy khách Websocket Máy chủ websocket Phần kết luận Cấp độ OSI Mô hình OSI (Open Systems Interconnection) là một mô hình khái niệm đặc trưng và tiêu chuẩn hóa các chức năng của một hệ thống truyền thông theo các cấp độ trừu tượng. Mô hình này được Tổ chức Tiêu chuẩn hóa Quốc tế (ISO) phát triển vào năm 1978. Nó bao gồm bảy lớp: : Lớp này xử lý việc truyền và nhận các bit thô qua kênh. Các giao thức ở lớp này mô tả giao diện vật lý và các đặc điểm của phương tiện, bao gồm biểu diễn bit, tốc độ truyền, cáp vật lý, thẻ và thiết kế đầu nối. Lớp vật lý : Lớp này cung cấp khả năng truyền dữ liệu qua phương tiện vật lý và xử lý các lỗi có thể xảy ra ở cấp độ vật lý. Lớp liên kết dữ liệu : Lớp này xác định đường dẫn (định tuyến) để truyền dữ liệu giữa các mạng. Lớp mạng : Lớp này quản lý việc gửi tin nhắn giữa các điểm và cung cấp độ tin cậy nếu cần thiết. Lớp vận chuyển : Tầng này quản lý việc thiết lập, duy trì và chấm dứt phiên giữa người dùng và ứng dụng. Tầng phiên : Lớp này đảm bảo tính độc lập của dữ liệu khỏi sự khác biệt trong biểu diễn dữ liệu (mã hóa) ở phía người gửi và người nhận. Lớp trình bày : Lớp này bao gồm các giao thức có mối quan hệ trực tiếp với các ứng dụng mà người dùng tương tác. Lớp ứng dụng Điều đáng chú ý là mỗi lớp được xây dựng dựa trên lớp trước đó. Các lớp phía trên lớp Vận chuyển (Phiên, Bản trình bày và Ứng dụng) có tính chuyên biệt cao và không thể giúp chúng tôi tổ chức nhiều người chơi trong thời gian thực. Do đó, chúng tôi phải dừng lại ở Tầng vận chuyển và sử dụng các giao thức TCP và UDP của nó để trao đổi dữ liệu tối ưu giữa những người chơi. Giao thức điều khiển truyền dẫn là một giao thức hướng kết nối, có nghĩa là giao tiếp xảy ra giữa hai thiết bị thiết lập kết nối để trao đổi dữ liệu. Giao thức này đảm bảo độ tin cậy vì nó đảm bảo rằng tất cả dữ liệu được truyền sẽ đến đích theo đúng thứ tự. Nếu bất kỳ dữ liệu nào bị mất trong quá trình truyền, TCP sẽ tự động thử lại yêu cầu cho đến khi tất cả dữ liệu được truyền thành công. TCP Thiết lập kết nối bao gồm các bước sau: Máy khách và máy chủ tạo các ổ cắm để trao đổi dữ liệu qua TCP. Máy khách gửi một phân đoạn SYN (đồng bộ hóa) đến máy chủ với một cổng đích được chỉ định. Máy chủ chấp nhận phân đoạn SYN, tạo ổ cắm riêng và gửi phân đoạn SYN-ACK (đồng bộ hóa-xác nhận) tới máy khách. Máy khách chấp nhận phân đoạn SYN-ACK và gửi phân đoạn ACK (xác nhận) đến máy chủ để hoàn tất quá trình thiết lập kết nối. Một kết nối hai chiều đáng tin cậy hiện đã được thiết lập. Máy khách TCP Ví dụ dưới đây cho thấy cách triển khai cơ bản của ứng dụng khách TCP và có thể được mở rộng để hoạt động với logic trò chơi và dữ liệu cụ thể. Mã kết nối với máy chủ có địa chỉ IP và cổng được chỉ định, sau đó gửi và nhận dữ liệu thông qua kết nối. Luồng mạng được sử dụng để nhận dữ liệu không đồng bộ từ máy chủ. 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(); } } Phương thức thiết lập kết nối đến máy chủ tại địa chỉ IP và cổng đã chỉ định. Việc nhận dữ liệu từ máy chủ được thực hiện theo phương thức , trong khi việc truyền dữ liệu được thực hiện theo phương thức . Dữ liệu nhận được được xuất ra bàn điều khiển bằng cách sử dụng . ConnectToServer(string ipAddress, int port) ReceiveData(IAsyncResult result) SendData(string message) Debug.Log Máy chủ TCP Ví dụ mã bên dưới đại diện cho một máy chủ TCP đơn giản trong Unity. Mã khởi tạo máy chủ, lắng nghe cổng được chỉ định và chấp nhận các kết nối đến từ máy khách. Sau khi kết nối với máy khách, máy chủ sẽ gửi và nhận dữ liệu qua luồng mạng. Phương thức khởi tạo máy chủ trên cổng đã chỉ định và bắt đầu lắng nghe các kết nối đến. Khi kết nối máy khách được thiết lập, phương thức được thực thi, phương thức này sẽ nhận dữ liệu từ máy khách và bắt đầu hoạt động không đồng bộ để nhận dữ liệu. StartServer(int port) HandleIncomingConnection(IAsyncResult result) Dữ liệu đã nhận được xử lý theo phương thức . Sau khi nhận dữ liệu từ máy khách, máy chủ có thể thực hiện xử lý cần thiết hoặc gửi dữ liệu trở lại máy khách. ReceiveData(IAsyncResult result) Phương thức gửi dữ liệu tới máy khách thông qua luồng mạng. Dữ liệu được chuyển đổi thành một mảng byte và được gửi đến máy khách. 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); } } Phương thức được sử dụng để xử lý kết nối máy khách đến. Sau khi chấp nhận kết nối, nó sẽ nhận được một luồng để trao đổi dữ liệu với máy khách và bắt đầu hoạt động không đồng bộ để nhận dữ liệu từ máy khách. HandleIncomingConnection Sau đó, phương thức tạo bộ đệm để nhận dữ liệu, dựa trên kích thước bộ đệm nhận được từ máy khách được kết nối. Một hoạt động không đồng bộ được khởi chạy để đọc dữ liệu từ luồng bằng bộ đệm đã tạo. Sau khi thao tác đọc hoàn tất, dữ liệu sẽ được chuyển đến phương thức để xử lý tiếp. ReceiveData Ngoài ra, phương thức khởi chạy một hoạt động không đồng bộ khác để chấp nhận các kết nối đến để có khả năng chấp nhận ứng dụng khách tiếp theo. Khi một máy khách kết nối với máy chủ, phương thức này sẽ được gọi để xử lý kết nối và máy chủ sẽ sẵn sàng chấp nhận không đồng bộ các máy khách tiếp theo. 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); } Phương thức được sử dụng để xử lý dữ liệu nhận được từ máy khách. Sau khi thao tác đọc dữ liệu hoàn tất, phương thức sẽ kiểm tra số byte đã đọc. Nếu số byte nhỏ hơn hoặc bằng 0, điều này có nghĩa là máy khách đã bị ngắt kết nối. Trong trường hợp này, phương thức đóng kết nối với máy khách và kết thúc thực thi. ReceiveData Nếu số byte được đọc lớn hơn 0, phương thức sẽ tạo một mảng byte cho dữ liệu nhận được và sao chép dữ liệu đã đọc vào mảng này. Sau đó, phương thức chuyển đổi các byte đã nhận thành một chuỗi bằng cách sử dụng mã hóa UTF-8 và xuất thông báo nhận được ra bàn điều khiển. 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); } Phương thức được sử dụng để gửi dữ liệu đến máy khách. Nó chuyển đổi chuỗi thông báo thành một mảng byte bằng mã hóa UTF-8 và ghi mảng này vào luồng mạng. Sau khi gửi dữ liệu, phương thức sẽ xóa luồng và xuất thông báo đã gửi tới bàn điều khiển. SendData Mặc dù độ tin cậy nghe có vẻ là một điểm cộng lớn, nhưng tính năng TCP này có thể gây ra sự cố trong các trò chơi nhiều người chơi thời gian thực. Việc truyền dữ liệu trong TCP có thể bị chậm lại do cơ chế truyền lại và kiểm soát luồng, điều này có thể dẫn đến độ trễ hoặc "độ trễ". Giao thức gói dữ liệu người dùng UDP là một giao thức đơn giản hơn không đảm bảo việc phân phối hoặc thứ tự gói tin. Điều này làm cho nó nhanh hơn nhiều so với TCP, vì nó không tốn thời gian thiết lập kết nối hoặc truyền lại các gói bị mất. Do tốc độ và sự đơn giản của nó, UDP thường được sử dụng trong các trò chơi mạng và các ứng dụng khác yêu cầu truyền dữ liệu thời gian thực. Tuy nhiên, việc sử dụng UDP yêu cầu các nhà phát triển quản lý việc truyền dữ liệu cẩn thận hơn. Vì UDP không đảm bảo việc phân phối, bạn có thể cần triển khai các cơ chế của riêng mình để xử lý các gói bị mất hoặc các gói đến không đúng thứ tự. Máy khách UDP Mã này thể hiện cách triển khai cơ bản của ứng dụng khách UDP trong Unity. Phương thức khởi tạo ứng dụng khách UDP và kết nối nó với máy chủ từ xa được chỉ định bởi địa chỉ IP và cổng. Máy khách bắt đầu nhận dữ liệu không đồng bộ bằng phương thức và gửi thông báo đến máy chủ bằng phương thức . StartUDPClient 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); } } Khi dữ liệu được nhận từ máy chủ, phương thức được gọi, phương thức này sẽ xử lý các byte đã nhận và chuyển đổi chúng thành một chuỗi. Tin nhắn nhận được sau đó được ghi vào bảng điều khiển. Máy khách tiếp tục nhận dữ liệu không đồng bộ bằng cách gọi lại . ReceiveData BeginReceive Phương thức chuyển đổi tin nhắn thành byte và gửi nó đến máy chủ bằng phương thức của máy khách UDP. SendData Send Máy chủ UDP Mã này thể hiện cách triển khai cơ bản của máy chủ UDP trong Unity. Phương thức khởi tạo máy chủ UDP trên cổng đã chỉ định và bắt đầu nhận dữ liệu không đồng bộ bằng phương thức . StartUDPServer BeginReceive Khi dữ liệu được nhận từ máy khách, phương thức được gọi, phương thức này sẽ xử lý các byte đã nhận và chuyển đổi chúng thành một chuỗi. Tin nhắn nhận được sau đó được ghi vào bảng điều khiển. Máy chủ tiếp tục nhận dữ liệu không đồng bộ bằng cách gọi lại . 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); } } Phương thức nhận một thông báo và một đại diện cho địa chỉ và cổng của máy khách. Nó chuyển đổi tin nhắn thành byte và gửi nó đến máy khách bằng phương thức của máy chủ UDP. SendData IPEndPoint Send Trong bối cảnh phát triển trò chơi, sự lựa chọn giữa TCP và UDP phần lớn phụ thuộc vào loại trò chơi của bạn. Nếu trò chơi của bạn yêu cầu cung cấp dữ liệu đáng tin cậy và thời gian phản hồi không phải là yếu tố quan trọng (ví dụ: trong chiến lược thời gian thực hoặc trò chơi theo lượt), thì TCP có thể là một lựa chọn phù hợp. Mặt khác, nếu trò chơi của bạn yêu cầu truyền dữ liệu nhanh và có thể xử lý một số trường hợp mất dữ liệu (ví dụ: trong game bắn súng góc nhìn thứ nhất hoặc game đua xe), thì UDP có thể là lựa chọn tốt nhất. ổ cắm web là một giao thức truyền thông cho phép thiết lập kết nối liên tục giữa trình duyệt và máy chủ. Sự khác biệt chính so với HTTP thông thường là nó cho phép giao tiếp hai chiều; máy chủ không chỉ có thể đáp ứng các yêu cầu của máy khách mà còn có thể gửi tin nhắn đến nó. WebSocket WebSocket là một giao thức cấp ứng dụng dựa trên TCP. Nó hỗ trợ các tin nhắn, không phải luồng, giúp phân biệt nó với TCP thông thường. WebSocket bao gồm chức năng bổ sung có thể thêm một số chi phí hoạt động. Đây là cách nó hoạt động, từng bước một: Máy khách gửi một yêu cầu HTTP đặc biệt được gọi là "Yêu cầu nâng cấp". Yêu cầu này thông báo cho máy chủ rằng máy khách muốn chuyển sang WebSocket. Nếu máy chủ hỗ trợ WebSocket và đồng ý chuyển đổi, nó sẽ phản hồi bằng một phản hồi HTTP đặc biệt xác nhận việc nâng cấp lên WebSocket. Sau khi trao đổi các tin nhắn này, một kết nối hai chiều liên tục được thiết lập giữa máy khách và máy chủ. Cả hai bên có thể gửi tin nhắn bất cứ lúc nào, không chỉ để đáp ứng yêu cầu từ phía bên kia. Bây giờ máy khách và máy chủ có thể gửi và nhận tin nhắn bất cứ lúc nào. Mỗi thông báo WebSocket được bao bọc trong các "khung" cho biết thời điểm thông báo bắt đầu và kết thúc. Điều này cho phép trình duyệt và máy chủ diễn giải chính xác thư, ngay cả khi chúng đến theo thứ tự hỗn hợp hoặc bị chia thành nhiều phần do sự cố mạng. Một trong hai bên có thể đóng kết nối bất cứ lúc nào bằng cách gửi một khung "đóng kết nối" đặc biệt. Phía bên kia có thể phản hồi bằng cách xác nhận việc đóng và sau đó, cả hai bên phải ngay lập tức ngừng gửi bất kỳ dữ liệu nào khác. Mã trạng thái cũng có thể được sử dụng để chỉ ra lý do đóng cửa. Máy khách WebSocket Đoạn mã dưới đây cung cấp một ví dụ về triển khai ứng dụng khách WebSocket trong Unity bằng ngôn ngữ C# và thư viện . WebSocketSharp Trong phương thức , được gọi khi khởi tạo đối tượng, một phiên bản mới của được tạo, khởi tạo với địa chỉ của máy chủ WebSocket của bạn. Start() WebSocket Sau đó, trình xử lý sự kiện và được thiết lập. OnOpen OnMessage 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); } } được kích hoạt khi kết nối được thiết lập với máy chủ. Trong ví dụ này, khi kết nối được thiết lập, một thông báo có nội dung "Player1" sẽ được gửi đến máy chủ. OnOpen được kích hoạt khi nhận được tin nhắn từ máy chủ. Tại đây, khi nhận được một tin nhắn, nội dung của nó sẽ được hiển thị trong bảng điều khiển. OnMessage Sau đó, phương thức được gọi, kết nối không đồng bộ với máy chủ. ConnectAsync() Máy chủ WebSocket Đoạn mã dưới đây là một ví dụ về việc tạo một máy chủ WebSocket. Trong phương thức , được gọi khi đối tượng được khởi tạo, một phiên bản mới của được tạo, được khởi tạo với địa chỉ của máy chủ WebSocket của bạn. Sau đó, dịch vụ WebSocket được thêm vào máy chủ, có sẵn tại đường dẫn . Sau đó, máy chủ được khởi động bằng phương thức . Start() WebSocketServer AuthBehaviour /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); } } là một lớp bắt nguồn từ xác định hành vi của máy chủ khi nhận tin nhắn từ máy khách. Ở đây, phương thức bị ghi đè, được gọi khi máy chủ nhận được tin nhắn từ máy khách. AuthBehaviour WebSocketBehavior OnMessage() Trong phương pháp này, văn bản thông báo trước tiên được trích xuất, sau đó một thông báo được xuất ra bảng điều khiển cho biết máy khách nào đã kết nối, sử dụng tên được truyền trong thông báo. Sau đó, máy chủ sẽ gửi lại một thông báo cho máy khách có chứa thông tin về việc hoàn thành xác thực. Phần kết luận Chúng ta đã thảo luận về cách tạo kết nối cho trò chơi Unity bằng TCP, UDP và WebSockets, cũng như ưu điểm và nhược điểm của từng loại. Bất chấp bản chất nhẹ của UDP và độ tin cậy của TCP, cả hai đều không phải là lựa chọn tốt để tổ chức nhiều người chơi trong thời gian thực trong trò chơi. UDP sẽ mất một số dữ liệu và TCP sẽ không cung cấp tốc độ truyền cần thiết. Trong bài viết tiếp theo, chúng ta sẽ thảo luận về cách tổ chức truyền dữ liệu đáng tin cậy bằng UDP.