TIL

TIL_2025-01-14

explosion149 2025. 1. 14.

원하는 게임을 만들기 위해선 Tcp 전송에 대해서 좀더 알아 볼 필요가 있었다.

using UnityEngine; // UnityEngine 네임스페이스 사용
using System; // 시스템 관련 기능 사용
using System.Net.Sockets; // 소켓 통신을 위한 네임스페이스
using System.Collections.Generic; // 리스트 사용을 위한 네임스페이스

// 게임 네트워크 클라이언트 클래스
public class GameNetworkClient : MonoBehaviour
{
    private TcpClient client; // TCP 클라이언트
    private NetworkStream stream; // 네트워크 스트림
    private byte[] receiveBuffer = new byte[1024]; // 수신 버퍼
    private List<byte> messageBuffer = new List<byte>(); // 메시지 버퍼

    private string serverIP = "127.0.0.1"; // 서버 IP 주소
    private int serverPort = 3000; // 서버 포트 번호

    // MonoBehaviour의 Start 메서드
    void Start()
    {
        Debug.Log("시작 시도중..."); // 시작 로그
        ConnectToServer(); // 서버 연결 시도
    }

    // MonoBehaviour의 OnDestroy 메서드
    void OnDestroy()
    {
        Debug.Log("OnDestroy 호출됨"); // 종료 로그
        DisconnectFromServer(); // 서버 연결 종료
    }

    // 서버에 연결하는 메서드
    private void ConnectToServer()
    {
        try
        {
            Debug.Log($"서버 연결 시도 중... IP: {serverIP}, Port: {serverPort}"); // 서버 연결 로그
            client = new TcpClient(); // TCP 클라이언트 인스턴스 생성
            client.Connect(serverIP, serverPort); // 서버에 연결

            Debug.Log("서버 연결 성공, 스트림 얻기 시도..."); // 연결 성공 로그
            stream = client.GetStream(); // 네트워크 스트림 가져오기

            Debug.Log("수신 시작 시도..."); // 수신 시작 로그
            BeginReceive(); // 비동기 수신 시작

            Debug.Log("서버에 연결됨"); // 연결 완료 로그

            // 테스트 메시지 전송
            Debug.Log("테스트 메시지 전송 시도..."); // 테스트 메시지 전송 로그
            SendChatMessage("안녕하세요!"); // 채팅 메시지 전송
            SendPosition(new Vector3(1.0f, 2.0f, 3.0f)); // 위치 정보 전송
        }
        catch (Exception e)
        {
            Debug.LogError($"서버 연결 실패: {e.Message}\n스택 트레이스: {e.StackTrace}"); // 연결 실패 로그
        }
    }

