컴퓨터 과학(CS)/네트워크

HTTP/1.1, HTTP/2, QUIC 차이 (TCP, UDP 개념 포함)

Bentist 2021. 12. 16. 16:30

HTTP 버전별 차이를 설명하기 이전에 위 그림의 통신 모델에서 Transport(전송) 계층의 역할부터 알아본다.

 

전송 계층은 목적지(서버)에 신뢰할 수 있는 데이터를 전송하기 위해 필요하다. 물론 물리 계층, 데이터링크 계층, 네트워크 계층만으로 목적지에 데이터를 보낼 수 있지만, 중간에 데이터가 손상되거나 유실되더라도 이들 계층에서는 아무것도 해주지 않는다. 데이터가 제대로 전달 되었는지의 역할을 전송 계층에서 해주는 것이다.

 

전송 계층의 특징을 보면 신뢰성/정확성효율성으로 구분할 수 있다. 신뢰할 수 있고 정확한 데이터를 전달하는 통신연결형 통신이라 하고, 효율적으로 빠르게 데이터를 전달하는 통신비연결형 통신이라 한다.

 

일반적으로 신뢰성과 정확성이 보장되지 않은 통신은 사용하고 싶지 않겠지만, 동영상 같은 경우 정확한 데이터 전송보다 빠른 전송이 필요해 비연결형 통신을 한다. 데이터가 늦게 도착해서 화면이 버벅거리는 것보다 데이터가 조금 유실되더라도 원활하게 보는 것이 좋기 때문에!

전송 계층의 연결형 통신 프로토콜TCP가 사용되고, 비연결형 통신 프로토콜UDP가 사용된다.

 

TCP로 전송할 때 붙이는 헤더를 TCP 헤더라 하고, 이 헤더에는 목적지까지 데이터를 제대로 전송하기 위해 필요한 정보들이 들어있다. 연결형 통신인 TCP(Transmission Control Protocol)는 데이터를 전송하기 전에 커넥션(connection)이라는 가상의 독점 통신로를 확립해야 한다. 이 연결을 확립한 후에 데이터를 전송할 수 있다. 이를 위해 TCP헤더의 코드 비트 항목 중에서 SYN, ACK를 사용하여 패킷 요청을 세번 교환하는데 이것을 '3-way handshake'라 한다.

(연결 확립을 요청 - 연결 확립 응답, 요청 - 연결 확립 응답)

SYN - SYN/ACK - ACK 연결 후, 데이터 전송

만약 TCP에서는 패킷(데이터의 작은 조각) 유실이 발생하면 유실된 데이터에 대한 ACK(확인 응답)가 도착하지 않는다.

송신 측에서는 ACK가 올 때까지 기다리지만, ACK가 결국 오지 않으면 다시 데이터를 보내게 된다.

 

그런데 TCP가 데이터 유실 등의 정보를 어떻게 알고 전송 제어(TC: Transmission Control)를 한다는 걸까?

예를 들어 한줄로 서야하는 A, B, C라는 사람(패킷)들이 서울(출발지)에서 출발하여 부산(목적지)으로 간다고 하자.
그런데 A, B, C가 순차적으로 가는 상황에서 B가 길을 잘못 들어서 분실되었다. 하지만 목적지에서는 A, B, C가 모두 필요한지 모르고 A, C만 보고 다 왔다고 착각할 수 있다. 그렇기 때문에 A ,B, C라는 패킷에 1, 2, 3이라는 일련 번호를 부여하여 '이 데이터가 몇 번째 데이터'인지 알려주는 역할을 하여 신뢰성을 확보한다.

UDP(User Datagram Protocol)연결을 설정하지 않고 수신자가 데이터를 받을 준비를 확인하는 단계인 3-way handshake 없이 일방적으로 데이터를 전송하여 데이터의 순서를 보장하지 않는다.

즉, 연결(connection)을 위한 논리적인 경로가 없어서 각각의 패킷은 다른 경로로 전송되고, 각각의 패킷은 독립적인 관계를 지니게 되는데 이렇게 데이터를 서로 다른 경로로 독립적으로 처리하게 되고, 이러한 프로토콜을 UDP라고 한다.

신뢰성은 낮지만, 전송 속도가 빠르다는 장점이 있다.

TCP VS UDP


HTTP

웹 서비스에서 클라이언트와 웹 서버 간에 정보를 주고(request) 받기(response) 위해 사용되는 네트워크 프로토콜

