앞서 진행한 소켓 프로그래밍 구현에서 ServerCore 폴더에
Listener 파일을 추가한다.
문지기 즉, 소켓을 만드는 것을 따로 파일로 빼서 관리할 예정이다.
생성된 Listener 클래스 안에 소켓을 생성한다.
Socket _listenSocket;
소켓을 초기화 하는 함수는 따로 만들 것이다.
기존 코드를 보면 (빨간 줄 무시)
endPoint를 받아서 소켓을 생성하고 Bind도 하는 것을 볼 수 있다.
(*기존 코드 : 전 글 확인..)
따라서 Init함수 인자로 endPoint를 넣어주며 Init() 함수를 다음과 같이 만들어 준다.
Init()
public void Init(IPEndPoint endPoint)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream,
ProtocolType.Tcp);
//문지기 교육
_listenSocket.Bind(endPoint);
//영업 시작
//backlog : 최대 대기수
_listenSocket.Listen(10);
}
그 후 Accept는 따로 Accept 함수를 만들어 빼준다.
public Socket Accept()
{
return _listenSocket.Accept();
}
이제 Program.cs 파일로 돌아가서 문지기 역할을 하는 Listener를 생성해준다.
그리고 문지기를 초기화 하는 코드는 listener의 Init 함수와 Accept 함수 호출로 대체해준다.
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
...
_listener.Init(endPoint,OnAcceptHandler);
Console.WriteLine("Listening...");
while (true)
{
Socket clientSocket = _listener.Accept();
}
}
}
하지만 현재 형태는 블로킹 함수로 이루어진 상황이다.
이런 경우 사용자가 10000명이라고 가정했을 때, 무한 대기를 계속 하고 있는 상황이 발생할 수 있으므로
논 블로킹 형태로 구현해야 한다.
(*블로킹 : 요청받는 함수가 작업을 모두 마치고 나서야 요청자에게 제어권을 넘겨줌 (그동안 요청자는 아무것도 하지않고 기다림)
논블로킹 : 요청받은 함수가 요청자에게 제어권을 바로 넘겨줌 (그동안 요청자는 다른 일을 할 수 있음)
따라서 Accept , Receive, Send는 모두 비동기 형식 (논 블로킹) 으로 바꿔보자
AcceptAsync()로 비동기 형태로 만들어준다.
이렇게 되면, 클라이언트 요청이 올때까지 무작정 기다리는 것이 아니라 다른 작업을 쭉 수행하다가
요청이 들어오면 그 때 CallBack 방식으로 알려주는 형태이다.
public Socket Accept()
{
_listenSocket.AcceptAsync();
return _listenSocket.Accept();
}
좀 더 세부적으로 함수를 만들어보자.
Accpet 메소드 안에 있는 _listenSocket.AcceptAsync() 코드를 지우고 RegisterAccept 매소드 안에 다음과 같이 넣는다.
요청 여부에 대한 리턴값을 pending에 할당하여 확인하면서 그 값이 false이면 상황을 종료하면 된다.
void RegisterAccept(SocketAsyncEventArgs args)
{
bool pending = _listenSocket.AcceptAsync(args);
if (pending == false)
OnAcceptCompleted(null, args);
}
void OnAcceptCompleted(SocketAsyncEventArgs args)
{
...
}
pending이 true일 때는, 아까 말한 거꾸로 메시지를 알려주도록 하는 작업을 해보자.
초기화 함수에 넣어주어 처음 시작 때 무조건 한번 실행되도록 할 것이다.
public void Init(IPEndPoint endPoint)
{
... //(위 코드 참고)
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
EventHandler를 사용하기 위해 콜백 함수인 OnAcceptCompleted를 넣어주고,
형식을 맞춰주기 위해 object sender라는 인자를 추가한다.
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
//TODO
_onAcceptHandler.Invoke(args.AcceptSocket);
}else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args);
}
accept가 되면 그 이후에 데이터를 보내고 받는 작업을 TODO에서 진행한다.
아래 작업을 수행한다고 보면 된다.
accept가 완료 됐을 때 이를 처리하는 변수를 Action으로 만들어준다.
다음과 같이 _onAcceptHandler를 만들자.
그리고 Init 함수에서 onAcceptHandler를 인자로 추가하고 핸들러를 연결해준다.
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream,
ProtocolType.Tcp);
_onAcceptHandler += onAcceptHandler;
//문지기 교육
_listenSocket.Bind(endPoint);
//영업 시작
//backlog : 최대 대기수
_listenSocket.Listen(10);
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
그리고 OnAcceptCompleted 매소드를 다음과 같이 수정한다.
_onAcceptHandler를 Invoke 시키고 소켓을 넣어주면 된다.
Socket clientSocket = listenSocket.Accept();
원래 Accept할 때 받은 소켓을 전달해 주는 것이다.
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
//TODO
_onAcceptHandler.Invoke(args.AcceptSocket);
}else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args);
}
_onAcceptHandler의 Invoke 결과는 다시 프로그램 클래스에게 전달해 주어야 한다.
다음과 같이 프로그램안에 넣어준다.
static void OnAcceptHandler(Socket clientSocket)
{
try
{
//받는다
byte[] recvBuff = new byte[1024];
int recvBytes = clientSocket.Receive(recvBuff);
String recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"[From Client] {recvData}");
//보낸다
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
clientSocket.Send(sendBuff);
//쫓아낸다.
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
}
Main 문은 다음과 같이 수정한다.
리스너를 통해 초기화 한 부분에 OnAcceptHandler를 넣어준다.
클라이언트가 들어오면 onAcceptHandler를 통해 알려달라는 의미이다.
static void Main(string[] args)
{
...
_listener.Init(endPoint,OnAcceptHandler);
}
중요한 것..!
이벤트를 재사용 하기 위해서는 소켓을 null로 초기화 해 주어야 한다.
RegisterAccept 메소드에 다음과 같이 추가하자
args.AcceptSocket = null; //이벤트 재사용 시, 초기화 필수
최종 코드
Program.cs
class Program
{
static Listener _listener = new Listener();
static void OnAcceptHandler(Socket clientSocket)
{
try
{
//받는다
byte[] recvBuff = new byte[1024];
int recvBytes = clientSocket.Receive(recvBuff);
String recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"[From Client] {recvData}");
//보낸다
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
clientSocket.Send(sendBuff);
//쫓아낸다.
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
}
static void Main(string[] args)
{
//DNS (Domain Name System)
//도메인 등록 후 -> IP주소 찾게끔 만들면 유지보수가 용이함
string host = Dns.GetHostName(); //PC의 호스트명을 받는다 (호스트명은 IP 주소 대신 사용할 수 있는 식별 이름)
IPHostEntry ipHost = Dns.GetHostEntry(host); //해당 호스트의 IP 관련 주소들을 받는다.
IPAddress ipAddr = ipHost.AddressList[0]; //여러개의 주소를 담고있는 리스트를 반환하며 이중 첫번째 값을 할당한다(여러개의 주소를 뱉어주는 경우도 있음)
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); //7777은 포트 번호 , IP 주소와 포트 번호를 할당한다.
_listener.Init(endPoint,OnAcceptHandler);
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
Listener.cs
class Listener
{
Socket _listenSocket;
Action<Socket> _onAcceptHandler;
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler += onAcceptHandler;
//문지기 교육
_listenSocket.Bind(endPoint);
//영업 시작
//backlog : 최대 대기수
_listenSocket.Listen(10);
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
}
void RegisterAccept(SocketAsyncEventArgs args)
{
args.AcceptSocket = null; //이벤트 재사용 시, 초기화 필수
bool pending = _listenSocket.AcceptAsync(args);
if (pending == false)
OnAcceptCompleted(null, args);
}
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
//TODO
_onAcceptHandler.Invoke(args.AcceptSocket);
}else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args);
}
public Socket Accept()
{
return _listenSocket.Accept();
}
}
클라이언트는 동일
'게임 서버 > 네트워크' 카테고리의 다른 글
[c#/네트워크] TCP와 UDP 차이 (0) | 2022.12.22 |
---|---|
[c#/네트워크] 소켓 프로그래밍 (0) | 2022.12.12 |
[c#/유니티] 네트워크 기초 - 스위치와 라우터 (0) | 2022.12.12 |