    // 비동기 수신 시작 메서드
    private void BeginReceive()
    {
        try
        {
            if (stream == null) // 스트림이 null인 경우
            {
                Debug.LogError("스트림이 null입니다!"); // 에러 로그
                return; // 메서드 종료
            }
            stream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, OnReceiveCallback, null); // 비동기 읽기 시작
            Debug.Log("비동기 읽기 시작됨"); // 비동기 읽기 시작 로그
        }
        catch (Exception e)
        {
            Debug.LogError($"수신 시작 실패: {e.Message}\n스택 트레이스: {e.StackTrace}"); // 수신 시작 실패 로그
        }
    }

    // 수신 콜백 메서드
    private void OnReceiveCallback(IAsyncResult ar)
    {
        try
        {
            Debug.Log("OnReceiveCallback 호출됨"); // 콜백 호출 로그
            if (stream == null) // 스트림이 null인 경우
            {
                Debug.LogError("콜백에서 스트림이 null입니다!"); // 에러 로그
                return; // 메서드 종료
            }

            int bytesRead = stream.EndRead(ar); // 읽은 바이트 수
            Debug.Log($"받은 바이트 수: {bytesRead}"); // 수신된 바이트 수 로그

            if (bytesRead <= 0) // 수신된 데이터가 없는 경우
            {
                Debug.LogWarning("받은 데이터가 없음, 연결 종료"); // 경고 로그
                DisconnectFromServer(); // 연결 종료
                return; // 메서드 종료
            }

            // 수신한 데이터를 메시지 버퍼에 추가
            messageBuffer.AddRange(new ArraySegment<byte>(receiveBuffer, 0, bytesRead)); // 메시지 버퍼에 추가
            ProcessMessages(); // 메시지 처리
            BeginReceive(); // 다음 수신 대기
        }
        catch (ObjectDisposedException e) // 스트림이 이미 닫힌 경우
        {
            Debug.LogWarning($"스트림이 이미 닫혔습니다: {e.Message}"); // 경고 로그
        }
        catch (Exception e)
        {
            Debug.LogError($"수신 콜백 에러: {e.Message}\n스택 트레이스: {e.StackTrace}"); // 에러 로그
            DisconnectFromServer(); // 연결 종료
        }
    }

    // 메시지를 처리하는 메서드
    private void ProcessMessages()
    {
        Debug.Log($"메시지 처리 시작. 버퍼 크기: {messageBuffer.Count}"); // 메시지 처리 시작 로그
        while (messageBuffer.Count >= 4) // 최소 헤더 크기 확인
        {
            Debug.Log("메시지 언패킹 시도..."); // 메시지 언패킹 시도 로그
            var (messageLength, messageType, payload) = MessageParser.UnpackMessage(messageBuffer.ToArray()); // 메시지 언패킹

            Debug.Log($"메시지 길이: {messageLength}, 타입: {messageType}"); // 메시지 길이와 타입 로그

            if (messageBuffer.Count >= messageLength + 4) // 전체 메시지를 수신한 경우
            {
                HandleMessage(messageType, payload); // 메시지 처리
                messageBuffer.RemoveRange(0, messageLength + 4); // 처리된 메시지를 버퍼에서 제거
            }
            else // 메시지가 완전하지 않은 경우
            {
                Debug.Log("메시지가 완전하지 않음, 더 기다림"); // 메시지가 불완전하다는 로그
                break; // 루프 종료
            }
        }
    }

    // 수신된 메시지를 처리하는 메서드
    private void HandleMessage(MessageParser.MessageType messageType, byte[] payload)
    {
        try
        {
            Debug.Log($"메시지 타입 {messageType} 처리 시도"); // 메시지 타입 처리 시도 로그
            switch (messageType)
            {
                case MessageParser.MessageType.Chat: // 채팅 메시지 타입 처리
                    var chatMessage = MessageParser.DeserializeChatMessage(payload); // 채팅 메시지 역직렬화
                    Debug.Log($"[채팅] {chatMessage.userName}: {chatMessage.content}"); // 채팅 메시지 로그
                    break;

                case MessageParser.MessageType.ServerResponse: // 서버 응답 메시지 타입 처리
                    string response = System.Text.Encoding.UTF8.GetString(payload); // 응답 메시지 역직렬화
                    Debug.Log($"[서버 응답] {response}"); // 서버 응답 로그
                    break;

                case MessageParser.MessageType.Position: // 위치 메시지 타입 처리
                    var position = MessageParser.DeserializePosition(payload); // 위치 정보 역직렬화
                    Debug.Log($"[위치] X: {position.x:F2}, Y: {position.y:F2}, Z: {position.z:F2}"); // 위치 정보 로그
                    break;

                default: // 알 수 없는 메시지 타입 처리
                    Debug.Log($"알 수 없는 메시지 타입: {messageType}"); // 알 수 없는 메시지 타입 로그
                    break;
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"메시지 처리 중 에러: {e.Message}\n스택 트레이스: {e.StackTrace}"); // 메시지 처리 중 에러 로그
        }
    }

    // 채팅 메시지를 전송하는 메서드
    public void SendChatMessage(string message)
    {
        try
        {
            Debug.Log($"채팅 메시지 전송 시도: {message}"); // 채팅 메시지 전송 시도 로그
            byte[] payload = MessageParser.SerializeChatMessage("Player", message); // 채팅 메시지 직렬화
            SendMessage(MessageParser.MessageType.Chat, payload); // 메시지 전송
        }
        catch (Exception e)
        {
            Debug.LogError($"채팅 전송 실패: {e.Message}\n스택 트레이스: {e.StackTrace}"); // 채팅 전송 실패 로그
        }
    }

    // 위치 정보를 전송하는 메서드
    public void SendPosition(Vector3 position)
    {
        try
        {
            Debug.Log($"위치 전송 시도: {position}"); // 위치 전송 시도 로그
            byte[] payload = MessageParser.SerializePosition(position); // 위치 정보 직렬화
            SendMessage(MessageParser.MessageType.Position, payload); // 메시지 전송
        }
        catch (Exception e)
        {
            Debug.LogError($"위치 전송 실패: {e.Message}\n스택 트레이스: {e.StackTrace}"); // 위치 전송 실패 로그
        }
    }

    // 메시지를 서버로 전송하는 메서드
    private void SendMessage(MessageParser.MessageType messageType, byte[] payload)
    {
        try
        {
            if (stream == null) // 스트림이 null인 경우
            {
                Debug.LogError("스트림이 null이라 메시지를 보낼 수 없습니다!"); // 에러 로그
                return; // 메서드 종료
            }

            byte[] message = MessageParser.PackMessage(messageType, payload); // 메시지 패킹
            stream.Write(message, 0, message.Length); // 메시지를 스트림에 전송
            Debug.Log($"메시지 타입 {messageType} 전송 완료, 크기: {message.Length}"); // 메시지 전송 완료 로그
        }
        catch (Exception e)
        {
            Debug.LogError($"메시지 전송 실패: {e.Message}\n스택 트레이스: {e.StackTrace}"); // 메시지 전송 실패 로그
        }
    }

    // 서버와의 연결을 종료하는 메서드
    private void DisconnectFromServer()
    {
        Debug.Log("서버 연결 종료 시도..."); // 연결 종료 시도 로그

        if (stream != null) // 스트림이 존재하는 경우
        {
            Debug.Log("스트림 닫기"); // 스트림 닫기 로그
            stream.Close(); // 스트림 종료
            stream = null; // 스트림 null로 설정
        }

        if (client != null) // 클라이언트가 존재하는 경우
        {
            Debug.Log("클라이언트 닫기"); // 클라이언트 닫기 로그
            client.Close(); // 클라이언트 종료
            client = null; // 클라이언트 null로 설정
        }

        Debug.Log("서버 연결 종료 완료"); // 연결 종료 완료 로그
    }

    // 애플리케이션 종료 시 호출되는 메서드
    void OnApplicationQuit()
    {
        Debug.Log("어플리케이션 종료"); // 애플리케이션 종료 로그
        DisconnectFromServer(); // 서버 연결 종료
    }
}


