동기란?
- 요청과 결과가 동시에 진행
- 요청중인 경우 요청받는 함수의 작업이 완료되었는지 계속 확인
- 설계가 간단하고 직관적
비동기란?
- 요청과 결과가 동시에 진행X
- 연관 되어있는 함수들이 서로 언제 시작하고, 언제 일을 마치는지 서로 신경쓰지 않는다.
- 요청을 받는 함수가 완료되었는지 여부를 알아서 알려주는 방식
- 요청중인 경우 결과 값을 바로 응답받지 않아도 된다.
- 동기 방식에 비해 설계가 복잡
블로킹이란?
- 요청(부모함수)는 요청한 작업이 끝날 때까지 다른 작업을 하지않고 대기 (그 동안은 모든 일이 중지)
- 다른 함수를 호출할 때, 제어권도 넘겨주고 작업이 끝난뒤 돌려받는다.
- 요청받은 함수는 모든 실행을 마치고 최종 return 값을 돌려준다.
논블로킹이란 ?
- 요청자는 요청한 작업이 수행되는 동안 다른 작업 가능 (기다리는 동안에도 다른 작업 수행 가능)
- 다른 함수를 호출할 때, 제어권을 넘겨주기는 하나 바로 돌려받는다.
제어권: 함수 내용을 실행시킬 수 있는 권리
결과값: 함수의 리턴 값
동기 방식과 비동기 방식에 대한 코드와 코드에 대한 설명을 주석으로 적어놨습니다.
동기 방식
ServerCore Program Class
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
class Program
{
static Listener _listenr = new Listener();
static void Main(string[] args)
{
//DNS 사용
//하드 코딩으로 IP를 직접 넣을 경우 차후 서버 IP가 변경되었을때 문제 발생
string host = Dns.GetHostName(); //호스트 이름을 불러온다.
IPHostEntry ipHost = Dns.GetHostEntry(host); //DNS에 등록된 ip주소들을 불러와 저장한다.
IPAddress ipAddr = ipHost.AddressList[0]; //ip주소들중 하나를 불러와 저장
IPEndPoint endPoint = new IPEndPoint(ipAddr, 50001);
//소켓 생성
//소켓을 endPoint ip주소의 주소 패밀리 값(ipv4), 소켓 유형을 TCP으로 생성한다.
Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
//Socket Bind
//Socket을 엔드포인트와 연결
listenSocket.Bind(endPoint);
listenSocket.Listen(10);//Socket을 수신 대기 상태로 설정(최대 10개의 연결 대기열를 받게 설정)
while (true)
{
Console.WriteLine("Listening...");
//서버에 입장 시킨다.
//Socket.Accept()
//수신 대기 소켓의 연결 요청 큐에서 보류 중인 첫 번째 연결 요청을 동기적으로 추출하고
//새 Socket연결을 만들고 반환
//게임은 논블로킹 방식의 함수를 채택해야함
//연결 요청이 없을 경우 연결 요청이 있을때까지 서버는 아무 일도 할 수 없다.
Socket clientSocket = listenSocket.Accept();
//연결 요청이 없을 경우 대기
//연결된 소켓에서 byte를 받는다.
byte[] recvBuff = new byte[1024];
//받는다.
//Socket.Recive()
//지정된 Socket을 사용하여 수신 버퍼에 바인딩된 SocketFlags의 데이터를 받는다.
// Receive()는 연결된 Socket이 받은 byte의 수를 반환
int recvBytes = clientSocket.Receive(recvBuff);
//byte[]를 UTF8 string으로 변환
//GetString(변환할 byte[], 시작 index, byte수)
//문자열을 받는다고 가정하기에 사용 가능한 방법
string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
Console.WriteLine($"[From Client] {recvData}");
//보낸다.
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!"); //문자열을 byte로 변환
clientSocket.Send(sendBuff); //소켓에 전달한다.
//쫒아낸다.
clientSocket.Shutdown(SocketShutdown.Both); //Socket에서 보내기 및 받기를 사용할 수 없도록 설정한다.
clientSocket.Close(); //소켓 연결을 닫고 연결된 리소스를 모두 해제
}
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
}
Client Program Class
System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
//DNS 사용
//하드 코딩으로 IP를 직접 넣을 경우 차후 서버 IP가 변경되었을때 문제 발생
string host = Dns.GetHostName(); //호스트 이름을 불러온다.
IPHostEntry ipHost = Dns.GetHostEntry(host); //DNS에 등록된 ip주소들을 불러와 저장한다.
IPAddress ipAddr = ipHost.AddressList[0]; //ip주소들중 하나를 불러와 저장
IPEndPoint endPoint = new IPEndPoint(ipAddr, 50001);
//테스트용 반복 설정
while (true)
{
//Socket 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
//원격 호스트에 대한 연결을 설정
//연결이 되었을 경우 아래의 코드들 실행
//블로킹계열은 게임 서버에서 쓰면 안되지만 기초라 사용
socket.Connect(endPoint);
Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}");
//보낸다.
for(int i = 0; i < 5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes("Hello World!" + i);
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());
}
//0.1초 대기
Thread.Sleep(100);
}
}
}
}
비동기 방식
ServerCore Program Class
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
class Program
{
private static Listener _listener = new Listener();
static void OnAcceptHandler(Socket clientSocket)
{
try
{
//세션 생성후 시작
Session session = new Session();
session.Start(clientSocket);
//보낸다
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!"); //문자열을 byte로 변환
session.Send(sendBuff);
Thread.Sleep(1000);
session.Disconnect();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
static void Main(string[] args)
{
//DNS 사용
//하드 코딩으로 IP를 직접 넣을 경우 차후 서버 IP가 변경되었을때 문제 발생
string host = Dns.GetHostName(); //호스트 이름을 불러온다.
IPHostEntry ipHost = Dns.GetHostEntry(host); //DNS에 등록된 ip주소들을 불러와 저장한다.
IPAddress ipAddr = ipHost.AddressList[0]; //ip주소들중 하나를 불러와 저장
IPEndPoint endPoint = new IPEndPoint(ipAddr, 50001);
//listener class 설정
_listener.Init(endPoint, OnAcceptHandler);
Console.WriteLine("Listening...");
while (true) { }
}
}
}
ServerCore Listener Class
using System;
using System.Net;
using System.Net.Sockets;
namespace ServerCore
{
internal class Listener
{
private Socket _listenSocket;
private Action<Socket> _onAcceptHandler;
public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
//소켓 생성
//소켓을 endPoint ip주소의 주소 패밀리 값(ipv4), 소켓 유형을 TCP으로 생성한다.
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
//대리자 할당
_onAcceptHandler += onAcceptHandler;
//Socket Bind
//Socket을 엔드포인트와 연결
_listenSocket.Bind(endPoint); //소켓에 ip주소, 포드 번호 할당
//backlog : 최대 대기수
_listenSocket.Listen(10); //Socket을 수신 대기 상태로 설정(최대 10개의 연결 대기열를 받게 설정)
//SocketAsyncEventArgs Class: 비동기 소켓 통신을 할때 송신과 수신 자체 작업을 담당
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
//데이터를 주고 받을 때 발생하는 이벤트에 OnAcceptCompleted()함수 추가
//Socket.AcceptAsync()를 만들어준 다음 EventHandler<SocketAsyncEventArgs>()를 넣어준 경우 새로운 쓰레드를 만든다음 실행한다.
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
//등록한다.
RegisterAccept(args);
}
//연결 요청 대기(비동기, 논 블로킹 방식)
void RegisterAccept(SocketAsyncEventArgs args)
{
//SocketAsyncEventArgs.AcceptSocket 초기화
args.AcceptSocket = null;
//Socket.AcceptAsync();
//소켓에서 들어오는 연결 시도를 허용하도록 비동기 작업으로 수행
//I/O작업이 동기적, 즉 바로 완료가 된 경우 참
//I/O작업이 보류중인경우는 거짓 반환
bool pending = _listenSocket.AcceptAsync(args);
//최대 대기수를 설정했기에 사용자가 작정하고 공격하지 않는 이상, 스택오버플로우는 발생할 가능성이 현저히 낮다.
if (!pending)
OnAcceptCompleted(null, args);
}
//연결이 완료됬을때 실행
//레드존(독립적인 스레드에서 실행이 되고 있기에 레이스컨디션 발생 가능성이 있다. 위험하기에 잘 관리를 해야함)
//레이스 컨디션: 두개 이상의 프로세스 혹은 스레드가 공유 자원을 서로 사용하려고 경합 하는 현상
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
//에러가 없이 완료가 된 경우
if(args.SocketError == SocketError.Success)
{
//대리자를 실행한다.
//SocketAsyncEventArgs.AcceptSocket(): 사용할 소켓 또는 비동기 소켓 메서드와의 연결을 수락하기 위해 생성된 소켓을 가져오거나 설정
//아래 대리자에서는 연결 요청이 완료되고 에러없이 실행됬을때 해당 연결 소켓을 불러와 매개 변수로 입력
_onAcceptHandler?.Invoke(args.AcceptSocket);
//TODO
}
//에러가 있을 경우
else
Console.WriteLine(args.SocketError.ToString());
//다음 순번을 연결시키기 위해 실행.
//OnAcceptCompleted()와 RegisterAccept()를 서로 실행하게 함으로써 매개변수 SocketAsyncEventArgs를 새로 생성하지 않고 재사용
//이렇게 함으로써 성능은 올라가나 SocketAsyncEventArgs에 이미 들어가있는 값이 계속 남아있기에 초기화를 한번씩 해야한다.
RegisterAccept(args);
}
}
}
ServerCore Session Class
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Threading;
namespace ServerCore
{
internal class Session
{
private Socket _socket;
private int _disconnected = 0;
private Queue<byte[]> _sendQueue = new Queue<byte[]>();
private List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
private SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
private SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
private object _lock = new object();
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
//데이터 버퍼 설정
_recvArgs.SetBuffer(new byte[1024], 0, 1024);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
//실행
RegisterRecv();
}
//데이터를 보낼때 실행
public void Send(byte[] sendBuff)
{
//데이터 버퍼 설정
//동시 다발적으로 Send요청이 들어온 경우 SocketAsyncEventArgs.SetBuffer()를 통해 buff의 설정을 바꾸게 되면 차후 문제가 생김
//데이터를 Send하고있을때 또 다른 Send요청이 들어와 SetBuff로 Buff의 설정을 바꿔버리게되면 오류
//_sendArgs.SetBuffer(sendBuff, 0, sendBuff.Length); 문제 발생 코드
//멀티 스레드 프로그래밍 환경상 동시 다발적으로 Send()를 호출할 수 있기에 락을 걸어놔야한다.
//한번에 하나의 스레드만 실행할 수 있도록 한다.
lock (_lock)
{
_sendQueue.Enqueue(sendBuff); //수신할 byte[]를 큐에 넣어놓는다.
//만약 대기 순번이 없을 경우 Send 등록 후 실행
if (_pendingList.Count == 0)
RegisterSend();
}
}
//Send 등록
private void RegisterSend()
{
//큐가 빌때까지 반복
while (_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
//SocketAsyncEventArgs.BufferList()
//데이터 버퍼 배열을 가져오거나 설정
//주의점: BufferList와 SetBuffer를 동시에 사용할 경우 에러 발생
_pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
//BufferList의 경우 = 형태로 데이터를 넘겨줘야함
//공식 문서에도 없지만 BufferList를 만들때 이렇게 만들어졌다 함(inflearn 루키스 강의)
_sendArgs.BufferList = _pendingList;
//서버에 가장 많은 부하를 주는 곳은 Send와 Recieve하는 부분이다.
//차후 수정이 필요함
//_sendArgs.SetBuffer(buff, 0, buff.Length); (위 while문으로 대체)
bool pending = _socket.SendAsync(_sendArgs);
if(!pending)
OnSendCompleted(null, _sendArgs);
}
private void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
//RegisterSend()를 통해 실행되는 방법만 있을 경우에는 lock을 하지 않아도 되지만
// _sendArgs.Completed에 등록시켜놓았기에 동시에 실행될 수 있는 환경이다.
//그러므로 lock을 시켜놓는게 안전
lock (_lock)
{
//받은 데이터가 0byte 이상을 받은 경우 && 오류가 발생하지 않은 경우
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
//Recieve와 다르게 Send는 재귀함수로 실행 하면 안됨
//byte를 성공적을 보낸 경우 다음 진행되야할 일이 딱히 많지 않다.
//성공적으로 데이터를 보냈으면 보낸 데이터들을 초기화한다.
_sendArgs.BufferList = null;
_pendingList.Clear();
Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");
//만약 send 처리 중 누군가 다른 데이터를 Send했다면 _sendQueue에 등록시켜 놓기에
//이 곳에서 _sendQueue에 등록되있는 데이터를 확인 후 RegisterSend()호출
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed... {e}");
}
}
else
{
Disconnect();
}
}
}
//소켓 연결 해제
public void Disconnect()
{
//_disconnected 변수를 1로 변경하고, 변경 전 원래 _disconnected의 값을 반환
//특정 값이나 객체가 무조건 1번씩만 교체되어야 하는 상황에서 여러 번 교체되는 오류를 방지
//멀티 스레드 환경에는 _disconnected 변수가 여러번 교체 될 수 있기에 이렇게 변경 해야한다.
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
_socket.Shutdown(SocketShutdown.Both); //Socket에서 보내기 및 받기를 사용할 수 없도록 설정한다.
_socket.Close(); //소켓 연결을 닫고 연결된 리소스를 모두 해제
}
#region 네트워크 통신
//Recieve 등록
private void RegisterRecv()
{
//Socket.ReceiveAsync();
//소켓에서 데이터를 받을 연결 된 비동기 요청을 시작
//I/O작업이 동기적, 즉 바로 완료가 된 경우 거짓
//I/O작업이 보류중인경우는 참 반환
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
//Recieve작업이 완료되면 실행되는 함수
private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
//소켓 작업 중 0byte이상을 받은 경우 && 오류가 발생하지 않은 경우
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
//byte[]를 UTF8 string으로 변환
//GetString(변환할 byte[], 시작 index, byte수)
//문자열을 받는다고 가정하기에 사용 가능한 방법
string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
Console.WriteLine($"[From Client] {recvData}");
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed... {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
DummyClient Program Class
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
class Program
{
static void Main(string[] args)
{
//DNS 사용
//하드 코딩으로 IP를 직접 넣을 경우 차후 서버 IP가 변경되었을때 문제 발생
string host = Dns.GetHostName(); //호스트 이름을 불러온다.
IPHostEntry ipHost = Dns.GetHostEntry(host); //DNS에 등록된 ip주소들을 불러와 저장한다.
IPAddress ipAddr = ipHost.AddressList[0]; //ip주소들중 하나를 불러와 저장
IPEndPoint endPoint = new IPEndPoint(ipAddr, 50001);
//테스트용 반복 설정
while (true)
{
//Socket 설정
Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
//원격 호스트에 대한 연결을 설정
//연결이 되었을 경우 아래의 코드들 실행
//블로킹계열은 게임 서버에서 쓰면 안되지만 기초라 사용
socket.Connect(endPoint);
Console.WriteLine($"Connected 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());
}
//0.1초 대기
Thread.Sleep(100);
}
}
}
}
참고
'네트워크 공부' 카테고리의 다른 글
TCP / IP 계층 구조 3(네트워크 계층1) (0) | 2023.11.13 |
---|---|
TCP / IP 계층 구조 2(물리, 링크 계층) (0) | 2023.10.27 |
TCP / IP 계층 구조 1(특징) (0) | 2023.10.24 |