41. 채팅 패킷 문자열 길이의 단위 혼동 (C#)

난이도 중 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// [시나리오] 다국어 채팅 패킷 역직렬화/브로드캐스트 (MMORPG 채팅 서버)
//
// 프로토콜 스펙(chat/v3):
//   ChatSay = [ushort payloadLen][byte channel][ushort msgLen][msg bytes ...]
//   - msg 는 UTF-8 로 인코딩된다.
//   - msgLen 은 "UTF-8 인코딩 결과의 바이트 수"이며 상한은 256 이다.
//   - payloadLen 은 자기 자신(2바이트)을 제외한 나머지 전체의 바이트 수다.
//   - 클라이언트는 한국어/일본어/아랍어 등 다국어와 이모지(😀, 🐉 등)를 보낸다.
//
// 서버 동작:
//   - 수신 스트림에서 ChatSay 를 하나씩 꺼내 검증하고, 같은 채널에 접속한 다른
//     세션들에게 재직렬화해서 브로드캐스트한다.
//   - 상한을 넘는 메시지는 서버가 잘라서 내보낸다(클라 UI 가 뒤에 "..." 를 붙인다).
//   - 별도로 채팅 로그 릴레이(운영툴 연동)에도 같은 메시지를 흘려보낸다.
//
// [요구사항]
//   - 수신 경로와 송신 경로가 스펙과 같은 길이 단위를 사용해야 한다.
//   - TCP 스트림 위에서 패킷 경계가 어긋나지 않아야 한다.
//   - 서버가 잘라 보낸 메시지도 클라에서 정상적으로 표시되어야 한다.
//
// [과제]
//   아래 ChatCodec / ChatBroadcaster 를 리뷰하라. (A)(B) 지점이 어떤 입력에서
//   어떤 결과를 만드는지 구체적인 입력 예시(이모지, 한글 등)를 들어 설명하고,
//   그 결과가 이후 수신 처리에 어떻게 이어지는지까지 서술한 뒤 수정안을 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

public sealed class ChatMessage
{
    public byte Channel;
    public long SenderId;
    public string Text = string.Empty;
}

public static class ChatProtocol
{
    public const int MaxMessageLen = 256;   // 스펙: UTF-8 바이트 수 상한
    public const byte OpChatSay = 0x21;
}

public sealed class ChatCodec
{
    // 수신 누적 버퍼에서 ChatSay 하나를 꺼낸다. 아직 다 안 왔으면 false.
    public bool TryDecode(byte[] buf, int offset, int count, out ChatMessage msg, out int consumed)
    {
        msg = null;
        consumed = 0;
        if (count < 5) return false;

        int p = offset;
        ushort payloadLen = BitConverter.ToUInt16(buf, p); p += 2;
        if (count < 2 + payloadLen) return false;

        byte channel = buf[p]; p += 1;
        ushort msgLen = BitConverter.ToUInt16(buf, p); p += 2;

        string text = Encoding.UTF8.GetString(buf, p, msgLen);

        // (A)
        if (text.Length > ChatProtocol.MaxMessageLen)
            throw new InvalidDataException($"chat message too long: {text.Length}");

        // (A)
        p += text.Length;

        msg = new ChatMessage { Channel = channel, Text = text };
        consumed = p - offset;
        return true;
    }

    public byte[] Encode(ChatMessage msg)
    {
        string text = msg.Text;

        // (B)
        if (text.Length > ChatProtocol.MaxMessageLen)
            text = text.Substring(0, ChatProtocol.MaxMessageLen);

        byte[] body = Encoding.UTF8.GetBytes(text);

        // (A)
        ushort msgLen = (ushort)text.Length;

        int payloadLen = 1 + 2 + body.Length;
        var outBuf = new byte[2 + payloadLen];
        int p = 0;
        BitConverter.GetBytes((ushort)payloadLen).CopyTo(outBuf, p); p += 2;
        outBuf[p] = msg.Channel; p += 1;
        BitConverter.GetBytes(msgLen).CopyTo(outBuf, p); p += 2;
        Buffer.BlockCopy(body, 0, outBuf, p, body.Length);
        return outBuf;
    }
}

public sealed class ChatBroadcaster
{
    private readonly ChatCodec _codec = new();
    private readonly Dictionary<byte, List<Session>> _channels = new();
    private readonly MemoryStream _relayStream = new();

    public void OnChatSay(Session from, ChatMessage msg)
    {
        msg.SenderId = from.PlayerId;
        byte[] packet = _codec.Encode(msg);

        if (_channels.TryGetValue(msg.Channel, out var members))
        {
            foreach (var s in members)
            {
                if (s == from) continue;
                s.Send(packet);
            }
        }

        WriteRelayLog(msg);
    }

    // 운영툴 릴레이: 같은 메시지를 별도 스트림으로 흘려보낸다.
    private void WriteRelayLog(ChatMessage msg)
    {
        var w = new BinaryWriter(_relayStream, Encoding.UTF8, leaveOpen: true);
        w.Write(ChatProtocol.OpChatSay);
        w.Write(msg.SenderId);
        w.Write(msg.Channel);

        // (B)
        string line = msg.Text.Length > ChatProtocol.MaxMessageLen
            ? msg.Text.Substring(0, ChatProtocol.MaxMessageLen)
            : msg.Text;
        w.Write(line);
        w.Flush();
    }
}

public sealed class Session
{
    public long PlayerId;
    public void Send(byte[] packet) { /* 송신 큐에 적재 */ }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.