커스텀 프로토콜 디코딩
내장 포맷으로는 인식되지 않는 비공개 또는 사내 소켓 프로토콜을 캡처했는데, 남는 건 바이트 더미뿐이신가요? 커스텀 디코딩은 짧은 스크립트로 도구에게 그것을 읽는 법을 가르쳐 줍니다. 연속된 바이트 스트림을 개별 메시지로 잘라내고, 프로토콜 헤더를 벗겨내고, 필요하면 압축을 풀고, 나머지는 도구의 자동 구조 인식에 맡기세요.
이 페이지는 직접 디코더를 작성하려는 분을 위한 것입니다. 함수를 어떻게 작성하는지, 언제 호출되는지, 반환값이 무엇을 뜻하는지 설명합니다.
1. 개념: 프레임 단위 디코딩 훅
섹션 제목: “1. 개념: 프레임 단위 디코딩 훅”여러분은 디코딩 훅을 작성하고, 도구는 그 훅에 연결의 바이트를 넘기면서 하나의 질문을 반복합니다. “여기서부터 완전한 메시지 하나를 잘라낼 수 있나요?” 여러분이 메시지 하나를 잘라내고 사용한 바이트 수를 알려주면, 도구는 나머지를 가지고 다시 묻고, 스트림이 완전히 잘릴 때까지 이 과정을 반복합니다.
네트워킹 프레임워크에서 누적 바이트 디코더를 작성해 본 적이 있다면 사고방식은 동일합니다. 연속된 바이트 스트림이 주어지면, 메시지 경계를 여러분이 직접 정한다는 것입니다. 두 가지 핵심 규약이 있습니다.
- 방향은 신경 쓸 필요 없습니다: 연결의 송신 스트림과 수신 스트림은 각각 별도로 여러분의 훅을 거치며, 도구가 잘라낸 각 메시지에 방향을 자동으로 표시합니다. 스크립트는 “어떻게 자를지”만 처리하면 됩니다.
- 잘라낸 뒤의 렌더링은 처리하지 않습니다: 여러분이 밀어낸 모든 메시지는 다시 자동 인식을 거칩니다. 그 안에 protobuf, JSON, plist가 들어 있으면 한 단계 더 구조화되어 파싱됩니다. 여러분은 프레이밍, 헤더 벗기기, 압축 해제만 책임지면 됩니다.
2. 디코드 함수: decode(buf, out)
섹션 제목: “2. 디코드 함수: decode(buf, out)”디코더 전체는 함수 하나입니다.
function decode(buf, out) { // buf: 현재 파싱 대기 중인 바이트 스트림; out: 출력 수집기; return: 이번에 소비한 바이트 수}입력, 출력, 반환값이 무엇인지
섹션 제목: “입력, 출력, 반환값이 무엇인지”buf (입력): 파싱 대기 중인 바이트 스트림
- 타입:
ArrayBuffer. 그 내용은 “마지막으로 소비를 끝낸 지점부터 현재 끝까지” 남아 있는 모든 바이트입니다. - 속성
buf.byteLength: 지금 남아 있는 바이트 수 (호출할 때마다 늘어날 수 있으며, 메시지를 잘라낼 만큼 충분한지 판단하는 기준입니다). - ⚠️ 이것은
Uint8Array가 아니라 원시 바이트 버퍼이므로, 바이트를 직접 인덱싱할 수 없습니다 (buf[0]은undefined를 반환). 바이트를 읽으려면 3절의 내장 헬퍼(u8/u16be/sub…)를 쓰거나, 직접 뷰를 감싸세요:new Uint8Array(buf),new DataView(buf).
out (출력): 출력 수집기
- 타입: 일반 배열.
- 메서드
out.push(메시지 바이트): 메시지 하나를 밀어냅니다. 한 번의 호출에서 여러 개를 밀어낼 수 있습니다. - “메시지 바이트”는 다음이 될 수 있습니다: 내장 헬퍼(
sub(...),gunzip(...)등)가 반환한ArrayBuffer, 또는 문자열. 그 외 타입은 무시됩니다.
반환값: 소비한 바이트 수
- 타입: 정수(
number),buf의 시작 지점부터 사용한 바이트 수. > 0: 앞에서 이만큼 바이트를 먹어 치우고 메시지 하나를 잘라냈다는 뜻 → 도구는 그것을 버리고 나머지를 가지고 다시 호출합니다.0/return없음 / 음수: “앞에 아직 완전한 메시지가 없다” 또는 “더 이상 잘라낼 것이 없다”는 뜻 → 도구는 루프를 멈추고 남은 바이트를 원시 데이터로 표시합니다.- 반환값은 (오버런 방지를 위해) 버퍼 길이 안으로 자동 클램핑되지만, 실제로 사용한 진짜 바이트 수를 반환하세요.
언제 호출되는지 (생명주기)
섹션 제목: “언제 호출되는지 (생명주기)”- 도구는 연결의 송신 스트림에 한 번, 수신 스트림에 한 번 패스를 실행합니다. 각 패스에서
decode는 루프로 호출됩니다. 매번 남은 미소비 바이트를 넘겨주면, 여러분은 메시지 하나를 잘라내고 소비한 수를 반환하며, 0을 반환할 때까지 이 과정이 반복됩니다. - 메시지는 소비가 확정되어야만 나타납니다: 여러분이 밀어낸 메시지는 > 0을 함께 반환할 때만 표시됩니다. 밀어냈는데 0을 반환하면, 루프가 멈추고 그 push는 버려집니다. 따라서 메시지를 밀어내는 것은 반드시 그것이 몇 바이트를 차지했는지 반환하는 것과 짝을 이뤄야 합니다.
- 루프가 끝난 뒤, 소비되지 않은 후행 바이트는 사라지지 않습니다. 하나의 원시 데이터 조각으로 표시됩니다 (예: 미완성 메시지의 뒷부분).
상태와 멱등성
섹션 제목: “상태와 멱등성”- 각 스트림(방향)은 별도의, 완전히 새로운 스크립트 환경을 갖습니다. 송신 스트림과 수신 스트림은 서로 섞이지 않습니다.
- 호출 간에 상태(카운터, 이전 메시지의 타입 등)를 유지하려면 변수를
decode바깥에 선언하세요. 그것들은 같은 스트림 안에서는 호출 간에 유지되고, 방향이 바뀌거나 재디코딩할 때 초기화됩니다. - 디코딩은 이미 캡처된 데이터 위에서 실행되며, 반복 재실행이 가능합니다. 스크립트를 수정하고 저장한 뒤 다시 클릭하면, 같은 연결을 새 규칙으로 재디코딩합니다.
오류는 아무것도 망가뜨리지 않습니다
섹션 제목: “오류는 아무것도 망가뜨리지 않습니다”- 스크립트가 예외를 던지거나 한 번의 실행이 타임아웃되면, “이번에는 아무것도 소비하지 않음”으로 처리되어 남은 바이트가 원시 데이터로 표시되고, 캡처는 데이터 손실 없이 계속 실행됩니다.
- 다시 말해, 망가진 스크립트가 할 수 있는 최악은 디코딩에 실패해 원시 바이트를 남기는 것뿐입니다. 연결을 죽이지 않으니 마음껏 수정하세요.
- 그리고 오류는 삼켜지지 않습니다: 예외와 타임아웃은 결과 위쪽의 디버그 출력 패널(5절 참고)에 표시되며, 송신
↑인지 수신↓스트림인지 라벨이 붙습니다. 그것을 따라가면 곧장 해결로 이어집니다.
3. 내장 헬퍼 (스크립트에서 바로 사용 가능)
섹션 제목: “3. 내장 헬퍼 (스크립트에서 바로 사용 가능)”바이트 읽기, 정수 읽기, 텍스트 변환, 압축 해제 같은 흔한 작업은 모두 내장되어 있어, 직접 다시 만들 필요가 없습니다.
| 분류 | 시그니처 | 설명 |
|---|---|---|
| 서브슬라이스 추출 | sub(buf, off[, len]) |
off부터 슬라이스(len을 생략하면 끝까지), ArrayBuffer 반환 |
| 정수 읽기 | u8(buf, off) / u16be / u16le / u32be / u32le (buf, off) |
빅엔디안 / 리틀엔디안으로 부호 없는 정수 읽기 |
| 텍스트 변환 | hex(buf) / ascii(buf) |
16진수 문자열로 변환 / 텍스트로 읽기 |
| 압축 감지 | gzipMagic(buf) |
gzip인지 여부 (불리언 반환) |
| 압축 해제 | gunzip / inflate / unzstd / lz4dtx (buf) |
압축 해제; 불가능하면 오류 없이 입력을 그대로 반환 |
| 디버그 | log(...args) / console.log(...args) |
디버그 패널에 출력 (5절 참고) |
표준 바이트 읽기/쓰기 기능(DataView, Uint8Array 등)도 바로 사용할 수 있습니다.
4. 예제
섹션 제목: “4. 예제”① 길이 접두사 [4바이트 빅엔디안 길이][페이로드], 가장 흔한 비공개 프로토콜 형태:
function decode(buf, out) { if (buf.byteLength < 4) return 0 // 길이 헤더가 아직 다 도착하지 않음, 대기 const total = 4 + u32be(buf, 0) // 전체 메시지 = 4바이트 헤더 + 페이로드 if (buf.byteLength < total) return 0 // 전체 메시지가 아직 다 도착하지 않음, 대기 out.push(sub(buf, 4, total - 4)) // 헤더를 벗기고 페이로드를 자동 인식에 넘김 return total // 이 메시지를 소비, 다음 것을 자르러 계속}② 구분자 / 줄 단위로, 개행 같은 표시로 프레임을 자르기:
function decode(buf, out) { const i = ascii(buf).indexOf('\n') if (i < 0) return 0 // 아직 개행이 없음, 대기 out.push(sub(buf, 0, i)) // 이 줄을 밀어냄 (개행 제외) return i + 1 // 개행까지 함께 소비}③ 전체를 한 번에 처리, 스트림 전체를 하나의 메시지로 취급, 예를 들어 전체를 압축 해제:
function decode(buf, out) { out.push(gzipMagic(buf) ? gunzip(buf) : buf) // gzip이면 풀기 return buf.byteLength // 전부 소비, 루프가 바로 끝남}④ 타입에 따라 분기, 헤더에 타입 필드가 있어 타입마다 다르게 처리:
function decode(buf, out) { if (buf.byteLength < 4) return 0 const total = 4 + u32be(buf, 0) if (buf.byteLength < total) return 0 const type = u8(buf, 4) // 첫 바이트가 메시지 타입 const body = sub(buf, 5, total - 5) out.push(type === 2 ? gunzip(body) : body) // 타입 2는 압축됨 return total}에디터에는 이 모든 패턴에 대한 바로 쓸 수 있는 템플릿이 있습니다. 하나 삽입하고 숫자 몇 개만 손보면 바로 동작합니다.

5. 사용법과 디버깅 방법
섹션 제목: “5. 사용법과 디버깅 방법”- 작성하고 저장하기: 디코더 에디터에서 만들고, 이름을 붙이고, 저장합니다. 템플릿을 삽입해 시작하고, 내장 함수 빠른 참조를 바로 옆에 두세요.
- 적용하기: 이해가 안 되는 연결에 대해 마우스 오른쪽 버튼을 눌러 “다음으로 디코드” → 여러분의 디코더를 선택하면, 그 연결의 송수신 트래픽 전체가 즉시 여러분의 규칙대로 잘려서 표시됩니다.
- 디버그와 변수 확인:
decode안에서log(...)또는console.log(...)로 어떤 값이든 출력하세요: 바이트 수, 타입 필드,hex(sub(buf, 0, 8))등. 출력은 “다음으로 디코드” 결과 위쪽의 “디버그 출력” 패널에 나타나며, 각 줄은 송신↑인지 수신↓스트림에서 왔는지 라벨이 붙습니다. 스크립트 오류와 타임아웃도 같은 패널에 방향 라벨과 함께 표시됩니다. 이렇게 출력하고 위치를 짚는 것이 무작정 수정하는 것보다 훨씬 빠릅니다.샌드박스에는 완전한 브라우저 / Node의
console이 없습니다.console.log와 그에 상응하는log만 이 패널에 연결되어 있습니다.TextDecoder,fetch,setTimeout등은 사용할 수 없습니다. 바이트를 텍스트로 읽으려면ascii(buf)를 쓰세요. - 수정하고 바로 확인하기: 재캡처가 필요 없습니다. 스크립트를 저장하고 “다음으로 디코드”를 다시 클릭하면, 같은 연결이 새 규칙으로 그 자리에서 재디코딩됩니다. 원하는 대로 잘릴 때까지 반복하세요.
- 구조로 넘어가기: 디코드된 각 조각은 자동 인식으로 되돌려지므로, protobuf, JSON, plist가 한 단계 더 구조화되어 파싱되며, 데이터 검사 및 디코딩의 여러 뷰를 통해 볼 수 있습니다.
- 디코더는 이름을 붙여 목록으로 보관할 수 있으며, 언제든 추가, 수정, 삭제할 수 있습니다.
6. 언제 사용하는가
섹션 제목: “6. 언제 사용하는가”- 내장 포맷으로 인식되지 않는 사내 또는 비공개 소켓 프로토콜(흔한 형태는 길이 접두사 + protobuf / JSON / 바이너리)을 캡처했을 때, 디코더를 작성해 잘라내고 읽을 수 있는 구조로 복원하세요.
- 커스텀 헤더로 감싸졌거나 한 차례 압축을 거친 바이트 덩어리를 읽을 수 있는 내용으로 되돌리고 싶을 때.
7. API 레퍼런스
섹션 제목: “7. API 레퍼런스”디코드 진입점, 내장 헬퍼 함수, 런타임 환경을 한데 모은 빠른 참조입니다. buf를 받는 모든 함수는 ArrayBuffer 또는 문자열을 받습니다.
디코드 진입점
섹션 제목: “디코드 진입점”decode(buf, out) → number| 용도 | 디코더의 유일한 진입점, 반드시 구현해야 함. buf의 앞에서 메시지를 잘라 out에 push하고, 이번에 소비한 바이트 수를 반환합니다. |
buf |
ArrayBuffer, 파싱 대기 중인 남은 바이트 스트림. 길이는 buf.byteLength로 확인. 바이트를 인덱싱할 수 없으니 아래 헬퍼나 new DataView(buf) / new Uint8Array(buf)를 쓰세요. |
out |
Array, 출력 수집기. out.push(bytes)로 메시지 하나를 밀어냅니다(bytes는 ArrayBuffer 또는 문자열; 그 외 타입은 무시). 한 번에 여러 개를 밀어낼 수 있습니다. |
| 반환 | number, buf의 앞에서 소비한 바이트 수. > 0이면 계속; 0 / 음수 / 반환 없음 → 멈추고, 남은 바이트는 원시 데이터로 표시. |
| 호출 규약 | 연결의 송신 스트림과 수신 스트림에 각각 한 번씩 패스 실행; 각 패스는 매번 남은 미소비 바이트를 넘기며 반복 호출하고, 0을 반환할 때까지 계속. |
내장 헬퍼 함수
섹션 제목: “내장 헬퍼 함수”서브슬라이스 추출
| 시그니처 | 반환 | 설명 |
|---|---|---|
sub(buf, off) |
ArrayBuffer |
off부터 끝까지 슬라이스 |
sub(buf, off, len) |
ArrayBuffer |
off부터 len 바이트 슬라이스; 범위를 벗어난 off / len은 오류 없이 자동 클램핑 |
정수 읽기 (부호 없음; off가 범위를 벗어나면 0 반환)
| 시그니처 | 반환 | 설명 |
|---|---|---|
u8(buf, off) |
number |
1바이트 읽기 |
u16be(buf, off) / u16le(buf, off) |
number |
2바이트 읽기, 빅엔디안 / 리틀엔디안 |
u32be(buf, off) / u32le(buf, off) |
number |
4바이트 읽기, 빅엔디안 / 리틀엔디안 |
텍스트 변환
| 시그니처 | 반환 | 설명 |
|---|---|---|
hex(buf) |
string |
소문자 16진수로 변환 |
ascii(buf) |
string |
텍스트로 읽어냄 (ASCII / 텍스트 내용에 적합) |
압축 / 압축 해제 (압축 해제할 수 없으면 입력을 그대로 반환; 단일 압축 해제 한도 약 16 MB)
| 시그니처 | 반환 | 설명 |
|---|---|---|
gzipMagic(buf) |
boolean |
gzip 매직 넘버로 시작하는지 여부 |
gunzip(buf) |
ArrayBuffer |
gzip 압축 해제 |
inflate(buf) |
ArrayBuffer |
zlib / deflate 압축 해제 |
unzstd(buf) |
ArrayBuffer |
zstd 압축 해제 |
lz4dtx(buf) |
ArrayBuffer |
블록 LZ4 스트림 압축 해제 |
디버그
| 시그니처 | 반환 | 설명 |
|---|---|---|
log(...args) |
(없음) | 인자들을 한 줄로 합쳐 “디버그 출력” 패널에 출력; ArrayBuffer는 16진수로 표시 |
console.log(...args) |
(없음) | log와 동일 (console.log에 익숙한 분에게 편리) |
런타임 환경
섹션 제목: “런타임 환경”- 표준 JavaScript:
ArrayBuffer, 타입 배열(Uint8Array등),DataView,JSON,Math,RegExp,Date같은 흔한 내장 객체를 모두 사용할 수 있으며, 더 낮은 수준의 바이트 읽기/쓰기가 필요하면new DataView(buf)/new Uint8Array(buf)를 바로 쓸 수 있습니다. - 디버그:
log(...)/console.log(...)는 “디버그 출력” 패널에 출력됩니다. 스크립트 오류와 타임아웃도 거기에 출력되며, 송신↑인지 수신↓스트림에서 왔는지 라벨이 붙습니다. - 제공되지 않는 것은 그 외 브라우저 / Node 전용 기능들입니다:
log이외의console메서드,TextDecoder/TextEncoder,fetch,setTimeout,require등은 모두 사용할 수 없습니다. 바이트를 텍스트로 읽으려면ascii(buf)를 쓰세요. - 각
decode호출에는 실행 시간 한도가 있습니다: 무한 루프나 타임아웃은 중단되어 “아무것도 소비하지 않음”으로 처리되므로 도구가 멈추지 않습니다.
프록시 캡처로 돌아가기 · 관련: 데이터 검사 및 디코딩 · 로컬 NIC 캡처