CHAPTER5 - HTTP/2 프로토콜-2

2019. 3. 4. 11:24Learning HTTP2


5.4 스트림

HTTP/2 규격에서는 스트림을 'HTTP/2 연결이 이루어진 클라이언트와 서버 사이에서 독립적이고 양방향으로 교환되는 일련의 프레임 모음'으로 정의하고 있다. 스트림은 하나의 연결 위에서 개별 HTTP 요청/응답의 쌍을 구성하는 일련의 프레임 모음으로 생각할 수 있다. 클라이언트는 요청을 할 때 새 스트림을 개시한다. 그러면 서버는 동일한 스트림 위에서 응답한다.이는 h1의 요청/응답 흐름과 비슷하지만, 중요한 차이점으로 h2에서는 프레이밍 덕분에 다수의 요청과 응답이 서로를 차단하지 않고 뒤섞여 배치될 수 있다. 스트림 식별자(프레임 헤더의 6~9번째 바이트)는 프레임이 어떤 스트림에 속해 이:ㅆ는지를 나타낸다.

클라이언트는 서버와 h2 연걸을 수립한 후, HEASERD 프레임을 전송해 새 스트림을 시작하며, 헤더가 다수의 프레임에 걸쳐 전송되어야 한다면 COUNTINUATION 프레임을 사용할 수 있다.(COUNTINUATION 프레임에 대한 더 자세한 내용은 'COUNTINUATION 프레임' 보충 설명을 참조하라). 이 HEADER 프레임은 보통 HTTP 요청이나 응답을 포함한다. 그 다음 스트림은 스트림 식별자를 증가시킨 새 HEADERS 프레임을 전송하면 개시된다.


COUNTINUATION 프레임

HEADERS 프레임의 플래그 필드에 ENED_HEADERS 비트를 설정하면 더 이상 전송할 헤더가 없음을 나타낸다. HEADERS 프레임 하나로 HTTP 헤더를 수용하지 못하는 경우(예를 들어, 프레임의 크기가 현재 최대 프레임 크기 설정값보다 더 큰 경우), END_HEADERS 플래그는 설정되지 않고 하나 이상의 COUNTINUATION 프레임이 뒤이어 전송된다. CONTINUATION 프레임을 특별한 HEADERS 프레임으로 생각할 수 있다. HEADERS 프레임을 재사용한다면, 뒤에 전송되는 HEADERS 프레임 페이로드에 적절한 처리를 해주어야 한다. 프레임 헤더를 반복해서 붙일 것인가? 그렇게 했을 때, 프레임 사이에 충돌이 발생하면 어떻게 할 서인가? 프로토콜 개발자들은 언젠가 문제의 근원이 될 수도 있기 때문에 이처럼 모호한 경우를 좋아하지 않는다. 이를 고려해, 워킹 그룹은 구현상의 혼란을 피하기 위해 그 목적을 명시한 프레임 유형을 추가하도록 결졍했다.

HEADERS와 CONTINUATION 프레임은 순차 전송되어야 한다는 요건 때문에, CONTINUATION 프레임을 사용하면 다중화의 이점을 얻을 수 없거나 적어도 반감시킨다는 점에 주의해야 한다. CONTINUATION 프레임은 중요한 용도(큰 헤더)가 있지만 꼭 필요할 때만 사용해야 한다.
 


5.4.1. 메시지

HTTP 메시지란, HTTP 요청이나 응답을 일컫는 총칭이다. 앞 절에서 언급한 것첯럼, 요청/응답 메시지의 쌍을 전송하기 위해 스트림이 생성된다. 하나의 메시지는 최소한 하나의 HEADERS 프레임(스트림을 개시한다)으로 구성되어 있으며, HEADERS 프레임뿐만 아니라 CONTINUATION과 DATA 프레임도 추가로 포함될 수 있다. [그림 5-2]는 일반적인 GET 요청의 예시 흐름을 보여준다.

그림 5-2 GET 요청 메시지와 응답 메시지

또한 [그림 5-3]은 POST 메시지의 프레임 흐름을 보여준다. POST는 보통 클라이언트가 전송한 데이터가 포함되어 있다는 점이 GET과 다르다는 것을 기억해야 한다.

그림 5-3 POST 요청 메시지와 응답 메시지

요청/응답이 메시지 헤더와 메시지 본문으로 나누어지는 h1과 마찬가지로, h2 요청/응답도 HEADERS와 DATA 프레임으로 나누어진다.

