본문 바로가기

CS/네트워크

[HTTP] 10. HTTP/2.0

이 포스팅은 "HTTP-완벽 가이드" 책을 학습하고 정리한 것입니다.

https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=49731592

 

HTTP 완벽 가이드

HTTP 규약이 어떻게 동작하고 웹 기반 애플리케이션을 개발하는 데 어떻게 사용하는지 설명한다. 하지만 이 책은 단순히 HTTP에 대해서만 다루지는 않는다. HTTP가 효율적으로 동작하도록 함께 사

www.aladin.co.kr

 

 

 


 

 

HTTP/2의 목표

  • 멀티플렉싱을 지원하여 레이턴시 이슈를 낮춘다.
  • HTTP 헤더를 압축하여, 네트워크를 효율적으로 사용한다.
  • 요청의 우선순위 부여 기능 추가한다.
  • 서버 푸시 기능 추가한다.

 

 

 

HTTP/0.9 부터 시작해서 HTTP/3.0이 나온 시점까지의 역사적 과정의 순서는 아래와 같다.

  1. HTTP/0.9 : 1991년(미사용)
  2. HTTP/1.0 : 1996년(미사용)
  3. HTTP/1.1 : 1997년(표준)
  4. HTTP/2.0 : 2015년(표준)
  5. HTTP/3.0 : 2022년(표준)

 

10.1 HTTP/2.0의 등장 배경

 

 HTTP/1.1의 메시지 포멧은 구현의 단순성과 접근성에 주안점을 두고 최적화되었다. 그러다 보니 성능은 어느 정도 희생시킬 수 밖에 없었다. 커넥션 하나를 통해서 요청과 응답의 전송이 단일하게 이뤄지는 방식은 간단하지만, 응답을 받아야만 다음 요청을 보낼 수 있기 때문에, low latency를 피할 수 없었다.

 

 물론, 이를 해결하기 위해 ‘병렬 커넥션’ , ‘파이프라인 커넥션’이 도입되었지만, 성능에 대한 근본적인 해결책이 되지는 못했다. 각 해결방안의 단점은 아래와 같다.

  • 병렬 커넥션 : 병렬 커넥션의 핵심은 커넥션에서의 지연시간을 겹치게하는 것이다. 그런데, 네트워크 대역폭이 낮을 경우, 메시지를 전송하는데 시간이 오래걸려, 병렬 커넥션이 마치 단일 커넥션처럼 동작할 수 있다. 또한, 다수의 커넥션은 메모리를 많이 소모한다는 단점을 갖는다.
  • 파이프라인 커넥션 : 첫번째 요청을 통해 지속 커넥션인지 확인 받은 이후, 다음 요청들이 전송될 수 있다. 즉, 첫번째 요청을 기다려야 된다. 또한, 응답을 받기 전까지, 요청을 보낸 메시지들을 메모리에 유지해야 한다는 단점이 있다.

 HTTP WG(working group)은 구글의 SPDY 프로토콜을 받아들였다. SPDY 프로토콜은 low latency 문제를 해결하기 위해 아래의 기능을 추가하였다.

  • 헤더를 압축하여 대역폭을 절약
  • 하나의 TCP 커넥션에 여러 요청을 동시에 보내 속도를 올림
  • 서버가 능동적으로 클라이언트에게 리소스를 푸시하는 기능 추가

 

10.3 HTTP/1.1과의 차이점

 

10.3.0 바이너리 프레이밍 레이어

 

 HTTP/1.1과 HTTP/2.0을 구분짖는 가장 큰 특징은 HTTP/1.1은 텍스트 포멧인데 반해, HTTP/2.0은 바이너리 프레이밍 레이어라는 점이다. HTTP/2.0에서 단순히 HTTP/1.1을 바이너리 데이터로 변환했다는 의미는 아니다. HTTP/2.0은 아래와 같은 구조를 갖는다.

 

 

 HTTP/1.1의 문법은 그대로 유지하면서, 해당 데이터를 바이너리 데이터로 담는다. 이러한 방식을 통해 기존에 HTTP/1.1을 사용하던 어플리케이션은, 기존과 동일하게 HTTP 메시지를 작성해도 되며, 해당 메시지 하단의 바이너리 프레이밍 계층이 해당 메시지를 바이너리 데이터로 변환할 것이다. 즉, HTTP/1.1을 유지하면서, 성능상의 이점은 추가할 수 있게 한 것이다.

 

