ARPANET에서 HTTP/3까지 여정을 떠나요
1950년대, 미국의 국방, 연구 및 교육 기관은 정보와 리소스를 공유하기 위한 최초의 컴퓨터 네트워크를 개발했습니다. 이 네트워크는 메인프레임 컴퓨터와 단말기 사이의 통신으로 시작되었고, 나중에는 컴퓨터 간의 연결로 발전했습니다. 그러나 이 네트워크는 같은 지역 내의 컴퓨터끼리만 연결할 수 있었으며, 데이터 교환 기능도 제한적이었습니다.
ARPANET: 인터넷의 탄생
1969년, 미국 국방부의 고등연구계획국(ARPA, Advanced Research Project Agency)가 ARPANET(Advanced Research Projects Agency Network) 프로젝트를 시작했습니다. 이 프로젝트는 민감한 군사 데이터를 안전하게 전송하고, 미국 전역의 주요 연구 그룹을 연결하는 것이 목표였습니다. ARPANET은 냉전 시기의 긴장 상태에서 미군이 적의 공격으로부터 통신 시스템을 보호하기 위한 중요한 요구사항을 충족시키기 위해 개발되었습니다. 이 시스템은 네트워크 일부가 파괴되더라도 남아 있는 네트워크가 여전히 작동할 수 있는 컴퓨터 간의 상호 접속 방식을 도입했습니다.
미국 글로벌 정책 싱크탱크인 RAND(Research ANd Development)의 폴 배런(Paul Baran) 팀은 미 공군의 지원을 받아 “분산 커뮤니케이션에 관한 연구(On Distributed Communication)”를 수행했습니다. 배런은 이 보고서에서 “중복 연결(redundancy of connectivity)”의 원리를 제안했습니다. 이는 탈중앙집중적 명령 및 통제 시스템을 구축함으로써 시스템의 일부분이 파괴되더라도 전체 시스템의 손실을 최소화할 수 있다는 것이었습니다.
DARPA(Defense Advanced Research Projects Agency)의 지원하에 1968년까지 ARPANET의 기본 구조와 사양이 확정되었고, 사설 연구 기관인 BBN(Bolt, Beranek, and Newman)이 패킷 교환기인 IMP(Interface Message Processors)를 개발했습니다.
1969년에는 4개의 호스트 컴퓨터가 연결되면서 ARPANET이 시작되었습니다. 먼저 UCLA(University of California, Los Angeles)의 네트워크측정센터(Network Measurement Center)가 최초의 ARPANET 노드로 선정되었고, 한 달 후에는 STI(Stanford Research Institute)가 두 번째 노드로 연결되었습니다. 이렇게 연결된 두 노드를 통해 UCLA와 SRI 간의 호스트 간 메시지 전송이 이루어졌습니다. 이어서 UC 산타바바라와 유타대학교 등이 연결되면서 1969년 최초의 인터넷망인 ARPANET이 4개의 호스트 컴퓨터를 연결하는 네트워크로 구축되었습니다. 이후 ARPANET에 연결되는 컴퓨터가 늘어나 1971년 말까지 23개 호스트 컴퓨터가 연결되었습니다.
CSNET: ARPANET의 확장
1981년, 미국 국립과학재단(NSF, National Science Foundation)이 ARPANET에 직접 접속할 수 없는 학술 및 연구 기관의 컴퓨터 과학 부서를 위해 CSNET(Computer Science Network)을 개발했습니다.
CSNET은 초기 3년 동안 국립과학재단의 자금으로 지원되었습니다. 이 프로젝트는 위스콘신 대학 매디슨 캠퍼스(University of Wisconsin–Madison)의 로렌스 랜드웨버(Lawrence Landweber)를 중심으로 대학 컨소시엄이 준비한 제안서에서 시작되었습니다.
첫 번째 관리팀은 랜드웨버(Landweber), 패버(Farber), 피터 J. 덴닝(Peter J. Denning), 앤써니 C. 허른(Anthony C. Hearn), 그리고 NSF의 빌 커른(Bill Kern)으로 구성되었습니다. CSNET이 완전히 가동되면서 시스템과 네트워크 운영은 1984년까지 매사추세츠주 케임브리지의 볼트 베런크 앤 뉴먼(Bolt Beranek and Newman, BBN)에서 리처드 에드미스턴(Richard Edmiston)이 이끄는 팀으로 이전되었습니다.
피터 J. 덴닝(Peter J. Denning), 더글러스 커머(Douglas Comer), 그리고 폴 맥네브(Paul McNabb)로 구성된 퍼듀(Purdue) 팀은 ARPANET 인프라 외부의 사이트가 공용 X.25 네트워크를 통해 연결할 수 있도록 커널 인터페이스를 설계하고 구축하는 역할을 맡았습니다. 이를 통해 TCP/IP 네트워크 스택이 있는 시스템은 X.25 네트워크 장치를 사용할 수 있으며 IP 데이터그램은 동적으로 할당된 X.25 세션을 통해 전송됩니다. ARPANET에 액세스 가능한 사이트가 ARPANET에 대한 게이트웨이 역할을 하여 ARPANET이 아닌 사이트가 ARPANET에 접근 가능하게 해주었습니다.
CSNET은 초기에는 3개의 사이트로 시작하여 1982년까지는 24개 사이트로 확장되었습니다. 그 이후에는 84개 이상의 사이트로 확대되어 호주, 캐나다, 프랑스, 독일, 한국 및 일본과 같은 다른 국가와도 연결되었습니다. 마침내 CSNET은 180개 이상의 기관을 연결하는 데 성공했습니다.
TCP/IP의 등장
1983년까지 ARPANET 컴퓨터는 서로 통신하기 위해 NCP(Network Control Protocol)를 사용했습니다. 이 프로토콜은 단방향으로 설계되어 양방향 통신을 위해 두 개의 포트 번호를 사용하여 두 개의 연결을 설정했습니다. 각 응용 계층 애플리케이션 또는 프로토콜에는 홀수와 짝수 포트가 할당되었습니다. 이러한 방식으로 NCP는 서로 다른 ARPANET 호스트 컴퓨터에서 실행되는 프로세스 간의 연결과 흐름 제어를 제공했습니다. 또한, 원격 로그인 및 파일 전송과 같은 애플리케이션 서비스는 NCP 위에 구축되어, NCP를 사용하여 다른 호스트 컴퓨터에 대한 연결을 처리했습니다.
ARPANET에서는 네트워크 내의 물리 계층, 데이터 링크 계층, 네트워크 계층 프로토콜이 각각의 IMP에 구현되었습니다. 호스트는 서로 다른 종류의 인터페이스를 사용하여 IMP에 연결되었습니다. 이러한 IMP-Host 인터페이스는 하위 프로토콜 계층을 제공하여, NCP(Network Control Protocol)는 주로 AHHP(ARPANET Host-to-Host Protocol)와 ICP(Initial Connection Protocol)로 구성된 전송 계층을 제공했습니다. AHHP는 단방향 흐름 제어 데이터 스트림을 전송하는 절차를 정의했고, ICP는 호스트 프로세스 간에 양방향 통신을 설정하는 절차를 정의했습니다.
그러나 NCP는 데이터그램이 아닌 신뢰할 수 있는 바이트 스트림만 제공했습니다. 이러한 구조는 유연성이 부족하며 확장성 문제를 야기했습니다. 따라서 상위 계층이 하위 계층에서 필요한 것만 접근할 수 있도록 계층화가 필요했습니다. 이에 DARPA(Defense Advanced Research Projects Agency)는 1973년에 빈턴 서프(Vinton Cerf)와 로버트 칸(Robert Kahn)에게 ARPANET을 넘어 인터네트워킹을 가능하게 하는 새로운 프로토콜을 설계하도록 지시했습니다.
서프와 칸은 1974년에 이러한 새로운 프로토콜을 개발하기 시작했고, 이것이 TCP(Transmission Control Program)으로 알려지게 되었습니다. TCP는 근거리 통신망이나 인트라넷, 인터넷에 연결된 컴퓨터에서 실행되는 프로그램 간에 안정적으로, 순서대로, 에러없이 데이터를 교환할 수 있는 프로토콜입니다. 그러나 TCP만으로는 인터넷 프로토콜 제품군을 완성하는 데 한계가 있었습니다. 이에 1978년에 Cerf와 Kahn은 TCP를 신뢰할 수 있는 연결 지향 서비스를 담당하는 프로토콜로 남기고, 연결 없이 패킷을 전달하는 비신뢰성 프로토콜인 IP(Internet Protocol)를 개발했습니다.
1983년 1월 1일, 플래그 데이(Flag Day)로 알려진 날에 ARPANET은 NCP에서 TCP/IP 프로토콜로 변경되었습니다. TCP/IP의 등장으로 ARPANET은 단순한 연결 기반의 단방향 통신 시스템에서 패킷 교환 네트워크로 진화했습니다. TCP/IP는 데이터그램과 신뢰할 수 있는 연결 기반 서비스를 모두 제공하여 다양한 종류의 컴퓨터와 네트워크를 효율적으로 연결하는 데 중요한 역할을 했습니다.
그 후에 웹이 있었고
1989년, CERN(Conseil Européen pour la Recherche Nucléaire)의 팀 버너스-리(Tim Berners-Lee)가 “Information System: A Proposal”를 통해 제안한 네트워크 하이퍼텍스트 시스템은 월드와이드웹(WWW, World Wide Web)의 탄생을 이끌었습니다. 이는 전 세계의 대학과 연구소들이 상호 연구를 위해 정보를 신속하게 교환할 수 있도록 하기 위한 것이었습니다. 웹의 개념은 하이퍼링크로 링크되고 연결된 문서를 통해 명확하게 드러났습니다. 이러한 하이퍼링크는 사용자가 쉽게 다른 문서로 이동할 수 있도록 도와주었습니다. 결과적으로 사용자들은 웹 상의 다양한 정보와 리소스에 쉽게 접근할 수 있게 되었습니다.
버너스-리와 그의 팀은 CERN에서 HTTP(HyperText Transfer Protocol)과 HTML(HyperText Markup Language)을 개발했습니다.
HTML은 웹 페이지의 구조를 정의하는 마크업 언어로, 텍스트, 이미지, 링크 등을 포함하여 웹 문서를 작성하는 데 사용됩니다. HTML은 브라우저와 같은 HTML 처리 장치의 행동을 정의하는 자바스크립트, 스타일 및 레이아웃을 정의하는 CSS와 같은 스크립트를 포함하거나 불러올 수 있습니다. HTML은 요소(element)와 속성(attribute)으로 이루어져 있으며, 브라우저는 이를 해석하여 사용자에게 웹 페이지를 표시합니다.
HTTP는 웹에서 정보를 주고받을 수 있는 프로토콜입니다. 주로 HTML 문서를 주고받는 데에 사용되며, 클라이언트와 서버 간의 통신을 처리합니다. HTTP는 요청(request)과 응답(response)으로 이루어진 프로토콜로, 클라이언트가 서버로부터 웹페이지나 이미지 등의 정보를 요청하면, 서버는 해당 요청에 응답하여 필요한 정보를 클라이언트에게 전송합니다.
이러한 기술의 결합으로 웹은 사용자들이 정보를 공유하고 검색하며 상호 작용할 수 있는 환경을 제공하게 되었습니다. 또한, HTTP와 HTML은 개방적이고 유연한 특성을 갖고 있어 웹의 성장과 발전을 주도했습니다.
그 후에는 정보로 가득 찼습니다.
2000년대 초반에 웹 2.0이 등장하면서 사용자들은 정보를 더욱 쉽게 생성, 수정, 공유할 수 있는 환경을 경험했습니다. 이는 정보 소비자에서 참여자로의 전환을 촉진했습니다. 웹 1.0 시대에는 단방향으로 정보를 소비하는 모습이 일반적이었지만, 웹 2.0은 사용자들이 직접 콘텐츠를 생산하고 쌍방향으로 소통할 수 있는 환경을 제공했습니다.
이런 특징은 사용자 중심의 콘텐츠 생성을 가능하게 했습니다. 블로그, 위키피디아, 소셜 미디어 플랫폼 등을 통해 개인이나 그룹은 자신의 의견, 정보, 경험을 쉽게 공유할 수 있었습니다. 특히 소셜 미디어 플랫폼은 사용자들이 친구, 가족, 동료들과 소통하고 정보를 공유하는 환경을 제공하여 사회적 관계망이 확장되고 정보가 신속하게 확산될 수 있도록 돕았습니다.
웹 애플리케이션의 사용자 경험은 AJAX(Asynchronous JavaScript and XML)와 같은 기술의 도입으로 크게 향상되었습니다. 이를 통해 사용자들은 더욱 빠르고 반응적인 웹 애플리케이션을 경험할 수 있게 되었습니다. 또한, 다양한 서비스들이 오픈 API를 통해 상호 연결되고 데이터를 교환할 수 있게 되었기 때문에 웹 애플리케이션의 통합성과 확장성이 증대되었습니다.
그리고 RSS(Really Simple Syndication) 피드를 통해 사용자들은 실시간으로 새로운 콘텐츠 업데이트를 받아볼 수 있게 되었습니다. 더불어, 사용자 지향의 검색 엔진이 등장하여 개인화된 검색 결과를 제공함으로써 검색 환경도 개선되었습니다. 이러한 변화들은 인터넷 사용자들에게 더욱 편리하고 맞춤화된 정보 접근을 가능케 했습니다.
이러한 변화는 인터넷을 단순한 정보 제공 매체에서 사용자들 간의 상호작용과 협업이 이루어지는 플랫폼으로 변화시켰습니다. 이제 사용자들은 정보를 소비하는 것뿐만 아니라 직접 참여하고 콘텐츠를 생성할 수 있으며, 이를 통해 보다 다양하고 역동적인 온라인 경험을 할 수 있게 되었습니다.
자유로운 상호작용을 위해
상호작용하는 웹 서비스의 중요성이 증가함에 따라 실시간 데이터 전송이 필수적으로 요구되었습니다. 이에 따라 클라이언트가 요청을 보내는 것이 아닌 서버가 데이터를 클라이언트에게 실시간으로 전송해야 하는 필요성이 대두되었습니다. 이를 해결하기 위해 Hidden iframe streaming과 Long Polling이 사용되었습니다.
Hidden iframe은 HTML 문서에 숨겨진 프레임으로, 사용자에게는 보이지 않습니다. Hidden iframe streaming은 클라이언트가 이를 이용하여 서버로부터 데이터를 주기적으로 가져오는 방식으로 동작합니다. 이 방법은 실시간 업데이트가 필요한 상황에서 유용하지만, 네트워크 오버헤드가 발생할 수 있습니다.
Long Polling은 클라이언트가 서버에 요청을 보내고, 서버가 새로운 데이터가 생길 때까지 응답을 지연시키는 방식입니다. 이를 통해 실시간 데이터 전송을 구현할 수 있지만, 클라이언트가 주기적으로 새로운 데이터를 요청하여 서버에 부하를 유발할 수 있습니다. 또한 연결을 유지하기 위해 리소스가 소모될 수 있습니다.
하지만, 이러한 방식은 TCP 핸드셰이크와 HTTP 헤더 부하로 인해 비효율적이었습니다. 이는 데이터 전송 속도를 저하시키고, 서버 및 클라이언트의 부담을 늘릴 수 있었습니다.
이 문제를 해결하고 웹의 전이중 통신을 실현하고자 WebSocket이 고안되었습니다. WebSocket은 웹에서 실시간 양방향 통신을 가능하게 하는 프로토콜로, 하나의 TCP 접속에 전이중 통신 채널을 제공합니다. 이 프로토콜은 HTML5 사양에서 처음 등장하였으며, 2011년에 RFC 6455로 표준화되었습니다. 또한, 웹 IDL의 WebSocket API는 W3C에 의해 표준화되고 있습니다.
WebSocket은 웹 브라우저와 웹 서버 간의 통신을 효율적으로 처리하며, 실시간 데이터 전송을 가능케 합니다. 이 프로토콜은 대부분의 주요 브라우저에서 지원되어 웹 개발자들은 더 동적이고 반응적인 웹 애플리케이션을 구축할 수 있게 되었습니다.
WebSocket의 핸드셰이크
WebSocket은 HTTP와는 달리 전이중 통신을 지원하며, TCP 위에서 메시지 스트리밍이 가능합니다. 또한, HTTP와 호환되어 WebSocket의 핸드셰이크는 HTTP Upgrade 요청을 통해 이루어집니다.
클라이언트가 WebSocket 핸드셰이크를 위해 다음과 같은 요청을 보낼 수 있습니다:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
여기서 Request-URI
는 WebSocket 연결의 엔드포인트를 식별합니다. 이를 통해 단일 IP 주소에서 여러 엔드포인트를 제공할 수 있습니다. 클라이언트는 핸드셰이크의 Host
헤더에 호스트 이름을 포함하여 클라이언트와 서버가 사용 중인 호스트를 명확히 전달할 수 있습니다.
추가적인 헤더는 옵션을 선택하는 데 사용됩니다. Sec-WebSocket-Protocol
헤더는 클라이언트가 허용하는 응용 프로그램 수준 프로토콜을 나타냅니다. 서버는 허용되는 프로토콜 중 하나를 선택하여 해당 값을 핸드셰이크에서 반환하여 선택한 프로토콜을 클라이언트에게 알립니다.
Sec-WebSocket-Protocol: chat
Origin
헤더는 웹 브라우저에서 권한없는 교차 출처 리소스 공유(CORS, Cross-Origin Resource Sharing)를 막기 위해 사용됩니다. 서버는 WebSocket 연결 요청을 생성하는 스크립트에 대한 정보를 받습니다. 서버가 이 원본에서의 연결을 수락하지 않으려면 적절한 HTTP 오류 코드를 보내 연결을 거부할 수 있습니다.
핸드셰이크를 받았음을 증명하려면 서버는 두 가지 정보를 가져와서 응답을 작성해야 합니다. 첫 번째 정보는 Sec-WebSocket-Key
헤더에서 가져옵니다. 이 헤더 값에 문자열 형식의 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”를 연결합니다. 이는 WebSocket 프로토콜을 이해하지 않는 네트워크 엔드포인트에 의해 사용될 가능성이 적은 전역 고유 식별자(GUID)입니다. 그런 다음 SHA-1로 해시를 하고 이를 base64로 인코딩해야 합니다. 이 값은 서버의 핸드셰이크에서 base64로 인코딩된 값인 Sec-WebSocket-Key
헤더로 다시 전달됩니다.
Sec-WebSocket-Key
헤더 필드 값이 “dGhlIHNhbXBsZSBub25jZQ==”이라면, 서버는 문자열 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”를 연결하여 문자열 “dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11”를 만들고 SHA-1로 해시한 이후 base64로 인코딩해야 합니다. 이 값은 “s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”와 같습니다.
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
서버의 응답은 일반적인 HTTP 응답의 형식을 가지고 있습니다. 첫 번째 줄은 상태 코드 101을 포함합니다.
HTTP/1.1 101 Switching Protocols
101이 아닌 다른 상태 코드는 핸드셰이크가 완료되지 않았음을 나타내며 HTTP를 계속 사용함을 의미합니다.
Connection
와 Upgrade
헤더는 HTTP 업그레이드를 완료합니다.
Sec-WebSocket-Accept
헤더 필드는 서버가 연결을 수락할 것인지 여부를 나타냅니다. 이 헤더 필드가 존재하면 반드시 클라이언트의 Sec-WebSocket-Key
에서 보낸 값의 해시와 미리 정의된 GUID가 포함되어야 합니다.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
이러한 필드들은 클라이언트에 의해 확인됩니다. 만약 Sec-WebSocket-Accept
값이 예상되는 값과 일치하지 않거나, 헤더 필드가 누락되었거나, HTTP 상태 코드가 101이 아닌 경우 연결이 설정되지 않으며 WebSocket 프레임이 전송되지 않습니다.
정상적으로 핸드셰이크가 완료되면, HTTP 통신에 사용되었던 TCP 연결을 사용하여 WebSocket의 프레임을 주고 받을 수 있습니다.
WebSocket에서 연결을 종료하는 과정은 제어 프레임을 사용합니다. 한 피어가 특정한 제어 프레임을 보내면 연결이 종료됩니다. 이런 프레임을 받은 다른 피어는 Close 프레임을 보낸 적이 없다면 응답으로 Close 프레임을 보냅니다. 그 후 첫 번째 피어는 더 이상 데이터를 받지 않을 것이라고 확신하고 연결을 닫습니다. 제어 프레임을 보낸 후에는 해당 피어는 더 이상 데이터를 보내지 않으며, 제어 프레임을 받은 후에는 수신된 데이터를 처리하지 않습니다.
양쪽 모두가 Close 메시지를 보내고 수신한 후에 WebSocket 연결이 종료되고 사용 중이던 TCP 연결을 닫아야 합니다. 서버는 TCP 연결을 즉시 닫아야 하지만 클라이언트는 일정 시간 내에 TCP Close를 받지 못한 경우에도 연결을 닫을 수 있습니다.
종료 코드
엔드포인트가 Close 프레임을 전송할 때, 미리 정의된 상태 코드를 사용할 수 있습니다. 이 상태 코드는 본문의 처음 두 바이트에 들어갑니다. 그 이후에는 UTF-8로 인코딩된 메시지가 포함될 수 있습니다.
-
1000: 연결이 설정된 목적이 달성되었음을 나타냅니다.
-
1001: 엔드포인트가 “사라짐”을 나타냅니다. 예를 들어, 서버가 다운되거나 브라우저가 페이지를 이동한 경우입니다.
-
1002: 엔드포인트가 프로토콜 오류로 연결을 종료하고 있음을 나타냅니다.
-
1003: 엔드포인트가 수용할 수 없는 유형의 데이터를 받았기 때문에 연결을 종료하고 있음을 나타냅니다.
-
1004: 예약됨. 향후에 특정 의미가 정의될 수 있습니다.
-
1005: 1005는 예약된 값이며, 엔드포인트에서 Close 제어 프레임의 상태 코드로 설정해서는 안 됩니다. 실제로 상태 코드가 포함되지 않은 것을 나타내는 상태 코드로 사용됩니다.
-
1006: 1006은 예약된 값이며, 엔드포인트에서 Close 제어 프레임의 상태 코드로 설정해서는 안 됩니다. 연결이 비정상적으로 종료되었음을 나타내는 상태 코드로 사용됩니다. 즉, Close 제어 프레임을 보내거나 받지 않은 상태에서 연결이 닫혔음을 나타냅니다.
-
1007: 데이터가 메시지 내에 일치하지 않는 유형의 데이터를 받았기 때문에 연결을 종료하고 있음을 나타냅니다.
-
1008: 엔드포인트가 정책을 위반하는 메시지를 수신하여 연결을 종료하고 있음을 나타냅니다. 이것은 다른 더 적합한 상태 코드(예: 1003 또는 1009)가 없거나 정책에 대한 구체적인 세부 정보를 숨기기 위해 사용될 수 있는 일반적인 상태 코드입니다.
-
1009: 엔드포인트가 처리할 수 있는 메시지가 너무 크기 때문에 연결을 종료하고 있음을 나타냅니다.
-
1010: 1010은 클라이언트가 서버로 하나 이상의 확장을 협상하기를 기대했지만, 서버가 WebSocket 핸드셰이크 응답 메시지에 이를 반환하지 않았을 경우에 사용됩니다. 필요한 확장 목록은 Close 프레임에서 상태 코드 뒤에 UTF-8로 인코딩되어 전달되어야 합니다. 이 상태 코드는 서버가 사용하지 않으며 WebSocket 핸드셰이크를 실패시킬 수 있습니다.
-
1011: 서버가 요청을 충족시키지 못한 예기치 않은 상태를 만나 연결을 종료하고 있음을 나타냅니다.
-
1015: 1015는 예약된 값이며, 엔드포인트에서 Close 제어 프레임의 상태 코드로 설정해서는 안 됩니다. TLS 핸드셰이크를 수행하지 못한 연결 종료를 나타내기 위해 사용됩니다.
WebSocket의 프레임
WebSocket는 하나의 데이터를 여러 프레임의 시퀀스로 전송합니다. 이 프레임은 여러 필드로 구성되어 있으며, 기본 프레임 프로토콜은 이러한 필드를 정의합니다.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 :
+ - - - - - - - - - - - - - - - +-------------------------------+
: | Masking-key, if MASK set to 1 :
+-------------------------------+-------------------------------+
: ... | Payload Data :
+-------------------------------- - - - - - - - - - - - - - - - +
: ... :
+---------------------------------------------------------------+
: ... |
+---------------------------------------------------------------+
-
FIN(1 비트): 메시지의 마지막 프레임인지를 나타냅니다. 첫 번째 프레임이면서 동시에 마지막 프레임일 수도 있습니다.
-
RSV1, RSV2, RSV3(각각 1 비트): 서버와 클라이언트가 값을 확장하자고 협상되지 않으면 기본값인 0으로 설정되어야 합니다.
- Opcode(4 비트): “Payload Data”의 해석을 정의합니다.
- %x0: 전체 데이터의 일부인 프레임을 나타냅니다.
- %x1: 텍스트 프레임을 나타냅니다.
- %x2: 바이너리 프레임을 나타냅니다.
- %x3-7: 추가적인 비제어 프레임을 위해 예약되어 있습니다.
- %x8: 연결 종료를 나타냅니다.
- %x9: 핑을 나타냅니다.
- %xA: 퐁을 나타냅니다.
- %xB-F: 추가적인 제어 프레임을 위해 예약되어 있습니다.
-
Mask(1 비트): “Payload Data”가 마스크되어 있는지를 정의합니다. 클라이언트에서 서버로 전송되는 모든 프레임에는 이 비트가 1로 설정됩니다. 마스킹은 악의적인 클라이언트측 코드가 데이터를 정확하게 제어하는 것을 방지하기 위한 보안 기능입니다. 4바이트의 마스크 키를 사용하여 Payload와 같이 XOR 연산을 합니다.
-
Payload length(7 비트, 7+16 비트 또는 7+64 비트): “Payload Data”의 길이를 바이트 단위로 나타냅니다.
-
Masking-key(0 또는 4 바이트): 클라이언트에서 서버로 전송되는 모든 프레임에는 마스킹 키가 포함됩니다.
-
Payload data((x+y) 바이트): “Extension Data”와 “Application Data”를 합친 것입니다.
-
Extension data(x 바이트): 확장이 협상된 경우를 제외하고는 0바이트입니다.
- Application data(y 바이트): 애플리케이션에 따른 데이터로, 확장 데이터 이후의 나머지 프레임을 차지합니다.
더 빠르게, HTTP/2
HTTP/1.1은 초기 웹 환경에서 효율적으로 동작했지만, 웹 페이지의 크기와 복잡성이 증가함에 따라 몇 가지 한계가 드러나기 시작했습니다.
-
Head-of-Line Blocking: HTTP/1.1에서는 한 번에 하나의 요청만 처리되며, 다음 요청은 이전 요청의 응답을 받은 후에 처리됩니다. 이러한 방식은 Head-of-Line Blocking이 발생하여, 하나의 요청이 지연되면 그 뒤의 요청들도 모두 대기해야 하므로 전체 페이지의 로딩 시간이 길어지는 문제가 있습니다.
-
Header Overhead: HTTP/1.1은 각 요청과 응답에 대한 헤더 정보를 중복해서 전송합니다. 이는 헤더 오버헤드를 유발하여 대역폭을 낭비하고 통신 지연을 초래합니다. 특히, 웹 페이지의 크기가 커질수록 이러한 오버헤드는 더욱 심각해집니다.
Google은 이러한 HTTP/1.1의 한계를 극복하기 위해 SPDY 프로젝트를 시작했습니다. SPDY는 웹 페이지의 로딩 시간을 줄이고 보안을 강화하는 데 중점을 두며, 헤더 오버헤드를 줄이고 효율적인 통신을 가능하게 하는 등의 여러 기술적 개선을 제안했습니다.
SPDY 프로젝트는 HTTP/2의 기초가 되었으며, HTTP/2는 2015년에 공식적으로 표준으로 채택되었습니다. HTTP/2의 등장으로 인해 웹 페이지의 로딩 속도와 효율성이 크게 향상되었습니다.
-
Binary Framing Layer: HTTP/2에서는 HTTP 메시지를 작은 프레임으로 분할하여 전송하는 이진 형태의 프레임워크를 도입합니다. 이렇게 함으로써 하나의 프레임이 손상되더라도 다른 프레임은 영향을 받지 않아 전체 통신의 신뢰성이 높아집니다.
-
Multiplexing: HTTP/1.1에서는 요청과 응답이 순차적으로 처리되어야 했지만, HTTP/2에서는 메시지가 작은 프레임으로 분할되고 다중화를 통해 병렬로 처리할 수 있어서 성능을 크게 향상시켰습니다. 이를 위해 여러 프레임을 멀티플렉싱하여 요청과 응답을 동시에 처리합니다. 그러나 이로 인해 하나의 연결에 여러 요청과 응답이 섞이면서 패킷의 순서가 엉망이 되는 문제가 발생했습니다. 이에 대한 해결책으로 클라이언트는 우선순위 지정 트리를 사용하여 각 스트림에 식별자를 설정하여 패킷의 순서를 유지하고자 합니다.
-
Header Compression: HTTP/2에서는 중복되는 헤더 정보를 미리 정의된 헤더 테이블에 저장하고, 인덱스 번호를 사용하여 해당 헤더를 참조합니다. 이를 통해 동일한 헤더가 여러 번 전송되는 대신, 헤더의 실제 내용이 아닌 인덱스 번호만 전송하여 대역폭을 효율적으로 활용할 수 있습니다. HTTP/2는 동적 헤더 압축을 지원하여 새로운 헤더 필드가 추가되거나 변경될 때에도 효율적으로 처리할 수 있습니다. 이를 통해 네트워크 부하를 줄이고 통신 속도를 향상시킬 수 있습니다.
-
Server Push: HTTP/2에서는 클라이언트가 요청하지 않은 리소스를 미리 서버에서 클라이언트로 전송합니다. 서버는 HTML 문서가 참조하는 이미지, CSS 파일, JS 파일 등의 리소스를 스스로 파악하여 클라이언트에게 미리 push합니다. 이를 통해 클라이언트가 HTML 문서를 파싱하고 추가적인 요청을 보내는 데 걸리는 시간과 회전 지연을 줄일 수 있습니다.
HTTP/2의 프레임
HTTP/2 프레임은 다음과 같은 구조를 가지고 있습니다:
0 1 2 3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+----------------------------------------------+----------------+
| Length | Type |
+---------------+-+----------------------------+----------------+
| Flags |R| Stream Identifier... :
+---------------+-+---------------------------------------------+
: ... | Frame Payload... :
+---------------------------------------------------------------+
: ... |
+---------------------------------------------------------------+
-
Length(3 바이트): 프레임 페이로드의 길이를 나타냅니다. 최대 프레임 크기인 2^24 Bytes를 초과하면 SETTING 프레임으로 설정을 해야 합니다.
- Type(1 바이트): 프레임의 유형을 나타냅니다. 프레임 유형은 헤더, 데이터, 우선 순위, 설정 등 여러 가지가 있습니다.
- DATA (유형 코드: 0x0): 메시지의 실제 데이터를 전송합니다.
- HEADERS (유형 코드: 0x1): 요청 또는 응답의 헤더를 전송합니다.
- PRIORITY (유형 코드: 0x2): 스트림의 우선 순위를 지정합니다.
- RST_STREAM (유형 코드: 0x3): 스트림을 중단하고 에러를 통지합니다.
- SETTINGS (유형 코드: 0x4): 연결 또는 스트림에 대한 구성 설정을 전송합니다.
- PUSH_PROMISE (유형 코드: 0x5): 서버가 클라이언트에게 리소스를 푸시하기 위해 사용됩니다.
- PING (유형 코드: 0x6): 연결 상태를 확인하기 위한 핑을 전송합니다.
- GOAWAY (유형 코드: 0x7): 서버가 클라이언트에게 더 이상의 스트림을 생성하지 말라고 알립니다.
- WINDOW_UPDATE (유형 코드: 0x8): 윈도우 크기를 업데이트하여 흐름 제어를 수행합니다.
- CONTINUATION (유형 코드: 0x9): 여러 HEADERS 프레임이 필요한 경우에 사용되어 헤더 블록을 계속 전송합니다.
-
Flags(1 바이트): 프레임의 특정 특성을 나타내는 플래그입니다. 각 플래그는 프레임 유형에 따라 다르게 해석될 수 있습니다.
-
R(1 비트): 예약 필드로, 현재 사용되지 않습니다.
-
Stream Identifier(31 비트): 프레임과 연관된 스트림을 식별하는 데 사용됩니다.
-
Type(1 바이트): 프레임의 유형을 나타냅니다. 프레임 유형은 헤더, 데이터, 우선 순위, 설정 등 여러 가지가 있습니다.
- Frame Payload(가변 길이): 실제 프레임 데이터를 포함하는 부분입니다. 프레임의 유형에 따라 페이로드의 내용이 달라집니다.
데이터 프레임 페이로드
데이터 프레임은 요청되거나 응답된 객체의 데이터를 나타내며, 이 데이터는 최대 프레임 길이에 따라 하나 이상의 프레임으로 나누어질 수 있습니다. 패딩 길이 필드와 패딩 자체는 메시지 크기를 숨기기 위해 선택적으로 포함됩니다.
프레임 헤더의 Flags를 END_STREAM(0x1) 또는 PADDED(0x8)로 설정하여 스트림의 마지막 프레임임을 알려주거나, 패딩을 활성화할 수 있습니다.
0 1 2 3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+--------------+------------------------------------------------+
| Pad Length | Data... :
+--------------+------------------------------------------------+
: ... |
+---------------------------------------------------------------+
| Padding... :
+---------------------------------------------------------------+
: ... |
+---------------------------------------------------------------+
-
Pad Length(1 바이트): 패딩 필드의 길이를 나타냅니다. PADDED 플래그가 프레임 헤더에 설정된 경우에만 존재합니다.
-
Data(가변): 프레임의 실제 데이터를 포함합니다.
-
Padding(가변): 패딩 필드로, 패딩 길이에 따라 설정됩니다. 모든 바이트는 0으로 설정됩니다.
헤더 프레임 페이로드
헤더 프레임은 요청 및 응답의 헤더를 전송하는 데 사용되며, 일반적으로 스트림을 열 때 전송됩니다. 헤더는 여러 프레임으로 분할될 수 있으며, 모든 헤더가 모이면 End Stream 플래그가 설정됩니다. 또한, 이 스트림은 우선순위를 나타내기 위한 Priority를 설정할 수 있는 영역을 가지고 있습니다. 이 영역은 스트림을 열 때 최초로 설정됩니다.
프레임 헤더의 Flags를 END_STREAM(0x1) 또는 PADDED(0x8)로 설정하여 스트림의 마지막 프레임임을 알려주거나, 패딩을 활성화할 수 있습니다. 또한, END_HEADERS(0x4) 플래그를 사용하여 마지막 해더 프레임을 나태내거나, PRIORITY(0x20) 플래그를 사용하여 E, Stream dependency, Weight 필드를 활성화할 수 있습니다.
0 1 2 3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+--------------+-+----------------------------------------------+
| Pad Length |E| Stream dependency... :
+--------------+-+-------------+--------------------------------+
: ... | Weight | Header Block Fragment... :
+--------------+---------------+--------------------------------+
: ... |
+---------------------------------------------------------------+
| Padding... :
+---------------------------------------------------------------+
: ... |
+---------------------------------------------------------------+
-
Pad Length(1 비트): 패딩 필드의 길이를 나타냅니다. PADDED 플래그가 프레임 헤더에 설정된 경우에만 존재합니다.
-
E(1 비트): 현재 스트림이 의존하는 다른 스트림에 배타적으로 의존하는지를 나타내는 비트입니다.
-
Stream dependency(31 비트): 현재 스트림이 의존하는 다른 스트림의 ID를 나타냅니다. 이는 현재 스트림의 우선 순위를 결정하는 데 사용됩니다.
-
Weight(1 바이트): 현재 스트림의 상대적인 가중치를 나타냅니다.
-
Header Block Fragment(가변): HPACK 압축 형식으로 압축된 헤더 데이터가 여기에 포함됩니다. 이는 요청이나 응답의 헤더 정보를 나타냅니다.
-
Padding(가변): 패딩 필드로, 패딩 길이에 따라 설정됩니다. 모든 바이트는 0으로 설정됩니다.
HTTP/2에서의 WebSocket 확장
HTTP는 여러 버전 간에 리소스 수준의 의미론을 일관되게 제공하는 반면, 연결 관리 수준에서의 호환성은 미흡합니다. 특히 WebSocket과 같은 프로토콜은 HTTP 연결 관리 세부 정보에 의존하면서 HTTP/2의 다중화로 인해 WebSocket 프로토콜의 핸드셰이크에서 필요한 연결 관리 헤더 필드나 상태 코드의 호환성 유지가 더욱 복잡해졌습니다.
RFC 8441은 HTTP/2에서 WebSocket을 초기화하는 새로운 방법을 제안합니다. 이를 통해 하나의 TCP 연결이 두 가지 프로토콜을 효율적으로 처리할 수 있습니다.
HTTP/2에서 WebSocket을 시작하기 위해서는 CONNECT 메소드를 사용하여 요청을 확장해야 합니다. 이 요청은 “:protocol” 가상 헤더를 포함하며 값은 “websocket”으로 설정됩니다. 이렇게 하면 WebSocket 핸드셰이크를 위한 웹소켓 프로토콜로의 전환을 간편하게 할 수 있습니다. 이 가상 헤더를 감지하면 해당 프로토콜로 연결을 변경합니다.
또한, 새로운 SETTINGS 매개변수 “SETTINGS_ENABLE_CONNECT_PROTOCOL”을 도입하여 클라이언트가 확장된 CONNECT 메소드를 사용할 수 있도록 합니다. 이 설정을 변경하려면 HTTP/2의 SETTINGS 프레임을 사용합니다. SETTINGS 프레임은 HTTP/2 연결의 설정을 조정하는 데 사용됩니다. 이 설정은 연결의 시작 시점에 설정되며, 활성화된 연결 중에도 필요에 따라 조정될 수 있습니다.
밑바닥부터 바꾸어, HTTP/3
HTTP/2의 등장으로 웹 통신의 성능과 효율성은 상당히 개선되었지만, 여전히 몇 가지 한계가 존재했습니다.
-
Head-of-Line Blocking: HTTP/1.1에서는 파이프라인 기술을 통해 병렬적으로 리소스를 빠르게 얻도록 하려고 했지만, 이로 인해 Head Of Line Blocking 문제가 발생했습니다. HTTP/2에서도 TCP 레이어에서 이러한 문제가 남아있었습니다. HTTP/2는 하나의 TCP 커넥션으로 여러 개의 스트림 데이터를 병렬 전송하는 방식을 채택했지만, 네트워크 어딘가에서 하나의 패킷이 손실되거나 지연되면 해당 패킷 이후의 모든 데이터 전송이 중단될 수 있었습니다.
-
TCP 오버헤드: TCP 연결 설정 시에는 보안을 위해 TLS Handshake 과정이 필요했습니다. 이는 클라이언트와 서버 간에 암호화된 연결을 설정하기 위한 추가적인 라운드트립 시간과 계산 비용을 필요로 했습니다.
Google은 QUIC(Quick UDP Internet Connections) 프로젝트를 시작하여 이러한 한계를 극복하고자 했습니다. HTTP/3은 QUIC 프로토콜을 기반으로 하여 개발되었습니다. QUIC은 TCP보다 연결 설정 및 해제에 필요한 오버헤드가 적으며, TCP의 혼잡 제어 문제를 회피할 수 있습니다. 이에 따라 HTTP/3은 기존의 TCP 및 TLS 연결을 대체하여, 웹 통신의 성능과 보안을 향상시키는 데 중점을 두었습니다.
-
빠른 연결: HTTP/3는 Zero Round-Trip Time (0-RTT) 핸드셰이크 기능을 제공하여 초기 연결 설정 시에 레이턴시를 최소화합니다. 클라이언트는 이전 통신에서 얻은 서버 정보를 캐시하고, 다음 연결 시에 추가적인 핸드셰이크 없이 데이터를 전송할 수 있습니다. 또한, 64비트 Connection ID를 통해 연결을 식별하여 클라이언트 IP나 포트가 변경되어도 컨넥션을 유지할 수 있습니다.
-
Head-of-Line Blocking 해결: 스트림 자체를 독립적으로 여러개로 독립 스트림으로 분리하여 병렬적으로 전송할 수 있습니다. 이는 네트워크에서 패킷 손실이나 지연이 발생해도 다른 스트림이 영향을 받지 않고 전송될 수 있습니다.
-
패킷 손실 감지와 복구 시간 단축: HTTP/3은 QUIC을 사용하여 패킷 손실을 빠르게 감지하고 복구합니다. TCP에선 재전송된 패킷이 원래 전송된 패킷보다 빠르게 도착한다면, 수신 측에서는 이 패킷이 새로운 데이터인지 아니면 이전에 도착한 패킷의 재전송인지를 판단하기 어려울 수 있습니다. QUIC은 TCP와 달리 재전송 모호 문제를 해결하여 패킷 손실 복구 시간을 단축합니다.
-
강화된 보안: HTTP/3와 QUIC은 기본적으로 TLS 1.3 암호화를 사용합니다. QUIC 내에 TLS가 포함되어 있어 헤더 영역도 함께 암호화됩니다. 이는 중간자 공격 및 데이터 변조와 같은 공격으로부터 데이터를 효과적으로 보호합니다.
HTTP/3에서의 WebSocket 확장
RFC 9220은 HTTP/3에서의 WebSocket 확장에 대한 세부 사항을 정의합니다. 이것은 RFC 8441과 유사한 웹 소켓 연결 방식을 HTTP/3 프로토콜에 적응시킵니다.
HTTP/3에서의 WebSocket 연결은 HTTP/2와 유사하지만, TCP 대신에 HTTP/3 스트림을 활용합니다.
HTTP/3의 스트림은 UDP 위에서 구축되며, 양방향 데이터 흐름을 형성하는 일련의 바이트입니다. 각 스트림은 고유한 식별자를 가지며, 클라이언트와 서버 간의 통신을 관리합니다. 이러한 스트림은 독립적으로 작동하며, 하나의 연결에서 병렬로 전송될 수 있습니다. 스트림의 종료는 일반적으로 HTTP/3의 FIN 비트를 통해 이루어지며, 이는 TCP 연결 종료와 유사한 역할을 합니다. FIN 비트가 설정되면 해당 스트림은 종료되며, 더 이상의 데이터 전송이 이루어지지 않습니다. 그러나 특정 상황에서는 스트림 오류가 발생할 수 있습니다. 이러한 경우, 오류는 HTTP/3의 스트림 오류 메커니즘을 통해 처리됩니다. 예를 들어, 서버가 요청을 처리할 수 없거나 클라이언트가 연결을 취소하는 경우 스트림 오류가 발생할 수 있습니다. 이러한 오류는 H3_REQUEST_CANCELLED와 같은 오류 코드를 사용하여 표시됩니다.
이렇듯 HTTP/3에서 WebSocket을 사용하는 과정은 TCP 대신에 HTTP/3 스트림을 통해 이루어지며, UDP 기반의 프로토콜을 사용하므로 일부 차이가 있습니다.
참고 자료
- WebSocket, 위키피디아
- Network_Control_Protocol_(ARPANET), 위키피디아
- History_of_the_Internet, 위키피디아
- Transmission_Control_Protocol, 위키피디아
- CSNET, 위키피디아
- 인터넷의 역사, nordvpn
- 인터넷의 역사와 ARPANET, 면역학자 배용석
- The WebSocket Protocol
- Bootstrapping WebSockets with HTTP/2
- Bootstrapping WebSockets with HTTP/3
- Hypertext Transfer Protocol Version 2 (HTTP/2)
- Learning HTTP/2
댓글남기기