프로토콜 구상
TRex 게임은 구조적으로 멀티는 불가능한 게임이므로 네트워크 통신으로 구현할 수 있는 것은 클라이언트 게임 종료시의 점수(Socre)를 서버에 저장하고, 이를 클라이언트가 원할 때 전송받아 출력을 하는 정도라고 생각함
따라서 헤더와 바디를 아래처럼 구상(※ 이것이 C#이다 - Chapter21. 네트워크 프로그래밍 - p.790 ~ p.817 참조)하여 프로토콜을 만들어 보자
1. 헤더(12, 고정크기)
> MSGID(4) : 메시지 식별 번호
> MSGTYPE(4) : REQ(점수 등록 요청), REP(요청에 대한 회신), SCORE_REQ(점수 결과 요청), SCORE_RES(점수 결과 회신)
> BODYLEN(4) : 메시지 본문 길이
2. 바디
> REQ인 경우 : SCORE(4)
> REP인 경우 : MSGID(4), RESPONE(1)
> SCORE_REQ인 경우 : WANT_SCORE(1)
> SCORE_REP인 경우 : MSGID(4), SCORE(4), RESULT(1)
공통 라이브러리 만들기
SCORE_P.sln
└ SCORE_P
└ Message.cs
└ Header.cs
└ Body.cs
└ MessageUtil.cs
<Message.cs>
namespace SCORE_P
{
public class CONSTANTS
{
// MSGTYPE
public const uint REQ = 1;
public const uint REP = 2;
public const uint SCORE_REQ = 3;
public const uint SCORE_RES = 4;
}
// 인터페이스 IMessage 정의
public interface IMessage
{
byte[] GetBytes();
int GetSize();
}
// Message 클래스 정의
// Message 클래스는 Header와 Body를 더한 byte 배열 또는 이것의 크기를 반환함
public class Message : IMessage
{
// Header와 Body 객체 생성
public Header Header { get; set; }
// Body는 여러 종류가 있으므로 IMessage 인터페이스로 선언됨
public IMessage Body { get; set; }
public byte[] GetBytes()
{
byte[] bytes = new byte[GetSize()];
// Header와 Body에도 IMessage 인터페이스가 구현되어 있음
// 따라서 각 목적에 맞는 구현에 따라 GetBytes() 호출을 할 수 있음
Header.GetBytes().CopyTo(bytes, 0);
Body.GetBytes().CopyTo(bytes, Header.GetSize());
// 결과적으로 Header와 Body의 bytes를 합친 총 bytes를 반환하는 메서드가 되는 것
return bytes;
}
public int GetSize()
{
// 위와 같이 각 목적에 맞는 구현에 따라 GetSize() 호출을 할 수 있음
return Header.GetSize() + Body.GetSize();
}
}
}
<Header.cs>
using System;
namespace SCORE_P
{
public class Header : IMessage
{
// 헤더를 구성하는 필드
public uint MSGID { get; set; } // 4byte
public uint MSGTYPE { get; set; } // 4byte
public uint BODYLEN { get; set; } // 4byte
// 기본 생성자
// 바이트 배열을 받아 필드를 초기화하는 생성자
public Header() { }
public Header(byte[] bytes)
{
MSGID = BitConverter.ToUInt32(bytes, 0);
MSGTYPE = BitConverter.ToUInt32(bytes, 4);
BODYLEN = BitConverter.ToUInt32(bytes, 8);
}
// 인터페이스 구현(IMessage)
// 초기화된 필드를 바이트 배열로 변환하여 반환
public byte[] GetBytes()
{
byte[] bytes = new byte[12];
byte[] temp = BitConverter.GetBytes(MSGID);
Array.Copy(temp, 0, bytes, 0, temp.Length);
temp = BitConverter.GetBytes(MSGTYPE);
Array.Copy(temp, 0, bytes, 4, temp.Length);
temp = BitConverter.GetBytes(BODYLEN);
Array.Copy(temp, 0, bytes, 8, temp.Length);
return bytes;
}
// 초기화된 필드의 총 크기를 반환(고정크기)
public int GetSize()
{
return 12;
}
}
}
<Body.cs>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SCORE_P
{
// Body 클래스 정의
// Body는 MSGTYPE에 따라 MessageUtil.cs에서 분기가 됨
// 총 4부분(REQ, REP, SCORE_REQ, SCORE_RES)로 나뉨
// 1. 요청(REQ)
public class BodyRequest : IMessage
{
public int SCORE; // 4byte
// 기본 생성자
// 바이트 배열을 받아 필드를 초기화하는 생성자
public BodyRequest() { }
public BodyRequest(byte[] bytes)
{
SCORE = BitConverter.ToInt32(bytes, 0);
}
// 인터페이스 구현(IMessage)
// 초기화된 필드를 바이트 배열로 변환하여 반환
public byte[] GetBytes()
{
byte[] bytes = new byte[GetSize()];
byte[] temp = BitConverter.GetBytes(SCORE);
Array.Copy(temp, 0, bytes, 0, temp.Length);
return bytes;
}
// 초기화된 필드의 크기를 반환
public int GetSize()
{
return sizeof(int);
}
}
// 2. 응답(REP)
public class BodyResponse : IMessage
{
public uint MSGID; // 4byte
public byte RESPONSE; // 1byte
// 기본 생성자
// 바이트 배열을 받아 필드를 초기화하는 생성자
public BodyResponse() { }
public BodyResponse(byte[] bytes)
{
MSGID = BitConverter.ToUInt32(bytes, 0);
RESPONSE = bytes[4];
}
// 인터페이스 구현(IMessage)
// 초기화된 필드를 바이트 배열로 변환하여 반환
public byte[] GetBytes()
{
byte[] bytes = new byte[GetSize()];
byte[] temp = BitConverter.GetBytes(MSGID);
Array.Copy(temp, 0, bytes, 0, temp.Length);
bytes[temp.Length] = RESPONSE;
return bytes;
}
// 초기화된 필드의 크기를 반환
public int GetSize()
{
return sizeof(uint) + sizeof(byte);
}
}
// 3. 점수 요청(SCORE_REQ)
public class BodyScoreRequest : IMessage
{
public byte WANT_SCORE; // 1byte
// 기본 생성자
// 바이트 배열을 받아 필드를 초기화하는 생성자
public BodyScoreRequest() { }
public BodyScoreRequest(byte[] bytes)
{
WANT_SCORE = bytes[0];
}
// 인터페이스 구현(IMessage)
// 초기화된 필드를 바이트 배열로 변환하여 반환
public byte[] GetBytes()
{
byte[] bytes = new byte[GetSize()];
bytes[0] = WANT_SCORE;
return bytes;
}
// 초기화된 필드의 크기를 반환
public int GetSize()
{
return sizeof(byte);
}
}
// 4. 점수 결과(SCORE_RES)
public class BodyScoreResult : IMessage
{
public uint MSGID; // 4byte
public uint SCORE; // 4byte
public byte RESULT; // 1byte
// 기본 생성자
// 바이트 배열을 받아 필드를 초기화하는 생성자
public BodyScoreResult() { }
public BodyScoreResult(byte[] bytes)
{
MSGID = BitConverter.ToUInt32(bytes, 0);
SCORE = BitConverter.ToUInt32(bytes, 4);
RESULT = bytes[8];
}
// 인터페이스 구현(IMessage)
// 초기화된 필드를 바이트 배열로 변환하여 반환
public byte[] GetBytes()
{
byte[] bytes = new byte[GetSize()];
byte[] temp = BitConverter.GetBytes(MSGID);
Array.Copy(temp, 0, bytes, 0, temp.Length);
temp = BitConverter.GetBytes(SCORE);
Array.Copy(temp, 0, bytes, 4, temp.Length);
bytes[8] = RESULT;
return bytes;
}
// 초기화된 필드의 크기를 반환
public int GetSize()
{
return sizeof(uint) + sizeof(uint) + sizeof(byte);
}
}
}
<MessageUtil.cs>
using System;
using System.IO;
namespace SCORE_P
{
public class MessageUtil
{
// 송수신(Send/Read) 메서드 구현
// Send
// Stream에 Message를 바이트 배열로 변환하여 전송
public static void Send(Stream writer, Message msg)
{
writer.Write(msg.GetBytes(), 0, msg.GetSize());
}
// Read
// Stream에서 바이트 배열을 읽어 Message로 변환하여 반환
public static Message Read(Stream reader)
{
// Header 전용 헬퍼 필드
int totalRead = 0; // 누적 읽은 바이트 수
int sizeToRead = 12; // 읽어야 할 바이트 수 (Header 고정 크기)
byte[] hBuffer = new byte[sizeToRead]; // Header용 버퍼
// Header
while (sizeToRead > 0)
{
byte[] buffer = new byte[sizeToRead];
int recv = reader.Read(buffer, 0, sizeToRead);
if(recv == 0) return null;
buffer.CopyTo(hBuffer, totalRead);
totalRead += recv;
sizeToRead -= recv;
}
Header header = new Header(hBuffer); // 바이트 배열 내용을 가진 버퍼로 Header 객체 생성
// Body 전용 헬퍼 필드
totalRead = 0; // 누적 읽은 바이트 수 초기화
sizeToRead = (int)header.BODYLEN; // 읽어야 할 바이트 수 (Header에 정의된 BODYLEN)
byte[] bBuffer = new byte[header.BODYLEN]; // Body용 버퍼
// Body
while (sizeToRead > 0)
{
byte[] buffer = new byte[sizeToRead];
int recv = reader.Read(buffer, 0, sizeToRead);
if(recv == 0) return null;
buffer.CopyTo(bBuffer, totalRead);
totalRead += recv;
sizeToRead -= recv;
}
// Body는 여러 종류가 있으므로 IMessage 인터페이스로 선언됨
IMessage body = null;
switch (header.MSGTYPE) // MSGTYPE에 따라 분기하여 Body 객체 생성
{
case CONSTANTS.REQ:
body = new BodyRequest(bBuffer);
break;
case CONSTANTS.REP:
body = new BodyResponse(bBuffer);
break;
case CONSTANTS.SCORE_REQ:
body = new BodyScoreRequest(bBuffer);
break;
case CONSTANTS.SCORE_RES:
body = new BodyScoreResult(bBuffer);
break;
default:
throw new Exception(String.Format("Unknown MSGTYPE : {0}", header.MSGTYPE));
}
// Header와 Body를 가진 Message 객체 생성 및 반환
return new Message() { Header = header, Body = body };
}
}
}
REQ의 SCORE -> REQ_SCORE,
REP의 RESPONSE -> REP_SCORE,
SCORE_REP의 SCORE -> RES_SCORE
로 명확하게 표시하는 게 다음부터는 더 좋을 것 같다
변수명을 귀찮아서 자꾸 대충 짓는데 다른 곳 한번 둘러보고 오면 이 변수가 뭘하는 역할이지? 하고 있다
처음부터 잘 지어야 한다
'LMS 7 > 개발일지' 카테고리의 다른 글
| 25.10.14 개발일지 / C# 명코파크 (2일차) (0) | 2025.11.13 |
|---|---|
| 25.10.13 개발일지 / C# 명코파크 (1일차) (0) | 2025.11.13 |
| 25.10.08 개발일지 / C# T-Rex Endless Runner - 3 / 주요 메서드 정리 및 영상 (0) | 2025.11.13 |
| 25.10.07 개발일지 / C# T-Rex Endless Runner - 2 / 이벤트, 필드, 메서드 등 멤버 정의 (0) | 2025.11.13 |
| 25.10.06 개발일지 / C# T-Rex Endless Runner - 1 / 프로젝트 생성 및 디자인 배치 (0) | 2025.11.11 |