(https://bentist.tistory.com/35)

HTTP/0.9 GET 메소드

HTTP 초기 버전에는 버전 번호가 없었다. HTTP/0.9는 이후에 차후 버전과 구별하기 위해 0.9로 불리게 됐다.

HTTP/0.9의 요청은 단일 라인으로 구성되며 리소스에 대한 경로로 가능한 메서드는 GET이 유일했다.

HTTP 메소드란 클라이언트가 웹 서버에게 사용자 요청의 목적을 알리는 수단이다.

<요청>

GET /mypage.html

<응답> 또한 파일 내용 자체로 극도로 단순하다.

<HTML> A very simple HTML page </HTML>

 HTTP 헤더가 없었는데 이는 HTML 파일만 전송될 수 있으며, 상태 혹은 오류 코드도 없었다.


HTTP/1.0 헤더의 등장: HTML 파일 외 (이미지, 영상) 등도 전송 가능

헤더라는 것에 여러 정보를 포함하기 시작했다.
  • 버전 정보인 HTTP/1.0이 GET 라인에 붙어서 전송되었다.
  • 상태 코드 또한 응답의 시작 부분에 붙어 전송되어, 브라우저가 요청에 대한 성공과 실패를 알 수 있고 그 결과에 대한 동작을 할 수 있게 되었다.
  • HTTP 헤더 개념은 요청과 응답 모두를 위해 도입되어, 유연하고 확장 가능하게 되었다.
  • 새로운 HTTP 헤더의 도움으로, HTML 파일 외에 다른 문서도 전송이 가능. (Content-Type 덕분)
  • Connection: keep-alive단기 커넥션 유지가 가능했지만 기본 디폴트도 아니었고, 이 헤더를 이해 못하는 프록시들이 있어서 멍청한 프록시라는 문제점이 발생 (HTTP/1.1에서는 지속 커넥션을 통해 개선)
# 요청 메세지
GET /mypage.html HTTP/1.0                		# start-line
User-Agent: NCSA_Mosaic/2.0 (Windows 3.1)   		# 요청 해더


# 응답 메세지
200 OK --> 상태 코드
Date: Tue, 15 Nov 1994 08:12:31 GMT			# 일반(General) 헤더
Server: CERN/3.0 libwww/2.17				# 응답 헤더
Content-Type: text/html					# 앤터티/개체 해더(보통 응답 헤어에 포함)

<HTML>							# 응답된 데이터
A page with an image
  <IMG SRC="/myimage.gif">
</HTML>

HTML이 아닌 파일인 이미지 파일 요청 메세지

# 요청 메세지
GET /myimage.gif HTTP/1.0
User-Agent: NCSA_Mosaic/2.0 (Windows 3.1)


# 응답 메세지
200 OK
Date: Tue, 15 Nov 1994 08:12:32 GMT
Server: CERN/3.0 libwww/2.17
Content-Type: text/gif

(image content)

HTTP/1.1 표준 프로토콜

HTTP/1.1에서는 많은 개선 사항들이 있었다.

 

1) 커넥션(SYN, ACK, FIN의 handshake) 재사용

  • HTTP/1.0에서는 요청이 1개 보내질 때마다 커넥션이 1개 매번 새롭게 생성(Short-lived connections)되었고 응답이 도착한 이후에 연결을 닫는 형태로 유지되었다. HTTP/1.1에서는 Persistent Connection을 통해 지정한 timeout동안 [요청과 응답] 여러 개를 받을 수 있도록 커넥션을 닫지 않고 재사용을 할 수 있게 하였다.
  • 연결 - [요청/응답] - [요청/응답] - [요청/응답] - 종료

2) 파이프라이닝

  • 하나의 Connection에서 요청에 대한 응답을 기다리지 않고 요청을 연속적으로 보내는 기능.
  • 첫번째 요청에 대한 응답을 기다리지 않고 두번째, 세번째 요청을 연속적으로 보내 그 순서에 맞춰 응답을 받는 방식으로 지연 시간을 줄이는 방법을 도입하였다. 
  • 연결 - [요청1, 요청2, 요청3] - [응답1, 응답2, 응답3] - 종료

 

그런데 파이프라이닝에 문제가 있었다. 바로 Head of Line Blocking이다.

