LabHub
学习 学习路径 课程

SSE — 服务端先说话的方式

照规范写一个解析器

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

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": ...}.

단계

  1. 조각과 줄 끝 — feed 가 아무 데서나 잘려도 같은 결과
  2. 필드 규칙 — 주석 · 콜론 없는 줄 · data 이어 붙이기
  3. id 버퍼는 초기화되지 않는다
  4. retry 는 ASCII 숫자일 때만
  5. 되돌림 창 — replay
  6. 재접속 헤더 — resume_headers
  7. 직접 재서 적기 — /root/work/resume/07-gap.txt
  8. 정리 — /root/work/resume/08-notes.md

참고

조각과 줄 끝

/root/work/resume/client.pyStreamParser 를 만드세요. 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) 를 추가하세요. eventsid 를 가진 딕셔너리 목록입니다. 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 를 운영에 올렸을 때 실제로 사람을 부르는 세 가지입니다.