照规范写一个解析器
한국어 원문으로 표시합니다.
목표
WHATWG HTML 표준의 이벤트 스트림 파싱 규칙을 직접 구현하고, 그 위에 재접속으로 이어 받는 장치를 올립니다.
왜 중요한가
SSE 는 "그냥 HTTP" 라서 라이브러리 없이도 쓸 수 있습니다. 그래서 실무에서는
split("\n\n") 한 줄짜리 파서가 아주 흔하고, 그 파서는 조각으로 나뉘어
도착하는 입력에서 곧바로 무너집니다. TCP 는 여러분이 보낸 단위로 도착하는 것을
보장하지 않기 때문입니다.
이어 받기는 더합니다. id 버퍼가 이벤트마다 초기화되는지 아닌지, 잘린 마지막
이벤트를 내보내는지 아닌지에 따라 재접속 뒤 화면이 비거나 겹칩니다. 규칙은
명세에 다 적혀 있고, 이 실습은 그 규칙을 코드로 옮기는 일입니다.
만들 것
/root/work/resume/client.py 에서 아래를 제공합니다.
| 이름 | 계약 |
|---|---|
StreamParser |
feed(조각) → 이번에 완성된 이벤트 목록. last_event_id · reconnection_time 속성 |
replay(events, last_event_id) |
서버 쪽 되돌림 창 — 그 id 뒤를 돌려준다 |
resume_headers(parser) |
재접속 때 실을 헤더 딕셔너리 |
이벤트 하나는 딕셔너리입니다: {"event": ..., "data": ..., "id": ...}.
단계
- 조각과 줄 끝 —
feed가 아무 데서나 잘려도 같은 결과 - 필드 규칙 — 주석 · 콜론 없는 줄 · data 이어 붙이기
id버퍼는 초기화되지 않는다retry는 ASCII 숫자일 때만- 되돌림 창 —
replay - 재접속 헤더 —
resume_headers - 직접 재서 적기 —
/root/work/resume/07-gap.txt - 정리 —
/root/work/resume/08-notes.md
참고
- 7단계가 읽는 스트림 조각은 이미지에 들어 있습니다:
/opt/fixtures/sse-resume/cut.txt. 마지막 이벤트가 빈 줄 없이 끊겨 있습니다. - 표준 라이브러리만으로 충분합니다. pip install 은 네트워크가 없어 되지 않습니다.
- 흔한 실수 둘: 조각 경계에 걸친
\r\n을 두 줄로 세는 것, 그리고 파이썬str.isdigit()이 전각 숫자에도 참을 돌려준다는 것.
조각과 줄 끝
/root/work/resume/client.py 에 StreamParser 를 만드세요. feed(조각) 은 이번 조각으로 완성된 이벤트 목록을 돌려줍니다. 입력이 어디서 잘려 도착해도 결과가 같아야 하고, 줄 끝은 CRLF · LF · CR 셋 다 받아야 합니다. 맨 앞의 BOM 한 개는 버립니다.
mkdir -p /root/work/resume. 아직 처리하지 못한 꼬리를 인스턴스 버퍼에 남겨 두고, 줄 끝을 찾을 수 있을 때까지만 처리하세요. 함정 하나 — 버퍼가 \r 로 끝나면 그게 \r\n 의 앞쪽일 수 있으니 다음 조각을 기다려야 합니다. 이 단계에서는 data: 만 다뤄도 됩니다.
필드 규칙
명세의 필드 규칙을 넣으세요. 콜론으로 시작하는 줄은 무시, 콜론이 없는 줄은 줄 전체가 필드 이름이고 값은 빈 문자열, data 는 값을 이어 붙이며 개행을 하나씩 더하고, event 는 이벤트 타입을 바꿉니다(기본은 message). 모르는 필드는 무시합니다.
값 앞의 공백은 하나만 뗍니다. 내보낼 때는 data 버퍼가 빈 문자열인지를 개행을 떼기 전에 확인하고, 비어 있지 않으면 마지막 개행 하나만 제거합니다. 그래서 data 한 줄만 있는 블록은 data 가 빈 문자열인 이벤트로 나갑니다.
id 버퍼와 마지막 id 문자열
id 필드를 처리하세요. 값에 U+0000 NULL 이 들어 있으면 그 필드를 무시하고, 아니면 내부 id 버퍼를 그 값으로 바꿉니다. 재접속에 쓰는 last_event_id 는 이벤트를 내보내는 자리에서 그 버퍼 값으로 갱신되며, 내보낸 뒤에도 초기화되지 않습니다. 내보내는 이벤트의 id 에는 그 값을 담습니다.
명세는 내보내기 단계에서 ①마지막 id 문자열을 버퍼 값으로 바꾸고 ②data 버퍼가 비었으면 거기서 돌아가라고 적습니다. 순서가 그대로라 id: 만 있고 data 가 없는 블록도 재접속 자리는 옮기지만, 빈 줄이 오지 않아 내보내기 자체가 일어나지 않으면 옮기지 않습니다. 그리고 id 는 문자열입니다 — 정수로 바꾸지 마세요.
retry 는 ASCII 숫자일 때만
retry 필드를 처리하세요. 값이 ASCII 숫자로만 이루어졌을 때만 reconnection_time 을 그 정수로 바꾸고, 아니면 무시합니다. 초기값은 None 입니다. retry: 만 있고 data 가 없는 블록은 이벤트가 아닙니다.
파이썬의 str.isdigit() 은 전각 숫자에도 참을 돌려줍니다. value.isascii() and value.isdigit() 처럼 두 조건을 함께 보세요. 빈 문자열도, +100 도, 3s 도, 3.5 도 무시 대상입니다.
서버 쪽 되돌림 창
replay(events, last_event_id) 를 추가하세요. events 는 id 를 가진 딕셔너리 목록입니다. last_event_id 가 비어 있거나 None 이면 전부, 창 안에 있는 id 면 그 뒤부터, 창에 없는 id 면 None 을 돌려줍니다.
None 은 "이어 받을 수 없으니 처음부터 다시 받으라" 는 신호입니다. 남아 있는 것만 보내면 가운데가 비고, 그 사실을 아무도 모릅니다. id 비교는 문자열 그대로 하세요 — b7-1042 같은 id 가 실제로 쓰입니다.
재접속 헤더
resume_headers(parser) 를 추가하세요. 파서의 last_event_id 가 빈 문자열이 아닐 때만 {"Last-Event-ID": 값} 을 돌려주고, 아니면 빈 딕셔너리를 돌려줍니다.
명세는 마지막 id 문자열이 빈 문자열이 아닐 때만 헤더를 싣습니다. 빈 값을 실어 보내면 서버는 "0번 뒤부터" 인지 "처음부터" 인지 구별할 수 없습니다. 헤더 이름의 대소문자와 하이픈을 그대로 쓰세요.
잘린 이벤트를 직접 재 본다
/opt/fixtures/sse-resume/cut.txt 를 통째로 여러분의 파서에 흘려 넣고, 나온 값을 /root/work/resume/07-gap.txt 에 이름=값 세 줄로 적으세요. events — 내보낸 이벤트 수. last_event_id — 그때의 마지막 id. retry_ms — 그때의 reconnection_time.
그 파일은 마지막 이벤트가 빈 줄 없이 끊겨 있습니다. 명세는 그런 이벤트를 내보내지 말라고 적으므로, 세 숫자가 전부 그 규칙에 달려 있습니다. 채점기가 같은 파일을 여러분의 파서로 다시 흘려 넣어 적힌 값과 대조하니 눈으로 어림하지 마세요.
무엇이 빈 자리를 막았나
/root/work/resume/08-notes.md 에 세 줄 이상. Last-Event-ID 가 없으면 재접속 뒤 무엇이 보이는지, 잘린 이벤트를 내보내면 무엇이 어긋나는지, 되돌림 창을 벗어난 id 에 남은 것만 보내면 무엇이 생기는지.
본문에 Last-Event-ID, 재접속, 잘린(또는 불완전) 이 들어가야 합니다. 이 셋이 SSE 를 운영에 올렸을 때 실제로 사람을 부르는 세 가지입니다.