WebSocketStream: WebSocket API로 스트림 통합

백프레셔를 적용하여 앱이 WebSocket 메시지에 빠지거나 WebSocket 서버에 메시지로 플러딩되지 않도록 합니다.

토마스 슈타이너
토마스 슈타이너

배경

WebSocket API

WebSocket APIWebSocket 프로토콜에 자바스크립트 인터페이스를 제공하여 사용자의 브라우저와 서버 간 양방향 양방향 통신 세션을 열 수 있습니다. 이 API를 사용하면 서버에 응답을 보내지 않고도 서버에 메시지를 보내고 이벤트 기반 응답을 수신할 수 있습니다.

Streams API

Streams API를 사용하면 JavaScript가 네트워크를 통해 수신된 데이터 청크의 스트림에 프로그래매틱 방식으로 액세스하여 원하는 대로 처리할 수 있습니다. 스트림 컨텍스트에서 중요한 개념은 백프레셔입니다. 이는 단일 스트림 또는 파이프 체인이 읽기 또는 쓰기 속도를 조절하는 프로세스입니다. 스트림 자체 또는 파이프 체인의 후반부에 있는 스트림이 여전히 사용 중이며 아직 더 많은 청크를 수락할 준비가 되지 않았다면, 체인을 통해 역방향으로 신호를 보내 필요에 따라 전송을 늦춥니다.

현재 WebSocket API의 문제

수신된 메시지에 백프레셔를 적용할 수 없습니다.

현재 WebSocket API를 사용하면 WebSocket.onmessage에서 메시지에 대한 반응이 이루어지며, 서버에서 메시지가 수신될 때 EventHandler가 호출됩니다.

새 메시지가 수신될 때마다 대규모 데이터 크런칭 작업을 수행해야 하는 애플리케이션이 있다고 가정해 보겠습니다. 아마도 아래 코드와 유사한 흐름을 설정하게 될 것입니다. process() 호출의 결과를 await 처리했으므로 아무런 문제가 없습니다. 맞나요?

// A heavy data crunching operation.
const process = async (data) => {
  return new Promise((resolve) => {
    window.setTimeout(() => {
      console.log('WebSocket message processed:', data);
      return resolve('done');
    }, 1000);
  });
};

webSocket.onmessage = async (event) => {
  const data = event.data;
  // Await the result of the processing step in the message handler.
  await process(data);
};

틀렸습니다. 현재 WebSocket API의 문제는 백프레셔를 적용할 방법이 없다는 점입니다. 메시지가 process() 메서드가 처리할 수 있는 속도보다 더 빠르게 도착하면 렌더링 프로세스가 메시지를 버퍼링하여 메모리를 채우거나 100% CPU 사용률로 인해 응답하지 않거나, 또는 이 두 가지를 모두 수행합니다.

보낸 메일에 백프레셔를 적용하는 것은 인체공학적이지 않습니다

보낸 메시지에 백프레셔를 적용할 수 있지만, 이 경우 비효율적이고 인체공학적이지 않은 WebSocket.bufferedAmount 속성을 폴링해야 합니다. 이 읽기 전용 속성은 WebSocket.send() 호출을 사용하여 큐에 추가되었지만 아직 네트워크로 전송되지 않은 데이터의 바이트 수를 반환합니다. 이 값은 큐에 추가된 모든 데이터가 전송되면 0으로 재설정되지만, WebSocket.send()를 계속 호출하면 계속 상승합니다.

WebSocketStream API란 무엇인가요?

WebSocketStream API는 스트림을 WebSocket API와 통합하여 존재하지 않거나 인체공학적이지 않은 백프레셔의 문제를 처리합니다. 즉, 추가 비용 없이 배압을 '무료'로 적용할 수 있습니다.

WebSocketStream API의 추천 사용 사례

이 API를 사용할 수 있는 사이트의 예는 다음과 같습니다.

  • 특히 동영상 및 화면 공유에서 상호작용을 유지해야 하는 고대역폭 WebSocket 애플리케이션
  • 마찬가지로 동영상 캡처 및 기타 애플리케이션에서 서버에 업로드해야 하는 많은 데이터를 브라우저에서 생성합니다. 백프레셔를 사용하면 클라이언트는 메모리에 데이터를 축적하는 대신 데이터 생성을 중지할 수 있습니다.

현재 상태

