HTTP 완벽 가이드 - HTTP/2.0
by Gunju Ko
HTTP 2.0
이 글은 HTTP 완벽 가이드의 책 내용을 정리한 글입니다.
1. HTTP/2.0의 등장 배경
- HTTP/1.1의 메시지 포맷은 구현의 단순성과 접근성에 주안점을 두고 최적화됨
- 커넥션 하나를 통해 요청 하나를 보내고 그에 대한 응답 하나만을 받는 HTTP의 메시지 교환 방식은 단순함 면에서는 더할 나위 없었지만, 응답을 받아야한 그 다음 요청을 보낼 수 있기 때문에 심각한 회전 지연(latency)을 피할 수 없었음.
- HOL Blocking 문제가 발생함 - 하나의 요청에 대한 처리가 늦어지게 되면, 그 후의 요청에 대한 처리도 늦어지게 됨
- 구글은 SPDY(스피디)라느 프로토콜을 내놓았고 SPDY는 기존의 HTTP에 속도를 개선하기 위한 여러 기능을 추가한 것이다.
- SPDY는 헤더 압축을 통해 대역폭을 절약함
- 하나의 TCP 커넥션에 여러 요청을 동시에 보냄
- 클라이언트가 요청을 보내지 않아도 서버가 능동적으로 리소스를 푸시하는 기능도 갖춤
- HTTP 작업 그룹은 SPDY를 기반으로 HTTP/2.0 프로토콜을 설계하기로 결정함
2. 개요
- HTTP/2.0은 서버와 클라이언트 사이의 TCP 커넥션 위에서 동작함
- TCP 커넥션을 초기화하는 것은 클라이언트임
- HTTP/2.0 요청과 응답은 길이가 정의된 한 개 이상의 프레임에 담김
- HTTP 헤더는 압축되어 담김
- 프레임에 담긴 요청과 응답은 스트림을 통해 보내진다. 한 개의 스트림이 한 쌍의 요청과 응답을 처리한다.
- 하나의 커넥션 위에 여러 개의 스트림이 동시에 만들어질 수 있다. 즉 여러개의 요청과 응답을 동시에 처리하는것이 가능하다.
- 스트림에 대한 흐름 제어와 우선순위 부여 기능도 제공한다.
- 서버 푸시를 도입함. 이를 통해 서버는 클라이언트에게 필요하다고 생각하는 리소스라면 그에 대한 요청을 명시적으로 받지 않더라도 능동적으로 클라이언트에게 보내줄 수 있음
- 호환성 유지를 위해 HTTP/2.0은 요청과 응답 메시지의 의미를 HTTP/1.1과 같도록 유지하고 있음
- 상태줄을 표현하던 404 Not Found는 404 값을 갖고 있는 “:status” 헤더로 표현하게 되었음
3. HTTP/1.1과의 차이점
3.1 프레임
- 모든 메시지는 프레임에 담겨 전송된다. 모든 프레임은 8 바이트 크기의 헤더로 시작하며, 최대 16383 바이트 크기의 페이로드가 온다.
- 프레임 헤더의 각 필드는 아래와 같다.
- 길이 : 페이로드의 길이를 타나내느 14비트 무부호 정수 (이 길이에 프레임 헤더는 포함되지 않음)
- 종류 : 프레임의 종류
- 플래그 : 플래그 값의 의미는 프레임의 종류에 따라 다르다.
- 스트림 식별자 : 31비트 스트림 식별자. 특별히 0은 커넥션 전체와 연관된 프레임을 의미한다.
- HTTP/2.0은 DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION 이라는 총 10가지 프레임을 정의하고 있으며, 페이로드 형식이나 내용은 프레임의 종류에 따라 다름
3.2 스트림과 멀티플렉싱
- 스트림은 HTTP/2.0 커넥션을 통해 클라이언트와 서버 사이에서 교환되는 프레임들의 독립된 양방향 시퀀스다.
- 한 쌍의 HTTP 요청과 응답은 하나의 스트림을 통해 이루어진다.
- HTTP/1.1은 한 커넥션을 통해 요청을 보냈을 때, 그에 따른 응답이 도착하고 나서야 같은 TCP 커넥션으로 다시 요청을 보낼 수 있다. 이로 인해 한 페이지에서 보내야 할 요청이 수십에서 수백에 달하는 오늘날에 회전 지연이 늘어나는 것을 피하기 어렵다.
- HTTP/2.0에서는 하나의 커넥션에 여러 개의 스트림이 동시에 열릴 수 있다.
- 스트림은 우선순위를 가질 수 있다. 단 요청이 우선순위대로 처리된다는 보장은 없다.
- 모든 스트림은 31비트의 무부호 정수로된 고유한 식별자를 가진다.
- 스트림이 클라이언트에 의해 초기화된 경우 이 식별자는 반드시 홀수여야 한다.
- 스트림이 서버에 초기화된 경우 이 식별자는 반드시 짝수여야 한다.
- 새로 만들어지는 스트림의 식별자는 이전에 만들어졌거나 예약된 스트림들의 식별자보다 커야 한다.
- 위 규칙을 어기는 식별자를 받았다면 에러 코드가 PROTOCOL_ERROR인 커넥션 에러로 응답해야 한다.
- 서버와 클라이언트는 스트림을 상대방과 협상 없이 일방적으로 만든다.
- 협상을 위해 TCP 패킷을 주고받느라 시간을 낭비하지 않아도 됨을 의미한다.
- HTTP/2.0 커넥션에서 한번 사용한 스트림 식별자는 다시 사용할 수 없다.
- 식별자가 고갈되면 커넥션을 다시 맺으면 된다.
3.3 헤더 압축
- HTTP/2.0에서 HTTP 메시지의 헤더를 압축하여 전송한다.
- 헤더는 HPACK 명세에 정의된 헤더 압축 방법으로 압축된 뒤 ‘헤더 블록 조각’들로 쪼개져서 전송된다. 받는쪽에서는 이 조각들을 이은 뒤 압축을 풀어 원래의 헤더 집합으로 복원한다.
- HPACK은 헤더를 압축하고 해제할 때 “압축 컨텍스트”를 사용한다.
- 오동작하지 않으려면 올바른 압축 컨텍스트를 유지해야 한다.
- 압축 컨텍스트는 수신한 헤더의 압축을 풀면 이에 영향을 받아 바뀐다.
- 송신측은 압축 컨텍스트가 변경되었다고 가정한다. 따라서 헤더를 받은 수신측은 반드시 압축 해제를 수행해야 한다. 만약 그럴 수 없다면 반드시 COMPRESSION_ERROR와 함께 커넥션을 끊어야 한다.
3.4 서버 푸쉬
- HTTP/2.0은 서버가 하나의 요청에 대해 응답으로 여러 개의 리소스를 보낼 수 있도록 해준다.
- 서버가 클라이언트에서 어떤 리소스를 요구할 것인지 미리 알 수 있는 상황에서 유용하다.
- 예를 들어 HTML 문서를 요청 받은 서버는 그 HTML 문서가 링크하고 있는 이미지, CSS 파일, 자바스크립트 파일 등의 리소스를 클라이언트에게 푸쉬할 수 있을 것이다.
- 서버는 클라이언트에게 자원을 푸쉬할 것임을 PUSH_PROMISE 프레임을 보내어 미리 알려줘야 한다.
- 클라이언트가 PUSH_PROMISE 프레임을 받게 되면 해당 프레임의 스트림은 클라이언트 입장에서는 “예약됨(원격)” 상태가 된다.
- 이 상태에서 클라이언트는 RST_STREAM 프레임을 보내어 푸쉬를 거절할 수 있다. RST_STREAM을 보내게 되면 그 스트림은 즉각 닫히게 된다.
- 사전에 PUSH_PROMISE 프레임을 보내는 이유는 서버가 푸시하려고 하는 자원을 클라이언트가 별도로 또 요청하게 되는 상황을 피하기 위함이다.
- 서버 푸시를 사용할 때 주의사항
- 서버 푸시를 사용하기로 했더라도, 중간에 프락시가 서버로부터 받은 추가 리소스를 클라이언트에게 전달하지 않을 수 있으며, 반대로 아무런 추가 리소스를 서버로부터 받지 않았음에도 클라이언트에게 추가 리소스를 전달할 수 있다.
- 서버는 안전하고 캐시 가능하고 본문을 포함하지 않은 요청에 대해서만 푸시를 할 수 있다.
- 푸시할 리소스는 클라이언트가 명시적으로 보낸 요청과 연관된 것이어야 한다. 서버가 보내는 PUSH_PROMISE 프레임은 원 요청을 위해 만들어진 스트림을 통해 보내진다.
- 클라이언트는 반드시 서버가 푸시한 리소스를 동일 출처 정책(Same Origin Policy)에 따라 검사해야한다.
- 서버 푸시를 끄고 싶다면 SETTINGS_ENABLE_PUSH을 0으로 설정하면 된다.
4. 알려진 보안 이슈
4.1 중개자 캡슐화 공격
- HTTP/2.0 메시지를 중간의 프락시가 HTTP/1.1 메시지로 변환할 때 메시지의 의미가 변질될 가능성이 있다.
- HTTP/2.0은 헤더 필드의 이름과 값을 바이너리로 인코딩한다. 이는 HTTP/2.0이 헤더 필드로 어떤 문자열이든 사용할 수 있게 해준다. 이는 정상적인 HTTP/2.0 요청이나 응답이 불법적이거나 위조된 HTTP/1.1 메시지로 변역되는 것을 유발할 수 있다.
4.2 긴 커넥션 유지로 인한 개인정보 누출 유려
- HTTP/2.0은 사용자가 요청을 보낼 때의 회전 지연을 줄이기 위해 클라이언트와 서버 사이의 커넥션을 오래 유지하는 것을 염두에 두고 있다. 이것은 개인정보의 유출에 악용될 가능성이 있다.