<Listener>
1. 이전 시간의 코드를 분리해서 정리
: Program.cs의 main 함수에 모든 것을 넣는 것이 아닌 Listener 소켓을 따로 빼어 정리
1) SeverCore 프로젝트 우클릭 -> 추가 -> 새항목 추가: Listener.cs 만들기
2) Listener 클래스에서 _listenSocket을 만든다 .
3) Init 함수를 구현하여 해당 함수 내에 _listenSocket 객체를 생성한다. -> 문지기기를 교육(endPoint 주소 할당) -> 영업을 시작하겠다는 Listen을 한다.
class Listener
{
Socket _listenSocket;
public void Init(IPEndPoint endPoint)
{
_listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
// 문지기 교육
_listenSocket.Bind(endPoint);
// 영업 시작
// backlog: 최대 대기수
_listenSocket.Listen(10);
}
4) Accept함수를 만들어서 _listenSocket.Accept()를 한다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace SeverCore
{
class Listener
{
Socket _listenSocket;
public void Init(IPEndPoint endPoint)
{
_listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
// 문지기 교육
_listenSocket.Bind(endPoint);
// 영업 시작
// backlog: 최대 대기수
_listenSocket.Listen(10);
}
public Socket Accept()
{
return _listenSocket.Accept();
}
}
}
5) Program.cs의 Main 함수에서 Listener 클래스를 이용하여 코드를 수정하면 아래와 같다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
// DNS (Donmain Name System)
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
// IPEndPoint: 도착 IP 주소
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호
try
{
_listener.Init(endPoint);
while (true)
{
Console.WriteLine("Listening....");
// 손님을 입장시킨다.
Socket clientSocket = _listener.Accept(); // 세션의 소켓
// 받는다.
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());
}
}
}
}
2. 문제: Blocking 계열의 함수를 사용한다. ex) Accept 함수
: SeverCore의 listenerSocket의 Accept 함수는 Client의 소켓이 Connect 함수를 이용하여 서버와 연결하겠다고 말하지않는 이상 무한정 기다리게 된다.
-> Blocking 계열의 함수를 사용하여 Send와 receive를 하게 되면 상대방이 나에게 메세지를 보내기 전까지 무한정 대기를 하게됨
-> NoneBlocking 함수로 바꿔야함
1) 비동기함수: AcceptAsync 사용
- 기존의 함수코드처럼 동기적으로 코드의 위에서 아래로 실행되는 것이 아닌 비동기적으로 특정 코드의 처리가 끝나기 전에 다음 코드를 실행할 수 있는 것을 뜻한다.
- 비동기함수를 이용하여 우선 Accept를 한 후, 클라이언트에서 콜백으로 연락을 주도록 구현
-> Accept 하는 부분과 실제로 처리되어 완료되는 부분, 2개로 나눠져야한다.
2) RegisterAccept 함수: 당장 완료되었다는 보장 없이 Accept 하겠다.
a) AcceptAsync 함수의 return값이 bool: pending 여부를 알려줌
b) pending이 false이면 pending 없이 바로 클라이언트의 connect이 와서 accept을 했다는 것을 의미 -> OnAcceptComplete 호출
만일 pending이 true이면, SocketAsyncEventArgs의 Event Handler를 통해 AcceptAsync가 완료되었을때, 거꾸로 알려주도록 코드를 구현해야한다.
void RegisterAccept(SocketAsyncEventArgs args)
{
// 당장 완료에 대한 보장없이 Accept
bool pending = _listenSocket.AcceptAsync(args);
if (pending == false) // pending 없이 바로 클라이언트의 connect이 와서 accept했다는 것을 의미
OnAcceptComplete(null, args);
}
c) init 함수에서 SocketAsyncEventArgs를 만들어준다.
args.Completed가 EventHandler 이기 때문에 델리게이트로 콜백으로 OnAcceptCompleted라는 것을 받아주고 싶다고 코딩
* OnAcceptCompleted 함수의 형식을 맞춰준다.
// 초기화 시에 이벤트 등록
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptComplete);
RegisterAccept(args); // 최초 한번은 args를 등록
d) 초기화시에 RegisterAccept함수에 args 등록
-> 클라이언트에서 Connect 요청이 오면 콜백방식으로 OnAcceptCompleted가 호출이 된다.
* AcceptAsync로 비동기적으로 실행: 만일 클라이언트의 Connect이 없으면 다음으로 넘어갔다가 Connect이 있을때 콜백으로 OnAcceptCompleted를 실행한다. (EventHandler ~(OnAcceptCompleted)에서 실행)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace SeverCore
{
class Listener
{
Socket _listenSocket;
public void Init(IPEndPoint endPoint)
{
_listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
// 문지기 교육
_listenSocket.Bind(endPoint);
// 영업 시작
// backlog: 최대 대기수
_listenSocket.Listen(10);
// 초기화 시에 이벤트 등록
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptComplete);
RegisterAccept(args); // 최초 한번은 args를 등록
}
void RegisterAccept(SocketAsyncEventArgs args)
{
// 당장 완료에 대한 보장없이 Accept
bool pending = _listenSocket.AcceptAsync(args);
if (pending == false) // pending 없이 바로 클라이언트의 connect이 와서 accept했다는 것을 의미
OnAcceptComplete(null, args);
}
}
}
3) OnAcceptComplete 함수
a) 결국 클라이언트가 Connect을 하게 되면 OnAcceptComplete 함수로 오게된다. SocketError를 확인한 후, 모든 일을 끝내면 다시 클라이언트와의 Connect을 위해 RegisterAccept을 등록해야한다.
void OnAcceptComplete(object sender, SocketAsyncEventArgs args)
{
// 에러가 없이 실행이 되었다.
if (args.SocketError == SocketError.Success)
{
// TO DO
}
else
Console.WriteLine(args.SocketError.ToString());
// 다음 턴을 위해 다시 Register 등록
RegisterAccept(args);
}
b) TO DO 에서 이전 시간의 Program.cs의 메인 함수에서 클라이언트와 메세지를 Recieve & Send 했던 것을 구현해야한다
- 위의 처럼 콜백 방식을 이용하기 위해 Action<Socket> _onAcceptHandler를 선언한다.
- Init 함수에서도 Action을 인자로 받도록하여 _onAcceptHandler와 onAcceptHandler를 델리게이트로 연결되도록한다.
class Listener
{
Socket _listenSocket;
Action<Socket> _onAcceptHandler;
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
_listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler += onAcceptHandler; // _onAcceptHandler에다가 연결
c) 이전 Program.cs의 main 함수에서 소켓이 Accept한 후에 클라이언트의 Socket을 반환하였다.
- 위에 처럼 Client Socket을 반환하기 위해 args.AcceptSocket 이용: ClientSocket이 저장되어 있음
- OnAcceptCompleted 함수의 TO DO에서 _onAcceptHnadler.Invoke의 인자로 args.AcceptSocket을 넘겨준다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace SeverCore
{
class Listener
{
Socket _listenSocket;
Action<Socket> _onAcceptHandler;
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
_listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler += onAcceptHandler; // _onAcceptHandler에다가 연결
// 문지기 교육
_listenSocket.Bind(endPoint);
// 영업 시작
// backlog: 최대 대기수
_listenSocket.Listen(10);
// 초기화 시에 이벤트 등록
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptComplete);
RegisterAccept(args); // 최초 한번은 args를 등록
}
void RegisterAccept(SocketAsyncEventArgs args)
{
// 이전의 데이터 날리기
args.AcceptSocket = null;
// 당장 완료에 대한 보장없이 Accept
bool pending = _listenSocket.AcceptAsync(args);
if (pending == false) // pending 없이 바로 클라이언트의 connect이 와서 accept했다는 것을 의미
OnAcceptComplete(null, args);
}
void OnAcceptComplete(object sender, SocketAsyncEventArgs args)
{
// 에러가 없이 실행이 되었다.
if (args.SocketError == SocketError.Success)
{
// TO DO
_onAcceptHandler.Invoke(args.AcceptSocket); // ClientSocket을 return해주는 부분이 AcceptSocket이다 .
}
else
Console.WriteLine(args.SocketError.ToString());
// 다음 턴을 위해 다시 Register 등록
RegisterAccept(args);
}
public Socket Accept()
{
return null;
}
}
}
d) Program.cs의 main 함수에서 OnAcceptHandler 함수를 정의한다.
- ClientSocket을 인자로 받고 Receive & Send를 해준 부분을 다음과 같이 옮긴다.
- Main 함수에서는 이제 콜백으로 Event로 클라이언트와 통신하기 때문에 while문에서 필요한 코드가 없으나 프로그램을 종료시키지 않기 위해 작성
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
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);
}
}
static void Main(string[] args)
{
// DNS (Donmain Name System)
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
// IPEndPoint: 도착 IP 주소
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호
_listener.Init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening....");
while (true)
{
}
}
}
}
4) RegisterAccept 함수에서 SocketAsyncEventArgs의 args를 계속해서 재사용하기 때문에 초기화 하는 작업이 중요하다.
- 아래와 같이 args.AcceptSocket을 null로 초기화
void RegisterAccept(SocketAsyncEventArgs args)
{
// 이전의 데이터 날리기
args.AcceptSocket = null;
// 당장 완료에 대한 보장없이 Accept
bool pending = _listenSocket.AcceptAsync(args);
if (pending == false) // pending 없이 바로 클라이언트의 connect이 와서 accept했다는 것을 의미
OnAcceptComplete(null, args);
}
3. Test
- 아래와 같이 DummyClient에서 여러개의 메세지를 보내도록 While 문으로 감싼다
- Thread.Sleep으로 약간의 타임을 늦춘다. (너무 빠르면 과부화)
- 결과 아래와 같이 계속 통신
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
// DNS (Donmain Name System)
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
// IPEndPoint: 도착 IP 주소
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호
while (true)
{
// 휴대폰 설정
Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
try
{
// 문지기에게 문의
socket.Connect(endPoint);
Console.WriteLine($"Connect To {socket.RemoteEndPoint.ToString()}");
// 보낸다.
byte[] sendBuff = Encoding.UTF8.GetBytes("Hello World!");
int sendBytes = socket.Send(sendBuff);
// 받는다
byte[] recvBuff = new byte[1024];
int recvBytes = socket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"[From Server] {recvData}");
// 나간다
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
}
}
}