10.3.1 프레임

 HTTP/2.0.에서 모든 메시지는 한개 이상의 프레임에 담겨 전송된다. HTTP/2.0은 HTTP 메시지를 조각내어 작은 패킷단위로 만든다. 조각난 데이터는 프레임이라는 새로운 단위에 담겨진다. 하나의 메시지는 여러개의 프레임에 담겨질 수 있다. 프레임의 구조는 아래와 같다.

  • Length : 페이로드의 길이를 나타내는 14비트 unsigned integer이다. 이 길이에 프레임 헤더는 포함되지 않는다.
  • Type : 프레임의 종류(DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION)
  • Flags : 8비트 플래그. 플래그 값의 의미는 프레임의 종류에 따라 다르다.
  • Stream Identifier : 31비트 스트림 식별자. 특별히 0은 커넥션 전체와 연관된 프레임임을 의미한다.

 

여기서 몇 가지 의문이 생겼다.

  • 전송된 프레임들 중, 특정 프레임이 특정 메시지를 지칭한다는 것을 어떻게 알 수 있을까?
  • 동일한 스트림에서, 각 메시지를 구별하는 식별자는 무엇인가?

 위 질문에 결론부터 말하자면, 질문 자체가 잘못되었다. 하나의 스트림은 하나의 HTTP 트랜잭션을 위해 존재하며, 그 이후 종료된다. 추가적인 HTTP 트랜잭션은 새로운 스트림을 필요로 한다. 따라서, 스트림을 통과하고 있는 메시지는 반드시 (물리적인 시점을 기준으로) 하나이기 때문에, 이를 식별할 필요가 없다. 아래 이미지는 RFC 7540에서 발췌한 “Stream Lifecycle”이다. 한 번 open되고, HTTP 트랜잭션이 종료되면, 스트림도 closed 됨을 볼 수 있다.

 

 

 각 프레임이 어떤 스트림(필자는 HTTP 트랜잭션 단위로 이해함)에 속하는지만 식별하면 된다. 여러 스트림은 물리적으로는 하나의 커넥션을 통해 통신되기 때문이다.

 

 스트림을 가상의 통로로 가정했을 때, 통로에는 다수의 프레임이 통과할 수는 있지만, 반드시 하나의 메시지만 통과할 것이다.

 

 

HTTP/2 HEADERS and DATA Frames

I am trying to understand HTTP/2 in detail. I read this article about streams, messages and frames: https://hpbn.co/http2/#streams-messages-and-frames. I don't know if I got the concept right. I c...

stackoverflow.com

 

 위에서 HTTP 메시지는 하나 이상의 프레임으로 조각내어 진다고 말하였다. TCP에서와 같이 단순히 (개념상의 의미없이) 바이너리 단위로 쪼개지는 것은 아니다. HTTP/2.0에서 HTTP 메시지는 개념 단위의 프레임으로 나눠지게 된다. 그리고 각 메시지에서 어떤 의미를 갖는지 나타내기 위해, 프레임의 헤더에는 Type이라는 필드가 존재하는 것이다.

 

 구체적으로 보자면, HTTP 메시지는 1 또는 2 개의 HEADER 프레임과 0 또는 N 개의 DATA 프레임으로 구성된다( 추가적인 HEADER 프레임이 맨 뒤에 있을 수 있음). 아래 내용은, RFC 7540 에서 예시로 든 프레임들의 조합니다.

 

 간단한 GET 요청은, 하나의 HEADER 프레임으로 구성된다.

GET /resource HTTP/1.1           HEADERS
     Host: example.org          ==>     + END_STREAM
     Accept: image/jpeg                 + END_HEADERS
                                          :method = GET
                                          :scheme = https
                                          :path = /resource
                                          host = example.org
                                          accept = image/jpeg

 

 HTTP body를 포함하는 POST 요청은, HEADER 프레임 & CONTINUATION 프레임 & DATA 프레임 으로 구성된다.

     POST /resource HTTP/1.1          HEADERS
     Host: example.org          ==>     - END_STREAM
     Content-Type: image/jpeg           - END_HEADERS
     Content-Length: 123                  :method = POST
                                          :path = /resource
     {binary data}                        :scheme = https

                                      CONTINUATION
                                        + END_HEADERS
                                          content-type = image/jpeg
                                          host = example.org
                                          content-length = 123

                                      DATA
                                        + END_STREAM
                                      {binary data}

 

 이로서, 하나의 메시지는 여러개의 의미단위 프레임으로 구성됨을 확인하였다. 주목할 점은 마지막 헤더를 의미하는 HEADER 프레임은 “flag_end_headers = true” 라는 플래그가 지정되고, 마지막 프레임을 의미하는 프레임은 “flag_end_stream = true” 플래그가 지정된다는 것이다. 이러한 플래그를 통해 각 프레임을 하나의 메시지로 재결합할 수 있다.

 

