Dure: TCP framing + broker

TCP is a byte stream, not messages — frame it yourself

Résumé bullet

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.)

Deep-dive
  • 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
Code
// 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;
}
Say it (English)
  • ·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.
Why it matters — Opendoor / as an engineer
  • 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.
Trade-offs (what you give up)
  • 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.
Push it further
  • 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. 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. 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. 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. 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. 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. 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. 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. 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단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.