Head of Line(HOL) Blocking은 파이프라이닝이 응답 순서는 보장해야 하기 때문에, 첫번째 요청이 서버에서 처리시간이 너무 오래 걸리면, 두 세번째 요청에 대한 응답도 마냥 기다려야 하는 비효율이 발생한다. CSS 리소스에 대한 요청이 먼저 처리된다고 하더라도 HTML 요청이 먼저 도착했기 떄문에 HTML 응답이 전송될 때까지 CSS응답은 block된다.

 

이를 해결하고자, Domain Sharding(도메인 샤딩) 이라는 방법을 썼는데

도메인명을 여러개 설정해서 같은 서버에 정적파일(이미지, CSS, JS)을 병렬로 요청하면, 여러개의 커넥션을 맺을 수 있게 된다. 이렇게 되면 리소스를 병렬적으로 동시에 받을 수 있게 된다.

그러나 브라우저별로 도메인 커넥션 갯수 제한(크롬: 6개)도 있고 근본적인 해결책은 아니었다.

 

3) 무거운 Header 구조

  • HTTP/1.1의 헤더에는 많은 정보들이 저장되어 있는데, 매 요청시마다 중복된 Header값을 전송하게 되어 불필요하게 Header값이 커지게 된다. 


HTTP/2 HTTP/1.x에 비해 더 적은 TCP 연결을 사용, 네트워크에 더 친화적

성능향상과 확장에 초점을 맞추었다.

1) 바이너리 메시지 프레이밍 계층을 사용하여 보다 효율적인 메시지 처리를 가능.
2) 하나의 연결(단일 TCP Connection)에서 요청 및 응답 Multiplexing(다중화)을 허용하여 네트워크 리소스를 보다 효율적으로 사용하고 지연 시간을 줄임.
3) 요청의 우선순위 지정을 허용함으로써 더 중요한 요청은 응답이 더 빨리 완료되도록 하여 성능을 더욱 개선.
4) 서버 푸시로 클라이언트가 명시적으로 요청하지 않아도 서버가 추가적인 리소스를 미리 클라이언트로 보냄.

5) 헤더 필드 압축을 도입.

 

HTTP 메세지 전송 방식의 변화 - 바이너리 프레이밍 계층

  • 바이너리(이진) 프레이밍 계층이 생겼다. 이제 텍스트 형식의 메세지를 보내는 것이 아닌, 프레임(frame)이라는 단위로 텍스트를 바이너리로 변환해서 전송다.
  • 줄바꿈으로 구분되는 일반 텍스트 HTTP/1.x 프로토콜과 달리, 모든 HTTP/2 통신은 더 작은 메시지와 프레임으로 분할되며, 각각은 바이너리 형식으로 인코딩된다.
  • 따라서 클라이언트와 서버는 서로를 이해하기 위해 새 바이너리 인코딩 메커니즘을 사용해야 하며, HTTP/1.x 클라이언트는 HTTP/2 전용 서버를 이해하지 못하며 그 반대도 마찬가지이다.
  • 바이너리로 변환해서 전송하므로, 파싱 및 전송 속도가 빠르고 네트워크 오류 발생 가능성이 적어진다. 네트워크 대기 시간 감소 및 처리량 향상, 응답 분할 공격과 같은 HTTP/1.x의 텍스트 특성과 관련 보안 문제를 제거한다.

새 바이너리 프레이밍 메커니즘이 도입됨에 따라 HTTP/2의 용어부터 살펴보자.

클라이언트와 서버가 연결된 하나의 TCP 커넥션 안에 스트림이라는 데이터 양방향 흐름이라는 게 있다. 이 안에 프레임이라는 단위가 들어가서 요청 및 응답 메세지가 되는 것이다. HTTP/1.1에서는 요청과 응답이 메시지라는 단위로 완벽하게 구분되어 있었으나, HTTP/2에서는 Stream이라는 단위를 통해 요청과 응답이 묶일 수 있는 구조가 만들어졌다고 볼 수 있다. 

 

스트림, 메시지 및 프레임

  • 스트림: TCP 커넥션 안에서 전달되는 바이트의 양방향 흐름이며, 하나 이상의 메시지가 전달
  • 프레임: HTTP/2에서 통신 최소 단위이며, 각 최소 단위에는 하나의 HEADERS frame 또는 DATA frame를 포함
  • 프레임 헤더는 특정 스트림에 속하는 메시지에 매핑된다.
  • 메시지: 각 메시지는 하나의 논리적 HTTP 메세지(요청 또는 응답)이며, 다수의 프레임으로 이루어져있다.

