기존의 소켓 프로그래밍 예제에는 치명적인 문제가 남아있습니다.
치명적인 문제점은 바로 블로킹
즉, 동기 방식으로 서버가 클라이언트의 Connect 요청에 Accept를 수행한다는 것입니다.
동기 방식에서는 서버가 Connect 요청이 오기전
그러니까 서버가 Accept를 수행하기 전까지는 다른 작업을 하지 않고 계속 기다리게 됩니다.
그리고 이는 작업을 처리하는데 있어서 낭비로 이어지게 됩니다.
따라서 기존의 코드를 수정하여 논블로킹, 즉 비동기 방식으로 변경하여
클라이언트로부터 Connect 요청이 오는 경우, Accept를 수행하고
그렇지 않은 경우 다른 작업을 수행하는 방식으로 변경하겠습니다.
코드 작업을 하기 전,
간단하게 동기와 비동기 방식을 시각적으로 표현해보았습니다.
- 동기 방식(=블로킹)
서버에서 한번 Listen을 시작한 후,
클라이언트에서 Connect 요청을 하여 Accept하지 않는 경우 무한 대기합니다.
- 비동기 방식(=언블로킹)
비동기 방식의 경우 다르게 동작합니다.
Server에서 ClientSocket을 Listen으로 설정하여도, Accept를 수행할 때까지 무한히 대기하지 않고
다른 별개의 로직을 수행합니다.
그러다 Client로부터 Connect 요청이 들어오면 Accept를 수행하고 이후 Accept에 맞는 로직을 수행합니다.
이제 코드를 통해 이를 구현해보겠습니다.
- 비동기 방식으로 구현한 Socket 프로그래밍
기존의 Server 코드에서 전부 수행하던 동작을 2개로 쪼개어 관리합니다.
1) Server의 Client 소켓을 Listen 상태로 설정하고 이후 Accept까지 수행하는 Listener
internal class Listener
{
Socket _listenSocket;
Action<Socket> _onAcceptHandler;
// Server의 Client 소켓의 초기 상태를 설정
public void Init(IPEndPoint endPoint, Action<Socket> OnAcceptHandler)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// ClientSocket에 대해 Accept 이벤트 발생 시 호출할 함수를 연결
_onAcceptHandler += OnAcceptHandler;
// ClientSocket에 대해 bind
_listenSocket.Bind(endPoint);
// Listen 상태 설정 - 최대 대기수
_listenSocket.Listen(10);
// 설정된 Client 소켓에 대해 비동기 방식으로 동작하도록 설정
// 비동기 방식으로 성공적으로 연결되는 경우, OnAcceptCompleted를 호출
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
// Accept를 할 수 있도록 Listen 상태인 Client 소켓을 설정
void RegisterAccept(SocketAsyncEventArgs args)
{
// Listen - Accept가 무한 반복되는 구조이므로 이를 위해 AceeptSocket을 null로 초기화
args.AcceptSocket = null;
// pending은 대기 여부를 의미함 -> true == 클라이언트가 즉시 Connect 요청
// false == 클라이언트가 Connect 요청을 하지 않아 다른 로직을 수행가능함
bool pending = _listenSocket.AcceptAsync(args);
// 클라이언트로부터 Connect를 받아들일 준비를 한후에, 바로 요청이 온 경우 받아들임
if (pending == false)
OnAcceptCompleted(null, args);
// 아닌 경우, 클라이언트로부터 다른 로직을 수행하며 Connect 요청이 오기를 기다림
// TODO
}
// Accept되는 경우 호출할 함수
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
// Connect를 Accept 하는 과정에서 문제가 없다면 _onAcceptHandler에 연결된 함수를 실행하여 Accept
if (args.SocketError == SocketError.Success)
{
_onAcceptHandler.Invoke(args.AcceptSocket);
}
// 문제가 있다면 해당 문제를 출력
else
Console.WriteLine(args.SocketError.ToString());
// 클라이언트로부터 다음에 들어올 Connect 요청을 Accept하기 위해 다시 준비
RegisterAccept(args);
}
}
2) 기존의 Server
class Program
{
// Listen 소켓을 생성할 새로운 Listener 인스턴스 생성
static Listener serverListener = new Listener();
// Listener의 Accept 이벤트가 성공적으로 발생하면 호출될 콜백 함수
static void OnAcceptHandler(Socket clientSocket)
{
try
{
// Client로부터 받은 내용을 기록
byte[] recvBuffer = new byte[1024];
int recvBytes = clientSocket.Receive(recvBuffer);
string recvData = Encoding.UTF8.GetString(recvBuffer, 0, recvBytes);
Console.WriteLine($"From Client : {recvData}");
// Client로 전달
byte[] sendBuffer = Encoding.UTF8.GetBytes("Welcome to Server!!!");
clientSocket.Send(sendBuffer);
// Client와의 연결 중지 - 우선 Client의 ServerSocket 연결 중지
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
}
static void Main(string[] args)
{
// DNS를 사용
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// Listener의 Init 함수에 인자인 DNS의 IPEndPoint와 Accept 이벤트 발생시 호출할 콜백함수를 넘겨줌
serverListener.Init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening...");
while (true)
{
}
}
}
3) Client
class Program
{
static void Main(string[] args)
{
while (true)
{
// DNS를 사용
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// Server Socket 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
// Server에게 Connect 요청
socket.Connect(endPoint);
Console.WriteLine($"Connected to {socket.RemoteEndPoint.ToString()}");
// 서버로 전송
byte[] sendBuffer = Encoding.UTF8.GetBytes("Hello From Client");
int sendBytes = socket.Send(sendBuffer);
// 서버로부터 받음
byte[] recvBuffer = new byte[1024];
int recvBytes = socket.Receive(recvBuffer);
string recvData = Encoding.UTF8.GetString(recvBuffer, 0, recvBytes);
Console.WriteLine($"From Server : {recvData}");
// Server 소켓 종료
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
}
}
Client의 경우 기존의 코드에서 무한 반복문으로 실행하게끔 수정하여
무한히 서버에 메시지를 전송하게 됩니다.
이제 실제 실행결과를 확인해보겠습니다.
위와 같이 문제없이 서버 - 클라이언트가
소켓 프로그래밍 방식 그중에서도 비동기 방식으로 돌아가는 것을 확인할 수 있습니다.
'C#' 카테고리의 다른 글
Session_2 (0) | 2024.12.24 |
---|---|
Session_1 (0) | 2024.12.24 |
소켓 프로그래밍 기초 (0) | 2024.12.16 |
네트워크 기초 + 통신 모델 (0) | 2024.12.11 |
Thread Local Storage (0) | 2024.12.09 |