초보자용 시각 가이드 · 클라이언트 편
클라이언트는 별도 세계가 아니다. 서버와 똑같은 프레이밍·직렬화·디스패치·핸들러를 ClientBuilder 에 꽂아 만든다. 그래서 서버 핸들러를 클라이언트에 그대로 재사용할 수 있고, 서버-투-서버 통신도 특별한 경로가 되지 않는다.
서버 편을 먼저 봤다면 이건 그 짝이다 · 서버 조립 가이드 ↗
핵심 개념
서버 조립도와 같은 그림이다 — 단, ‘실행 모델’ 슬롯이 없다. 순서 보장은 서버의 몫이고, 클라이언트는 받은 프레임을 디스패치로 처리할 뿐이다.
서버와의 관계
클라이언트를 이해하는 가장 빠른 길은 서버와의 차이만 아는 것이다. 대부분은 같다.
클라이언트 생명주기
서버가 “대기”라면 클라이언트는 “왕복”이다. 순서대로 흐른다.
축을 골라 클라이언트를 만든다. 아직 접속 전 — 소켓은 열리지 않았다.
연결을 열고, 보안 축이 있으면 그 핸드셰이크까지 마친다. 버전 협상·콘텐츠 핑거프린트가 여기서 검사되고, 실패하면 예외로 나온다.
세션의 출력 파이프에 프레임을 쓴다. 서버와 같은 MessageId · 같은 프레이밍이다.
서버가 보낸 프레임(에코·응답·푸시)이 등록한 핸들러로 들어온다. 무상태라면 응답을 시퀀스로 짝지어 요청과 맞춘다.
정상 종료. 서버가 끊거나 네트워크가 죽으면 Connection.ConnectionClosed 취소 토큰이 신호한다.
프레임워크가 하지 않는다. 끊김을 감지해 백오프로 다시 ConnectAsync 하고, 상태 유지 세션이면 재개 토큰으로 세션을 복원한다 — 아래 참고.
최소 클라이언트 · 왕복
조립도의 슬롯이 코드에서 어떻게 채워지는지 그대로 보여준다. 서버와 같은 framing·MessageId(1) 로 짝을 이룬다.
// 클라이언트도 서버와 같은 프레이밍·인코더를 쓴다.
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 이 찍히면 왕복이 완성된 것이다. 전체 using 목록은 docs/GETTING-STARTED.md 2.3 절에 있다.
클라이언트의 핵심 결정
클라이언트에서 가장 자주 헷갈리는 지점이다. 둘 다 일부러 자동화하지 않았다.
백오프·재시도를 ClientBuilder 안에 감추면, 상위 계층이 “연결이 살아 있다”고 오해해 세션 재수립(인증·상태 복원)을 건너뛴다.
그래서 재접속은 이 위에서 조립한다 — ConnectionClosed 를 감지 → 백오프 → 다시 ConnectAsync → (필요하면) 세션 재개.
재접속할 때 “내가 그 세션의 주인”임을 증명하는 32바이트 난수다. 로그·메트릭에 노출되고 추측·열거될 수 있는 SessionId 대신 이 토큰으로 증명한다.
서버는 재개가 성공하면 새 토큰을 발급(회전)한다 — 클라이언트가 이걸 보관해야 다음 재접속이 된다. 도난돼도 다음 재개에서 무효가 되는 1회용이다.
재접속이 곧 새 요청이다. 복원할 세션이 없으니 재개 토큰도 필요 없다.
대신 응답은 순서 보장이 없으므로 시퀀스 번호로 요청과 짝짓는다.
막히면