즉 Frame이 여러개 모여 Message가 되고, 요청 및 응답 Message가 모여 스트림이 되는 구조이다.

  • 모든 통신은 단일 TCP 연결을 통해 수행되며 전달될 수 있는 양방향 스트림의 수는 제한이 없다.
  • 각 스트림에는 양방향 메시지 전달에 사용되는 고유 식별자우선순위 정보가 있다.

클라이언트-서버 간 하나의 TCP 연결 상태에서의 N개의 스트림 구성

위의 그림은 클라이언트-서버 간 하나의 TCP 연결 상태에서의 N개의 스트림 구성을 보여준다.

Stream 1은 [요청 메시지 하나/응답 메시지 하나]가 있고, 요청 메시지는 하나의 Header 프레임(GET 요청으로 Data 부분이 존재 하지 않음)으로 구성되어 있고, 응답 메시지는 각각 하나의 헤더 프레임, 데이터 프레임으로 구성되어 있다. Stream N의 그림을 보면, 복수개의 요청, 복수개의 응답 프레임이 하나의 스트림 안에 존재한다. (다중 요청/응답 가능)

 

요청 및 응답 다중화(Multiplexing)

HTTP/1.x에서 성능 개선을 위해 클라이언트가 병렬 요청을 수행하려는 경우, 여러 TCP 연결이 사용되어야 했다. (위에서 설명한 domain sharding) 이 동작은 연결당 한번에 하나의 응답만 전달되도록 보장하고, 기본 TCP 연결의 비효율적인 사용을 초래했다.

HTTP/2의 새 바이너리 프레이밍 계층은 이러한 제한을 없애주고 전체 요청 및 응답 다중화를 지원한다. 이를 위해 클라이언트와 서버가 HTTP 메시지를 독립된 프레임으로 세분화하고, 이 프레임을 끼워넣은(인터리빙)다음, 다른 쪽에서 다시 조립하도록 허용한다.

그래서 어떤 순서로 프레임이 도착하든 기다리지 않고 조립 하면 되므로, Head of Line Blocking이 해결된 것이다.

위 스냅샷은 동일한 연결(하나의 TCP Connection) 내의 여러 스트림을 캡쳐 한건데, 클라이언트는 DATA 프레임(스트림 5)을 서버로 전송 중인 반면, 서버는 스트림 1과 스트림 3의 인터리빙(끼워넣기)된 프레임을 클라이언트로 전송 중이다. 따라서 하나의 커넥션3개의 병렬 스트림이 존재한다. HTTP 메시지를 독립된 프레임으로 세분화하고 이 프레임을 인터리빙한 다음, 다른 쪽에서 다시 조립하는 기능은 HTTP/2에서 가장 중요한 기능 향상이다.

 

  • 여러 요청을 하나도 차단하지 않고 병렬로 인터리빙 가능
  • 여러 응답을 하나도 차단하지 않고 병렬로 인터리빙 가능
  • 단일 연결을 사용하여 여러 요청과 응답을 병렬로 전달

스트림 우선순위 지정(stream Priritization)

리소스(frame)간 전송 우선 순위를 설정이 가능하게 되었다. 각 스트림에는 1~256 사이의 정수 가중치가 할당될 수 있어스트림에 우선순위를 부여할 수 있다. 그럼 서버는 우선순위가 높은 응답클라이언트에 먼저 전달되도록 대역폭을 할당한다.

 

서버 푸시

서버가 단일 클라이언트 요청에 대해 여러 응답을 보낼 수 있다는 것이다.. 즉, 서버는 원래 요청에 응답할 뿐만 아니라 클라이언트가 명시적으로 요청하지 않아도 서버가 추가적인 리소스를 클라이언트에 푸시할 수 있다.

서버가 생각하기에 클라이언트한테 /page.html을 받으면, 서버는 어떤 리소스가 클라이언트에 또 필요한지 이미 알고 있어서 /script.js와 /style.css를 미리 보내버리는 기능이다.

 

헤더 압축(Header Compression)

중복되는 헤더의 크기를 줄여 페이지 로드 시간을 줄이는 기술이다.

Header 정보를 압축하기 위해 Static/Dynamic Header Table과 Huffman Encoding의 두 가지 기술을 사용한다.

