Unity ToyProject/EchoServer

[C#] EchoServer

김조성준 2025. 5. 14. 15:59

에코 서버 구성 & 흐름


 

에코 서버란? 클라이언트가 전송해 주는 데이터를 그대로 되돌려 전송해 주는 기능의 서버를 뜻한다.

다음은 서버와 클라이언트의 통신하는 순서를 나타낸 것이다.

 

서버

  • 접점을 구한다. (접점 = Ip 주소 + Port 번호) 
  • 리스너를(소켓) 생성하고 리스너에 접점을 바인드한다. (+ 최대 Listen 수도 설정)
  • Listen을 시작한 이후, 접속한 클라이언트를 Accept하고 Session을 만들어서 대응시킨다. (세션 = 연결 정보)
  • 클라이언트에게 보낼 정보를 Send하거나 받은 정보를 Receive하여 처리한다.

클라이언트

  • 클라이언트 소켓을 생성한다.
  • 접속할 접점 정보를 구하고 Connect를 시도한다.
  • 서버의 리스너에 의해 Accept되면 Send와 Receive를 하며 통신한다.
  • 서버와의 통신이 끝나면 ShutDown와 Close를 통해 소켓을 반드시 닫아준다.

 

 

에코 서버 기능 구현


 

다음은 메인 프로세스에서 동작하는 에코 서버의 동작 코드이다.

접접을 구한 후, 이 접점 정보를 리스너에 전달하여 소켓을 생성하고 바인드한다.

서버는 지속적으로 접속을 받아줘야 하므로 종료되지 않고 While문으로 계속 동작시킨다. 

 

 

 class ServerProgram
 {
      static void Main(string[] args)
      {
          string hostName = Dns.GetHostName();
          IPHostEntry entry = Dns.GetHostEntry(hostName);
          IPAddress address = entry.AddressList[0];
          IPEndPoint endPoint = new IPEndPoint(address, ServerOption.Port);   

          Listener listener = new Listener();
          listener.Init(endPoint);

          while (true)
          {
              ;
          }
      }
}

 

 

 

다음은 리스너 클래스로 앞서 말한대로 접점 정보를 받아, 클라이언트의 접속을 받는다.

  public class Listener
  {
      Socket listenerSocket;
      SocketAsyncEventArgs acceptArgs;

      public void Init(IPEndPoint _endPoint)
      {
          acceptArgs = new SocketAsyncEventArgs();

          listenerSocket = new Socket(_endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
          listenerSocket.Bind(_endPoint);
          listenerSocket.Listen(ServerOption.MaxPlayerCount);

          listenerSocket.Accept();
      }
}

 

 

다음은 에코 서버와 연결하여 통신할 더미 클라이언트로, 에코 서버와 구조는 거의 흡사하다.

    internal class DummyClient
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry entry = Dns.GetHostEntry(host);
            IPAddress address = entry.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(address, ServerInformation.PortNumber);
            
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            socket.Connect(endPoint);

            byte[] recvBuffer = new byte[1024];
            byte[] sendBuffer = Encoding.UTF8.GetBytes("[From Clinet / To Server] : Connect");
            socket.Send(sendBuffer);

            socket.Receive(recvBuffer);
            Console.WriteLine(Encoding.UTF8.GetString(recvBuffer, 0 , 1024));

            socket.Shutdown(SocketShutdown.Both);   
            socket.Close();
        }
    }

 

 

 

 

블로킹 계열


 

앞서 구현한 에코 서버에선 Connect, Accept, Send, Receive와 같은 블로킹 계열 함수를 사용했다.

블로킹 계열 함수의 단점으로는 해당 기능이 수행되기 전까지는 다른 기능을 수행할 수 없다는 치명적인 문제점이 존재한다.

C#의 BCL에서는 블로킹 계열 함수를 비동기로 사용할 수 있는 기능을 제공한다. 뒤에 Async를 붙이면 된다. 

 

논블로킹 계열로 변경했을 때, 해당 기능이 수행되는 시점을 알 수 없는 문제가 생기는데 SocketAsyncEventArgs를 사용하면 

이를 해결할 수 있다.

 

SocketAsyncEventArgs는 해당 기능이 수행되는 시점에 호출되도록 콜백 메서드를 등록할 수 있고, 연결된 소켓의 정보, 접점, 받는 버퍼 .. 등 다양한 정보를 알 수 있게 해준다.

 

다음은 리스너 소켓의 초기 설정 메서드로, Accept를 AcceptAsync로 변경한 것이다.

이처럼 논블로킹 계열 함수SocketAsyncEventArgs를 활용하여, 블로킹 계열의 함수가 주는 문제점을 개선할 수 있다. 

SocketAsyncEventArgs acceptArgs;

public void Init(IPEndPoint _endPoint)
{
	acceptArgs = new SocketAsyncEventArgs();
    listenerSocket = new Socket(_endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
    listenerSocket.Bind(_endPoint);
    listenerSocket.Listen(ServerOption.MaxPlayerCount);

    acceptArgs.Completed += new EventHandler<SocketAsyncEventArgs>(AfterAccept);
    OnAccept();
}

 void OnAccept()
 {
 	acceptArgs.AcceptSocket = null;
    bool isProgress = listenerSocket.AcceptAsync(acceptArgs);
    if (isProgress == false)
    	AfterAccept(null, acceptArgs);
}