TCP is a byte stream, not messages — frame it yourself
Resolved XML deserialization failures from partial TCP reads by implementing application-level message framing — buffering bytes until a complete delimiter-terminated message arrived. (+ a multi-client TCP DB-broker: thread-per-connection, dispatch by client type, stored procedures run server-side so clients never held DB credentials.)
- TCP is a byte stream: one send != one receive (MSS split / Nagle coalesce)
- Handing the live socket to the parser -> half a document -> deserialize fails
- Fix: buffer bytes until the message is complete, THEN parse
- Delimiter framing is fragile: tag in the body, trailing whitespace, coalesced messages
- Better: length-prefix — read 4-byte length, then readFully exactly that many
- Broker: clients send 'run this SP'; DB credentials live only on the server
// length-prefix framing: [4-byte length][payload]
void writeFramed(OutputStream out, byte[] p) throws IOException {
DataOutputStream dos = new DataOutputStream(out);
dos.writeInt(p.length); // 4-byte big-endian length header
dos.write(p);
dos.flush();
}
byte[] readFramed(InputStream in) throws IOException {
DataInputStream dis = new DataInputStream(in);
int len = dis.readInt(); // block until the 4 length bytes arrive
byte[] p = new byte[len];
dis.readFully(p); // loops until EXACTLY len bytes -> solves partial reads
return p;
}- ·TCP is a byte stream, not a message protocol — one send isn't one receive.
- ·So the XML parser sometimes saw half a document and failed.
- ·I fixed it on the read side: buffer bytes until the full message arrived, then parse.
- ·Honest scope: I owned the framing fix; the broker and thread model were already there.
- ·Today I'd use length-prefix framing — read the length, then readFully exactly that many bytes.
- Opendoor: any custom protocol, streaming, or chunked boundary needs the same framing discipline.
- Proves: I understand the network layer, not just HTTP — byte streams, framing, partial reads.
- Honesty: résumé says 'Contributed to' — I owned the read-side framing fix, not the architecture.
- Length-prefix means you must read the full payload before parsing — no streaming of huge messages.
- A spoofed length header lets one client force a giant allocation: DoS unless you cap max size.
- Thread-per-connection burns ~0.5-1MB stack each, so it caps out around C10K.
- SP-broker tightly couples clients to DB procs — hard to version, locked to one DB engine.
- Cap max frame length and reject oversized headers up front to kill the alloc-DoS.
- Swap thread-per-connection for bounded pool or NIO selector to scale past C10K.
- Whitelist stored-proc names server-side; drop the raw-SQL path the protocol allows.
- Replace the SP-broker with a thin REST/gRPC tier — decouples clients, easier to version.
Understand it step by step (한국어로 이해 → 영어로 말하기)
- 1
보내는 쪽이 메시지 하나(XML 문서 한 개)를 한 번에 보냈다. 받는 쪽도 당연히 한 번에 다 올 거라 생각했다.
// sender writes one whole message in a single call byte[] xml = doc.getBytes(StandardCharsets.UTF_8); out.write(xml); out.flush();
🗣 영어로 말해“The sender sends one whole message, like a single XML document.”
checking microphone…
- 2
그런데 TCP는 '메시지'가 아니라 그냥 '바이트가 줄줄 흐르는 물줄기'다. 보낸 한 번이 받는 쪽에선 두세 번에 나눠 도착할 수도, 여러 개가 붙어서 올 수도 있다. (편지가 아니라 호스에서 나오는 물이라고 보면 됨)
⚖️ Trade-off: 그냥 받는 대로 쓰면, 도착한 게 메시지 끝까지인지 중간인지 알 방법이 없다.
🔧 도구:TCPMSS (~1460B 조각)Nagle (작은 쓰기 합치기)
// one send can arrive as several reads (or several sends glued) int n = in.read(buf); // returns whatever bytes are buffered now, not the whole message // n may be < the full message length
🗣 영어로 말해“But TCP is a byte stream, not messages, so one send isn't one receive.”
checking microphone…
- 3
그래서 받자마자 바로 XML 파서한테 넘겼더니, 파서가 가끔 문서 절반만 보고 '이게 무슨 XML이냐'며 터졌다.
⚖️ Trade-off: 다 안 온 데이터를 파싱하니 deserialize(역직렬화)가 실패한다.
// reading directly and parsing what arrived = half a document int n = in.read(buf); parseXml(buf, 0, n); // throws: the document is incomplete
🗣 영어로 말해“So the XML parser sometimes saw half a document and failed.”
checking microphone…
- 4
고친 방법: 도착한 바이트를 바로 쓰지 말고, 메시지가 다 모일 때까지 통(버퍼)에 담아둔다. 다 모이면 그때 파싱한다.
✅ Fix: 받는 쪽에서 '완성될 때까지 기다렸다 처리'로 바꾸니 반토막 파싱이 사라졌다.
🔧 도구:read 루프byte buffer (바이트 통)
// buffer bytes until the full message arrived, THEN parse var acc = new ByteArrayOutputStream(); int n; while ((n = in.read(buf)) != -1) { acc.write(buf, 0, n); if (isComplete(acc)) { parseXml(acc.toByteArray()); break; } }🗣 영어로 말해“I fixed it on the read side: buffer bytes until the full message arrived, then parse.”
checking microphone…
- 5
'어디까지가 한 메시지'인지 표시하는 첫 방법은 끝에 특수 기호(구분자)를 붙이는 것. 그런데 이건 잘 깨진다 — 그 기호가 본문 안에 들어있거나, 끝에 공백이 붙거나, 메시지 여러 개가 들러붙으면 경계를 못 찾는다.
⚖️ Trade-off: 구분자 방식은 본문에 같은 기호가 있으면 경계가 무너지고, 메시지가 붙어 오면 헷갈린다.
🔧 도구:delimiter framing (구분자 방식)
// delimiter framing: read until a terminator byte (fragile) var acc = new ByteArrayOutputStream(); int b; while ((b = in.read()) != -1 && b != '\n') acc.write(b); // breaks if '\n' appears inside the payload or messages coalesce
🗣 영어로 말해“A delimiter at the end is fragile if it appears in the body or messages stick together.”
checking microphone…
- 6
더 튼튼한 방법: 메시지 앞에 '나 길이 몇 바이트야'를 먼저 붙인다. 받는 쪽은 길이 4바이트를 먼저 읽고, 딱 그만큼만 끝까지 읽는다. 그러면 반토막일 수가 없다.
✅ Fix: 길이를 먼저 읽고 정확히 그만큼 readFully로 채우면, 조각나서 와도 다 모일 때까지 알아서 기다린다.
🔧 도구:length-prefix framingDataOutputStream.writeIntDataInputStream.readFully
byte[] readFramed(InputStream in) throws IOException { var dis = new DataInputStream(in); int len = dis.readInt(); // read the 4-byte length first byte[] p = new byte[len]; dis.readFully(p); // loop until EXACTLY len bytes -> no partial read return p; }🗣 영어로 말해“Today I'd use length-prefix framing: read the length, then read exactly that many bytes.”
checking microphone…
- 7
길이 방식에도 새 위험이 있다 — 나쁜 클라이언트가 '나 2GB짜리야'라고 거짓 길이를 보내면, 서버가 그만큼 메모리를 잡으려다 뻗는다(DoS).
⚖️ Trade-off: 거짓 길이 헤더 하나로 서버가 거대한 메모리를 할당하게 만들 수 있다.
✅ Fix: 최대 길이 상한을 두고, 그보다 큰 길이가 오면 바로 거절하면 막힌다.
🔧 도구:max frame size cap
int len = dis.readInt(); if (len < 0 || len > MAX_FRAME) throw new IOException("frame too large"); byte[] p = new byte[len]; // cap first so a fake length can't force a huge alloc dis.readFully(p);🗣 영어로 말해“A fake length can force a huge allocation, so I cap the max size and reject anything bigger.”
checking microphone…
- 8
정직한 범위: 내가 고친 건 '받는 쪽 framing(메시지 경계 잡기)'이다. 그 위에 있던 broker(클라이언트가 'DB 프로시저 실행해줘'라고 부탁하면 서버만 DB 비번을 쥐고 대신 실행)와 연결당 스레드 하나 쓰는 구조는 원래 있던 거다.
⚖️ Trade-off: broker는 클라이언트를 DB 프로시저에 꽉 묶어서 버전 올리기 어렵고, 연결마다 스레드를 써서 동시 접속이 1만쯤 되면 한계가 온다.
✅ Fix: 지금이라면 broker 대신 얇은 REST/gRPC로 분리하고, 스레드도 풀이나 NIO로 바꿔 동시 접속을 늘린다.
🔧 도구:stored-proc brokerthread-per-connectionREST/gRPCNIO selector
🗣 영어로 말해“Honest scope: I owned the framing fix; the broker and thread model were already there.”
checking microphone…
8단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.