← 전체 글
SECS/GEM/약 17분 읽기/— 조회

장비가 S9를 보내줄 거라 기대하고 호스트를 짜면 안 된다

없는 Stream, 없는 Function, 깨진 본문을 리스너에 보낸 캡처. 돌아온 건 S9 하나와 SxF0 abort다. E5 stream 9와 MHEAD 읽기.

SECS/GEMMESSCADA문제 해결체크리스트

호스트가 S1F3을 보냈고 T3가 만료됐다. 캡처를 열어보면 요청 바이트는 나갔고, 그 뒤로 아무것도 안 들어왔다. 이쯤에서 개발자들이 자주 하는 말이 있다. "장비가 S9F5라도 보내줬으면 뭐가 틀렸는지 알았을 텐데."

문제는 그 기대가 자주 빗나간다는 것이다. SEMI E5는 stream 9에 시스템 에러 메시지를 정의해 두었지만, 장비가 어떤 상황에서 그걸 보내는지는 장비마다 다르다. 잘못된 메시지를 리스너 하나에 여러 형태로 던져봤더니, 돌아온 응답은 세 종류로 갈렸다. 그중 stream 9는 하나뿐이었다.

거절당한 primary message가 돌아오는 세 가지 형태: 헤더 바이트 3이 00인 SxF0 abort, 바이트 2가 09이고 본문이 B[10] MHEAD인 stream 9, 그리고 아무것도 오지 않아 호스트가 T3로 닫아야 하는 침묵 primary message 송신, 정상 응답 없음 무엇이 돌아왔나 abort — 같은 SystemBytes byte 3 = 00 stream 9 — MHEAD로 식별 byte 2 = 09 · B[10] 아무것도 안 옴 T3 SxF0 S9Fx 없음

같은 소켓, 다섯 가지 잘못된 메시지

시뮬레이터의 passive 리스너(127.0.0.1:5501)에 파이썬으로 소켓을 직접 열었다. fault는 하나도 걸지 않았다. 먼저 세션을 세운다.

TX Select.req     00 00 00 0A 00 0B 00 00 00 01 00 00 02 01
RX Select.rsp     00 00 00 0A 00 0B 00 00 00 02 00 00 02 01

SessionID 00 0B은 11. Select.rsp에서는 데이터 메시지가 Function을 싣는 헤더 바이트 3 자리에 Select Status가 들어가는데, 여기가 00이니 수락이다(E37, Select.rsp). SType은 바이트 5의 02다. 여기서부터 데이터 메시지를 보낼 수 있다.

첫 번째는 멀쩡한 S1F1이다.

TX S1F1 W=1       00 00 00 0A 00 0B 81 01 00 00 00 00 02 02
RX                00 00 00 0A 00 0B 01 00 00 00 00 00 02 02

보낸 쪽 바이트 2가 81이다. 최상위 비트가 W-bit, 나머지 7비트가 Stream 1. 바이트 3이 Function 1이다. 돌아온 쪽은 바이트 2가 01(W-bit 0, Stream 1), 바이트 3이 00. Function 0이다. E5에서 어느 stream이든 Function 0은 abort transaction이고, "네 요청은 여기서 끝났다, 답은 없다"는 뜻이다. S1F2가 아니다. SystemBytes 00 00 02 02는 요청과 같은 값이라 트랜잭션 매칭은 정상적으로 걸린다.

이제 일부러 틀린 것들이다.

TX S1F1 W=1, SessionID 99   00 00 00 0A 00 63 81 01 00 00 00 00 02 03
RX                          00 00 00 0A 00 63 01 00 00 00 00 00 02 03

TX S63F1 W=1                00 00 00 0A 00 0B BF 01 00 00 00 00 02 04
RX                          00 00 00 0A 00 0B 3F 00 00 00 00 00 02 04

TX S1F63 W=1                00 00 00 0A 00 0B 81 3F 00 00 00 00 02 05
RX                          00 00 00 0A 00 0B 01 00 00 00 00 00 02 05

세 개 모두 E5가 stream 9 메시지를 정의해 둔 상황이다. 모르는 Device ID는 S9F1, 구현하지 않은 Stream은 S9F3, 구현하지 않은 Function은 S9F5. 그런데 실제로 온 건 전부 SxF0이다.

  • SessionID 99는 이 장비의 것이 아닌데, 장비는 S9F1을 보내는 대신 받은 00 63을 그대로 되돌려 담고 S1F0을 보냈다. HSMS의 SessionID 필드가 E5에서 말하는 Device ID 자리다(E37, Header 정의). 즉 여기서 Device ID 불일치를 잡아낼 기회가 있었는데 안 잡았다.
  • S63F1의 응답은 바이트 2가 3F, 즉 W-bit 0에 Stream 63이고 바이트 3이 00. S63F0이다. 없는 stream에 대해 stream 번호를 그대로 되받아 abort를 보낸다.
  • S1F63도 마찬가지로 S1F0.