위 그림처럼 클라이언트가 두 번의 요청을 보낸다고 가정하면 HTTP/1.x의 경우 두 개의 요청 Header에 중복값이 존재해도 그냥 중복 전송한다. 하지만 HTTP/2에선 Header에 중복값이 존재하는 경우 Static/Dynamic Header Table 개념을 사용하여 중복 Header를 검출하고 중복된 Headerindex값만 전송하고 중복되지 않은 Header정보의 값은 Huffman Encoding기법으로 인코딩 처리하여 전송한다.


QUIC UDP 기반의 통신 프로토콜, TCP 자체의 HOL Blocking 해결

QUIC("퀵")은 구글에서 설계된 범용 목적의 전송 계층 통신 프로토콜이다. QUIC는 구글 크롬에서부터 구글 관련 제품 대부분의 기본 프로토콜로, TCP의 성능을 개선하고자 UDP기반으로 만들어진 프로토콜이다.

UDP는 처리 속도가 빠른 반면 데이터의 신뢰성 확보가 어렵지만, QUIC 계층을 추가하여 TCP만큼 신뢰성을 제공할 수 있게 하였다. 

 

전송 속도 향상, Connection UUID로 서버와 연결 수립

QUIC이론적으로 전송 왕복 시간(RTT, Round-Trip Time)이 0이다. 첫 연결에서 설정에 필요한 정보와 함께 데이터를 함께 보낸다. 연결을 성공하면 설정을 캐싱해서 다음 연결 때, handshake 필요없이 바로 데이터 전송이 가능하도록 되었다. QUICConnection UUID라는 고유한 패킷 식별자를 사용하므로 한번이라도 클라이언트와 서버가 데이터 전송을 수행했다면 이동중 로밍이 발생하거나 심지어 IP 주소가 변경되어도 커넥션을 다시 수립할 필요가 없다.

보안성 향상

1) TLS 적용

QUIC은 TLS 암호화를 기본적으로 적용하고 있다. 과거 SPDY와 마찬가지로 구글에서 설계한 네트워크 프로토콜은 기본적으로 암호화를 적용하고 있고, 서버의 포트도 디폴트가 443 UDP 포트를 사용하고 있다.

 

2) IP Spoofing / Replay Attack 방지

QUIC에서는 필요에 따라 Source Address Token을 발급하여 출발지 IP를 변조 및 재생공격에 대한 검증을 수행한다. 마치 TCP 통신에서 Sequence Number를 이용하여 신뢰 관계를 맺은 클라이언트인지 검증하는 것과 같은 기능을 수행한다.

 

'독립 스트림'으로 더향상된 멀티플렉싱(multiplexing) - TCP 자체의 HOL Blocking 방지

HTTP/2에서도 멀티플렉싱 기능을 제공하기는 했다. 그러나 TCP 자체에도 Head of Line Blocking 이슈가 있다.

만약 TCP 통신에서는 다수의 스트림을 하나의 TCP Connection으로 전송 중에 유실된 패킷이 발생하면 다른 스트림의 데이터도 유실된 패킷이 재전송 될 때까지 대기상태(Block)가 되지만, QUIC에서는 유실된 패킷이 속한 데이터를 제외한 다른 데이터는 별도의 Block없이 지속적으로 처리 할 수 있도록 설계되었다.

위 그림을 보면, QUIC은 하나의 UDP Connection 안에서 데이터를 동시에 전송하는 스트림을 독립적으로 다중화하여 스트림 중 하나의 데이터가 손실이 발생했을 때, 다른 스트림의 데이터 전달을 차단하지 않아 HoL Blocking을 방지할 수 있다.


HTTP/3 UDP 기반QUIC 프로토콜을 사용

HTTP/1.1, HTTP/2와 다르게 HTTP/3UDP 기반의 QUIC 프로토콜을 사용한다.

사실 HTTP/3는 처음에는 HTTP-over-QUIC이라는 이름을 가지고 있었는데, IETF(Internet Engineering Task Force) 내 HTTP 작업 그룹과 QUIC 작업 그룹의 의장인 마크 노팅엄이 이 프로토콜의 이름을 HTTP/3로 변경할 것을 제안했고, 2018년 11월에 이 제안이 통과되어 HTTP-over-QUIC이라는 이름에서 HTTP/3으로 변경되게 되었다.

 

 

HTTP/2 참조: https://developers.google.com/web/fundamentals/performance/http2?hl=ko

HTTP/1.1, HTTP/2 속도 비교: https://www.youtube.com/watch?v=nqgDMXDH9vU