서버 공부

[서버 공부] 미니 프로젝트: 채팅 방 만들기 2편

myjeongjun 2025. 2. 2. 23:02

이번 시간에는 실제 패킷을 보내고 Parsing하여 출력까지 해보겠습니다.

 

우선 보내줘야할 내용을 담을 SendBuffer를 설계해야한다.

SendBuffer는 ReceiveBuffer와는 다르게 만약 게임에서 패킷을 주고받을때 100명의 유저가 제각각움직이면 100 * 100 = 1만개의 패킷이 이동하기때문에 세션 내부에서 버퍼를 매번 복사하면서 만들어 주는것보단 스레드에다가 버퍼를 할당해줘서 담아오고,예약하기를 처리하는것이 부하가 덜 걸린다고한다.

(제대로 이해한건진 잘 모르겠지만 모든 세션에 개인 sendBuffer를 둬서 자기꺼만 담는것보다 일꾼(스레드)에게 sendBuffer를 쥐어줘서 여기저기 세션에서 보내는 값을 담아서 처리하는게 더 효율적이라는 느낌?)

 

그러므로 SendBuffer를 다룰때는 ThreaLocal을 사용해서 스레드마다 고유한 SendBuffer을 쓰도록 했다.

아래의 SendBuffer 클래스는 각 스레드에게 65535 * 100크기의 SendBuffer를 할당해주는 작업과

예약된 크기만큼 배열을 떼어주는 함수가 포함되어있다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    public class SendBufferHelper 
    {
        
        public static ThreadLocal<SendBuffer> currentBuffer = new ThreadLocal<SendBuffer>(() => { return null;  });

        public static int chunkSize { get; set; } = 65535 * 100;

        public static ArraySegment<byte> Open(int reservSize) 
        {
            if(currentBuffer.Value == null || currentBuffer.Value.freeSize< reservSize) 
            {
                currentBuffer.Value = new SendBuffer(chunkSize);
            }

            return currentBuffer.Value.Open(reservSize);

        }


        public static ArraySegment<byte> Close(int usedSize) //확정
        {
            return currentBuffer.Value.Close(usedSize);
        }


    }


    public class SendBuffer
    {
        byte[] _buffer;
        int _usedSize = 0;


        public int freeSize { get {return _buffer.Length - _usedSize; } }

        public SendBuffer(int chunkSize) 
        {
            _buffer = new byte[chunkSize];
        }

        public ArraySegment<byte> Open(int reserveSize) 
        {
            if (reserveSize > freeSize)
                return null;

            return new ArraySegment<byte>(_buffer,_usedSize, reserveSize);  


        }

        public ArraySegment<byte> Close(int usedSize) //확정
        {
            ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
            _usedSize += usedSize;

            return segment;
        }

    }
}

 

 

이제 할당해준 버퍼에 실제로 값을 적어넣어야한다. 게임에 적용시키려면 수많은 패킷을 정의 해놓아야하지만 이건 채팅만 적는 프로그램이니 서버에서 문자열을 보내는 패킷만 정의해준다.

 public class Server_Packet
 {
     public int playerid;
     public string message;
     

     public ArraySegment<byte> Write() 
     {
         ArraySegment<byte> segment = SendBufferHelper.Open(4096); //버퍼 할당

         //segment에 써서 보낼수있는지 확인작업
         ushort count = 0;
         bool success = true;

         //Span은 배열에 대한 참조 뷰를 제공하는 타입 원본 수정도 가능
         Span<byte> s = new Span<byte>(segment.Array,segment.Offset,segment.Count);

         //패킷 사이즈는 ushort범위내에서 표현 미리 크기만 할당해 놓고 마지막에 계산해서 놓음
         count += sizeof(ushort);

         //패킷 종류도 ushort범위내에서 표현
         success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.S_Chat);
         count += sizeof(ushort);

         //누구한테 보낼지, 클라는 여기서 자기한테 해당안되면 걸러지는듯?
         success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerid);
         count += sizeof(int);


         //실제 채팅을 바이트 배열로 변환 
         ushort messageToByte = (ushort)Encoding.Unicode.GetBytes(this.message, 0, this.message.Length, segment.Array, segment.Offset + count + sizeof(ushort));
         
         //채팅을 바이트로 바꾼걸 다시 길이가 몇인지로 바꿔서 s에집어넣음
         success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), messageToByte);

         count += sizeof(ushort);
         count += messageToByte;

         //마지막 패킷크기
         success &= BitConverter.TryWriteBytes(s, count);

         if (!success)
             return null;


         return SendBufferHelper.Close(count);
     }



 }

 

처음 클라이언트와 서버에 연결되었을때 결국엔 클라이언트들도 MainLobby의 세션리스트에 들어가있을것이고 특정 채팅방에 들어가는 요청이 들어오면 MainLobby의 리스트에서 빼고 채팅방의 세션리스트로 넣어주는 동작을 취할것이다.

여기서 처음 연결되었을대 환영 메시지 패킷을 보내보도록 하자. 

MainLobby.cs에 환영 메시지 패킷을 만들고 보내주는 함수를 만들어준다.

       public void Welcome() 
        {
            foreach (ClientSession session in _sessions) 
            {
                Server_Packet packet = new Server_Packet();
                packet.playerid = session.sessionID;
                packet.message = "메인로비에 입장했습니다!";

                ArraySegment<byte> segment = packet.Write();

                session.Send(segment);  
            }
        }

session.Send를통해 sendqueue에 순차적으로 집어넣고 sendasync를 실행시킬것이다.

 

클라이언트 쪽에선 RecvRegister를 해놓은 상태이니 해당 패킷을 recvasync를 통해서 받아올것이다.

 

클라쪽에서 받은 패킷을  recvBuffer로 받아준후 Parsing을 위해 PacketManager로 넘겨주면

        public override void OnRecvPacket(ArraySegment<byte> buffer)
        {
            Console.WriteLine($"{buffer.Count}만큼 수신!");

            PacketManager.Instance.OnRecvPacket(this, buffer);
        }

 

 

BitConverter함수를 이용해서 해당 내용을 얻을수있다.

 internal class Packet
 {
     public int playerID;
     public string message;

     public void Read(ArraySegment<byte> segment) 
     {
         ushort count = 0;

         ReadOnlySpan<byte> s = new ReadOnlySpan<byte>(segment.Array,segment.Offset,segment.Count);

         ushort size = BitConverter.ToUInt16(s.Slice(count, s.Length));
         count += sizeof(ushort); ;
         Console.WriteLine($"패킷 사이즈 : {size}");
         ushort ID = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
         count += sizeof(ushort); ;
         Console.WriteLine($"패킷 아이디 : {ID}");

         this.playerID = BitConverter.ToInt32(s.Slice(count, s.Length - count));
         count += sizeof(int);
         Console.WriteLine($"플레이어 아이디 : {this.playerID}");

         ushort chatLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
         count += sizeof(ushort);
         Console.WriteLine($"채팅 길이 : {chatLen}");
         this.message = Encoding.Unicode.GetString(s.Slice(count, chatLen));
         count += chatLen;
         Console.WriteLine($"채팅 내용 : {this.message}");



     }


 }

 

 

 

 

왼쪽 클라1, 오른족 클라 2

 

서버 쪽

 

서버 연결과 패킷을 받은 시점이 거의 동시에 이루어져서 출력 순서가 뒤바뀌는 현상이 존재한다.

 

항상 명확하게 출력순서를 지키기위해 server의 일부 내용을 servercore로 옮기거나 반대로하면 수정이 가능할거같은데, 간단한 프로젝트이니 너무 목매이지 않기로 했다.