public class BinaryWriter
{
    private byte[] buffer; // 데이터를 저장할 버퍼
    private int position;   // 현재 쓰기 위치
    private int capacity;   // 버퍼의 총 크기

    // 초기 크기를 지정하여 생성자 호출
    public BinaryWriter(int initialSize = 1024)
    {
        buffer = new byte[initialSize]; // 초기 버퍼 생성
        position = 0; // 현재 쓰기 위치 초기화
        capacity = initialSize; // 버퍼 용량 설정
    }

    // 필요한 용량을 확보하는 메서드
    private void EnsureCapacity(int needed)
    {
        // 현재 위치와 필요한 용량이 버퍼 용량 이내인지 확인
        if (position + needed <= capacity) return;

        // 필요한 용량에 따라 새로운 버퍼 크기 결정
        capacity = System.Math.Max(capacity * 2, position + needed);
        byte[] newBuffer = new byte[capacity]; // 새로운 버퍼 생성
        System.Buffer.BlockCopy(buffer, 0, newBuffer, 0, position); // 기존 데이터를 새로운 버퍼로 복사
        buffer = newBuffer; // 버퍼 교체
    }

    // int 값을 버퍼에 쓰는 메서드
    public void WriteInt32(int value)
    {
        EnsureCapacity(4); // 4바이트를 쓰기 위해 용량 확보
        System.BitConverter.GetBytes(value).CopyTo(buffer, position); // 값을 바이트 배열로 변환하여 버퍼에 기록
        position += 4; // 현재 위치 업데이트
    }

    // float 값을 버퍼에 쓰는 메서드
    public void WriteFloat(float value)
    {
        EnsureCapacity(4); // 4바이트를 쓰기 위해 용량 확보
        System.BitConverter.GetBytes(value).CopyTo(buffer, position); // 값을 바이트 배열로 변환하여 버퍼에 기록
        position += 4; // 현재 위치 업데이트
    }

    // string 값을 버퍼에 쓰는 메서드 (길이 + 데이터)
    public void WriteString(string value)
    {
        if (string.IsNullOrEmpty(value)) value = ""; // null 또는 빈 문자열 처리
        byte[] strBytes = System.Text.Encoding.UTF8.GetBytes(value); // 문자열을 UTF-8 바이트 배열로 변환
        WriteInt32(strBytes.Length); // 문자열 길이 기록
        WriteBytes(strBytes); // 문자열 데이터 기록
    }

