DoIP 클라이언트를 오래 켜 두고 테스트하다 보면 이런 구간이 나온다.
- 처음 연결은 정상이다
- Routing Activation도 이미 끝난 상태다
- 한동안 아무 요청도 안 보낸다
- 다시 요청하면 첫 번째 UDS만 timeout처럼 사라진다
- 두 번째 시도부터는 재연결 후 다시 된다
이럴 때는 ECU 응답 timeout이나 UDS 서비스 문제부터 보기 쉽다.
하지만 실제로는 그보다 아래에서 이미 죽은 TCP socket을 아직 살아 있다고 믿는 상태가 먼저 문제인 경우가 많다.
오늘 메모는 DoIP에서 유휴 뒤 첫 요청만 죽을 때 stale socket을 어떻게 의심하고 정리할지다.
결론부터
DoIP에서 진단 세션은 UDS만의 상태가 아니다.
그 아래 TCP 연결 상태도 같이 살아 있어야 한다.
그런데 앱은 보통 이런 착각을 하기 쉽다.
socket fd exists
-> connected flag still true
-> Routing Activation was done before
-> so request should work
실제로는 중간 게이트웨이, ECU, 스위치, NAT 장비, 방화벽 중 누군가가
이미 연결을 정리했을 수 있다.
그래서 유휴 뒤 첫 요청 timeout은
UDS 요청 실패라기보다 stale TCP socket에 첫 패킷을 던진 결과일 때가 많다.
왜 첫 요청만 유독 죽어 보이나
유휴 상태에서는 송수신이 없으니
앱이 연결이 죽었다는 사실을 알 기회가 없다.
T0 Routing Activation 성공
T1 오랫동안 idle
T2 상대가 조용히 socket 정리
T3 앱은 모름
T4 첫 UDS 요청 전송 시도
T5 write는 성공처럼 보이거나 늦게 실패
T6 응답 대기 후 timeout
T7 그제야 재연결 시도
즉 실제 장애 시점은 T2인데,
개발자가 로그에서 보는 시점은 T6이라 원인이 멀어 보인다.
흔한 패턴 1: Tester Present가 없으니 세션이 아니라 연결이 먼저 죽는다
Tester Present를 안 보내면 보통은 세션 유지 문제가 먼저 떠오른다.
그런데 그보다 앞서 TCP 연결 자체가 정리되는 경우도 많다.
- UDS 세션 timeout 전에 TCP idle timeout이 먼저 온다
- 게이트웨이가 유휴 진단 소켓을 닫는다
- 중간 네트워크 장비가 오래된 flow를 제거한다
이 상태에서 앱은 “세션을 유지 못 했나?”라고 보기 쉽다.
하지만 실제 증상은 세션이 아니라 transport 경로가 이미 끊긴 것일 수 있다.
흔한 패턴 2: Alive Check와 Tester Present를 같은 의미로 본다
이 둘은 이름이 비슷해서 자주 섞인다.
하지만 보는 층위가 다르다.
Tester Present
-> UDS 세션 유지 목적
Alive Check / TCP keepalive
-> 연결 생존 여부 확인 목적
Tester Present를 주기적으로 보내더라도,
그 요청 자체가 stale socket 위에서 나가고 있다면 이미 늦다.
반대로 TCP keepalive나 Alive Check가 있다고 해도
UDS 세션 timeout 정책까지 자동으로 해결되지는 않는다.
즉 구현에서는
세션 유지와 연결 생존 확인을 분리해서 봐야 한다.
관련 맥락은 DoIP 통신이 가끔 끊긴다: Alive Check / TCP Keepalive / Tester Present를 분리해서 보자에서도 같이 볼 수 있다.
흔한 패턴 3: 재연결 조건이 “소켓 에러” 하나뿐이다
코드가 아래처럼 짜여 있으면 유휴 장애를 늦게 잡는다.
send()
-> no immediate error
-> wait response
-> timeout
-> retry same socket again
이 구조에서는 이미 죽은 연결을 한 번 더 믿게 된다.
보다 안전한 쪽은 이런 식이다.
idle duration exceeded
-> probe or reconnect policy check
-> first request sent
-> if no response and socket generation is old
-> close and re-open deterministically
핵심은 단순 timeout 횟수보다
현재 소켓이 마지막으로 정상 왕복한 시각을 같이 보는 것이다.
패킷은 안 이상해 보여서 더 헷갈린다
이 문제는 캡처를 보면 더 혼란스러울 수 있다.
- 첫 요청 패킷이 나간 것처럼 보인다
- 하지만 응답은 없다
- 재연결 후 같은 요청은 바로 성공한다
그러면 개발자는 첫 요청 payload를 의심하기 쉽다.
하지만 payload가 아니라 그 요청이 실린 연결 세대가 낡았던 것일 수 있다.
특히 DoIP에서 ECU Reset 후 다시 안 붙는다: 소켓 종료와 재연결 순서를 같이 봐야 한다에서 다룬 것처럼,
소켓 세대 관리를 느슨하게 두면 재연결 뒤에도 같은 착시가 반복된다.
구현 쪽에서 무난한 패턴
나는 보통 아래 세 가지를 분리한다.
1. UDS 세션 상태
- 현재 diagnostic session
- 마지막 Tester Present 시각
- pending request 존재 여부
2. TCP 연결 상태
- socket generation id
- 마지막 정상 송수신 시각
- 마지막 Alive Check 성공 시각
3. 재연결 정책
- idle threshold 초과 시 probe를 보낼지
- 첫 요청 timeout이면 바로 재연결할지
- 특정 ECU는 무조건 새 Routing Activation을 할지
간단히 적으면 이런 구조다.
if idle_too_long:
mark socket as suspicious
before sending new UDS:
if socket is suspicious:
reconnect_and_route_activate()
무조건 aggressive reconnect를 하라는 뜻은 아니다.
다만 “연결이 아직 살았는가”를 확인하는 기준이
단순 connected = true 플래그 하나면 부족하다는 뜻이다.
로그를 이렇게 남기면 빨라진다
이 이슈는 timeout 한 줄만으로는 거의 안 풀린다.
나는 보통 아래 항목을 같이 남긴다.
- 마지막 정상 UDS 왕복 시각
- 마지막 Tester Present 시각
- 마지막 Alive Check 또는 keepalive 성공 시각
- 현재 socket generation id
- 유휴 뒤 첫 요청이 어느 generation에서 나갔는지
이 다섯 줄이 있으면
“왜 첫 요청만 죽지?”보다
“이 요청이 이미 낡은 연결 위에서 나갔나?”를 먼저 볼 수 있다.
빠른 체크리스트
- 유휴 뒤 첫 요청 실패 시 현재 TCP socket이 마지막으로 언제 정상 왕복했는지 기록하는지 확인
- Tester Present와 Alive Check를 같은 상태 변수나 같은 타이머로 섞어 처리하지 않는지 확인
- 응답 timeout 뒤 같은 socket으로 한 번 더 보내는 재시도 루프가 있는지 확인
- 재연결 시 socket generation과 Routing Activation generation을 같이 갱신하는지 확인
- 특정 ECU나 게이트웨이가 idle 진단 연결을 먼저 닫는 패턴이 있는지 실제 캡처로 확인
함께 보면 좋은 글
- DoIP 통신이 가끔 끊긴다: Alive Check / TCP Keepalive / Tester Present를 분리해서 보자
- Routing Activation은 됐는데 첫 UDS가 실패한다: SA/TA와 재연결 순서를 같이 보자
- DoIP에서 ECU Reset 후 다시 안 붙는다: 소켓 종료와 재연결 순서를 같이 봐야 한다
한 줄 요약
DoIP에서 유휴 뒤 첫 UDS 요청만 timeout처럼 사라진다면, 세션보다 먼저 이미 끊긴 TCP socket을 앱이 계속 살아 있다고 믿고 있지 않은지 확인하는 편이 빠르다.
추천 키워드
DoIP, UDS, TCP socket, Routing Activation, Alive Check, Tester Present
DevBJ | 오늘을살자, Log Today