2019. 2. 28. 11:45ㆍLearning HTTP2
이번 장에서는 전송되는 프레임 수준까지 내려가, HTTP/2가 저수준에서 동작하는 방식을 설명한다. 이를 통해 HTTP/2 프로토콜이 제공하는 많은 장점을 이해할 수 있을 것이다. 이번 장을 다 읽고 나면, 독자들 스스로 h2 환경을 조율하고 디버깅하면서 프로토콜을 활용할 수 있을 만큼 충분한 정보를 얻게 될 것이다. HTTP/2 프로토콜을 구현하려는 목적이라면, RFC 7540이 훌륭한 출발점이다.
5.1 HTTP/2의 계층
HTTP/2는 일반ㅇ적으로 두 계층으로 나눌 수 있다. 하나는 h2 다중화 기능의 핵심인 프레이밍 계층이고 다른 하나는 전통적인 HTTP 및 관련 데이터 부분을 포함하는 데이터 계층 또는 http 계층이다. 이 두 계층을 따로 구분하여 완전히 별개인 것으로 생각하기 쉽다. 규격을 잘 읽어보면, 재사용 가능한 구조인 프레이밍 계층과 HTTP를 전송하도록 설계된 데이터 계층 사이에는 경계를 나누기 어려운 부분이 있다. 예를 들어, 규격은 엔드포인트와 양방향성(많은 메시징 애플리케이션에 필요한 특성)에 관한 이야기로 시작해, 클라이언트, 서버, 요청, 응답에 관한 이야기로 넘어간다. 프레이밍 계층에 관해 읽을 때, 프레이밍 계층의 목적은 다름 아닌 HTTP를 주고받기 위한 것이라는 사실을 잊지 말아야 한다.
데이터 계층은 HTTP/1.1과 하위 호환성을 유지하도록 설계되었지만 h1에 친숙하고 전송 중인 메시지를 바로 읽는 데 익숙하던 개발자들을 여러 번 놀라게 할 만큼 많은 h2만의 특성이 있다.
바이너리 프로토콜
h2 프레이밍 계층은 바이너리 프레임 형식의 프로토콜이다. 이 때문에 기계는 쉽게 파싱할 수 있지만 사람이 읽기는 어렵다.
헤더 압축
바이너리 프로토콜만으로는 충분하지 않았는지, h2에서는 헤더가 압축된다. 이는 중복된 바이트의 전송을 줄이는 데 효과가 있다.
다중화
선호하는 디버깅 도구로 h2를 전송 중인 연결을 들여가보면, 요청과 응답이 뒤섞여 있는 것을 볼 수 있을 것이다.
암호화
그 외에, 전송 중인 데이터의 대부분이 암호화 되기 때문에, 전송되는 즉시 판독하기 더 어려워졌다.
이 각각의 주제를 하나씩 살펴볼 것이다.
5.2 연결
HTTP/2 세션의 기본 요소는 바로 연결이다. 연결이란 클라이언트가 개시한 TCP/IP 소켓으로 정의되며, HTTP 요청을 전송할 엔티티를 의미한다. 여기까지는 h1과 다를 게 없다. 하지만, 상태를 저장하지 않는 h1과는 달리, h2는 h2 위에서 흐르는 모든 프레임과 스트림이 연관된 연결 수준의 요소들을 함께 묶는다. 여기에는 연결 설정값과 헤더 테이블(이 둘은 이번 장 나중에 자세히 설명한다)이 포함된다. 이는 모든 h2 연결에는 이전 버전에는 없던 오버헤드가 어느 정도 있음을 암시한다. 이는 오버헤드의 이득이 그에 드는 비용보다 훨씬 크기 때문에 의도된 것이다.
사용자가 통신하고자 하는 프로토콜을 엔드포인트가 지원하는지를 확인하는 절차인 프로토콜 탐색은 쉬운 절차가 아니다. HTTP/2는 두 가지 탐색 메커니즘을 제공한다. 연결이 암호화되지 않은 경우, 클라이언트는 Upgrade 헤더를 활용해 h2로 통신하려는 요구를 전달한다. 서버가 h2로 통신할 수 있으면, '101Switching Protocols' 응답을 회신한다. 이 절차에 한 번의 왕복이 추가로 소요된다. 하지만 연결이 TLS로 수립되는 경우, 클라이언트는 ClientHello 메시지 않에 ALPN을 설정하여 h2로 통신하려는 요구를 전달하며, 서버도 같은 방식으로 응답한다. 이 방식에는 h2는 추가적인 왕복 없이 주고받기 한 번으로 협상이 이루어진다. SPDY와 초기 버전의 h2는 NPN을 사용해 h2 협상을 했다ㅣ NPN은 2014년 중반에 ALPN으로 대체되었다. h2 지원을 표시하는 마지막 한 가지 방법은 HTTP Alternative Services, 즉 Alt-Svc를 사용하는 것이다. 서버는 클라이언트에 보낼 응답 헤더에 Alt-Svc를 설정하여 다음 요청에서 더 나은 프로토콜을 사용할 수 있다는 사실을 표시할 수 있다. 이는 점점 더 많은 브라우저가 지원하고 있는 매우 유연한 기능이다. Alt-Svc가 ALPN을 대체하려고 의도된 것은 아니지만 알아두어야 할 강력한 기능이다. |
클라이언트 엔드포인트가 h2로 통신한다는 사실을 서버에 다시 한번 더 알려주기 위해, 클라이언트는 연결 전문이라는 마법의 옥텟스트림을 연결의 첫 번째 데이터로 전송한다. 이는 주로 클라이언트가 평문HTTP/1.1에서 업그레이드된 경우를 위해 의도된 것이다. 이 스트림은 16진수로 다음과 같다.
- 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
ASCII로 변환하면 다음을 얻을 수 있다.
- PRI*HTTP/2.0\r\n\r\nSM\r\n\r\n
이 문자열의 목적은 어떤 이유로든 서버(또는 중재자)가 h2로 통신할 수 없게 된 경우, 명시적인 오류가 발생하게 하는 것이다. 이 메시지는 의도적으로 h1 메시지처럼 보이게 구성되었다. 정상 동작하는 h1 서버가 이 문자열을 수신한 경우, 메시지 안의 메서드(PRI)나 버전(HTTP/2.0)을 인식하지 못해 오류를 반환할 것이며, 이 덕분에 h2 클라이언트는 무언가 잘 못되엇따는 것을 분명히 알 수 있게 된다.
이 마법의 문자열에 이어 SETTINGS프레임이 뒤따른다. 서버는 h2로 통신할 수 있음을 확인해주기 위해 클라이언트의 SETTINGS 프레임을 왁인(ACK)하고, 자신의 SETTINGS 프레임으로 응답한다(이것도 곧이어 확인된다). 이제 서로 동의가 이루어 졌고 h2를 시작할 수 있다. 이 절차가 가능한 한 효율적일 수 있도록 그동안 많은 작업이 이루어졌다. 겉보디에는 이 절차가 다소 복잡해 보일 수 있지만 클라이언트는 서버의 SETTINGS 프레임이 도착할 것을 가정하고 미리 프레임 전송을 시작할 수 있다. 프레임을 미리 전송한 클라이언트가 어떤 이유로 SETTINGS 프레임보다 다른 무언가를 먼저 수신한다면, 그 협상은 실패하고 모두가 GOAWAY 프레임을 수신하게 된다.
연결 전문에는 두 가지 '비밀' 메시지가 포함되어 있다. 첫 번째는 미국 NSA의 PRISM 감시 프로그램에 대한 충자를 담고 있다. HTTP/2의 초기 개발이 진행되던 당시에 PRISM 프로그램이 폭로되었으며, 몇몇 재치 있는 사람들이 PRISM 프로그램을 HTTP/2 프로토콜에 영원히 남기기로 결정했다(독자들은 프로토콜 개발자들이 유머 감각이 없다고 생각할지도 모른다). 두 번째는 HTTP/2.0에 대한 흔적이다. HTTP의 향후 버전에서는 의미적인 하위 호환성이 보장되지 않을 것이라는 점을 나타내기 위해 프로토콜 개발 초기에 .0이 삭제되었다. 하지만 전문에서는 삭제되지 않았다. 이 책에서는 역사적 정확성과 문맥상 필요한 곳에서 HTTP/2.0를 사용한다. |
5.3 프레임
앞서 언급한 바와 같이, http/2는 프레임 형식의 프로토콜이다. 프레이밍은 프로토콜의 사용자가 쉽게 읽고, 파싱하고, 생성할 수 있는 방식으로 중요한 모든 것들을 포장하는 방법을 말한다. 반면, h1은 프레임 형식이 아니라 텍스트 구분 형식이다. 다음 간단한 예를 보자.
GET / HTTP/1.1 <crlf> Host: www.example.com <crlf> Connection: keep-alive <crlf> Accept: text/html, application/xhtml,application/xml;q=0.9... <crlf> User-agent: Mozilla/5.0 (macintosh; Intel Mac OS X 10_ 11_ 4)... <crlf> Accept-Encoding: fzip, delate, sdch <crlf> Accept-Language: en-US,en;q=0.8 <crlf> Cookie: pfy_cbc_lb=p-browse-w; customerZipCode=99912|N;ltc=%20;... <crlf> <crlf>
|
이런 것을 파싱하는 일은 어렵지 않지만 느리고 오류가 발생하기 쉽다. 구분자(여기서는 <crlf>)를 만나기까지 계속 문자를 읽어야 하며, 규격을 잘 따르지 않은 클라이언트가 <if>만 전송하는 경우도 고려해야 한다. 상태 기계는 다음과 유사할 것이다.
loop while(!CRLF) read bytes end while if first line parse line ad the Request-Line else if line is empty break out of the loop # We are done else if line starts with non-whitespace parse the header line into a key/value pair else if line start with space add the continuation header to the previous header end if end loop # Now go on to ready the request/response based on whatever was # in the Trasger-encoding header and deal with all of the vagaries # of browser bugs
|
이러한 코드는 쉽게 작성할 수 있으며 수도 없이 많이 만들어져 왔다. h1 요청/응답을 파싱할 때의 문제점은 다음과 같다.
- 한 번에 하나의 요청/응답만 전송이 이루어진다. 모든 전송이 끝날 때까지 파싱해야 한다.
- 파싱에 얼마나 남ㅎ은 메모리가 사용될지 알 수 없다. 이는 많은 질문을 낳는다. 한 줄을 읽어 어떤 버퍼에 넣을 것인가? 그 줄이 너무 길면 어떻게 할 것인가? 버퍼 크기를 늘려서 재할당해야 하는가, 아니면 400 오류를 반환해야 하는가? 이런 유형의 질문을 통해, 효율적이고 빠른 메모리 작업이 쉽지 않음을 알 수 잇다.
반면에, 프레임을 사용하면 수신자는 무엇을 수신할지를 미리 앟 수 있다. 프레임 형식의 프로토콜, 특히 h2는 프레임의 전체 크기를 알려주는 길이 필드로 시작한다. [그림 5-1]은 HTTP/2 프레임 구조를 보여준다.
그림5-1 HTTP/2 프레임 헤더
처음 9bytes(옥텟)는 모든 프레임마다 동일하다. 이 9bytes만 읽으면 전체 프레임의 바이트 크기를 정확하게 알 수 있다. 각 필드에 대한 설명은 [표 5-1]을 참조하라.
표 5-1 HTTP/2 프레임 헤더 필드
이름 |
길이 |
설명 |
Length |
3bytes |
프레임 페이로드의 길이를 표시 (~ -1bytes). 최대 프레임 크기의 기본값은 bytes며, 더 큰 프레임 크기는 SETTINGS 프레임으로 요청해야 한다. |
Type |
1byte |
츠레임의 유형을 표시([표 5-2] 참조) |
Flags |
1byte |
프레임 유형별 플래그 |
R |
1bit |
예약된 비트로 설정해서는 안 된다. 설정하면 심각한 결과를 초래할 수 있다. |
Stream Identifier |
31bits |
각 스트림의 고유 식별자 |
Fame Payload |
가변 |
실제 프레임 내용, 프레임 페이로드의 길이가 길이 필드에 표시된다. |
이제 모든 것이 결정되어 있기 때문에, 파싱 로직은 다음과 같을 것이다.
loop Read 9 bytes off the wire Length = the first three bytes Read the payload based on the length Take the appropriate action based on the frame type. end loop
|
이것은 작성하고 관리하기 훨낀 더 쉽다. 또한 h1의 구분자 형식에 비해 매우 중요한 두 번째 장점이 있다. h1에서는 완전한 요청이나 응답을 전송한 후에야 다음 요청/응답을 전송할 수 있었다. h2에서는 프레임 형식 덕분에, 요청과 응답을 서로 뒤섞는 방식, 즉 다중화가 가능하다. 다중화는 3.1.3절 'HTTP/1의 문제점'의 'HOL 블로킹'에서 설명한 Hpl 블로킹과 같은 문제를 해결하는 데 도움을 준다.
HTTP/2 프로토콜에는 10개의 프레임 유형이 있다. [표 5-2]에서 간략한 설명을 볼 수 있으며, 각각에 대해 더 자세히 알고 싶다면 부록 A를 바로 확인해도 좋다.
표 5-2 HTTP/2 프레임 유형
이름 |
ID |
설명 |
DATA |
0x0 |
특정 스트림의 핵심 내용을 전송한다. |
HEADERS |
0x1 |
HTTP 헤더를 포함하며, 선택적으로 우선순위를 포함할 수 있다. |
PRIORITY |
0x2 |
스트림 우선순위와 의존성을 표시 또는 변경한다. |
RST_STREAM |
0x3 |
엔드포인트가 스트립을 닫도록 허용한다(보통, 오류가 발생한 경우). |
SETTINGS |
0x4 |
련결 매개변수를 주고받는다. |
PUSH_PROMISE |
0x5 |
서버가 무언가를 보내려 한다는 사실을 클라이언트에 알려준다. |
PING |
0x6 |
연결을 시험하고 RTT를 측정한다. |
GOAWAY |
0x7 |
상대방이 새로운 스트림 수신했음을 엔드포인트에 알려준다. |
WINDOW_UPDATE |
0x8 |
엔드포인트가 얼마나 많은 바이트를 수신할 수 있는지 주고받는다(흐름제어에 사용). |
CONTINUATION |
0x9 |
HEADER 블록을 확장하는 데 사용한다. |
HTTP/2는 extension 프레임이라는 새오룬 프레임 유형을 처리하는 기능이 있다. 이 기능은 클라이언트와 서버 구현자가 완전히 새로운 프로토콜을 만들 필요 없이 새 프레임 유형을 시험 할 수 있는 메커니즘을 제공한다. 규격상 판독하지 못하는 프레임은 폐기하게 되어 있기 때문에, 새 프레임 유형이 핵심 프로토콜 동작에 영향을 주어서는 안 된다. 물론 래플리케이션이 새 프레임 유형을 사용할 때 중간에 위치한 프락시가 그 프레임을 폐기해 버리는 경우도 문제가 될 수 있다. |
'Learning HTTP2' 카테고리의 다른 글
CHAPTER5 - HTTP/2 프로토콜-3 (0) | 2019.03.05 |
---|---|
CHAPTER5 - HTTP/2 프로토콜-2 (0) | 2019.03.04 |
CHAPTER4 - HTTP/2로의 전환 (0) | 2019.02.26 |
CHAPTER3 - 웹을 파헤치는 이유와 방법 -2 (0) | 2019.02.26 |
CHAPTER3 - 웹을 파헤치는 이유와 방법 -1 (0) | 2019.02.25 |