여기서 호스트 코드가 다치는 지점은 분명하다. 스트림 9만 골라 보는 수신 경로를 짜 두었다면 이 세 건은 전부 "정체 불명의 응답"으로 흘러가고, 로그에는 T3 만료만 남는다. Function 0은 실패 신호다. 실패로 취급하고 트랜잭션을 그 자리에서 닫아야 한다.

S9F7은 본문이 깨졌을 때 왔다

stream 9가 실제로 온 건 한 번, 본문을 일부러 잘라 보냈을 때다.

TX S1F3 W=1  00 00 00 10 00 0B 81 03 00 00 00 00 02 06 01 02 B1 04 00 00
RX S9F7      00 00 00 16 00 0B 09 07 00 00 00 00 03 E9 21 0A 00 0B 81 03 00 00 00 00 02 06

보낸 쪽 본문은 01 02 B1 04 00 00이다. List 2개를 선언해 놓고 첫 아이템이 U4 4바이트라고 써 놓았는데 실제로는 2바이트만 있다. 아이템 트리가 본문 끝을 넘어간다.

돌아온 프레임을 쪼개면 이렇다.

00 00 00 16    길이 22 (헤더 10 + 본문 12)
00 0B          SessionID 11
09             바이트 2 — W-bit 0, Stream 9
07             Function 7 → S9F7
00             PType 0 (SECS-II)
00             SType 0 (데이터 메시지)
00 00 03 E9    SystemBytes 1001 — 장비가 새로 뽑은 값
21 0A          아이템 헤더 — 포맷 B, 길이 바이트 1개, 10바이트
00 0B 81 03 00 00 00 00 02 06    MHEAD

S9F7은 E5의 IDN, Illegal Data다. 그리고 이 캡처에 이 글의 요지가 두 개 다 들어 있다.

첫째, S9의 SystemBytes는 여러분 요청의 것이 아니다. 요청은 00 00 02 06이었는데 S9F7은 00 00 03 E9, 1001을 달고 왔다. 장비가 자기 카운터에서 뽑은 값이다. SystemBytes로 키를 잡은 트랜잭션 테이블에 이건 안 걸린다.

둘째, MHEAD가 그걸 메운다. 본문은 B[10] 아이템 하나이고, 내용은 문제가 된 메시지의 헤더 10바이트 그대로다. 다시 헤더로 읽으면 SessionID 11, 바이트 2 81이니 W-bit 1에 Stream 1, Function 3, SystemBytes 00 00 02 06. 앞에서 보낸 S1F3이 정확히 그 값이었다. 마지막 4바이트로 원 요청을 찾으면 된다.

아이템 헤더 21이 어떻게 나온 건지는 SECS-II 길이 바이트는 바이트 수를 센다에 정리해 뒀다.

소켓은 멀쩡했다

TX Linktest.req   00 00 00 0A 00 0B 00 00 00 05 00 00 02 07
RX Linktest.rsp   00 00 00 0A 00 0B 00 00 00 06 00 00 02 07

TX Separate.req   00 00 00 0A 00 0B 00 00 00 09 00 00 02 08
   (응답 없음 — E37에 Separate.rsp는 없다)

Linktest는 즉답이다. SType 5에 SType 6이 같은 SystemBytes로 돌아온다. 소켓도 세션도 멀쩡했고, 앞의 이상 동작은 전부 데이터 메시지 레벨에서 일어난 일이다. Separate.req에 답이 없는 건 고장이 아니라 E37이 그 SType에 응답을 정의하지 않았기 때문이다.

stream 9가 무엇인지

E5는 stream 9를 시스템 에러용으로 잡아두었다. 받은 메시지를 애플리케이션 수준에서 해석하기 전에, 프로토콜·포맷 수준에서 거절할 때 쓴다. 전부 W-bit 0의 primary message이고 응답이 정의되어 있지 않다.

메시지E5 약어언제본문
S9F1UDNDevice ID를 모른다MHEAD
S9F3USNStream을 구현하지 않았다MHEAD
S9F5UFNFunction을 구현하지 않았다MHEAD
S9F7IDN본문 데이터가 잘못됐다MHEAD
S9F9TTN트랜잭션 타이머가 만료됐다SHEAD
S9F11DLN데이터가 너무 길다MHEAD
S9F13CTN대화(conversation) 타임아웃L[2] {MEXP, EDID}

