70 KB짜리 recipe를 S7F3에 담아 보냈다. send()는 70,031 byte를 다 썼다고 돌려준다. 그리고 답이 없다. T3가 터지기도 전에 다음 send()가 EPIPE로 깨지고, 설비 쪽 packet log를 열어 보면 그 message가 아예 없다. Reject.req도 없고 S9F7도 없다. 기록 자체가 없다.
item header 탓이 아니다. SECS-II(E5) item header는 length byte를 1개에서 3개까지 선언할 수 있고, 3개면 16,777,215 byte를 길이로 적을 수 있다. 16 MB다. 문제는 그 item을 태우고 가는 HSMS frame이 먼저 죽는다는 것. 이 listener에서 경계는 message length field 65,536이고, 65,537에서 socket이 RST로 끊겼다. 선언할 수 있는 상한과 도착할 수 있는 상한은 다른 숫자다.
length byte 수는 선언하는 값이다
E5 item header의 첫 byte는 두 조각으로 나뉜다. 위쪽 6 bit가 format code, 아래쪽 2 bit가 length byte가 몇 개 따라오는지다. binary item의 format code는 8이라서, 첫 byte는 length byte 1개면 21, 2개면 22, 3개면 23이 된다. 그 다음에 선언한 개수만큼 big-endian 길이가 오고, 그 길이가 item 본문 byte 수다.
여기서 host 개발자가 자주 넘어가는 지점: 개수는 길이에서 계산되는 게 아니라 선언된다. 400 byte를 2 length byte로 적어도 되고 3 length byte로 적어도 된다. 둘 다 적법하고, 둘 다 통한다. 아래 capture의 2번과 3번이 그걸 한 socket에서 확인한 것이다.
capture — 같은 payload, length byte 개수만 다르게
세 개의 real socket으로 listener를 밖에서 두드렸다. hex는 host 쪽에서 직접 frame을 만들고 받은 그대로고, 설비 쪽 packet log와 교차 확인했다.
1. Select.req → Select.rsp, 그리고 S1F13 → S1F14
H→E 00 00 00 0A 00 0B 00 00 00 01 00 00 01 01
E→H 00 00 00 0A 00 0B 00 00 00 02 00 00 01 01
H→E 00 00 00 0C 00 0B 81 0D 00 00 00 00 01 02 01 00
E→H 00 00 00 37 00 0B 01 0E 00 00 00 00 01 02 01 02
21 01 00 01 02 41 15 ...
여기는 준비 작업이다. S1F13을 먼저 안 보내면 이 listener는 data message에 SxF0을 돌려주고 끝난다. 이 부분은 E30 통신 gate 쪽 얘기고, 지금 보려는 건 그 다음이다.
2. PPBODY 400 byte, length byte 2개 → S7F4
H→E 00 00 01 AA 00 0B 87 03 00 00 00 00 01 03
01 02 41 09 45 54 43 48 5F 42 41 53 45 22 01 90 ... (wire 430 byte)
E→H 00 00 00 0D 00 0B 07 04 00 00 00 00 01 03 21 01 00
length field 00 00 01 AA = 426 = header 10 + body 416이다. body는 L[2] 2 byte, PPID A "ETCH_BASE" 11 byte, PPBODY item header 22 01 90 3 byte, payload 400 byte. 22가 length byte 2개를 선언하고 01 90이 400이다. 답은 21 01 00, ACKC7 0.
3. 같은 400 byte, length byte 3개 → 역시 S7F4
H→E 00 00 01 AB 00 0B 87 03 00 00 00 00 01 05
01 02 41 09 45 54 43 48 5F 42 41 53 45 23 00 01 90 ... (wire 431 byte)
E→H 00 00 00 0D 00 0B 07 04 00 00 00 00 01 05 21 01 00
payload는 2번과 같은 400 byte인데 wire는 1 byte 길어졌다. item header가 22 01 90에서 23 00 01 90으로 늘었기 때문이고, 그게 전부다. 답은 2번과 같은 ACKC7 0. decoder는 길이를 보고 추측하지 않는다 — 선언을 읽는다.
4. 경계: length field 65,536은 통과한다
H→E 00 01 00 00 00 0B 87 03 00 00 00 00 01 04
01 02 41 09 45 54 43 48 5F 45 44 47 45 22 FF E6 ... (wire 65,540 byte)
E→H 00 00 00 0D 00 0B 07 04 00 00 00 00 01 04 21 01 00
PPBODY 65,510 byte, length field 00 01 00 00 = 65,536 = 정확히 64 KiB다. 통과하고 ACKC7 0이 돌아온다. 참고로 이 body는 65,535 이하라서 아직 length byte 2개(22 FF E6)로 충분하다.
5. 한 byte 더: 65,537에서 socket이 끊긴다
H→E 00 01 00 01 00 0B 87 03 00 00 00 00 02 03
01 02 41 09 45 54 43 48 5F 45 44 47 45 22 FF E7 ... (wire 65,541 byte)
E→H (없음 — ECONNRESET)
PPBODY를 1 byte 늘려 65,511로 만들었다. length field는 65,537. send()는 65,541 byte를 다 썼다고 하고, recv()는 ECONNRESET를 던진다. 같은 socket에 Linktest.req를 하나 더 보내려 하면 EPIPE다. 이 message는 설비 쪽 packet log에 RX로 남지 않았다 — 같은 연결의 Select.req와 S1F13은 남아 있고, S7F3만 없다. listener는 4 byte 길이 prefix를 읽은 시점에 socket을 버린다. body가 도착하기도 전이고, 그래서 기록할 message가 없다.
6. 70,000 byte PPBODY, length byte 3개 → 역시 RST
H→E 00 01 11 8B 00 0B 87 03 00 00 00 00 03 03
01 02 41 09 45 54 43 48 5F 48 55 47 45 23 01 11 70 ... (wire 70,031 byte)
E→H (없음 — ECONNRESET)
여기서 처음으로 length byte 3개가 필요해서 쓰였다. 23 01 11 70 = 70,000이고, 16 MB까지 쓸 수 있는 header다. 그런데 length field가 70,027이라 5번과 같은 자리에서 죽는다. 이 listener에서 3 length byte item은 선언만 가능하고 도착은 불가능하다 — 65,535를 넘는 item은 그 자체로 frame을 65,536 위로 밀어 올리기 때문이다.
65,536은 이 listener의 숫자다
분명히 해 두자. 64 KiB는 구현이 고른 값이고 SEMI 숫자가 아니다. E37은 message 앞에 4 byte length field를 두고, 그 field의 폭 자체는 훨씬 큰 message를 표현한다. 실제 상한은 설비 벤더가 코드에 박아 둔 값이고, 나는 이 listener의 값(65,536 통과 / 65,537 RST)만 byte로 확인했다. 다른 설비가 어디서 끊는지는 확인하지 못했다. SECS-I(E4)로 올라온 tool은 또 다른 천장을 가지는데 그건 이번 run에서 건드리지 않았다.
그리고 S7F4의 ACKC7 0도 그대로 믿으면 안 된다. 이 listener는 data message에 고정된 ack를 돌려주는 reply table을 쓴다(코드에서 읽었다). 그러니까 위 2·3·4번의 21 01 00이 증명하는 건 frame이 받아들여져 stream 7 function 3으로 올바르게 decode됐다는 것까지다. 진짜 설비가 그 recipe를 저장하겠다는 뜻이 아니다. ACKC7 값의 의미는 E5 표와 벤더 문서에서 가져와야 하고, 둘이 다르면 tool이 맞다.
host 쪽에서 할 일
- interface spec에서 최대 message size를 숫자로 받아 둔다. "HSMS 지원"은 답이 아니다. 끊는 자리가 적혀 있지 않으면 통합 시험 첫날에 recipe로 그걸 알아내게 된다.
- PPBODY 길이를 보내기 전에 검사한다. 상한을 넘으면 socket을 죽이는 게 아니라 host 안에서 거절해야 한다. socket이 죽으면 그 연결에 걸려 있던 다른 transaction도 같이 죽는다.
- 큰 recipe는 S7F3로 나르지 않는다. E5에는 host가 긴 PPBODY를 쪼개 보내는 표준 수단이 없다. tool이 그 frame을 받거나, recipe가 다른 경로로 가거나 둘 중 하나고, 이건 host stack이 고칠 문제가 아니라 interface spec에서 정할 문제다.
- 진단 로그에 length field를 남긴다. "no reply"와 "connection reset"은 다른 증상이다. 뒤쪽이면 T3를 기다릴 이유가 없고, 설비 로그에는 아무것도 없을 테니 network 탓이라는 소리를 듣게 된다. 내가 보낸 length field 숫자가 그 주장을 끝내는 유일한 증거다.
큰 PPBODY 전에 S7F1로 승인을 먼저 받는 쪽이 왜 싼지는 recipe는 S7F3부터 보내는 게 아니다에 따로 적었다. 다만 S7F1도 frame 상한은 구해주지 못한다. 그건 S7F3가 socket에 올라간 뒤의 일이다.
같은 경계를 직접 때려 보고 싶으면 SECS/GEM simulator에 passive listener를 띄우고 length field만 1씩 올려 보면 된다. 65,536에서 ack가 오고 65,537에서 socket이 사라지는 걸 보는 데 1분이면 된다.