    // 바이트 배열을 버퍼에 쓰는 메서드
    public void WriteBytes(byte[] data)
    {
        if (data == null || data.Length == 0) return; // null 또는 빈 배열 처리
        EnsureCapacity(data.Length); // 배열 길이를 쓰기 위해 용량 확보
        System.Buffer.BlockCopy(data, 0, buffer, position, data.Length); // 바이트 배열을 버퍼에 기록
        position += data.Length; // 현재 위치 업데이트
    }

    // 최종 버퍼를 가져오는 메서드
    public byte[] GetBuffer()
    {
        byte[] result = new byte[position]; // 현재까지 기록된 데이터를 위한 배열 생성
        System.Buffer.BlockCopy(buffer, 0, result, 0, position); // 버퍼의 유효 데이터를 복사
        return result; // 최종 바이트 배열 반환
    }
}

public class BinaryReader
{
    private byte[] buffer; // 읽을 데이터가 있는 버퍼
    private int position;   // 현재 읽기 위치

    // 버퍼를 지정하여 생성자 호출
    public BinaryReader(byte[] data)
    {
        buffer = data; // 읽을 데이터 설정
        position = 0; // 현재 읽기 위치 초기화
    }

    // int 값을 읽는 메서드
    public int ReadInt32()
    {
        CheckCanRead(4); // 남은 데이터가 충분한지 확인
        int value = System.BitConverter.ToInt32(buffer, position); // 현재 위치에서 int 값을 읽음
        position += 4; // 현재 위치 업데이트
        return value; // 읽은 값 반환
    }

    // float 값을 읽는 메서드
    public float ReadFloat()
    {
        CheckCanRead(4); // 남은 데이터가 충분한지 확인
        float value = System.BitConverter.ToSingle(buffer, position); // 현재 위치에서 float 값을 읽음
        position += 4; // 현재 위치 업데이트
        return value; // 읽은 값 반환
    }

    // string 값을 읽는 메서드 (길이 + 데이터)
    public string ReadString()
    {
        int length = ReadInt32(); // 문자열 길이 읽기
        CheckCanRead(length); // 남은 데이터가 충분한지 확인
        string value = System.Text.Encoding.UTF8.GetString(buffer, position, length); // 문자열 데이터 읽기
        position += length; // 현재 위치 업데이트
        return value; // 읽은 문자열 반환
    }

    // 지정한 길이만큼 바이트 배열을 읽는 메서드
    public byte[] ReadBytes(int length)
    {
        if (length <= 0) return new byte[0]; // 0 또는 음수 길이 처리
        CheckCanRead(length); // 남은 데이터가 충분한지 확인
        byte[] result = new byte[length]; // 결과 배열 생성
        System.Buffer.BlockCopy(buffer, position, result, 0, length); // 버퍼에서 데이터를 복사
        position += length; // 현재 위치 업데이트
        return result; // 읽은 바이트 배열 반환
    }

    // 남은 데이터가 충분한지 확인하는 메서드
    private void CheckCanRead(int count)
    {
        if (position + count > buffer.Length) // 읽을 수 있는 데이터가 버퍼를 초과하는 경우
            throw new System.Exception($"버퍼 읽기 실패: 필요한 크기 {count}, 남은 크기 {buffer.Length - position}"); // 예외 발생
    }
}
using UnityEngine;

// 메시지 파서 클래스
public class MessageParser
{
    // 메시지 타입을 정의하는 열거형
    public enum MessageType
    {
        None = 0,            // 기본값
        Chat = 1,            // 채팅 메시지
        ServerResponse = 2,   // 서버 응답 메시지
        Position = 3         // 위치 정보 메시지
    }

    // 채팅 메시지 구조체 - 사용자 이름과 내용을 포함
    public struct ChatMessage
    {
        public string userName; // 채팅을 보낸 사용자 이름
        public string content;   // 채팅 내용
    }

    // 위치 정보 구조체 - 3D 위치 좌표
    public struct PositionMessage
    {
        public float x; // X 좌표
        public float y; // Y 좌표
        public float z; // Z 좌표
    }

    // 채팅 메시지를 직렬화하여 바이트 배열로 변환
    public static byte[] SerializeChatMessage(string userName, string content)
    {
        var writer = new BinaryWriter(); // BinaryWriter 인스턴스 생성
        writer.WriteString(userName);     // 사용자 이름을 기록
        writer.WriteString(content);       // 채팅 내용을 기록
        return writer.GetBuffer();         // 직렬화된 바이트 배열 반환
    }

