jhpka's blog

[HTTP] SSE, HTTP 정리

Admin User

SSE의 HTTP 메서드

SSE는 GET 메서드만 사용함.

이유:

  • 서버→클라이언트 단방향 통신이므로 GET으로 충분
  • 처음 한 번만 요청하면 연결이 계속 유지됨
javascript
// 클라이언트: GET 요청 1회로 연결 시작
const eventSource = new EventSource('/events');
// 서버가 계속 데이터 푸시
eventSource.onmessage = (event) => {
  console.log(event.data);
};

HTTP 버전과 Keep-Alive

HTTP/1.0

  • Keep-alive 기본값: OFF
  • 매 요청마다 새 TCP 연결 생성
  • 사용하려면 Connection: keep-alive 헤더 필요

HTTP/1.1

  • Keep-alive 기본값: ON
  • 하나의 TCP 연결로 여러 요청/응답 재사용
  • SSE는 이 특성을 활용해 긴 연결 유지

HTTP/2

  • 멀티플렉싱으로 더 효율적
  • 여러 SSE 스트림 동시 처리 가능
  • 하지만 SSE에 필수는 아님

TCP 연결 = 소켓 쌍

하나의 TCP 연결 구성

text
클라이언트: 192.168.1.100:54321
      ↕  (이 4개 값이 하나의 소켓 쌍)
서버: 93.184.216.34:443

SSE 동작

text
같은 소켓 쌍 유지:
GET /events →
           ← data: message1
           ← data: message2
           ← data: message3
           ... (연결 계속 유지)

왜 클라이언트가 먼저 요청해야 하나?

1. 방화벽/NAT 때문

클라이언트 선빵 시:

text
1. 클라이언트 → 서버: 연결 요청
2. 방화벽: "이 세션 추적 시작"
3. 서버 → 클라이언트: 응답 및 지속적 데이터 전송
4. 방화벽: "허용된 세션이니 통과!"

서버 선빵 시:

text
1. 서버 → 클라이언트: 연결 시도
2. 방화벽: "요청하지 않은 연결, 차단!"

2. HTTP 프로토콜 규칙

HTTP의 근본 원칙:

  • 요청-응답(Request-Response) 프로토콜
  • 클라이언트가 반드시 먼저 요청
  • 서버는 응답만 가능
  • RFC 스펙에 명시된 설계 철학

모든 HTTP 버전 공통:

  • HTTP/1.1: 클라이언트 요청 → 서버 응답
  • HTTP/2: 클라이언트 요청 → 서버 응답
  • HTTP/3: 클라이언트 요청 → 서버 응답

SSE 연결 흐름 상세

초기 연결 과정

text
클라이언트                           서버
    |                                 |
    |--- GET /events ---------------->|
    |    Connection: keep-alive       |
    |    Accept: text/event-stream    |
    |                                 |
    |<-- 200 OK ---------------------|
    |    Content-Type: text/event-stream
    |    Cache-Control: no-cache     |
    |    Connection: keep-alive       |
    |                                 |
    |<-- data: message1 -------------|
    |<-- data: message2 -------------|
    |<-- data: message3 -------------|
    |        ... (연결 유지)          |

핵심 헤더

http
# 클라이언트 요청
GET /events HTTP/1.1
Accept: text/event-stream
Connection: keep-alive

# 서버 응답
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

SSE vs 일반 HTTP 요청

일반 HTTP 요청

text
클라이언트                           서버
    |                                 |
    |--- GET /data ------------------>|
    |<-- 200 OK + 데이터 ------------|
    |                                 |
   연결 종료 (또는 풀로 반환)

SSE 요청

text
클라이언트                           서버
    |                                 |
    |--- GET /events ---------------->|
    |<-- 200 OK ---------------------|
    |<-- data: msg1 -----------------|
    |<-- data: msg2 -----------------|
    |<-- data: msg3 -----------------|
    |        ... (계속 유지)          |

차이점:

  • 일반 요청: 응답 후 연결 종료 또는 재사용 대기
  • SSE: 응답 헤더 후에도 연결 유지, 계속 데이터 전송

왜 서버가 먼저 연결할 수 없나?

기술적 이유

1. TCP 연결 시작 불가

python
# 서버 입장
# ❌ 불가능: 클라이언트 IP/포트를 모름
socket.connect(('어디?', '몇 번 포트?'))

# ✅ 가능: 특정 포트에서 대기
socket.bind(('0.0.0.0', 80))
socket.listen()

2. 클라이언트의 동적 포트

text
클라이언트가 사용하는 포트는 매번 다름:
- 첫 번째 연결: 192.168.1.100:54321
- 두 번째 연결: 192.168.1.100:54322
- 세 번째 연결: 192.168.1.100:54323

서버는 클라이언트가 다음에 어떤 포트를 쓸지 알 수 없음

3. 방화벽/NAT 통과 불가

text
가정/회사 네트워크:

[클라이언트] ---> [NAT/방화벽] ---> [인터넷] ---> [서버]
  사설 IP         공인 IP 변환

클라이언트 → 서버: 가능 (NAT가 매핑 생성)
서버 → 클라이언트: 불가능 (매핑 없음, 차단됨)

설계적 이유

HTTP는 클라이언트-서버 모델

text
클라이언트 역할:
- 연결 시작
- 요청 전송
- 서비스 소비자

서버 역할:
- 연결 대기
- 응답 전송
- 서비스 제공자

