<aside> 💡 브라우저 새로고침 없이 유저에게 상태변화를 실시간으로 보여주기 위해 도입
</aside>
SSE
router
def
로 라우터 함수를 선언하면 유저 수가 많아질 경우 그만큼 다른 api를 핸들링할 스레드 풀의 스레드 개수가 부족해지고 CPU 자원도 낭비되므로, async def
로 선언하여 코루틴 사용@api.get("", response_model=SSEMessage)
async def get_server_event(user_id: auth = Depends()):
channel_name = RedisKey.get_s_queue_key(user_id)
return EventSourceResponse(
sse_service.subscribe_channel(channel_name)
)
service
async def subscribe_channel(self, channel_name: str):
try:
q = RedisList(channel_name)
while True:
q_response = await q.get(is_blocking=True) # 큐에 데이터 쌓일 때까지 대기
if q_response is not None:
yield ServerSentEvent(
data=q_response,
retry=1000,
)
await asyncio.sleep(0.2)
except asyncio.CancelledError as e:
logger.info(f"{channel_name} -> {e}")
return
except BaseException as e:
logger.error(f"{channel_name} -> {e}")
return
message queue
put()
을 통해 queue에 메세지가 전송되면 해당 queue를 구독하고 있는 코루틴에서 get()
으로 그 메세지를 꺼내 클라이언트로 전송한다class RedisList:
"""
Redis Lists are an ordered list, First In First Out Queue
Redis List pushing new elements on the head (on the left) of the list.
The max length of a list is 4,294,967,295
"""
def __init__(self, queue_name):
self.key = queue_name
self.client = config.redis
def put(self, element):
self.client.lpush(self.key, element)
async def get(self, is_blocking=False, timeout=None):
"""sse router에 연결되어 redis 채널을 구독하는 해당 메서드만 비동기 처리"""
if is_blocking:
key, element = await config.aioredis.brpop(self.key, timeout=timeout)
else:
element = await config.aioredis.rpop(self.key)
return element
HTTP Protocol Version + 백엔드 인프라 http 버전
<aside> 💡
개발 서버에 실행 중인 Nginx 때문에 SSE 연결이 정상 작동하지 않았다
</aside>
nginx 기본 설정
proxy_http_version 1.0;
proxy_set_header Connection close;
proxy_buffering on;
proxy_read_timeout 60s;
TOBE(예시)
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
# 필요한 경우 조절
proxy_read_timeout 1800s;
X-Accel-Buffering: no
를 포함하여 이를 수신한 nginx에서 SSE 관련 응답만 버퍼링하지 않도록 처리할 수 있다<aside> 💡
클라이언트에서 일정 시간 동안 이벤트를 수신하지 못하는 경우 타임아웃이 발생해 브라우저에서 서버에 재연결 요청을 보낸다
</aside>
Last-Event-Id
요청 헤더에 담아 보내면 서버는 마지막 이벤트 ID 이후의 데이터를 전부 푸시하는 로직이 필요하다