[C#] EchoServer
에코 서버 구성 & 흐름
에코 서버란? 클라이언트가 전송해 주는 데이터를 그대로 되돌려 전송해 주는 기능의 서버를 뜻한다.
다음은 서버와 클라이언트의 통신하는 순서를 나타낸 것이다.
서버
- 접점을 구한다. (접점 = 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);
}