SSE 실전 예제

서버 구현 (FastAPI)

python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

async def event_generator():
    """SSE 이벤트 생성기"""
    count = 0
    while True:
        count += 1
        # SSE 형식: "data: 메시지\n\n"
        yield f"data: Message {count}\n\n"
        await asyncio.sleep(1)

@app.get("/events")
async def sse_endpoint():
    """SSE 엔드포인트"""
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
        }
    )

클라이언트 구현 (JavaScript)

javascript
// SSE 연결 생성
const eventSource = new EventSource('/events');

// 메시지 수신
eventSource.onmessage = (event) => {
    console.log('받은 데이터:', event.data);
};

// 연결 열림
eventSource.onopen = () => {
    console.log('SSE 연결 성공');
};

// 에러 처리
eventSource.onerror = (error) => {
    console.error('SSE 에러:', error);
    eventSource.close();
};

// 연결 종료
// eventSource.close();

클라이언트 구현 (Python)

python
import httpx
import asyncio

async def consume_sse():
    """SSE 스트림 소비"""
    async with httpx.AsyncClient() as client:
        async with client.stream('GET', 'http://localhost:8000/events') as response:
            async for line in response.aiter_lines():
                if line.startswith('data: '):
                    data = line[6:]  # "data: " 제거
                    print(f'받은 데이터: {data}')

# 실행
asyncio.run(consume_sse())

SSE 데이터 형식

기본 형식

text
data: 메시지 내용\n\n

여러 줄 데이터

text
data: 첫 번째 줄
data: 두 번째 줄
data: 세 번째 줄

이벤트 타입 지정

text
event: userConnected
data: {"userId": 123}

event: messageReceived
data: {"text": "안녕하세요"}

ID 지정 (재연결 지원)

text
id: 1
data: 첫 번째 메시지

id: 2
data: 두 번째 메시지

재연결 시간 설정

text
retry: 3000
data: 3초 후 재연결

SSE의 제약사항

1. 단방향 통신

text
클라이언트 → 서버: ❌ (초기 연결만 가능)
서버 → 클라이언트: ✅ (계속 가능)

해결책:

  • 양방향 필요 시 WebSocket 사용
  • 또는 SSE + 일반 HTTP 요청 조합

2. 브라우저 연결 제한

text
같은 도메인에 대한 SSE 연결 제한:
- 대부분 브라우저: 6개
- 초과 시 새 연결 대기

해결책:

  • HTTP/2 사용 (제한 완화)
  • 여러 도메인 사용
  • 하나의 SSE로 여러 이벤트 처리

3. 텍스트만 전송 가능

text
✅ 가능: JSON, XML, 일반 텍스트
❌ 불가능: 바이너리 데이터 직접 전송

해결책:

  • Base64 인코딩
  • WebSocket 사용

SSE vs WebSocket vs Long Polling

특성SSEWebSocketLong Polling
프로토콜HTTPWebSocket (ws://)HTTP
방향서버→클라이언트양방향양방향 (연속 요청)
연결지속적지속적일시적 (반복)
복잡도낮음중간높음
브라우저 지원대부분 (IE 제외)모든 최신 브라우저모든 브라우저
재연결자동수동 구현 필요자동 (새 요청)
오버헤드낮음낮음높음 (반복 요청)

언제 SSE를 사용하나?

✅ SSE가 적합한 경우

  • 서버에서 클라이언트로만 데이터 전송
  • 실시간 알림, 피드 업데이트
  • 주식 시세, 스포츠 스코어
  • 로그 스트리밍, 모니터링
  • 간단한 구현 필요
python
# 실시간 알림 예시
async def notification_stream(user_id: int):
    while True:
        notification = await get_user_notification(user_id)
        if notification:
            yield f"data: {notification.json()}\n\n"
        await asyncio.sleep(5)

❌ SSE가 부적합한 경우

  • 양방향 통신 필요
  • 바이너리 데이터 전송
  • 매우 낮은 지연시간 필요
  • 복잡한 프로토콜 필요
python
# 이런 경우는 WebSocket 사용
# - 채팅 애플리케이션
# - 멀티플레이어 게임
# - 협업 도구 (실시간 문서 편집)
# - 화상 통화

종합 정리

핵심 개념

개념설명
SSE 메서드GET만 사용 (단방향 통신)
Keep-AliveHTTP/1.1 기본 ON, 긴 연결 유지
소켓 쌍4-tuple로 식별되는 단일 TCP 연결
클라이언트 먼저HTTP 프로토콜 규칙, 방화벽 통과
연결 유지응답 후에도 계속 데이터 전송
데이터 형식data: 내용\n\n 텍스트 기반

SSE 체크리스트

✅ 구현 시 확인사항:

  1. Content-Type: text/event-stream 설정
  2. Cache-Control: no-cache 설정
  3. Connection: keep-alive 유지
  4. 데이터 형식: data: 내용\n\n
  5. 재연결 로직 구현 (자동 또는 수동)
  6. 에러 처리

✅ 최적화:

  1. 적절한 retry 시간 설정
  2. id 필드로 중복 방지
  3. 연결 수 제한 고려
  4. HTTP/2 사용 검토

❌ 피해야 할 것:

  1. 양방향 통신이 필요한데 SSE 사용
  2. 바이너리 데이터 직접 전송
  3. 재연결 로직 없이 구현
  4. 과도한 동시 연결
댓글을 불러오는 중...