참고로, HTTP 메시지는 HTTP/1.1의 RFC 7230에 정의되어 있다.

다음은 HTTP/1과 HTTP/2 메시지 간 눈에 띄는 차이 몇 가지다.


모든 것이 다 헤더다

h1은 메시지를 요청/상태 줄과 헤더로 나눈다. h2는 이런 구분을 없애고 그 줄들을 특별한 가상 헤더로 합친다. 다음은 HTTP/1.1에서 요청과 응답의 한 예다.


GET / HTTP1.1

Host: www.example.com

User-agent: Next-Great-h2-browser-1.0.0

Acccept-Encoding: compress, gzip


HRRP/1.1 200 OK

Content-type: text/plain

Content-Length: 2

...

 


HTTP/2에서는 다음과 같다.


:scheme: https

:method: GET

:path: /

:authority: www.example.com

User-agent: Next-Great-h2-browser-1.0.0

Acccept-Encoding: compress, gzip


:status: 200

Content-type: text/plain

...

 

요청과 상태 줄이 :scheme, :method, :path, :status 헤더로 나누어진 것에 주목하자. 또한 이러한 h2 헤더의 표현이 그대로 전송되지는 않는다는 것을 기억해야 한다. HEADERS 프레임의 설명은 A.3절 'HEADERS'의 'HGEADERS 프레임 필드'에서 볼 수 있으며, 5.6절 '헤더 압축(HPACK)'에서도 더 많은 정보를 찾을 수 있다.


청크 분할 인코딩이 필요 없다.

프레임의 세계에서 청크 분할이 필요한가? 청크 분할은 데이터의 길이를 미리 알릴 필요 없이 데이터를 전송하는 데 사용되었다. 프레임이 핵심 프로토콜의 일부인 h2에서는 더 이상 링크 분할이 필요 없다.


101 응답이 필요 없다.

'101 Switching Protocols' 응답은 h1에서 간혹 예상하지 못한 오류를 일으키곤 한다. 이 응답은 웹소켓 연결로 업그레이드하는 데 가장 흔히 사용된다. ALPN은 더 적은 왕복 오버헤드로 더 명확한 프로토콜 협상 결로를 제공한다.


5.4.2 흐름제어

스트림 흐름 제어는 h2의 새로운 기능 중 하나다. h1에서는 서버가 클라이언트의 수신 가능 속도를 고려하여 데이터를 전송하는 반면에, h2에서는 클라이언트가 전송 속도를 조절하는 기능을 제공한다(h2에서는 거의 모든 것이 대칭적이므로 서버도 같은 기능을 할 수 있다). 흐름 제어 정보는 WINDOW_UPDATE 프레임에 지정된다. 송신자는 자신이 수신할 수 있는 바이트 수를 프레임에 표시하여 상대측 엔드포인트에 알려준다. 상대측 엔드포인트는 전송된 데이터를 수신하여 처리한 후, WINDOW_UPDATE 프레임을 전송하여 자신이 수신할 수 있는 바이트 수를 갱신한다(초기의 많은 HTTP/2 구현자들은 데이터를 제대로 수신하지 못하는 문제에 대한 답을 찾기 위해 윈도우 업데이트 절차를 디버깅하느라 많은 시간을 보냈다). 이러한 제한 사항을 준수하는 것은 송신자의 책임이다.

클라이언트가 흐름제어를 하는 이유는 다양하다. 매우 현실적인 이유 하나는 여러 스트림이 서로 방해하지 않게 하기 위해서다. 또는, 클라이언트가 사용 가능한 대역폭이나 메모리를 제한하여, 처리할 수 있는 만큼만 데이터가 전송되도록 하면 효율성이 개선될 수 있다. 흐름 제어를 끌 수는 없지만, 흐름 제어 윈도우 크기를 최댓값인 -1로 설정하면, 적어도 2GB미만의 파일에 대해서는 실질적으로 비활성화할 수 있다. 유념해야 할 또 다른 경우는 중재자가 있을 때다. 때로는 HTTP 연결을 종료시킨 프락시나 CDN을 통해 콘텐츠가 전송되기도 한다. 프락시의 양측은 각각 다른 처리 능력을 갖출 수 있기 때문에, 흐름 제어를 사용하면 프락시는 양측을 계속 조율하여 프락시 자원을 과도하게 사용할 필요를 최소화할 수 있다.


흐름 제어 예