    // 바이트 배열에서 채팅 메시지를 역직렬화하여 구조체로 변환
    public static ChatMessage DeserializeChatMessage(byte[] payload)
    {
        var reader = new BinaryReader(payload); // BinaryReader 인스턴스 생성
        return new ChatMessage
        {
            userName = reader.ReadString(), // 사용자 이름 읽기
            content = reader.ReadString()    // 채팅 내용 읽기
        };
    }

    // 위치 정보를 직렬화하여 바이트 배열로 변환
    public static byte[] SerializePosition(Vector3 position)
    {
        var writer = new BinaryWriter(); // BinaryWriter 인스턴스 생성
        writer.WriteFloat(position.x);    // X 좌표를 기록
        writer.WriteFloat(position.y);    // Y 좌표를 기록
        writer.WriteFloat(position.z);    // Z 좌표를 기록
        return writer.GetBuffer();         // 직렬화된 바이트 배열 반환
    }

    // 바이트 배열에서 위치 정보를 역직렬화하여 구조체로 변환
    public static PositionMessage DeserializePosition(byte[] payload)
    {
        var reader = new BinaryReader(payload); // BinaryReader 인스턴스 생성
        return new PositionMessage
        {
            x = reader.ReadFloat(), // X 좌표 읽기
            y = reader.ReadFloat(), // Y 좌표 읽기
            z = reader.ReadFloat()   // Z 좌표 읽기
        };
    }

    // 메시지를 패킹하여 전체 메시지 생성 (길이 + 타입 + 페이로드)
    public static byte[] PackMessage(MessageType type, byte[] payload)
    {
        var writer = new BinaryWriter(); // BinaryWriter 인스턴스 생성
        writer.WriteInt32(payload.Length + 4); // 전체 메시지 길이 (페이로드 + 타입)
        writer.WriteInt32((int)type);          // 메시지 타입 기록
        writer.WriteBytes(payload);            // 페이로드 기록
        return writer.GetBuffer();             // 최종 메시지 바이트 배열 반환
    }

    // 바이트 배열에서 메시지를 언패킹하여 길이, 타입, 페이로드 반환
    public static (int messageLength, MessageType messageType, byte[] payload) UnpackMessage(byte[] message)
    {
        var reader = new BinaryReader(message); // BinaryReader 인스턴스 생성
        int messageLength = reader.ReadInt32(); // 메시지 길이 읽기
        MessageType messageType = (MessageType)reader.ReadInt32(); // 메시지 타입 읽기
        byte[] payload = reader.ReadBytes(messageLength - 4); // 페이로드 읽기
        return (messageLength, messageType, payload); // 메시지 길이, 타입, 페이로드 반환
    }
}
// BinaryWriter 클래스: 데이터를 바이너리 형식으로 작성하는 데 사용
export class BinaryWriter {
    constructor(initialSize = 1024) {
        this.buffer = Buffer.alloc(initialSize); // 초기 버퍼 생성
        this.position = 0; // 현재 쓰기 위치 초기화
        this.capacity = initialSize; // 버퍼의 총 용량 설정
    }

    // 필요한 용량을 확보하는 메서드
    ensureCapacity(needed) {
        // 현재 위치와 필요한 용량이 버퍼 용량 이내인지 확인
        if (this.position + needed <= this.capacity) return;

        // 필요한 용량에 따라 새로운 버퍼 크기 결정
        this.capacity = Math.max(this.capacity * 2, this.position + needed);
        const newBuffer = Buffer.alloc(this.capacity); // 새로운 버퍼 생성
        this.buffer.copy(newBuffer, 0, 0, this.position); // 기존 데이터를 새로운 버퍼로 복사
        this.buffer = newBuffer; // 버퍼 교체
    }

    // int32 값을 버퍼에 쓰는 메서드
    writeInt32(value) {
        this.ensureCapacity(4); // 4바이트를 쓰기 위해 용량 확보
        this.buffer.writeInt32LE(value, this.position); // 리틀 엔디안 방식으로 값 기록
        this.position += 4; // 현재 위치 업데이트
    }

    // float 값을 버퍼에 쓰는 메서드
    writeFloat(value) {
        this.ensureCapacity(4); // 4바이트를 쓰기 위해 용량 확보
        this.buffer.writeFloatLE(value, this.position); // 리틀 엔디안 방식으로 값 기록
        this.position += 4; // 현재 위치 업데이트
    }