| 단계 | 상태 | | ------------------------------------------ | ---------------------------- | | 1. 설명 만들기 | [완료][Explainer] | | 2. 사양의 초기 초안 만들기 | [진행 중][spec] | | 3. 의견 수집 및 디자인 반복 | [진행 중](#feedback) | | 4. 오리진 트라이얼 | [완료][ot] | | 5. 출시 | 시작하지 않음 |

WebSocketStream API 사용 방법

소개 예시

WebSocketStream API는 프라미스 기반이므로 최신 자바스크립트 환경에서 자연스럽게 처리할 수 있습니다. 먼저 새 WebSocketStream를 구성하고 WebSocket 서버의 URL에 전달합니다. 그런 다음 연결이 opened이 될 때까지 기다립니다. 이 경우 ReadableStream 또는 WritableStream이 발생합니다.

ReadableStream.getReader() 메서드를 호출하면 마침내 ReadableStreamDefaultReader를 얻게 됩니다. 그러면 스트림이 완료될 때까지, 즉 {value: undefined, done: true} 형식의 객체를 반환할 때까지 데이터를 read()할 수 있습니다.

따라서 WritableStream.getWriter() 메서드를 호출하면 최종적으로 WritableStreamDefaultWriter를 얻게 되고, 나중에 데이터를 write()할 수 있습니다.

  const wss = new WebSocketStream(WSS_URL);
  const {readable, writable} = await wss.opened;
  const reader = readable.getReader();
  const writer = writable.getWriter();

  while (true) {
    const {value, done} = await reader.read();
    if (done) {
      break;
    }
    const result = await process(value);
    await writer.write(result);
  }

백프레셔

약속된 백프레셔 기능은 어떤가요? 위에서 말씀드린 것처럼 추가 단계를 거치지 않고 '무료'로 제공됩니다. process()에 추가 시간이 걸린다면 파이프라인이 준비된 후에만 다음 메시지가 사용됩니다. 마찬가지로 WritableStreamDefaultWriter.write() 단계는 안전할 때만 진행됩니다.

고급 예시

WebSocketStream의 두 번째 인수는 향후 확장을 허용하는 옵션 백입니다. 현재 유일한 옵션은 protocols이며, WebSocket 생성자의 두 번째 인수와 동일하게 작동합니다.

const chatWSS = new WebSocketStream(CHAT_URL, {protocols: ['chat', 'chatv2']});
const {protocol} = await chatWSS.opened;

선택된 protocol 및 잠재적인 extensionsWebSocketStream.opened 프로미스를 통해 사용할 수 있는 사전의 일부입니다. 연결이 실패하는 경우 관련성이 없으므로 실시간 연결에 대한 모든 정보는 이 프로미스에서 제공됩니다.

const {readable, writable, protocol, extensions} = await chatWSS.opened;

종료된 WebSocketStream 연결에 관한 정보

WebSocket API의 WebSocket.oncloseWebSocket.onerror 이벤트에서 사용할 수 있었던 정보를 이제 WebSocketStream.closed 프로미스를 통해 사용할 수 있습니다. 비정상적으로 닫히면 프로미스가 거부되며, 그러지 않으면 서버에서 전송한 코드와 이유를 확인합니다.

가능한 모든 상태 코드와 그 의미는 CloseEvent 상태 코드 목록에 설명되어 있습니다.

const {code, reason} = await chatWSS.closed;

WebSocketStream 연결 닫기

WebSocketStream은 AbortController로 닫을 수 있습니다. 따라서 AbortSignalWebSocketStream 생성자에 전달합니다.

const controller = new AbortController();
const wss = new WebSocketStream(URL, {signal: controller.signal});
setTimeout(() => controller.abort(), 1000);

또는 WebSocketStream.close() 메서드를 사용할 수도 있지만 주요 목적은 서버로 전송되는 코드 및 이유를 지정하도록 허용하는 것입니다.

wss.close({code: 4000, reason: 'Game over'});

점진적 개선 및 상호 운용성

Chrome은 현재 WebSocketStream API를 구현하는 유일한 브라우저입니다. 기존 WebSocket API와의 상호 운용성을 위해 수신된 메시지에 백프레셔를 적용할 수 없습니다. 보낸 메시지에 백프레셔를 적용할 수 있지만, 이 경우 비효율적이고 인체공학적이지 않은 WebSocket.bufferedAmount 속성을 폴링해야 합니다.

기능 감지

WebSocketStream API가 지원되는지 확인하려면 다음을 사용하세요.

if ('WebSocketStream' in window) {
  // `WebSocketStream` is supported!
}

데모

지원되는 브라우저에서는 삽입된 iframe 또는 Glitch에서 직접 WebSocketStream API가 작동하는 것을 확인할 수 있습니다.

의견

Chrome팀은 WebSocketStream API 사용 경험에 관한 의견을 듣고자 합니다.

API 설계에 대해 알려주세요.

API에 예상한 대로 작동하지 않는 부분이 있나요? 아니면 아이디어를 구현하는 데 필요한 메서드나 속성이 누락되었나요? 보안 모델에 대한 질문이나 의견이 있으신가요? 해당 GitHub 저장소에 사양 문제를 제출하거나 기존 문제에 의견을 추가합니다.

구현 관련 문제 신고

Chrome 구현에서 버그를 발견하셨나요? 아니면 구현이 사양과 다른가요? new.crbug.com에서 버그를 신고합니다. 가능한 한 많은 세부정보와 간단한 재현 안내를 포함하고 Components 상자에 Blink>Network>WebSockets를 입력합니다. Glitch는 쉽고 빠르게 재현 케이스를 공유하는 데 효과적입니다.

API 지원 표시

WebSocketStream API를 사용할 계획인가요? 공개 지원은 Chrome팀이 기능의 우선순위를 정하는 데 도움이 되며 다른 브라우저 공급업체에 이러한 기능을 지원하는 것이 얼마나 중요한지 보여줍니다.

해시태그 #WebSocketStream를 사용하여 @ChromiumDev로 트윗을 보내고 사용 위치와 방법을 알려주세요.

유용한 링크

감사의 말씀

WebSocketStream API는 Adam RiceYutaka Hirano가 구현했습니다. Unsplash댄 무이의 히어로 이미지