10.3.2 스트림과 멀티플렉싱

 스트림은 HTTP/2.0 커넥션을 통해 클라이언트와 서버 사이에서 교환되는 프레임들의 독립된 양방향 시퀀스이다(논리적 개념). 한 쌍의 HTTP 요청과 응답은 하나의 스트림을 통해 이뤄지며, 클라이언트는 새 스트림을 만들어 그를 통해 HTTP 요청을 보낸다. 요청을 받은 서버는 그 요청과 같은 스트림으로 응답을 보낸다. 그러고 나면 스트림이 닫히게 된다.

 

 HTTP/2.0에서는 하나의 커넥션에 여러 개의 스트림이 동시에 열릴 수 있다. 이를 통해, HTTP/1.1에서의 하나의 커넥션에서 한 쌍의 메시지만 통신가능하다는 문제를 해결하였다.

 

 각 스트림을 구별하기 위해 Stream Identifier가 사용되며, 만약 클라이언트에 의해 Stream이 초기화되면 식별자는 반드시 홀수여야 하고, 서버에 의해 초기화되면 반드시 짝수여야 한다. 그리고 새로 만들어지는 식별자는 이전에 만들어졌거나 예약된 스트림들의 식별자보다 커야한다(유니크한 식별자를 만들기 위함). 앞에서 말한 규칙을 통해, 클라이언트와 서버는 식별자에 대한 협상없이 일방적으로 식별자를 만들 수 있다. 만약, 식별자가 고갈된다면, 새롭게 커넥션을 다시 맺으면 된다.

 

 스트림은 우선순위를 가질 수 있다. 중요한 리소스에 높은 우선순위를 부여하여, 해당 리소스를 우선적으로 받을 수 있다. 그러나, 우선순위를 따른다는 보장은 없다.

 

10.3.3 헤더 압축

 HTTP/1.1에서는 헤더의 크기가 문제가 되지 않았기 때문에, 헤더를 압축하지 않았다. 하지만 최근에는 헤더의 크기가 low latency와 대역폭에 문제를 야기하기 때문에, HTTP/2.0에서는 헤더 압축이 도입되었다. 헤더 압축 알고리즘으로 HPACK 명세를 채택하였다. 먼저 압축을 수행하고, 헤더 블록 조각들로 쪼개서 전송된다.

 

 HPACK은 헤더를 압축하고 해제할 때 ‘압축 컨텍스트(compression context)’를 활용하는데, 수신한 헤더의 압축을 풀면 압축 컨텍스트가 바뀐다. 송신측은 압축 컨텍스트가 수신측에서 바뀌었다고 가정하기 때문에, 수신측은 헤더를 사용하지 않을 지라도, 압축을 해제하여, 압축 컨텍스트를 바꿔야 한다.

 

10.3.4 서버 푸시

 HTTP/2.0은 서버가 하나의 요청에 대해 응답으로 여러 개의 리소스를 보낼 수 있도록 해준다. 이 기능은 서버가 클라이언트에서 어떤 리소스를 요구할 것인지 미리 알 수 있는 상황에서 유용하다. 리소스를 푸시하려는 서버는 먼저 클라이언트에게 자원을 푸시할 것임을 PUSH_PROMISE 프레임을 보내어 미리 알려주어야 한다. 이러한 이유는 서버가 푸시할려는 자원을 클라이언트가 중복으로 요청하는 것을 피하기 위함이다.

 

 클라이언트가 PUSH_PROMISE 프레임을 받게 되면 해당 프레임의 스트림(서버가 새롭게 생성한 스트림)은 클라이언트 입장에서는 ‘예약됨(원격)’ 상태가 된다. 이 상태에서 클라이언트는 RST_STREAM 프레임을 보내어 푸시를 거절할 수 있다.

 

 

 

 

 

참고자료

 

HTTP/2 소개  |  Articles  |  web.dev

HTTP/2 (또는 h2)는 웹에 푸시, 다중화 스트림 및 프레임 제어를 제공하는 바이너리 프로토콜입니다.

web.dev

 

 

HTTP/1.1 vs HTTP/2: What's the Difference? | DigitalOcean

 

www.digitalocean.com

 

 

HTTP: HTTP/2 - High Performance Browser Networking (O'Reilly)

What every web developer must know about mobile networks, protocols, and APIs provided by browser to deliver the best user experience.

hpbn.co

 

 

RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2)

 

www.rfc-editor.org

 

'CS > 네트워크' 카테고리의 다른 글

[HTTP] 14. 보안 HTTP  (2) 2024.01.10
[HTTP] 12. 기본 인증  (0) 2024.01.09
[HTTP] 07. 캐시 (2)  (2) 2024.01.08
[HTTP] 07. 캐시 (1)  (0) 2024.01.07
[HTTP] 06. 프락시  (2) 2024.01.07