1. 개요
TCP 서버는 클라이언트와의 안정적인 연결을 제공하기 위한 기본적인 통신 구조이다.
특히 C#에서는 System.Net.Sockets 네임스페이스를 통해 비교적 간단하게 TCP 서버를 구현할 수 있다.
그러나 단일 스레드로는 여러 클라이언트를 동시에 처리하기 어렵기 때문에,
멀티스레드 구조를 도입하여 동시성(concurrency) 을 확보하는 것이 중요하다.
이 글에서는 C#으로 멀티스레드 TCP 서버를 구성하는 방법을 설명한다.
2. 기본 구조
Node.js로 서버를 구현할 때와 마찬가지로, 소켓 프로그래밍의 흐름은 언제나 비슷하다.

- 소켓을 만들고 서버가 사용할 IP 주소와 포트 번호를 결합한다.
- 해당 포트에서 연결 요청이 수신되는지 주시(listen)한다.
- 클라이언트가 접속하면 전용 게임 세션을 생성하고 데이터를 송수신한다.
- 클라이언트 연결 종료 시, 소켓을 해제한다.
3. 일반적인 코드 예시(비동기X)
서버

클라이언트

출처 : jacking75/edu_CSharpNetworkProgramming: 컴투스 C# 네트워크 프로그래밍 학습
4. 비동기 서버 코드 예시
이 서버는 마치 "낚시터"에서 물고기를 기다리는 구조와 유사하다.
서버는 낚시꾼(Listener) 이 되어 클라이언트(물고기) 가 물기를 기다리고, 물면 바로 세션(Session) 을 할당해 처리한다.
전체 흐름 요약
1. 서버 준비 (주소 설정 → 소켓 생성 → Bind → Listen)
2. 비동기 Accept 대기 (낚시대를 던짐)
3. 클라이언트가 접속하면 OnAcceptCompleted 호출
4. 세션 할당 → 처리 시작
5. 다시 낚시대를 던져 다음 클라이언트를 기다림
1. 서버 시작 (Main())
string host = Dns.GetHostName(); // 현재 컴퓨터의 호스트명
IPHostEntry ipHost = Dns.GetHostEntry(host); // 호스트의 IP 목록 조회
IPAddress ipAddr = ipHost.AddressList[0]; // 그 중 하나 선택
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // IP + 포트를 묶어서 끝점 생성
- Dns 관련 함수는 로컬 환경에서 서버가 어떤 IP로 열릴지 정한다.
- 7777 포트로 서버를 열 준비를 함.
_listener.Init(endPoint, () => { return new GameSession(); });
- 실제 서버를 여는 함수 Init() 호출
- GameSession은 클라이언트별 연결을 처리할 객체이며, 이후에 연결되면 이걸로 세션을 할당한다.
2. Init() – 낚시터 개장
_listenSocket = new Socket(...);
_listenSocket.Bind(endPoint);
_listenSocket.Listen(10);
- 서버 소켓 생성 → 해당 포트에 바인딩 → 최대 10명까지 대기 설정
- 이제 클라이언트를 받을 준비가 되었다.
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
RegisterAccept(args);
- SocketAsyncEventArgs는 비동기 작업 결과를 담는 컨테이너다.
- 이벤트 핸들러 등록 (OnAcceptCompleted)
- 바로 첫 낚시대를 던짐 (RegisterAccept())
3. RegisterAccept() – 낚시대 던지기
args.AcceptSocket = null;
bool pending = _listenSocket.AcceptAsync(args);
- 이 객체는 재사용되므로 반드시 .AcceptSocket = null 초기화
- AcceptAsync()는 비동기 함수로, 클라이언트 접속을 기다림
- 반환값이 true면 아직 물고기가 안 물린 상태 (이벤트로 기다림)
- 반환값이 false면 즉시 물고기가 물린 상태이므로 콜백을 수동 호출
if (pending == false)
OnAcceptCompleted(null, args);
- 이미 연결되었다면 직접 OnAcceptCompleted를 호출해서 처리 시작
4. OnAcceptCompleted() – 물고기 낚임! 세션 처리
if (args.SocketError == SocketError.Success)
{
Session session = _sessionFactory.Invoke(); // GameSession 생성
session.Start(args.AcceptSocket); // 클라이언트 소켓 할당
session.OnConnected(args.AcceptSocket.RemoteEndPoint); // 연결된 클라이언트 정보 로그
}
else
Console.WriteLine(args.SocketError.ToString());
- 연결 성공 여부를 확인한 뒤, Session을 생성
- 세션에 소켓을 할당하고 OnConnected()로 후처리 시작
RegisterAccept(args); // 다음 연결을 위해 낚시대를 다시 던진다
- 한 명 처리 후 다음 손님을 받기 위해 낚시대를 다시 던진다 (재귀적으로 Accept)
추가 공부 - <세션>에 대해서
세션(Session)의 정의
세션(Session) 은 클라이언트 한 명과의 연결을 추상화한 객체이다.
즉, 클라이언트가 서버에 접속하면, 서버는 Socket 객체를 통해 해당 클라이언트와 연결되고, 이를 처리하기 위해 하나의 Session 인스턴스를 만들어 관리하게 된다.
왜 세션이라는 개념이 필요한가?
단순히 Socket만 사용해서 클라이언트를 처리할 수도 있다. 하지만 이렇게 하면 다음과 같은 문제가 생긴다.
- 클라이언트마다 어떤 데이터를 주고받고 있는지 추적하기 어렵다.
- 클라이언트별 상태, 예: 로그인 여부, 위치, 플레이어 정보 등을 보관할 공간이 없다.
- 클라이언트 연결이 끊어질 경우, 어떤 처리를 해줘야 할지 분리하기 어렵다.
그래서 Session 클래스를 따로 만들어 클라이언트 개별 로직을 전담시키는 구조로 분리하는 것이다.
session은 다음 역할을 수행한다.
- 세션은 "클라이언트 한 명을 담당하는 객체" 이다.
- 내부에는 Socket 객체가 들어 있으며, 수신/송신, 상태 관리, 연결 종료 등을 책임진다.
- 하나의 서버는 여러 개의 세션을 만들어 여러 클라이언트를 동시에 처리한다.
- 서버 입장에서는 “몇 번 접속한 누구”가 아니라, “이 세션을 통해 들어온 사용자”로 구분한다.
예시로 만들어볼 수 있는 Session 클래스 구조
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);
// ... 세부 구현
}
'C# > TIL(Today I Learned)' 카테고리의 다른 글
| 2025-04-19 <ArraySegment> (0) | 2025.04.19 |
|---|---|
| 2025-04-18 <SocketAsyncEventArgs 이해하기> (2) | 2025.04.18 |
| 2025-04-07 <캐시 철학에 대하여> (1) | 2025.04.07 |
| 2025-04-03 <DotNetEnv 패키지 활용법> (0) | 2025.04.03 |
| 2025-04-02 <Race Condition 해소를 위한 전략> (2) | 2025.04.01 |
댓글