초보자용 시각 가이드 · GETTING-STARTED + CHOOSING-AXES
완성된 서버가 아니라 부품(축)을 끼워 맞춰 만드는 프레임워크다. 전송·프레이밍·직렬화·실행 모델·세션을 각각 골라 ServerBuilder 에 꽂으면 서버가 된다. 이 페이지 하나로 첫 서버를 띄우고, 무엇을 언제 고를지 결정할 수 있다.
핵심 개념
아래가 이 프레임워크의 전부다. 각 슬롯에서 부품을 하나씩 고르면 그게 서버가 된다. 부품을 바꾸는 비용은 대개 .UseXxx() 한 줄이다.
가장 먼저 정할 것
이 한 가지가 나머지 선택의 절반을 결정한다. 둘 중 어느 쪽인지 먼저 고른다 — 두 프로필 모두 같은 핸들러 코드로 돌아간다.
게임·채팅처럼 서버가 먼저 말을 걸고, 연결이 계속 살아 있는 경우
요청-응답 API처럼 노드를 자유롭게 늘리고 줄이는 경우
💡 중간 형태(일부 메시지만 순서 보장)도 같은 부품으로 조립된다. 한쪽만 도는 추상화는 추상화가 아니라는 게 이 프레임워크의 합격 기준이다.
첫 실행 · 5분 경로
순서대로 따라가면 된다. 1번은 저장소 클론이 필요하고, 2번부터는 nuget 패키지만으로 조립한다.
dotnet run --project Samples/ChServerM.Samples.EchoServer -c Release — 인자 없이 실행하면 같은 핸들러를 TCP·인메모리 양쪽에서 돌려 자체 검증하고 결과를 알려준다.
dotnet new console 로 프로젝트를 만들고 dotnet add package ChServerM 하나면 이 가이드가 쓰는 축이 전부 들어온다. 아래 코드가 그 최소 서버다.
전송을 바꾸고, 순서 보장을 빼고, 타입 있는 메시지로 넘어간다 — 대부분 한 줄 교체다. 맨 아래 갈아끼우기 표 참고.
최소 예제 · 서버 + 클라이언트
서버와 클라이언트가 같은 프레이밍·디스패치를 쓴다 — 그게 이 프레임워크의 대칭이다. 조립도의 슬롯이 실제 코드에서 어떻게 채워지는지, 주석(// …)이 왜 그렇게 쓰는지 알려준다.
// 프레이밍: 최대 페이로드는 기본값에 기대지 말고 워크로드에 맞게 명시한다.
const int MaxPayload = 64 * 1024;
FramingOptions framing = new() { MaxPayloadLength = MaxPayload };
FixedHeaderFrameEncoder encoder = new(framing);
// 실행 모델: 같은 커넥션은 순차, 다른 커넥션끼리는 병렬.
await using PartitionedExecutionModel executionModel = new();
await using ChServerMServer server = new ServerBuilder()
.UseTransport(new TcpServerTransport(endPoint, tcpOptions)) // 전송
.UseFraming(new FixedHeaderFrameDecoder(framing), encoder) // 프레이밍
.UseExecutionModel(executionModel) // 순서 보장
.ConfigureDispatcher(d => d
// 메시지 ID 1: 받은 페이로드를 그대로 돌려보낸다. (ID 0 은 예약 센티넬)
.MapRaw(new MessageId(1), async context =>
{
await FrameWriter.WriteFrameAsync(
context.Connection.Output, encoder,
context.Envelope.MessageId, context.Payload,
FrameFlags.None, context.Envelope.Sequence, context.CancellationToken);
return DispatchStatus.Handled;
}))
.Build();
await server.StartAsync(); // 와이어 형식: [고정 헤더][페이로드]
// 클라이언트도 서버와 같은 프레이밍·인코더를 쓴다.
FramingOptions framing = new() { MaxPayloadLength = 64 * 1024 };
FixedHeaderFrameEncoder encoder = new(framing);
// 응답을 받아 둘 곳 — 핸들러가 여기에 결과를 넣는다.
TaskCompletionSource<byte[]> echoed = new(TaskCreationOptions.RunContinuationsAsynchronously);
await using ChServerMClient client = new ClientBuilder()
.UseTransport(new TcpClientTransport(tcpOptions)) // 서버와 같은 버퍼 임계값
.UseFraming(new FixedHeaderFrameDecoder(framing), encoder)
.ConfigureDispatcher(d => d
.MapRaw(new MessageId(1), context =>
{
// Payload 는 핸들러가 반환하면 무효 — await 너머로 들고 가려면 복사한다.
echoed.TrySetResult(context.Payload.ToArray());
return new ValueTask<DispatchStatus>(DispatchStatus.Handled);
}))
.Build();
// 접속 → 메시지 1개 전송 → 에코 수신.
ClientSession session = await client.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5000));
await FrameWriter.WriteFrameAsync(
session.Connection.Output, encoder, new MessageId(1),
"안녕, ChServerM"u8, FrameFlags.None, sequence: 0, session.Connection.ConnectionClosed);
Console.WriteLine($"에코: {Encoding.UTF8.GetString(await echoed.Task)}"); // → 에코: 안녕, ChServerM
await session.Connection.DisposeAsync();
에코: 안녕, ChServerM 이 찍히면 왕복이 완성된 것이다. 서버·클라이언트가 같은 MessageId(1) · 같은 프레이밍으로 짝을 이룬다. 전체 using 목록과 설명은 docs/GETTING-STARTED.md 에 있다.
무엇을 언제 꽂는가
처음이라면 기본 표시를 고르면 된다. 바꿀 이유가 생겼을 때 아래 “고르는 경우”를 본다.
전송이 무엇이든 프레이밍·직렬화·핸들러는 그대로다. 바꾸는 비용은 한 줄.
자체 클라이언트를 가진 상시 연결. 지연 바닥이 가장 낮다.
LB·프록시·인그레스 등 웹 인프라를 거쳐야 할 때. 고동시성에선 다중화가 TCP를 5.9배 역전.
브라우저 클라이언트. 메시지 경계는 프레이밍 축이 긋는다.
헤드오브라인 블로킹 회피, 0-RTT 재접속. TLS 필수(리눅스는 msquic).
테스트·프로세스 내 조립 검증. 소켓 없이 전체 파이프라인이 돈다.
C# ↔ C#. zero-encoding, 소스 생성으로 가장 빠르다.
크로스 언어 상호운용, 스키마 진화(모르는 필드 보존).
역직렬화 없이 큰 메시지에서 일부 필드만 랜덤 접근.
커넥션 ID 해시로 파티션 고정 → 같은 커넥션은 순차, 다른 커넥션은 병렬. 파티션 안엔 락도 없다.
스레드풀 병렬. 응답 순서가 섞이므로 클라이언트가 시퀀스로 짝짓는다.
⚠ 룸 축(RealTime.Rooms)은 파티션 실행 모델을 전제한다 — 무상태 프로필과 룸은 조합하지 않는다.
단일 노드, 커넥션 = 세션 수명. 상태 유지 프로필의 기본.
다중 노드 무상태 프로필. Redis Cluster 지원.
세션이 곧 영속 데이터(재시작 생존)이거나 이미 PostgreSQL 을 운영할 때.
코어 로직을 오염시키지 않고 데코레이터·미들웨어로 감싸는 관심사들. 안 꽂으면 없는 셈이다.
페이로드를 LZ4 블록으로 압축·해제하는 코덱. 압축률보다 속도(GB/s급)를 택하는 알고리즘이라 압축 레벨은 FAST 고정 — HC 레벨은 실시간 경로엔 수십 배 느리다.
핵심 와이어 형식이 [원본 길이 4B][블록] — 길이가 앞에 있어 버퍼를 잡기 전에 해제 상한을 검사한다(압축 폭탄 방어). 무상태라 전 커넥션이 공유하고 힙 할당 0.
언제 페이로드가 크고 반복적일 때
커넥션 파이프를 감싸는 전송 중립 TLS 데코레이터. TCP·WebSocket 등 어느 전송에나 그대로 끼운다(QUIC은 프로토콜 자체가 TLS). 기본은 TLS 1.3 전용.
핵심 클라이언트 인증서 검증 기본값이 전체 체인 + 호스트명 검증이다(accept-all 아님). 핸드셰이크 실패는 예외가 아니라 상태 — 폭주를 공격 신호로 관측한다.
언제 공인망을 지날 때
인증·인가 축. 비밀번호는 ASP.NET Core의 PBKDF2 v3(반복 60만) 해셔에 위임하고 레거시 해시 형식과 호환된다. 실제 검사는 Hosting 미들웨어 2종이 한다.
핵심 인증·인가 미들웨어는 라우팅보다 앞에 강제된다 — 순서가 뒤집히면 모르는 메시지 ID를 던지는 것만으로 인증을 우회한다. 위반 조립은 Build() 예외.
언제 사용자 신원이 필요할 때
System.Diagnostics.Metrics(BCL 표준) 기반 메트릭 싱크. dotnet-counters 가 즉시 읽고, OpenTelemetry·Prometheus 는 이 Meter 를 구독하는 배선만 얹으면 된다. Diagnostics.Http 는 /healthz·/readyz 프로브를 연다.
핵심 익스포터를 안 붙이면 거의 무비용(Enabled 검사만). 큐 깊이·드롭 수를 노출한다 — 조용한 유실은 관측되지 않으면 없는 것과 같다.
언제 프로덕션이면 항상
밸런스 표·요금표 같은 정적 CSV 를 기동 시 한 번 읽고 이후엔 조회만 하는 강타입 테이블. 스키마와 접근자를 한 선언에서 소스 생성한다(선언 오류는 CHSM2xxx 가 컴파일 타임에 잡음).
핵심 값이 파싱된 채 서수로 조회된다 — 조회마다 int.Parse 없이 컬처에도 안 흔들린다. 데이터 오류는 전부 로딩 시점에 전수 검증돼 조회는 실패하지 않는 게 계약. 파일 변경 시 리로드 가능.
언제 밸런스·룰 표를 코드에서 안전하게
셋은 서로를 참조하지 않는다 — 룸만, 공간만, 틱만 따로 쓸 수 있다. 전부 빼도 프레임워크는 성립한다.
세 가지 시간 프리미티브를 담은 어셈블리:
언제 고정 주기 시뮬레이션 · 대량 타이머 · 클라이언트 시간 정렬
룸 멤버십 + 1회 인코딩 브로드캐스트. 같은 페이로드를 N 명에게 보낼 때 직렬화·헤더 인코딩을 딱 한 번만 하고 참조 계수 프레임을 공유한다 — 멤버당 비용은 파이프로의 바이트 복사뿐이다.
핵심 멤버당 ~400ns · 0 B(정상 상태 브로드캐스트당 할당 0). 브로드캐스트 프레임 시퀀스는 0이라(헤더 공유) 커넥션별 일련번호가 필요한 메시지는 대상이 아니다. 파티션 실행 모델을 전제한다 — 무상태 프로필과는 조합하지 않는다.
언제 채팅·게임 룸, 관전, 존 방송
관심 영역(AOI) 균일 그리드. "내 주변 엔티티"를 전수 순회 O(전체) 대신 겹치는 셀만 본다 — 삽입·이동·제거 O(1), 셀 키는 모튼(Z-order) 코드. 여기에 집합 차분(입장/퇴장 판정)과 무할당 SAT 충돌 판정(OBB·AABB)이 더해진다.
핵심 범위 밖 좌표는 가장자리 셀로 클램프하되 진짜 위치는 따로 보관한다(셀은 후보 축소 수단이지 정답이 아님). 존 하나 = 파티션이라 락도 Concurrent* 도 없고, 이동·질의의 정상 상태 할당이 0.
언제 대규모 공간, 시야·근접 판정, 브로드캐스트 대상 필터링
3단계 · 다음
위 최소 서버에서 바꿀 수 있는 것들이 곧 프레임워크의 표면이다.
| 바꾸고 싶은 것 | 방법 | 참조 샘플 |
|---|---|---|
| 전송 (TCP → HTTP/2·WebSocket·QUIC·인메모리) | UseTransport(...) 한 줄 | StatelessWeb |
| 순서 보장 없이 병렬로 (무상태 웹) | UseExecutionModel 을 빼기 | StatelessWeb |
| 타입 있는 메시지 + 직렬화 | [MessageHandler] 선언 + MapGeneratedHandlers | EchoServer · StatelessWeb |
| 룸 입장·브로드캐스트 (게임·채팅) | RealTime.Rooms 선택 축 | GameRoom |
| TLS | .UseTransportSecurity(...) | Security.Tls.Tests |
막히면
대부분 런타임 교착 대신 시작 시점에 실패하도록 설계돼 있다 — 에러가 곧 안내다.