paint-brush
Unity 실시간 멀티플레이어, 2부: TCP, UDP, WebSocket 프로토콜by@dmitrii
4,201
4,201

Unity 실시간 멀티플레이어, 2부: TCP, UDP, WebSocket 프로토콜

2023년 Unity 네트워킹 환경 시리즈는 계속해서 실시간 멀티플레이어 게임의 데이터 전송 프로토콜에 중점을 둡니다. 플레이어 간의 최적의 데이터 교환을 위해 TCP 및 UDP를 사용하는 OSI 모델의 전송 계층이 탐색됩니다. TCP는 안정적인 데이터 전달을 보장하지만 지연이 발생할 수 있는 반면, UDP는 데이터 손실 가능성이 있지만 더 빠른 전송을 제공합니다. TCP 기반 애플리케이션 수준 프로토콜인 WebSocket은 지속적인 양방향 통신을 가능하게 하며 실시간 멀티플레이어 게임에 적합합니다. 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 네트워킹 환경에 관한 기사 시리즈는 계속됩니다! 오늘 게시물에서는 실시간 멀티플레이어 게임에서 사용되는 데이터 전송 프로토콜을 다룰 것입니다.


모두들 인사드립니다! 저는 MY.GAMES의 수석 소프트웨어 엔지니어인 Dmitrii Ivashchenko입니다. 다양한 네트워크 상호 작용 수준에 존재하는 프로토콜에 대한 간략한 개요부터 시작하겠습니다.


콘텐츠 개요

  • OSI 레벨
  • 전송 제어 프로토콜
    • TCP 클라이언트
    • TCP 서버
  • 사용자 데이터그램 프로토콜
    • UDP 클라이언트
    • UDP 서버
  • 웹소켓
    • 웹소켓 클라이언트
    • 웹소켓 서버
  • 결론


OSI 레벨

OSI(Open Systems Interconnection) 모델은 추상화 수준 측면에서 통신 시스템의 기능을 특성화하고 표준화하는 개념 모델입니다. 이 모델은 1978년 국제표준화기구(ISO)에 의해 개발되었습니다.

이는 7개의 레이어로 구성됩니다.


  1. 물리 계층 : 이 계층은 채널을 통한 원시 비트의 전송 및 수신을 처리합니다. 이 계층의 프로토콜은 비트 표현, 전송 속도, 물리적 케이블, 카드 및 커넥터 디자인을 포함하여 매체의 특성과 물리적 인터페이스를 설명합니다.


  2. 데이터 링크 계층 : 이 계층은 물리적 매체를 통한 데이터 전송을 제공하고 물리적 수준에서 발생할 수 있는 오류를 처리합니다.


  3. 네트워크 계층(Network Layer) : 네트워크 간 데이터 전송을 위한 경로(라우팅)를 결정하는 계층입니다.


  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보다 작거나 같으면 클라이언트 연결이 끊어졌음을 의미합니다. 이 경우 메서드는 클라이언트와의 연결을 닫고 실행을 종료합니다.


읽은 바이트 수가 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가 적합한 선택일 수 있습니다. 반면, 게임에서 빠른 데이터 전송이 필요하고 일부 데이터 손실을 처리할 수 있는 경우(예: 1인칭 슈팅 게임 또는 레이싱 게임) UDP가 최선의 선택일 수 있습니다.


웹소켓

WebSocket 은 브라우저와 서버 간의 지속적인 연결 설정을 허용하는 통신 프로토콜입니다. 일반 HTTP와의 주요 차이점은 양방향 통신이 가능하다는 것입니다. 서버는 클라이언트 요청에 응답할 수 있을 뿐만 아니라 메시지를 보낼 수도 있습니다.


WebSocket은 TCP 기반의 애플리케이션 수준 프로토콜입니다. 스트림이 아닌 메시지를 지원하므로 일반 TCP와 구별됩니다. WebSocket에는 성능 오버헤드를 추가할 수 있는 추가 기능이 포함되어 있습니다.


작동 방식은 단계별로 다음과 같습니다.


  • 클라이언트는 "업그레이드 요청"이라는 특수 HTTP 요청을 보냅니다. 이 요청은 클라이언트가 WebSocket으로 전환하기를 원한다는 것을 서버에 알립니다.


  • 서버가 WebSocket을 지원하고 전환에 동의하면 WebSocket으로의 업그레이드를 확인하는 특수 HTTP 응답으로 응답합니다.


  • 이러한 메시지를 교환한 후 클라이언트와 서버 간에 지속적인 양방향 연결이 설정됩니다. 양측은 상대방의 요청에 대한 응답뿐만 아니라 언제든지 메시지를 보낼 수 있습니다.


  • 이제 클라이언트와 서버는 언제든지 메시지를 보내고 받을 수 있습니다. 각 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 서버를 생성하는 예입니다.


개체가 초기화될 때 호출되는 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, WebSocket을 사용하여 Unity 게임에 대한 연결을 생성하는 방법과 각각의 장점과 단점에 대해 논의했습니다. UDP의 가벼운 특성과 TCP의 안정성에도 불구하고 게임에서 실시간 멀티플레이어를 구성하는 데에는 둘 다 좋은 선택이 아닙니다. UDP는 일부 데이터를 손실하고 TCP는 필요한 전송 속도를 제공하지 않습니다. 다음 기사에서는 UDP를 사용하여 안정적인 데이터 전송을 구성하는 방법에 대해 설명합니다.