    // 문자열을 버퍼에 쓰는 메서드 (길이 + 데이터)
    writeString(value) {
        const str = value || ""; // null 처리
        const strBuffer = Buffer.from(str, 'utf8'); // 문자열을 UTF-8 바이트 배열로 변환
        this.writeInt32(strBuffer.length); // 문자열 길이 기록
        this.writeBytes(strBuffer); // 문자열 데이터 기록
    }

    // 바이트 배열을 버퍼에 쓰는 메서드
    writeBytes(data) {
        if (!data || data.length === 0) return; // null 또는 빈 배열 처리
        this.ensureCapacity(data.length); // 배열 길이를 쓰기 위해 용량 확보
        data.copy(this.buffer, this.position); // 바이트 배열을 버퍼에 기록
        this.position += data.length; // 현재 위치 업데이트
    }

    // 최종 버퍼를 가져오는 메서드
    getBuffer() {
        return Buffer.alloc(this.position).fill(this.buffer, 0, this.position); // 유효한 데이터로 새 버퍼 생성
    }
}

// BinaryReader 클래스: 바이너리 형식으로 저장된 데이터를 읽는 데 사용
export class BinaryReader {
    constructor(buffer) {
        this.buffer = buffer; // 읽을 데이터가 있는 버퍼
        this.position = 0; // 현재 읽기 위치 초기화
    }

    // 남은 데이터가 충분한지 확인하는 메서드
    checkCanRead(count) {
        if (this.position + count > this.buffer.length) {
            throw new Error(`버퍼 읽기 실패: 필요한 크기 ${count}, 남은 크기 ${this.buffer.length - this.position}`); // 예외 발생
        }
    }

    // int32 값을 읽는 메서드
    readInt32() {
        this.checkCanRead(4); // 남은 데이터가 충분한지 확인
        const value = this.buffer.readInt32LE(this.position); // 현재 위치에서 int 값을 읽음
        this.position += 4; // 현재 위치 업데이트
        return value; // 읽은 값 반환
    }

    // float 값을 읽는 메서드
    readFloat() {
        this.checkCanRead(4); // 남은 데이터가 충분한지 확인
        const value = this.buffer.readFloatLE(this.position); // 현재 위치에서 float 값을 읽음
        this.position += 4; // 현재 위치 업데이트
        return value; // 읽은 값 반환
    }

    // 문자열을 읽는 메서드 (길이 + 데이터)
    readString() {
        const length = this.readInt32(); // 문자열 길이 읽기
        this.checkCanRead(length); // 남은 데이터가 충분한지 확인
        const value = this.buffer.toString('utf8', this.position, this.position + length); // 문자열 데이터 읽기
        this.position += length; // 현재 위치 업데이트
        return value; // 읽은 문자열 반환
    }

    // 지정한 길이만큼 바이트 배열을 읽는 메서드
    readBytes(length) {
        if (length <= 0) return Buffer.alloc(0); // 0 또는 음수 길이 처리
        this.checkCanRead(length); // 남은 데이터가 충분한지 확인
        const result = Buffer.alloc(length); // 결과 배열 생성
        this.buffer.copy(result, 0, this.position, this.position + length); // 버퍼에서 데이터를 복사
        this.position += length; // 현재 위치 업데이트
        return result; // 읽은 바이트 배열 반환
    }
}
import { BinaryWriter, BinaryReader } from './binaryHelper.js'; // BinaryWriter와 BinaryReader 가져오기

// 메시지 타입을 정의하는 객체
export const MessageType = {
    NONE: 0,              // 기본값
    CHAT: 1,              // 채팅 메시지
    SERVER_RESPONSE: 2,   // 서버 응답 메시지
    POSITION: 3           // 위치 정보 메시지
};

// 메시지 파서 클래스
export class MessageParser {
    // 채팅 메시지를 직렬화하여 바이트 배열로 변환
    static serializeChatMessage(userName, content) {
        const writer = new BinaryWriter(); // BinaryWriter 인스턴스 생성
        writer.writeString(userName);      // 사용자 이름을 UTF-8 문자열로 기록
        writer.writeString(content);        // 채팅 내용을 UTF-8 문자열로 기록
        return writer.getBuffer();          // 직렬화된 바이트 배열 반환
    }

