수업
using System.Net.Sockets;
using System.Net;
using System.Text;
namespace ServerCore
{
public 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(1000);
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($"[From Client] {recvData}");
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
class Program
{
static Listener _listener = new Listener();
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, () => { return new GameSession(); });
Console.WriteLine("Server Listening...");
while (true)
{
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ServerCore
{
class Listener
{
Socket _listenSocket;
Func<Session> _sessionFactory;
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
{
_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_sessionFactory += sessionFactory;
_listenSocket.Bind(endPoint);
_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)
{
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
else
{
Console.WriteLine($"Accept Error: {args.SocketError}");
}
RegisterAccept(args);
}
public Socket Accept()
{
return _listenSocket.Accept();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
namespace ServerCore
{
abstract public class Session
{
Socket _socket;
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
bool _pending = false;
int _disconnected = 0;
object _lock = new object();
public abstract void OnConnected(EndPoint endPoint);
public abstract void OnDisconnected(EndPoint endPoint);
public abstract void OnRecv(ArraySegment<byte> buffer);
public abstract void OnSend(int numOfBytes);
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)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
{
RegisterSend();
}
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnected, 1) == 1)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
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.Clear();
OnSend(_sendArgs.BytesTransferred);
if (_sendQueue.Count > 0)
{
RegisterSend();
}
else
{
_pending = false;
}
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed: {e}");
}
}
else
{
Disconnect();
}
}
}
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)
{
try
{
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
catch(Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed {e}");
}
}
else
{
}
}
#endregion
}
}
✅ 전체 구성
| 부 | 주요 주제 |
|---|
| 1부 | Session 클래스의 설계 목적, 비동기 수신 흐름, 추상 콜백 구조 |
| 2부 | 비동기 송신 처리, Send 큐 구조, Disconnect 처리, GameSession 확장 |
| 3부 | Listener와 세션 생성 흐름, Func<Session> 팩토리 패턴, 엔진-컨텐츠 단 분리 설계 |
📘 1부 – Session 클래스 구조와 비동기 수신 흐름
✅ 주제
- C# TCP 서버에서 클라이언트와의 통신을 처리하는 Session 클래스의 도입 목적과
비동기 방식의 ReceiveAsync 수신 처리 구조를 이해한다.
📚 개념
- 서버는 Accept 이후 각 클라이언트와 데이터를 주고받을 Session 객체가 필요하며,
이 객체는 소켓을 보관하고 직접 수신/송신을 담당한다.
- 수신은 비동기 이벤트 기반 구조로 구현되어야 다수의 클라이언트와의 동시 처리가 가능하다.
- 따라서
SocketAsyncEventArgs를 활용한 낚시대 패턴(이벤트 완료시 다시 등록)을 채택한다.
🧾 용어정리
| 용어 | 설명 |
|---|
Session | 클라이언트 연결 하나를 담당하는 통신 객체 |
ReceiveAsync() | 비동기 방식으로 데이터를 수신 |
SetBuffer() | 수신에 사용할 버퍼 설정 |
Completed | 수신 완료 시 호출되는 이벤트 핸들러 |
Interlocked.Exchange() | 멀티스레드 환경에서 원자적 변수 교체 |
BytesTransferred | 수신된 바이트 수 |
💻 코드 흐름 분석
1. Start() — 수신 준비
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += OnRecvCompleted;
_recvArgs.SetBuffer(new byte[1024], 0, 1024);
RegisterRecv();
}
- 수신용 SocketAsyncEventArgs 객체에 버퍼를 설정하고 수신 이벤트 핸들러 등록
RegisterRecv() 호출로 첫 낚시대 투척
2. RegisterRecv() — 수신 등록
void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
if (!pending)
OnRecvCompleted(null, _recvArgs);
}
- ReceiveAsync 호출
- 즉시 완료되면 바로 OnRecvCompleted() 호출
3. OnRecvCompleted() — 수신 완료 시 처리
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
else
{
Disconnect();
}
}
- 데이터 수신 성공 시 OnRecv 콜백 호출 후 다시 수신 대기 등록
- 실패 또는 연결 종료 시 Disconnect 호출
🧠 핵심
- Receive는 낚시 패턴: ReceiveAsync → OnRecvCompleted → RegisterRecv 반복
- Session은 추상 클래스로 정의되며, 이벤트 함수는 상속한 클래스에서 구현
- 비동기 구조는 반드시 끊김 없이 이어지도록 설계
📘 2부 – 비동기 송신 흐름 및 GameSession 확장
✅ 주제
Session.Send() 구조를 개선하여 큐 기반의 비동기 송신 처리 구현
- 멀티스레드 환경에서의 안전성을 확보하고,
실제 사용자 로직을 구현할 GameSession 클래스 확장까지 연결한다
📚 개념
- Send는 바로 실행하면 안 된다!
→ 큐에 쌓고 하나씩 순차적으로 처리해야 함
- 한 번에 하나의 송신만 활성화되도록 제어
→ _pendingList.Count == 0일 때만 전송 시작
BufferList를 활용해 여러 패킷을 한 번에 전송 가능
🧾 용어정리
| 용어 | 설명 |
|---|
_sendQueue | 보낼 데이터를 대기시키는 큐 |
_pendingList | 현재 전송 중인 패킷 리스트 |
BufferList | 여러 세그먼트를 한 번에 전송할 수 있게 해주는 SocketAsyncEventArgs 속성 |
SendAsync() | 비동기 송신 |
lock | 멀티스레드에서 자원 충돌 방지 |
💻 송신 흐름 코드
1. Send() — 전송 요청
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
2. 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)
OnSendCompleted(null, _sendArgs);
}
3. OnSendCompleted() — 전송 완료 시 다음 전송 또는 종료
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
_sendArgs.BufferList = null;
_pendingList.Clear();
OnSend(args.BytesTransferred);
if (_sendQueue.Count > 0)
RegisterSend();
}
else
{
Disconnect();
}
}
}
💬 GameSession 구현
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected: {endPoint}");
Send(Encoding.UTF8.GetBytes("Welcome to MMORPG Server!"));
Thread.Sleep(1000);
Disconnect();
}
public override void OnRecv(ArraySegment<byte> buffer)
{
string data = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"From Client: {data}");
}
public override void OnSend(int bytes) => Console.WriteLine($"Sent: {bytes} bytes");
public override void OnDisconnected(EndPoint endPoint) => Console.WriteLine($"Disconnected: {endPoint}");
}
🔑 핵심 정리
- 송신은 큐에 쌓고,
_pendingList로 전송
- 완료 시 다음 전송 예약 (OnSendCompleted → RegisterSend)
- GameSession은 추상 메서드를 오버라이드하여 실제 로직 작성
📘 3부 – Listener, 세션 팩토리 구조, 전체 연결 흐름
✅ 주제
- Listener 클래스에서 클라이언트 Accept 처리
Func<Session>으로 GameSession 주입
- 프로그램 전체 연결 구조를 하나로 통합
📚 개념
- Listener는 더 이상 세션을 직접 생성하지 않음
- 외부에서
Func<Session>을 통해 생성 책임을 위임받음
- 엔진(ServerCore) 은 구조만 제공하고,
컨텐츠(Server) 는 행동 정의만 담당
🧾 용어정리
| 용어 | 설명 |
|---|
Listener | 클라이언트 Accept 처리 담당 클래스 |
Func<Session> | 세션 객체를 외부에서 생성해 넘기는 델리게이트 |
GameSession | Session을 상속하여 게임 특화 로직을 구현 |
OnAcceptCompleted | Accept 완료 시 호출되는 이벤트 함수 |
💻 핵심 코드 흐름
1. Listener Init
_listener.Init(endPoint, () => { return new GameSession(); });
2. OnAcceptCompleted
void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
RegisterAccept(args);
}
🌐 프로그램 전체 흐름
[Main()]
→ Listener.Init(endpoint, GameSession 생성자 등록)
→ RegisterAccept() (낚시대)
→ AcceptAsync 완료 → OnAcceptCompleted()
→ GameSession 생성
→ session.Start(socket)
→ session.OnConnected()
→ Send("Welcome") → Disconnect()
🧠 핵심
| 구성요소 | 설명 |
|---|
| Listener | Accept 수신 및 Session 생성 |
| Session | 엔진 단의 송수신/해제 추상 처리 클래스 |
| GameSession | 컨텐츠 단에서 세션 이벤트 구현 |
Func<Session> | 외부에서 세션을 주입받기 위한 팩토리 |
| 분리 설계 | 엔진-컨텐츠 역할 분리, 유지보수 및 확장성 향상 |