모든 스트림의 시작 윈도우 크기는 기본적으로 65,536 bytes다. 클라이언트 엔드포인트 A가 이 기본값을 유지하고 상대측 B가 10,000 bytes를 보낸다고 가정해보자. B는 윈도우 크기를 계속 추적한다(현재 55,535 bytes). 이제 A가 5,000 bytes를 처리한 후, 자신의 윈도우 크기가 이제 60,535 bytes라고 알려주는 WINDOW_UPDATE 프레임을 보낸다. B는 이 프레임을 수신한 후, 큰파일(4GB)을 보내기 시작한다. B는 현재 윈도우 크기(이 경우 6-,353 bytes)까지만 보낼 수 있으며, 그 후 A가 더 많은 바이트를 수신할 준비가 되었음을 알려주기를 기다려야 한다. 이런 방식으로, A는 B가 데이터를 보낼 수 있는 최대량을 제어할 수 있다.
 


5.4.3 우선순위

스트림의 마지막 중요한 특성은 의존성이다. 현대의 브라우저는 웹 페이지에서 가장 중요한 요소를 먼저 요청하며, 이 덕분에 최적의 순서로 개체를 가져올 수 있어 성능이 개선되었따. 브라우저는 HTML을 수신하고 나면, 화면을 그리기 위해 CSS와 자바스크립트 같은 것을 요구한다. 다중화가 없다면, 응답이 완료되기를 기다린 후에야 새 개체를 요청할 수 있다. h2에서는, 클라이언트는 모든 자원 요청을 동시에 보낼 수 있고, 서버는 그 요청들의 처리를 한 번에 시작할 수 있다. 여기에는 브라우저가 h1의 암묵적인 우선순위 체계를 사용할 수 없다는 문제가 있다. 서버가 동시에 100개의 요청을 수신했을 때, 무엇이 더 중요한지에 대한 표시가 없으면, 서버는 모든 것을 거의 동시에 전송하여 덜 중요한 요소가 더 중요한 요소를 방해하게 될 것이다.

HTTP/2는 스트림 의존성을 통해 이를 해결한다. HEADERS와 PRIORITY 프레임을 사용한면, 클라이언트는 필요한 콘텐츠와 그 콘텐츠를 전송할 순서를 쉽게 주고받을 수 있다. 이는 의존성 트리와 그 트리 안에 상대적인 가중치를 선언하여 이루어진다.

  • 의존성은 특정 개체에 의존하는 다른 개체가 있음을 표시하여 그 특정 개체를 우선 전송해야 한다는 것을 클라이언트가 서버에 알려줄 방법을 제공한다.

  • 가중치를 사용하면, 공통 의존성이 있는 개체들의 우선순위를 정하는 방법을 클라이언트가 서버에 알려줄 수 있다.

다음의 간단한 웹사이트 예를 보자.


  • index.html
    -header.jpg
    -critical.js
    -less_critical.js
    -style.css
    -ad.js
    -photo.jpg

기준 HTML 파일을 수신한 후, 클라이언트는 파싱을 하고 의존성 트리를 만든 다음, 트리 안의 요소들에 가중치를 할당할 수 있다. 이번 예에서는 다음과 같은 트리를 가정할 수 있다.

  • index.html
    -style.css
    -critical.js
     -less_critical.js(가중치 20)
     -photo.jpg(가중치 8)
     -header.jpg(가중치 8)
     -ad.js(가중치 4)

이 의존성 트리에서, 클라이언트는 다른 무엇보다 style.css가 먼저 필요하며, 다음으로 criticcal.js가 필요하다고 알려준다. 이 두 파일이 없으면, 브라우저는 웹 페이지 렌더링을 진행 할 수 없다. critical.js를 수신한 후, 클라이언트는 나머지 개체들에 상대적 가중치를 부여한다. 가중치는 개체를 전송하는 데 들여야 할 상대적인 '노력'의 크기를 의미한다. 이번 예에서는 less_critical.js가 전체 가중치의 합계인 40중 20의 가중치를 가진다. 이는 서버가 다른 3개의 개체에 비해 less_critical.js를 전송하는 데 절반 가량의 시간이나 자원을 사용해야 함을 의미한다. 잘 동작하는 서버는 클라이언트가 개체들을 가나ㅡㅇ한 한 빨리 받을 수 있도록 최선을 다할 것이다. 결국, 무엇을 할지와 어떻게 우선순위를 따를지는 서버에 달려있다. 서버는 최선이라고 판단한 대로 실행한다. 우선순위를 지능적으로 처리하는 것이 h2 통신이 가능한 웹 서버들 간의 성능을 판가름하는 중요한 요소가 될 것이다.