Session 구현
Listener class를 따로 만들어서 보완했던 것 처럼, Receive, Send 함수 부분도 블로킹 방식에서 비동기 방식으로 보완을 해야한다. Receive는 Listener과 비슷하지만, Send는 조금 더 복잡하다. 먼저, ServerCore에서 Session class를 추가하자.
코드 작성 및 수정
Session
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
abstract class Session
{
Socket _socket;
int _disconnect = 0;
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
public abstract void OnConnected(EndPoint endPoint);
public abstract void OnRecv(ArraySegment<byte> buffer);
public abstract void OnSend(int numOfBytes);
public abstract void OnDisconnected(EndPoint endPoint);
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하는 경우가 있기 때문에, lock 사용
lock (_lock)
{
// 따로 따로 Send하지 않고, 한번에 모아서 전송하기 위해
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
void RegisterSend()
{
while (_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
_pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
_sendArgs.BufferList = _pendingList;
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null; // 더이상 굳이 pendingList를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
OnSend(_sendArgs.BytesTransferred);
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
public void Disconnect()
{
// Disconnect가 동시에 실행되는 것을 막기 위해
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
{
// TODO
try
{
//string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
//Console.WriteLine($"[FromClient] {recvData}");
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
Program (ServerCore)
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !!");
Send(sendBuff);
Thread.Sleep(5000);
Disconnect();
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
public override void OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[FromClient] {recvData}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
class Program
{
static Listener _listener = new Listener();
//static void OnAcceptHandler(Socket clientSocket)
//{
// try
// {
// //GameSession session = new GameSession();
// //session.Start(clientSocket);
// byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !!");
// session.Send(sendBuff);
// Thread.Sleep(1000);
// session.Disconnect();
// session.Disconnect();
// }
// catch (Exception e)
// {
// Console.WriteLine(e);
// }
//}
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
//_listener.Init(endPoint, OnAcceptHandler);
_listener.Init(endPoint, ()=> { return new GameSession(); });
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
}
DummyClient
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
internal class Program
{
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호 }
// 식당 주소 찾는 부분은 똑같을 거야.
while (true)
{
// 휴대폰 설정
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());
}
// 적절한 속도로 테스트를 위해
Thread.Sleep(100);
}
}
Listener
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ServerCore
{
internal class Listener
{
Socket _listenSocket;
//Action<Socket> _OnAcceptHandler;
Func<Session> _sessionFactory;
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
// 문지기(가 들고있는 휴대폰)
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // TCP로 할 때 설정
//_OnAcceptHandler += onAcceptHandler;
_sessionFactory += sessionFactory;
// 문지기 교육
_listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것
// 영업 시작
// backlog : 최대 대기수
_listenSocket.Listen(10);
for (int i = 0; i < 10; i++)
{
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) // 모든 게 잘 처리 됐다는 뜻
{
// 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고
// TODO
// _OnAcceptHandler.Invoke(args.AcceptSocket);
// GameSession session = new GameSession();
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
}
public Socket Accept()
{
return _listenSocket.Accept();
}
}
}
'서버 > 서버 이론' 카테고리의 다른 글
Connector 구현 (0) | 2025.04.20 |
---|---|
Listener 구현 (0) | 2025.04.12 |
소켓 프로그래밍 기초 (0) | 2025.04.09 |
네트워크 기초 이론 (0) | 2025.04.07 |
Thread Local Storage (TLS) (0) | 2025.04.06 |