S9F9와 S9F13만 본문이 MHEAD가 아니다. S9F9는 SHEAD를 싣는다. 응답을 기다리다 타이머가 터진 그 메시지, 즉 보낸 쪽이 보관하고 있던 헤더다. S9F13은 헤더가 아예 없고 아이템 두 개짜리 List다. MEXP는 기다리던 메시지 이름을 담은 ASCII("S6F12" 같은 형태), EDID는 기다리던 데이터의 ID이고 형식은 장비가 정한다. 파서를 B[10] 하나로 통일해 짜 두면 이 둘에서 깨진다.

호스트 개발자가 놓치기 쉬운 게 하나 더 있다. S9F9는 방향이 반대다. 장비가 여러분에게 "네가 제때 응답을 안 줬다"고 말하는 것이다. S9F9가 들어오기 시작하면 장비를 보지 말고 호스트의 응답 지연을 봐야 한다. 장비 알람 대시보드에 얹어두면 원인과 정반대인 쪽을 계속 보게 된다.

HSMS 레벨의 거절은 stream 9가 아니다

stream 9는 E5의 것이고, 그 아래 HSMS에는 자기 몫의 거절 메커니즘이 따로 있다. E37의 Reject.req(SType 7)다. reason 코드는 지원하지 않는 SType, 지원하지 않는 PType, 열려 있지 않은 transaction, 그리고 SELECTED가 아닌 상태에서 도착한 메시지를 가른다.

구분이 실무에서 의미가 있는 이유는 어느 층을 고쳐야 하는지가 달라서다. Reject.req가 오면 프레이밍이나 세션 상태 문제고, stream 9가 오면 프레임은 정상이고 그 안의 SECS-II 내용이 문제다. 참고로 위 캡처의 리스너는 Reject.req도 보내지 않는다.

그래서 호스트는 어떻게 짜야 하나

  • 실패 신호를 셋 다 받아라. SxF0(Function 0), stream 9, 그리고 타임아웃. 위 캡처에서 다섯 건 중 넷은 SxF0이었고 stream 9는 하나였다. 어느 하나만 보는 코드는 나머지에서 T3를 꽉 채운다.
  • SxF0은 같은 SystemBytes로, S9는 MHEAD로 매칭해라. abort는 여러분 트랜잭션의 짝이고 S9는 장비가 새로 여는 primary라, 같은 테이블에 같은 키로 넣으면 S9 쪽이 통째로 미아가 된다.
  • S9에 응답하지 마라. stream 9에는 응답 function이 정의되어 있지 않다. 거기에 뭘 보내면 그건 장비가 요청한 적 없는 새 primary message다.
  • S9가 온다고 가정한 복구 로직은 만들지 마라. 없는 SessionID에도, 없는 Stream에도, 없는 Function에도 이 장비는 stream 9를 안 보냈다. 보내는 장비도 있다. 어느 쪽이든 타임아웃은 항상 있어야 하는 마지막 그물이다.
  • S9F9는 호스트 쪽 지표로 세라. 장비 알람이 아니라 우리 쪽 응답 지연의 증거다.
  • 침묵을 만났을 때 세션부터 갈라라. 같은 소켓에 Linktest.req를 하나 보내면 몇 초 안에 끝난다. Linktest.rsp가 오면 세션은 살아 있고 문제는 데이터 메시지 쪽이다. 그것도 안 오면 T6 영역이다. 어느 타이머가 울렸는지 구분하는 법은 T3, T6, T7, T8 구분하기에 있다.

확인하지 못한 것

위 표의 약어와 본문 구조, Function 0의 의미, Reject.req의 reason 구분은 E5와 E37의 정의를 따른 것이지 특정 판본의 조항 번호를 대조한 것은 아니다. 조항 번호를 인용해야 하는 문서를 쓴다면 여러분 손에 있는 판본에서 직접 확인하는 게 맞다. S9F13의 EDID는 형식 자체가 장비 정의라 여기서 값을 단정할 수 없다.

그리고 이 캡처가 보여주는 건 리스너 하나의 동작이다. 실제 장비 중에는 S9를 성실히 보내는 것도 있다. 다만 호스트를 짤 때 그쪽에 기대면, 안 보내는 장비를 만나는 날 로그에 남는 건 타임아웃 한 줄뿐이다.

여기 나온 바이트는 전부 공개된 SECS/GEM 시뮬레이터의 passive 리스너에 소켓을 열고 직접 주고받은 것이다. 같은 방식으로 없는 Stream 번호를 하나 던져보면, 여러분 호스트 스택이 그 응답을 어떻게 기록하는지 바로 나온다. 그게 진짜 확인할 값이다.