DoIP 구현/디버깅하면서 은근 자주 나오는 사고가 하나 있다.
- 가끔만 DoIP NACK가 튀어나온다
- payload length mismatch 로그가 찍힌다
- 어떤 날은 잘 되는데, 부하 걸면 갑자기 깨진다
그리고 코드를 보면 보통 이렇게 되어 있다.
recv()로 읽은 바이트 = DoIP 메시지 1개
이 가정이 틀리면, DoIP 파서는 언젠가 무조건 깨진다.
오늘 글은 결론이 단순하다.
DoIP는 TCP 위에서 흐르는 “stream”이라서, 네가 직접 메시지 경계를 만들어야 한다.
TCP에서 제일 흔한 착각: recv()는 패킷을 돌려주지 않는다
TCP는 “바이트 스트림”이다.
- recv()가 8바이트 헤더를 딱 맞춰서 준다는 보장 없음
- payload까지 한 번에 준다는 보장도 없음
- 반대로, 메시지 2개를 한 번에 뭉쳐서 줄 수도 있음
그래서 아래 둘은 둘 다 정상이다.
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는 다행히 메시지 경계를 만들기 쉬운 편이다.
- 앞 8바이트(Generic Header)에 payload length가 들어있다
- 즉 “정확히 몇 바이트를 더 읽어야 하는지”를 알 수 있다
구조는 이거다.
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 만큼 제거
}
}
핵심은 두 가지다.
- “8바이트 헤더가 모일 때까지” 파싱하지 않는다
- “payload length만큼 모일 때까지” 메시지 처리를 하지 않는다
이걸 지키면 “가끔만” 터지던 현상이 대체로 사라진다.
payload length mismatch가 생기는 대표 패턴
실제로는 아래 패턴이 제일 많다.
1) recv() 한 번에 header+payload가 다 올 거라고 믿는다
recv() 결과를 바로 parse()
부하가 낮을 땐 우연히 맞는다.
부하가 올라가거나 OS/드라이버 조건이 바뀌면 깨진다.
2) 메시지 2개가 붙었는데 “뒤 메시지 헤더”를 payload로 착각한다
이 경우 흔한 로그가:
- payload length가 갑자기 비정상적으로 커짐
- inverse version mismatch
처럼 나온다.
사실은 payload가 아니라 “다음 메시지의 헤더”를 잘못 집어먹은 거다.
3) endian을 잘못 읽는다
Payload Type(2바이트), Payload Length(4바이트)는 네트워크 오더(big-endian)다.
특히 payload length를 little-endian으로 읽으면:
- 기대 길이가 16MB 같은 이상한 값으로 튀고
- 방어 코드가 없으면 버퍼가 터지거나, 타임아웃만 난다
그래서 길이 sanity check는 필수다.
디버깅할 때 빠르게 확인하는 포인트
TCP framing 문제는 “한 번만” 캡처를 잘 보면 바로 잡힌다.
나는 보통 아래 순서로 본다.
- RX 로그에 “recv된 바이트 수”를 찍고, 그 덩어리를 그대로 hex dump로 저장
- dump를 기준으로 앞 8바이트를 읽었을 때 payload length가 말이 되는지 확인
- “한 recv에 메시지 2개가 들어있던” 케이스가 있는지 확인
- 반대로 “헤더가 2~3번에 걸쳐 쪼개진” 케이스가 있는지 확인
- 버퍼 consume 로직이 msg_len만큼 정확히 빠지는지 확인(오프바이원 자주 터짐)
구현 팁: 방어 코드를 먼저 넣어두면 로그가 깨끗해진다
실무에서는 아래 방어가 특히 도움이 된다.
- payload length 상한선(
MAX_DOIP_PAYLOAD)을 작게 잡아서 조기 실패 - header field(version/inverse) 검사로 “프레이밍 깨짐”을 빨리 감지
- 메시지 단위 로그는 “헤더 + 첫 16바이트 payload”만 찍기(전체 덤프는 옵션)
프레이밍이 깨졌는데도 연결을 계속 유지하면,
이후 로그가 전부 노이즈가 된다.
추천 대상
- DoIP tester/ECU 스택을 직접 구현하는 개발자
- “가끔만” DoIP NACK / payload mismatch가 터지는 팀
- Wireshark로 보면 멀쩡한데, 내 파서만 자꾸 깨지는 경우
한 줄 요약
DoIP는 TCP stream 위에서 흐르기 때문에, Generic Header 8바이트와 payload length를 기준으로 “메시지 경계(framing)”를 직접 만들어 파싱해야 안정적으로 동작한다.
추천 키워드
DoIP, TCP stream, framing, payload length, parser, automotive ethernet
참고 자료
- ISO 13400-2:2025 - Road vehicles, Diagnostic communication over Internet Protocol (DoIP), Part 2
- Wireshark Display Filter Reference - DoIP (ISO13400) Protocol
- Wireshark User’s Guide - Following Protocol Streams
DevBJ | 오늘을살자, Log Today