** 전체 코드
- SeverCore에서 Listener.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace SeverCore
{
class Listener
{
Socket _listenSocket;
Action<Socket> _onAcceptHandler;
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
_listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
_onAcceptHandler += onAcceptHandler; // _onAcceptHandler에다가 연결
// 문지기 교육
_listenSocket.Bind(endPoint);
// 영업 시작
// backlog: 최대 대기수
_listenSocket.Listen(10);
// 초기화 시에 이벤트 등록
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptComplete);
RegisterAccept(args); // 최초 한번은 args를 등록
}
void RegisterAccept(SocketAsyncEventArgs args)
{
// 이전의 데이터 날리기
args.AcceptSocket = null;
// 당장 완료에 대한 보장없이 Accept
bool pending = _listenSocket.AcceptAsync(args);
if (pending == false) // pending 없이 바로 클라이언트의 connect이 와서 accept했다는 것을 의미
OnAcceptComplete(null, args);
}
void OnAcceptComplete(object sender, SocketAsyncEventArgs args)
{
// 에러가 없이 실행이 되었다.
if (args.SocketError == SocketError.Success)
{
// TO DO
_onAcceptHandler.Invoke(args.AcceptSocket); // ClientSocket을 return해주는 부분이 AcceptSocket이다 .
}
else
Console.WriteLine(args.SocketError.ToString());
// 다음 턴을 위해 다시 Register 등록
RegisterAccept(args);
}
public Socket Accept()
{
return null;
}
}
}
- SeverCore에서 Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SeverCore
{
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);
}
}
static void Main(string[] args)
{
// DNS (Donmain Name System)
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
// IPEndPoint: 도착 IP 주소
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호
_listener.Init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening....");
while (true)
{
}
}
}
}
- Dummy Client의 Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
// DNS (Donmain Name System)
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
// IPEndPoint: 도착 IP 주소
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호
while (true)
{
// 휴대폰 설정
Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
try
{
// 문지기에게 문의
socket.Connect(endPoint);
Console.WriteLine($"Connect To {socket.RemoteEndPoint.ToString()}");
// 보낸다.
byte[] sendBuff = Encoding.UTF8.GetBytes("Hello World!");
int sendBytes = socket.Send(sendBuff);
// 받는다
byte[] recvBuff = new byte[1024];
int recvBytes = socket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"[From Server] {recvData}");
// 나간다
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
}
}
}
* 참고 자료
https://www.howdy-mj.me/javascript/async/
자바스크립트 비동기 함수 알아보기
비동기(Asynchronous…
www.howdy-mj.me
'Development > 게임 서버' 카테고리의 다른 글
[Part 4 게임 서버 ] 네트워크 프로그래밍 4 (Session #3, Session #4) (0) | 2022.01.18 |
---|---|
[Part 4 게임 서버] 네트워크 프로그래밍 3(Session #1, Session #2) (0) | 2022.01.16 |
[Part 4 게임 서버] 네트워크 프로그래밍 1(네트워크 기초 이론 ~ 소켓 프로그래밍 입문 #2 (0) | 2022.01.14 |
[Part 4 게임 서버 ] 멀티쓰레드 프로그래밍 6(ReaderWriterLock ~ Thread Local Storage) (0) | 2022.01.13 |
[Part 4 게임 서버] 멀티쓰레드 프로그래밍 5(Lock 구현 이론 ~ AutoResetEvent) (0) | 2022.01.10 |