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++
// ============================================================================
// [코드리뷰 문제] C++ - 다국어 채팅 패킷의 문자열 길이 처리
// ----------------------------------------------------------------------------
// 시나리오 (프로토콜/버전 · 서버-클라):
// - 채팅 패킷 스펙(chat/v3):
// ChatSay = [uint16 payloadLen][uint8 channel][uint16 msgLen][msg bytes ...]
// * msg 는 UTF-8 로 인코딩된다.
// * msgLen 은 "UTF-8 인코딩 결과의 바이트 수"이며 상한은 256 이다.
// * payloadLen 은 자기 자신(2바이트)을 제외한 나머지 전체 바이트 수다.
// - 클라이언트는 한국어/일본어/아랍어와 이모지(😀, 🐉 등)를 보낸다.
// - 서버는 수신 스트림에서 ChatSay 를 하나씩 꺼내 검증하고, 같은 채널의 다른
// 세션에 재직렬화해 브로드캐스트한다. 상한을 넘는 메시지는 잘라서 내보낸다.
// - 운영툴 연동용으로 메시지를 고정 크기 wchar_t 버퍼에도 복사해 넘긴다.
//
// 과제:
// 아래 ChatCodec 을 리뷰하라. (A)(B) 지점이 어떤 입력에서 어떤 결과를 만드는지
// 구체적 입력 예시(이모지, 한글 등)를 들어 설명하고, 그 결과가 이후 수신 처리와
// 스트림 경계에 어떻게 이어지는지까지 서술한 뒤 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <stdexcept>
namespace chat {
constexpr std::size_t kMaxMsgLen = 256; // 스펙: UTF-8 바이트 수 상한
constexpr std::uint8_t kOpChatSay = 0x21;
struct ChatMessage {
std::uint8_t channel = 0;
std::int64_t senderId = 0;
std::string text; // UTF-8
};
class ChatCodec {
public:
// 수신 누적 버퍼에서 ChatSay 하나를 꺼낸다. 아직 다 안 왔으면 false.
bool tryDecode(const std::uint8_t* buf, std::size_t avail,
ChatMessage& out, std::size_t& consumed)
{
consumed = 0;
if (avail < 5) return false;
std::size_t p = 0;
std::uint16_t payloadLen = 0;
std::memcpy(&payloadLen, buf + p, 2); p += 2;
if (avail < static_cast<std::size_t>(2) + payloadLen) return false;
std::uint8_t channel = buf[p]; p += 1;
std::uint16_t msgLen = 0;
std::memcpy(&msgLen, buf + p, 2); p += 2;
std::string text(reinterpret_cast<const char*>(buf + p), msgLen);
const std::size_t len = messageLength(text);
// (A)
if (len > kMaxMsgLen) {
throw std::runtime_error("chat message too long");
}
// (A)
p += len;
out.channel = channel;
out.text = std::move(text);
consumed = p;
return true;
}
std::vector<std::uint8_t> encode(const ChatMessage& msg)
{
std::string text = msg.text;
// (B)
if (text.size() > kMaxMsgLen) {
text = text.substr(0, kMaxMsgLen);
}
// (A)
const std::uint16_t msgLen = static_cast<std::uint16_t>(messageLength(text));
const std::uint16_t payloadLen =
static_cast<std::uint16_t>(1 + 2 + text.size());
std::vector<std::uint8_t> outBuf(2 + payloadLen);
std::size_t p = 0;
std::memcpy(outBuf.data() + p, &payloadLen, 2); p += 2;
outBuf[p] = msg.channel; p += 1;
std::memcpy(outBuf.data() + p, &msgLen, 2); p += 2;
std::memcpy(outBuf.data() + p, text.data(), text.size());
return outBuf;
}
// 운영툴 연동: 메시지를 고정 크기 wide 버퍼에 복사한다.
void copyToWideBuffer(const std::string& text, wchar_t* dst, std::size_t dstCount)
{
std::size_t n = text.size();
if (n > dstCount) n = dstCount;
// (B)
std::memcpy(dst, text.data(), n * sizeof(wchar_t));
dst[n] = L'\0';
}
private:
// 메시지 길이 계산
static std::size_t messageLength(const std::string& s)
{
std::size_t n = 0;
for (std::size_t i = 0; i < s.size(); ++i) {
char c = s[i];
if ((c & 0x80) == 0) { // ASCII
++n;
continue;
}
// (A)
if (c >= 0xC0) { // 멀티바이트 시작 바이트
++n;
}
}
return n;
}
};
} // namespace chat 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.