    // 바이트 배열에서 채팅 메시지를 역직렬화하여 객체로 변환
    static deserializeChatMessage(payload) {
        const reader = new BinaryReader(payload); // BinaryReader 인스턴스 생성
        return {
            userName: reader.readString(), // 사용자 이름 읽기
            content: reader.readString()    // 채팅 내용 읽기
        };
    }

    // 위치 정보를 직렬화하여 바이트 배열로 변환
    static serializePosition(x, y, z) {
        const writer = new BinaryWriter(); // BinaryWriter 인스턴스 생성
        writer.writeFloat(x);               // X 좌표를 기록
        writer.writeFloat(y);               // Y 좌표를 기록
        writer.writeFloat(z);               // Z 좌표를 기록
        return writer.getBuffer();          // 직렬화된 바이트 배열 반환
    }

    // 바이트 배열에서 위치 정보를 역직렬화하여 객체로 변환
    static deserializePosition(payload) {
        const reader = new BinaryReader(payload); // BinaryReader 인스턴스 생성
        return {
            x: reader.readFloat(), // X 좌표 읽기
            y: reader.readFloat(), // Y 좌표 읽기
            z: reader.readFloat()   // Z 좌표 읽기
        };
    }

    // 메시지를 패킹하여 전체 메시지 생성 (길이 + 타입 + 페이로드)
    static packMessage(type, payload) {
        const writer = new BinaryWriter(); // BinaryWriter 인스턴스 생성
        writer.writeInt32(payload.length + 4);  // 전체 메시지 길이 (페이로드 + 타입)
        writer.writeInt32(type);                // 메시지 타입 기록
        writer.writeBytes(payload);             // 페이로드 기록
        return writer.getBuffer();              // 최종 메시지 바이트 배열 반환
    }

    // 바이트 배열에서 메시지를 언패킹하여 길이, 타입, 페이로드 반환
    static unpackMessage(message) {
        const reader = new BinaryReader(message); // BinaryReader 인스턴스 생성
        const messageLength = reader.readInt32(); // 메시지 길이 읽기
        const messageType = reader.readInt32();   // 메시지 타입 읽기
        const payload = reader.readBytes(messageLength - 4); // 페이로드 읽기
        return { messageLength, messageType, payload }; // 메시지 길이, 타입, 페이로드 반환
    }
}
// server.js
import net from 'net'; // net 모듈 가져오기
import { MessageParser, MessageType } from './messageParser.js'; // 메시지 파서와 타입 가져오기

// 연결된 클라이언트들을 저장할 Set
const connectedClients = new Set();

// TCP 서버 생성
const server = net.createServer((socket) => {
    console.log('클라이언트가 연결됨'); // 클라이언트 연결 로그

    // 클라이언트 추가
    connectedClients.add(socket);

    // 버퍼를 저장할 변수
    let buffer = Buffer.alloc(0); // 초기 빈 버퍼 생성

    // 연결된 클라이언트에게 환영 메시지 전송
    sendChatMessage(socket, "Server", "환영합니다! 서버에 연결되었습니다.");

    // 테스트용 메시지 전송
    setInterval(() => {
        // 랜덤 위치 생성
        const x = Math.random() * 100;
        const y = Math.random() * 100;
        const z = Math.random() * 100;
        sendPosition(socket, x, y, z);
    }, 5000);

    // 데이터 수신 이벤트
    socket.on('data', (data) => {
        try {
            // 받은 데이터를 버퍼에 추가
            buffer = Buffer.concat([buffer, data]);

            // 메시지 처리
            while (buffer.length >= 8) { // 헤더의 최소 크기 (길이 4 + 타입 4)
                // 메시지 길이 확인
                const messageLength = buffer.readInt32LE(0);

                // 메시지 길이가 비정상적인 경우
                if (messageLength <= 0 || messageLength > 1024 * 1024) {
                    console.error('Invalid message length:', messageLength);
                    buffer = Buffer.alloc(0); // 버퍼 초기화
                    break;
                }

                // 전체 메시지를 다 받았는지 확인
                if (buffer.length < messageLength + 4) {
                    break; // 전체 메시지를 수신할 때까지 대기
                }

                try {
                    // 메시지 파싱 시도
                    const { messageType, payload } = MessageParser.unpackMessage(buffer);

                    // 메시지 타입에 따른 처리
                    handleMessage(messageType, payload, socket);
                } catch (parseError) {
                    console.error('메시지 파싱 에러:', parseError); // 파싱 에러 로그
                }

                // 처리한 메시지를 버퍼에서 제거
                buffer = buffer.subarray(messageLength + 4); // 처리된 메시지 길이만큼 버퍼 업데이트
            }
        } catch (error) {
            console.error('데이터 처리 중 에러:', error); // 데이터 처리 중 에러 로그
            buffer = Buffer.alloc(0); // 버퍼 초기화
        }
    });

    // 연결 종료 이벤트
    socket.on('end', () => {
        connectedClients.delete(socket); // Set에서 클라이언트 제거
        console.log('클라이언트 연결 종료'); // 클라이언트 연결 종료 로그
    });

    // 에러 이벤트
    socket.on('error', (err) => {
        connectedClients.delete(socket); // Set에서 클라이언트 제거
        console.error('소켓 에러:', err); // 소켓 에러 로그
    });
});

