Skip to content
오늘을살자
Go back

DoIP는 TCP stream이다: recv()만 믿고 파싱하면 깨지는 이유

Edit page

DoIP 구현/디버깅하면서 은근 자주 나오는 사고가 하나 있다.

그리고 코드를 보면 보통 이렇게 되어 있다.

recv()로 읽은 바이트 = DoIP 메시지 1개

이 가정이 틀리면, DoIP 파서는 언젠가 무조건 깨진다.

오늘 글은 결론이 단순하다.

DoIP는 TCP 위에서 흐르는 “stream”이라서, 네가 직접 메시지 경계를 만들어야 한다.

TCP에서 제일 흔한 착각: recv()는 패킷을 돌려주지 않는다

TCP는 “바이트 스트림”이다.

그래서 아래 둘은 둘 다 정상이다.

case A) header가 쪼개져서 옴
  recv -> 3 bytes
  recv -> 5 bytes
  recv -> payload ...

case B) 메시지 2개가 합쳐져서 옴
  recv -> [msg1 header+payload][msg2 header+payload 일부]

이걸 “패킷이 깨졌다”라고 보면 디버깅이 꼬인다.

DoIP framing의 기준은 항상 Generic Header 8바이트

DoIP는 다행히 메시지 경계를 만들기 쉬운 편이다.

구조는 이거다.

Generic Header (8 bytes)
  - Protocol Version      1
  - Inverse Version       1
  - Payload Type          2 (big-endian)
  - Payload Length        4 (big-endian)
Payload (Payload Length bytes)

실무 포인트는 “읽는 순서”다.

1) 버퍼에 데이터를 계속 누적
2) 버퍼에 8바이트 이상 쌓이면 헤더 파싱
3) payload length를 보고, 전체 길이(8 + payload)를 기다림
4) 전체가 모이면 메시지 1개를 꺼내 처리
5) 버퍼에 남은 데이터가 있으면 다시 2)로 반복

제일 무난한 구현: 누적 버퍼 + while 루프

아래는 개념만 잡는 의사코드다.

// rx_buf: 누적 버퍼
// rx_len: 현재 누적된 길이

on_tcp_recv(bytes, n) {
  append(rx_buf, bytes, n);

  while (rx_len >= 8) {
    hdr = parse_doip_header(rx_buf[0..7]);

    if (!hdr.version_ok) { drop_or_close(); return; }
    if (hdr.payload_len > MAX_DOIP_PAYLOAD) { drop_or_close(); return; }

    msg_len = 8 + hdr.payload_len;
    if (rx_len < msg_len) break; // 아직 덜 옴

    msg = rx_buf[0..msg_len-1];
    handle_doip_message(msg);

    consume(rx_buf, msg_len); // 앞에서 msg_len 만큼 제거
  }
}

핵심은 두 가지다.

  1. “8바이트 헤더가 모일 때까지” 파싱하지 않는다
  2. “payload length만큼 모일 때까지” 메시지 처리를 하지 않는다

이걸 지키면 “가끔만” 터지던 현상이 대체로 사라진다.

payload length mismatch가 생기는 대표 패턴

실제로는 아래 패턴이 제일 많다.

1) recv() 한 번에 header+payload가 다 올 거라고 믿는다

recv() 결과를 바로 parse()

부하가 낮을 땐 우연히 맞는다.
부하가 올라가거나 OS/드라이버 조건이 바뀌면 깨진다.

2) 메시지 2개가 붙었는데 “뒤 메시지 헤더”를 payload로 착각한다

이 경우 흔한 로그가:

처럼 나온다.

사실은 payload가 아니라 “다음 메시지의 헤더”를 잘못 집어먹은 거다.

3) endian을 잘못 읽는다

Payload Type(2바이트), Payload Length(4바이트)는 네트워크 오더(big-endian)다.

특히 payload length를 little-endian으로 읽으면:

그래서 길이 sanity check는 필수다.

디버깅할 때 빠르게 확인하는 포인트

TCP framing 문제는 “한 번만” 캡처를 잘 보면 바로 잡힌다.

나는 보통 아래 순서로 본다.

  1. RX 로그에 “recv된 바이트 수”를 찍고, 그 덩어리를 그대로 hex dump로 저장
  2. dump를 기준으로 앞 8바이트를 읽었을 때 payload length가 말이 되는지 확인
  3. “한 recv에 메시지 2개가 들어있던” 케이스가 있는지 확인
  4. 반대로 “헤더가 2~3번에 걸쳐 쪼개진” 케이스가 있는지 확인
  5. 버퍼 consume 로직이 msg_len만큼 정확히 빠지는지 확인(오프바이원 자주 터짐)

구현 팁: 방어 코드를 먼저 넣어두면 로그가 깨끗해진다

실무에서는 아래 방어가 특히 도움이 된다.

프레이밍이 깨졌는데도 연결을 계속 유지하면,
이후 로그가 전부 노이즈가 된다.

추천 대상

한 줄 요약

DoIP는 TCP stream 위에서 흐르기 때문에, Generic Header 8바이트와 payload length를 기준으로 “메시지 경계(framing)”를 직접 만들어 파싱해야 안정적으로 동작한다.

추천 키워드

DoIP, TCP stream, framing, payload length, parser, automotive ethernet

참고 자료


DevBJ | 오늘을살자, Log Today


Edit page
Share this post on:

Previous Post
lwIP RAW TCP가 '가끔 멈춘다': tcp_recved() 안 치면 윈도우가 안 열린다
Next Post
CRC-32 구현 검증하기: 123456789 테스트 벡터와 Ethernet 파라미터