📄 비동기 리스너
지난 글[네트워크 서버 구축(C#) 12.소켓 프로그래밍]에서 간단한 소켓 통신을 하는 프로그램을 작성하였습니다. 하지만 작성했던 프로그램은 동기성 프로그램으로, 블로킹방식을 사용하기때문에 서버나 클라이언트에서 소켓을 받을때까지 기다려야 합니다. 직관적이고 간단하다는 장점이 있지만, 적합한 모델은 아니기때문에 비동기 방식의 리스너를 공부하여 이를 정리했습니다.
📑 Listener
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
class Listener
{
private Socket mListenSocket;
private Action<Socket> mOnAcceptHandler; //액션 델리게이트로 소켓을 받으면 실행하는 함수(Acion은 리턴 없음)
/// <summary>
/// 리스너를 초기화
/// </summary>
/// <param name="endPoint">IP주소와 포트주소</param>
/// <param name="onAcceptHandler">호출할 함수</param>
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
//리슨소켓 생성
//TCP 통신을 사용
mListenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
//소켓을 수신하여 수행할 함수를 mOnAcceptHandler에 추가
mOnAcceptHandler += onAcceptHandler;
//리슨소켓 설정
mListenSocket.Bind(endPoint);
//백로그 설정
mListenSocket.Listen(10);
//리슨을 위한 SocketAsyncEventArgs을 생성
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
//받음 완료에 대한 이벤트에 연결
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
//최초 1회 등록
RegisterAccept(args);
}
/// <summary>
/// 리슨을 위한 비동기 대기를 시작
/// </summary>
/// <param name="args">비동기 리슨에서 호출할 이벤트</param>
private void RegisterAccept(SocketAsyncEventArgs args)
{
args.AcceptSocket = null; //이전의 소켓에 대한 정보를 null로 처리하여 자체적으로 깨끗히 초기화
bool isPending = mListenSocket.AcceptAsync(args); //비동기 Accept (요청이 없어 실제로 Accept를 하지 않아도 리턴)
//pending이 true이면 작업이 보류중으로, 작업이 완료되면 args에서 complete 이벤트 발생
//즉시 연결된 경우 (보류 상태가 아닌경우)
if (isPending == false)
{
OnAcceptCompleted(null, args);
}
}
private void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success) //에러가 없는경우(Success인 경우)
{
//소켓을 인자로 넣어 호출
mOnAcceptHandler.Invoke(args.AcceptSocket);
}
else { Console.WriteLine(args.SocketError.ToString()); } //소켓 에러가 있는경우?
//새로운 비동기 리스너를 등록
RegisterAccept(args);
}
}
}
private Socket mListenSocket;
Action<Socket> mOnAcceptHandler; //액션 델리게이트로 소켓을 받으면 실행하는 함수(리턴 없음)
- Listener라는 클래스에서 사용될 소켓과 액션 델리게이트입니다.
- mOnAcceptHandler은 클라이언트로부터 소켓을 받아 통신하게되면 호출될 액션입니다.
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
- mOnAcceptHandler += onAcceptHandler;은 액션에 호출할 함수를 등록하는 과정입니다. mOnAcceptHandler = onAcceptHandler;도 가능합니다.
//리슨을 위한 SocketAsyncEventArgs을 생성
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
//받음 완료에 대한 이벤트에 연결
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
//최초 1회 등록
RegisterAccept(args);
- 소켓비동기이벤트를 생성합니다.
- args.Completed는 비동기 작업이 완료되었을때 호출되는 이벤트로, 이 이벤트가 발생될때 호출할 함수인 OnAcceptCompleted를 등록해줍니다.
- RegisterAccept(args);를 통하여 최초 1회 등록(completed 이벤트 대기)을 해줍니다.
private void RegisterAccept(SocketAsyncEventArgs args)
- 소켓을 받고, completed를 위한 비동기 이벤트를 사용합니다.
- pending이 false라면, 즉시 연결된 경우로 별도로 OnAcceptCompleted를 호출하여 처리를 해주고, true라면 작업이 보류상태(소켓이 들어오지 않음)입니다. 이 상태에서 작업이 완료되면 args에서 complete이벤트가 발생하여 complete에 연결된 OnAcceptCompleted가 호출됩니다.
private void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
- completed이벤트가 발생하여 호출되는 함수입니다. mOnAcceptHandler에 연결된 함수를 호출하고, args를 새로 등록해줍니다.
📑 Program(ServerCore)
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
class Program
{
static Listener mListener = new Listener();
/// <summary>
/// 소켓통신을 한 경우 비동기 리스너가 호출
/// </summary>
/// <param name="clientSocket">통신할 때 사용한 소켓</param>
static void OnAcceptHandler(Socket clientSocket)
{
try
{
//클라이언트로부터 수신
byte[] recieveBuffer = new byte[1024];
int recieveBytes = clientSocket.Receive(recieveBuffer); //recieveBuffer에 스트림을 넣고, 리턴값은 몇바이트를 받았는지 저장
string recieveData = Encoding.UTF8.GetString(recieveBuffer, 0, recieveBytes); //문자열로 획득
Console.WriteLine($"클라이언트로부터 받음: {recieveData}");
//클라이언트에게 송신
byte[] sendBuffer = Encoding.UTF8.GetBytes("Hello World From Server");
clientSocket.Send(sendBuffer);
//클라이언트 킥
clientSocket.Shutdown(SocketShutdown.Both); //미리 예고
clientSocket.Close(); //실질적으로 통신 해제
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
private static void Main(string[] args)
{
//DNS 사용
string host = Dns.GetHostName(); //현재 로컬의 호스트를 가져옴
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddress = ipHost.AddressList[0]; //배열로 들어있는 값 중에서 가장 첫번째 사용(임시)
IPEndPoint endPoint = new IPEndPoint(ipAddress, 7777); //엔드포인트(ip주소와 포트번호)
mListener.Init(endPoint, OnAcceptHandler); //클래스로 생성한 리슨소켓 초기화
Console.WriteLine("Listening...");
while (true) { } //메인스레드(프로그램)가 종료되지 않게 일단 무한루프
}
}
}
mListener.Init(endPoint, OnAcceptHandler); //클래스로 생성한 리슨소켓 초기화
- 리스너를 생성하고, 초기화해줍니다.
- 지속적으로 소켓을 받는 루프는 Listener에 있습니다.
static void OnAcceptHandler(Socket clientSocket)
- 델리게이트로 Listener에 의해서 호출될 함수입니다.
- 소켓에 있는 정보를 가져와 출력합니다.
📑 Program(DummyClient)
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
//DNS 사용
string host = Dns.GetHostName(); //현재 로컬의 호스트를 가져옴
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddress = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddress, 7777);
while (true)
{
//소켓 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
//연결 시도
socket.Connect(endPoint); //이곳에서 대기 (블로킹)
//연결 된 경우 진행
Console.WriteLine($"서버와 연결됨, {socket.RemoteEndPoint.ToString()}");
//서버에게 송신
byte[] sendBuffer = Encoding.UTF8.GetBytes("Hello World From Client");
int sendBytes = socket.Send(sendBuffer);
//서버로부터 수신
byte[] recieveBuffer = new byte[1024];
int recieveBytes = socket.Receive(recieveBuffer);
string recieveData = Encoding.UTF8.GetString(recieveBuffer, 0, recieveBytes);
Console.WriteLine($"서버로부터 수신: {recieveData}");
//연결 해제
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
}
}
}
- 지속적으로 통신을 요청하도록 하는 더미 클라이언트입니다.
'server > socket server' 카테고리의 다른 글
[구글 프로토콜 버퍼] 패킷 한글 주석 인코딩 해결 (0) | 2023.05.18 |
---|---|
[C# 서버] Async Session, Event (0) | 2023.02.06 |
[C# 서버] 소켓 프로그래밍 (0) | 2023.01.14 |
[C# 서버] Thread Local Storage (0) | 2023.01.12 |
[C# 서버] ReaderWriterLock 구현 (0) | 2023.01.11 |