// 채팅 메시지 전송 함수
function sendChatMessage(socket, userName, content) {
    try {
        const payload = MessageParser.serializeChatMessage(userName, content);
        const packedMessage = MessageParser.packMessage(MessageType.CHAT, payload);
        socket.write(packedMessage);
        console.log(`채팅 메시지 전송: ${userName}: ${content}`);
    } catch (error) {
        console.error('채팅 메시지 전송 중 에러:', error);
    }
}

// 위치 정보 전송 함수
function sendPosition(socket, x, y, z) {
    try {
        const payload = MessageParser.serializePosition(x, y, z);
        const packedMessage = MessageParser.packMessage(MessageType.POSITION, payload);
        socket.write(packedMessage);
        console.log(`위치 정보 전송: (${x}, ${y}, ${z})`);
    } catch (error) {
        console.error('위치 정보 전송 중 에러:', error);
    }
}

// 모든 클라이언트에게 브로드캐스트
function broadcast(messageType, payload) {
    for (const client of connectedClients) {
        try {
            const packedMessage = MessageParser.packMessage(messageType, payload);
            client.write(packedMessage);
        } catch (error) {
            console.error('브로드캐스트 중 에러:', error);
        }
    }
}

// 메시지 처리 함수
function handleMessage(messageType, payload, socket) {
    try {
        switch (messageType) {
            case MessageType.CHAT: { // 채팅 메시지 타입 처리
                const chatMessage = MessageParser.deserializeChatMessage(payload); // 메시지 역직렬화
                console.log('받은 메시지:', chatMessage); // 받은 메시지 로그

                // 모든 클라이언트에게 채팅 메시지 브로드캐스트
                broadcast(MessageType.CHAT, payload);
                break;
            }
            case MessageType.POSITION: { // 위치 메시지 타입 처리
                const position = MessageParser.deserializePosition(payload); // 위치 데이터 역직렬화
                console.log('받은 위치:', position); // 받은 위치 로그

                // 다른 클라이언트들에게 위치 정보 전달
                for (const client of connectedClients) {
                    if (client !== socket) {
                        sendPosition(client, position.x, position.y, position.z);
                    }
                }
                break;
            }
            default:
                console.log('알 수 없는 메시지 타입:', messageType); // 알 수 없는 메시지 타입 로그
                break;
        }
    } catch (error) {
        console.error('메시지 처리 중 에러:', error); // 메시지 처리 중 에러 로그
    }
}

// 서버 응답 전송
function sendServerResponse(socket, message) {
    try {
        const payload = Buffer.from(message, 'utf8'); // 메시지를 UTF-8로 변환
        const packedMessage = MessageParser.packMessage(MessageType.SERVER_RESPONSE, payload); // 메시지 패킹
        socket.write(packedMessage); // 클라이언트에게 응답 전송
    } catch (error) {
        console.error('응답 전송 중 에러:', error); // 응답 전송 중 에러 로그
    }
}

// 서버 시작
const PORT = 3000; // 서버 포트 설정
server.listen(PORT, () => {
    console.log(`서버가 포트 ${PORT}에서 실행 중입니다.`); // 서버 실행 로그
});

'TIL' 카테고리의 다른 글

TIL_2025-01-16  (0) 2025.01.17
TIL_2025-01-15  (0) 2025.01.16
TIL_2025-01-10  (0) 2025.01.10
TIL_2025-01-09  (0) 2025.01.10
TIL_2025-01-08  (0) 2025.01.08

댓글

💲 추천 글