네트워크 프로그래밍
우리는 이전 시간에 Accept를 비동기 함수로 변경했다. 그래서 해당 로직이 AcceptAsync 👉 RegisterAccept 👉 OnAcceptCompleted를 왔다갔다 하면서 호출이 되게끔 했다. 그러나 이 함수를 자세히 보면 '혹시 RegisterAccept 메서드 내 pending의 값이 false 처리가 되어 재귀적으로 뺑뺑이를 돌다 스택 오버플로우가 일어나지 않을까?'라는 의문이 들 수 있다. 그리고 이는 이론상으로는 가능하다. 하지만 이는 현실적으로는 일어날 수 없다. 왜냐하면 우리는 Init() 메서드 내부에서 Listen의 값을 10으로 설정해주었기 때문이다. 따라서 동시 다발적으로 pending의 값이 false로 뜨는 것은 현실적으로 일어날 수 없다.
또 현재 우리는 문지기가 한 명 밖에 없는데 게임이 너무 대박을 쳐 동시 다발적으로 많은 유저를 받아야한다면 for문을 이용해 백 로그의 값 만큼 SocketAsyncEventArgs를 새로이 만들어주어 처리하면 된다.
마지막으로 우리는 분명 메인 쓰레드에서 동작을 시켰고 별도의 Task나 쓰레드를 만든 기억이 없는데 어떻게 리스너에서 Accept 메서드가 실행되었을까? 이는 AcceptAsync 메서드를 실행하면 알아서 ThreadlPool에서 쓰레드를 하나 가져와 사용한다는 것을 알 수 있다.
Receive
Receive와 Send 영역 또한 메인 메서드에서 빼서 따로 구현 할 것이다. 이를 구분하기 위해 Session 이라는 클래스를 새로 만들어주고 해당 스크립트 내에서 처리할 것이다. Send는 Receive보다 구현하기 어렵기 때문에 Receive를 먼저 구현해보도록 하자.
class Session
{
Socket _socket;
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
// recvArgs.UserToken = this;
recvArgs.SetBuffer(new byte[1024], 0, 1024);
RegisterRecv(recvArgs);
}
}
먼저 받아와서 사용해야할 소켓을 선언해주고, 리스너의 Init과 동일한 방법으로 SocketAsyncEventArgs를 생성하고 이벤트 방식으로 동작할 수 있도록 OnRecvCompleted를 담아주도록 하자. 이 후 SetBuffer를 통해 Receive할 때 버퍼를 세팅해주도록 한다. 인자 값으로는 버퍼, 오프셋, 카운트가 들어간다. 오프셋은 버퍼를 어디서부터 시작하는 것을 뜻하고, 카운트는 어디서 끝나는 것을 뜻한다. 이 소스코드는 메인 쓰레드의 아래 소스코드와 같은 항목인데 단 한 줄로 처리할 수 있다. 😅
byte[] recvBuff = new byte[1024];
int recvBytes = clientSocket.Receive(recvBuff);
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvButes);
또 현재 스크립트에서는 주석으로 지웠지만 유저 토큰을 이라는 것을 사용할 수 있는데 해당 타입은 오브젝트 타입이기 때문에 숫자, 스트링, this 등 다양한 것들을 사용할 수 있다.
등록(Register)
void RegisterRecv(SocketAsyncEventArgs args)
{
bool pending = _socket.ReceiveAsync(args);
if (pending == false)
{
OnRecvCompleted(null, args);
}
}
리스너와 동일한 방식으로 구현하기 때문에 설명은 생략한다.
완료 이후(OnRecvCompleted)
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[From Client] {recvData}");
RegisterRecv(args);
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompleted Fail {e}");
}
}
else
{
}
}
먼저 BytesTransferred를 이용해 내가 몇 byte를 받았는지를 체크합니다. 왜냐하면 경우에 따라 0 byte가 오는 경우도 있기 때문입니다. ❓ 이게 뭔.. 말이 안된다고 생각할 수 있지만 상대방이 연결을 끊을 경우 가끔 0으로 오는 경우가 있기 때문에 0 byte 보다 큰지를 체크해야 된다고 한다. 또한 인코딩 과정에서는 최초에 args에 등록된 값을 사용하여 인코딩을 진행한다. 인자 값으로는 크기, 시작 위치, 개수를 나타낸다.
전송(Send)
다음 시간에 다룰 것이므로 현재는 간단하게 처리한다.
public void Send(byte[] sendBuff)
{
_socket.Send(sendBuff);
}
연결 종료(Disconnect)
연결 종료는 소켓에서 종료해야하기 때문에 해당 클래스에서 구현하는 것이 맞다. 그렇지만 아래와 같이 코드를 짜서 처리할 경우 다양한 이벤트가 일어나는 멀티 프로그래밍 환경에 대해 대응할 수 없다.
public void Disconnect()
{
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
예를 들어 동시다발적으로 Disconnect를 한다거나, 아니면 Disconnect를 같은 애가 두 번을 하게 되는 경우가 있으면 위와 같은 코드는 서버에서 바로 문제가 일어나게 된다. 따라서 _socket = null로 처리하는 방법이 있지만, 이는 멀티쓰레딩 환경에서 안전한 코드가 아니게 된다. 따라서 여태 주구장창~ 배웠던 lock을 이용해 아래와 같이 처리하도록 한다.
class Session
{
int _disconnected = 0;
...
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
{
return;
}
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
}
마지막으로 세션 코드의 전문과 프로그램 코드의 전문이다.
Program.cs
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
class Program
{
static Listener _listener = new Listener();
static void OnAcceptHanler(Socket clientSocket)
{
try
{
Session session = new Session();
session.Start(clientSocket);
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
session.Send(sendBuff);
Thread.Sleep(1000);
session.Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
static void Main(string[] args)
{
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
_listener.Init(endPoint, OnAcceptHanler);
Console.WriteLine("Listening...");
// 접속 할 때 까지 대기하기 위한 while
while (true)
{
;
}
}
}
}
Session.cs
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
class Session
{
Socket _socket;
int _disconnected = 0;
public void Start(Socket socket)
{
_socket = socket;
SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
// recvArgs.UserToken = this;
recvArgs.SetBuffer(new byte[1024], 0, 1024);
RegisterRecv(recvArgs);
}
public void Send(byte[] sendBuff)
{
_socket.Send(sendBuff);
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
{
return;
}
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
void RegisterRecv(SocketAsyncEventArgs args)
{
bool pending = _socket.ReceiveAsync(args);
if (pending == false)
{
OnRecvCompleted(null, args);
}
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[From Client] {recvData}");
RegisterRecv(args);
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompleted Fail {e}");
}
}
else
{
}
}
#endregion
}
}
실행
정상적으로 잘 된다! 🥰
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 4-3-7. 네트워크 프로그래밍 : Session #3 (Send 개선) (0) | 2023.09.22 |
---|---|
Part 4-3-6. 네트워크 프로그래밍 : Session #2 (Send 분리) (0) | 2023.09.22 |
Part 4-3-4. 네트워크 프로그래밍 : Listener (0) | 2023.09.21 |
Part 4-3-3. 네트워크 프로그래밍 : 소켓 프로그래밍 입문 (0) | 2023.09.19 |
Part 4-3-2. 네트워크 프로그래밍 : 통신 모델(OSI 7 계층) (0) | 2023.09.19 |