-
세션(Session) 관리: 멀티플레이어 게임의 핵심2024년 03월 20일
- 유니얼
-
작성자
-
2024.03.20.:42
728x90C# 게임 서버 만들기
멀티플레이어 온라인 게임 개발에서 세션 관리는 사용자 경험을 크게 향상시키는 핵심 기능입니다. 게임 서버와 클라이언트 간의 통신에서 세션을 사용하여 각 플레이어의 상태와 게임 진행 정보를 추적하고 관리합니다. 이 글에서는 C#을 사용한 게임 서버 개발에서 세션 관리의 중요성과 기본적인 구현 방법에 대해 살펴보겠습니다.
세션의 개념
세션은 플레이어가 게임 서버에 연결되어 있는 동안 생성되는 유니크한 인스턴스입니다. 각 세션은 플레이어의 게임 내 활동, 위치, 인벤토리 상태 등을 추적합니다. 이를 통해 게임 서버는 다음과 같은 이점을 얻습니다:
- 플레이어 식별: 각 세션에는 고유한 식별자가 있어, 서버가 플레이어를 구분하고 개별적으로 데이터를 관리할 수 있게 합니다.
- 게임 상태 유지: 플레이어의 게임 진행 상태를 세션을 통해 기록하고, 게임이 중단되었다가 재개될 때 이전 상태를 복원할 수 있습니다.
- 보안성 향상: 각 세션은 독립적으로 관리되므로, 한 플레이어의 세션 데이터가 다른 플레이어에게 노출되는 것을 방지합니다.
예제코드
아래의 예제는 C#을 사용한 서버-클라이언트 기반 네트워크 프로그래밍의 구조를 보여줍니다. 특히, TCP 소켓 통신에서 패킷을 처리하는 방식과 세션 관리를 위한 기본적인 틀을 제공합니다. 여기에, 간단한 클라이언트와 서버 간의 메시지 전송 예제를 추가하여, 주어진 코드를 활용하는 방법을 설명하겠습니다.
Session
using System; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; // 네트워크 세션 관리를 위한 기본 클래스 public abstract class Session { // 네트워크 통신에 사용되는 소켓 Socket _socket; // 연결 해제 상태를 체크하기 위한 변수 int _disconnected = 0; // 수신 데이터를 관리하기 위한 RecvBuffer 인스턴스 RecvBuffer _recvBuffer = new RecvBuffer(1024); // 송수신 처리를 동기화하기 위한 객체 object _lock = new object(); // 송신 데이터 큐 Queue<ArraySegment<byte>> _sendQueue = new Queue<ArraySegment<byte>>(); // 송신 대기 목록 List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>(); // 송신 이벤트 인자 SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs(); // 수신 이벤트 인자 SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs(); // 연결되었을 때 호출되는 추상 메소드 public abstract void OnConnected(EndPoint endPoint); // 데이터 수신 시 호출되는 추상 메소드 public abstract int OnRecv(ArraySegment<byte> buffer); // 데이터 송신 완료 시 호출되는 추상 메소드 public abstract void OnSend(int numOfBytes); // 연결 해제 시 호출되는 추상 메소드 public abstract void OnDisconnected(EndPoint endPoint); // 세션을 시작하는 메소드. 소켓과 연결 이벤트 핸들러를 설정합니다. public void Start(Socket socket) { _socket = socket; _recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted); _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted); RegisterRecv(); } // 데이터 송신 요청을 처리하는 메소드 public void Send(ArraySegment<byte> sendBuff) { lock (_lock) { _sendQueue.Enqueue(sendBuff); if (_pendingList.Count == 0) RegisterSend(); } } // 소켓 연결을 해제하는 메소드 public void Disconnect() { if (Interlocked.Exchange(ref _disconnected, 1) == 1) return; OnDisconnected(_socket.RemoteEndPoint); _socket.Shutdown(SocketShutdown.Both); _socket.Close(); } // 송신 데이터가 있을 경우 소켓에 비동기로 송신을 등록하는 메소드 void RegisterSend() { lock (_lock) { while (_sendQueue.Count > 0) { ArraySegment<byte> buff = _sendQueue.Dequeue(); _pendingList.Add(buff); } _sendArgs.BufferList = _pendingList; bool pending = _socket.SendAsync(_sendArgs); if (!pending) OnSendCompleted(null, _sendArgs); } } // 송신 완료 시 호출되는 콜백 메소드 void OnSendCompleted(object sender, SocketAsyncEventArgs args) { lock (_lock) { if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) { try { _sendArgs.BufferList = null; _pendingList.Clear(); OnSend(args.BytesTransferred); if (_sendQueue.Count > 0) RegisterSend(); } catch (Exception e) { Console.WriteLine($"OnSendCompleted Failed: {e}"); } } else { Disconnect(); } } } // 수신 대기를 등록하는 메소드 void RegisterRecv() { _recvBuffer.Clean(); ArraySegment<byte> segment = _recvBuffer.WriteSegment; _recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); bool pending = _socket.ReceiveAsync(_recvArgs); if (!pending) OnRecvCompleted(null, _recvArgs); } // 수신 완료 시 호출되는 콜백 메소드 void OnRecvCompleted(object sender, SocketAsyncEventArgs args) { if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) { try { if (!_recvBuffer.OnWrite(args.BytesTransferred)) { Disconnect(); return; } int processLen = OnRecv(_recvBuffer.ReadSegment); if (processLen < 0 || _recvBuffer.DataSize < processLen) { Disconnect(); return; } if (!_recvBuffer.OnRead(processLen)) { Disconnect(); return; } RegisterRecv(); } catch (Exception e) { Console.WriteLine($"OnRecvCompleted Failed: {e}"); } } else { Disconnect(); } } }
클라이언트에서 패킷 처리 구현하기
클라이언트 측에서 서버에 연결하고 간단한 메시지를 전송하는 예제입니다. 이 코드는 Session 클래스와 PacketSession 클래스를 사용하는 서버에 메시지를 보내고 응답을 기다립니다.
// 기본 패킷 구조를 정의하는 클래스 class Packet { public ushort size; // 패킷 크기 public ushort packetId; // 패킷 ID } // 플레이어 정보 요청 패킷 class PlayerInfoReq : Packet { public long playerId; // 요청하는 플레이어의 ID } // 플레이어 정보 응답 패킷 class PlayerInfoOk : Packet { public int hp; // 플레이어의 체력 public int attack; // 플레이어의 공격력 } // 사용될 패킷 ID를 정의하는 열거형 public enum PacketID { PlayerInfoReq = 1, PlayerInfoOk = 2, } // 서버 세션을 관리하는 클래스 class ServerSession : Session { // 연결이 성공적으로 이루어졌을 때 실행되는 메소드 public override void OnConnected(EndPoint endPoint) { Console.WriteLine($"OnConnected : {endPoint}"); // 테스트용 플레이어 정보 요청 패킷을 생성하여 보냅니다. PlayerInfoReq packet = new PlayerInfoReq() { size = 4, packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 }; for (int i = 0; i < 5; i++) { // 전송 버퍼를 엽니다. ArraySegment<byte> s = SendBufferHelper.Open(4096); ushort size = 0; bool success = true; size += 2; success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + size, s.Count - size), packet.packetId); size += 2; success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + size, s.Count - size), packet.playerId); size += 8; success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), size); // 전송 버퍼를 닫고 실제 전송 데이터 크기를 반영합니다. ArraySegment<byte> sendBuff = SendBufferHelper.Close(size); if (success) Send(sendBuff); // 데이터를 전송합니다. } } // 연결이 해제되었을 때 실행되는 메소드 public override void OnDisconnected(EndPoint endPoint) { Console.WriteLine($"OnDisconnected : {endPoint}"); } // 데이터를 수신했을 때 실행되는 메소드 public override int OnRecv(ArraySegment<byte> buffer) { string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); Console.WriteLine($"[From Server] {recvData}"); return buffer.Count; } // 데이터를 성공적으로 전송했을 때 실행되는 메소드 public override void OnSend(int numOfBytes) { Console.WriteLine($"Transferred bytes: {numOfBytes}"); } }
서버에서 패킷 처리 구현하기
기존 PacketSession 클래스의 OnRecvPacket 메소드를 구현하여, 클라이언트로부터 전송된 패킷을 처리합니다. 예를 들어, 간단한 에코 서버를 구현할 수 있습니다.
// 패킷의 기본 구조를 정의하는 클래스 class Packet { public ushort size; // 패킷 크기 public ushort packetId; // 패킷 ID } // 플레이어 정보 요청을 위한 패킷 구조를 정의하는 클래스 class PlayerInfoReq : Packet { public long playerId; // 요청하는 플레이어의 ID } // 플레이어 정보 응답을 위한 패킷 구조를 정의하는 클래스 class PlayerInfoOk : Packet { public int hp; // 플레이어의 체력 public int attack; // 플레이어의 공격력 } // 사용할 패킷 ID를 열거형으로 정의 public enum PacketID { PlayerInfoReq = 1, PlayerInfoOk = 2, } // 클라이언트 세션을 관리하는 클래스 class ClientSession : PacketSession { // 연결이 성공적으로 이루어졌을 때 호출됨 public override void OnConnected(EndPoint endPoint) { Console.WriteLine($"OnConnected : {endPoint}"); Thread.Sleep(5000); // 5초간 대기 Disconnect(); // 연결 해제 } // 패킷을 받았을 때 호출됨 public override void OnRecvPacket(ArraySegment<byte> buffer) { int pos = 0; // 패킷 크기와 ID를 읽어옴 ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset); pos += 2; ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + pos); pos += 2; // 패킷 ID에 따라 적절한 처리를 함 switch ((PacketID)id) { case PacketID.PlayerInfoReq: { long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + pos); pos += 8; // 플레이어 정보 요청 처리 } break; case PacketID.PlayerInfoOk: { int hp = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos); pos += 4; int attack = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos); pos += 4; // 플레이어 정보 응답 처리 // 예: Handle_PlayerInfoOk(); } break; default: break; } Console.WriteLine($"RecvPacketId: {id}, Size {size}"); } // 패킷 처리 예시 함수 (현재는 사용되지 않음) public void Handle_PlayerInfoOk(ArraySegment<byte> buffer) { // 플레이어 정보 응답 패킷을 처리하는 로직 } // 연결이 해제되었을 때 호출됨 public override void OnDisconnected(EndPoint endPoint) { Console.WriteLine($"OnDisconnected : {endPoint}"); } // 데이터를 성공적으로 보냈을 때 호출됨 public override void OnSend(int numOfBytes) { Console.WriteLine($"Transferred bytes: {numOfBytes}"); } }
이 예제 코드는 간단한 패킷 기반 통신을 구현합니다. 클라이언트는 서버에 메시지를 보내고, 서버는 받은 메시지를 처리한 후 에코로 응답합니다. 실제 게임 서버 개발에서는 훨씬 복잡한 로직과 다양한 패킷 타입을 처리하는 코드가 필요할 것입니다. 하지만, 이 예제는 소켓 프로그래밍과 패킷 기반 통신의 기초적인 이해를 돕기 위한 것입니다.
결론
C#과 ASP.NET을 활용한 웹 개발에서 세션은 사용자별로 데이터를 안전하게 저장하고, 웹 사이트 방문 동안 사용자의 상태를 유지하는 데 매우 중요합니다. 세션을 적절히 활용하면, 사용자 경험을 크게 개선하고, 웹 애플리케이션의 기능을 풍부하게 만들 수 있습니다. 이 글에서 소개된 기본적인 세션 관리 방법과 예제 코드를 바탕으로, 보다 다양한 세션 기반 기능을 탐색하고 구현해 보시길 바랍니다.
반응형다음글이전글이전 글이 없습니다.댓글