HTTP 완벽가이드 내용 정리
전 세계 모든 HTTP 통신은 패킷 교환 네트워크 프로토콜들의 계층화된 집합인 TCP/IP를 통해 이루어진다.
자세한 참고 사이트 - 브런치
TCP는 세그먼트라는 단위로 데이터 스트림을 잘게 나누고, IP 패킷이라는 봉투에 담아서 인터넷을 통해 데이터를 전달한다. 그리고 IP 패킷에는 다음의 정보가 담긴다.
IP 헤더는 발신지오 목적지 IP주소, 크기, 기타 플래그를 가진다. TCP 세그먼트 헤더는 TCP 포트 번호, TCP 제어 플래그, 그리고 데이터의 순서와 무결성을 검사하기 위해 사용되는 숫자 값을 포함한다.
컴퓨터는 여러 개의 TCP 커넥션을 가지고 있다. TCP 포트는 회사 내선번호와 같다. IP 주소는 컴퓨터에 연결되고 포트 번호는 애플리케이션으로 연결된다. TCP 커넥션은 네 가지 값으로 식별한다.
<발신지 IP주소, 발신지 포트, 수신지 IP주소, 수신지 포트>
서로 다른 두 개의 TCP 커넥션은 네 가지 커넥션 구성요소의 값이 모두 같을 수 없다.
운영체제는 TCP 커넥션의 생성과 관련된 여러 기능을 제공하며, 그 중에서도 소켓 API를 활용하여 데이터를 주고 받는 과정을 구현하는 소켓 프로그래밍이 있다.
위와 같이 클라이언트와 서버 각각 소켓을 가지고 있으며, 각 주체와 상황에 따라서 소켓의 동작이 달라진다.
HTTP는 TCP 바로 위에 있는 계층이기 때문에, TCP의 성능에 영향을 많이 받는다. 따라서 TCP 성능의 특성을 이해함으로써 최적화된 HTTP 애플리케이션을 구현할 수 있다.
클라이언트나 서버거 너무 많은 데이터를 내려받거나 복잡하고 동적인 자원들을 실행하지 않는 한, 대부분의 HTTP 지연은 TCP 네트워크 지연 때문에 발생한다. 그 원인으로는 아래와 같은 예를 들 수 있다.
이러한 TCP 네트워크 지연은 하드웨어의 성능, 네트워크와 서버의 전송 속도, 요청/응답 메시지의 크기, 클라이언트와 서버 간의 거리 등에 따라 크게 달라진다. 또한 TCP 프로토콜의 기술적인 복잡성도 지연에 영향을 끼친다.
TCP 관련 지연에 영향을 주는 요소는 다음과 같다.
TCP 핸드셰이크: 클라이언트와 서버 사이의 논리적인 접속을 성립하기 위하여 three-way handshake 라는 것을 사용한다. 정확한 전송을 보장하기 위해 상대방 컴퓨터와 사전에 전송 조건을 맞춰가는 과정을 의미한다.
위의 그림을 순서대로 풀면 다음과 같다.
크기가 작은 HTTP 트랜잭션은 50% 이상의 시간을 TCP 구성(1,2번)에 사용한다. 추후에는 이러한 지연을 제거하기 위해 HTTP가 이미 존재하는 커넥션을 어떻게 재활용하는지 알아본다.
인터넷 자체는 완벽한 패킷 전송을 보장하지 않기 때문에 TCP가 자체적인 확인 체계를 갖는다.
각 TCP 세그먼트는 순번과 데이터 무결성 체크섬을 갖는다. 수신자는 세그먼트를 온전히 받았을 경우 확인응답(ACK) 패킷을 송신자에게 반환한다. 이 때, 확인응답은 그 크기가 작기 때문에 별도의 패킷을 만들어 보내기 보다는 동일 방향으로 송출되는 데이터 패킷에 편승시킨다. 확인응답과 데이터 패킷을 하나로 묶어 보다 효율적으로 네트워크를 사용하기 위함이다. 이러한 편승을 늘리기 위해 TCP 내부적으로 확인응답 지연 알고리즘을 구현한다. 보통 0.1~0.2초 동안 버퍼에 저장해두고, 데이터 패킷을 찾는 것이다. 확인응답의 송출을 지연하여 최대한 데이터 패킷에 많이 편승시키고자 한다.
그러나 요청과 응답 단 두가지의 형식으로만 이루어지는 HTTP 동작 방식은, 송출되는 데이터 패킷이 많지 않기 때문에 오히려 잦은 지연을 발생시킨다.
TCP는 인터넷의 급작스러운 부하와 혼란을 방지하기 위해 초기에는 데이터 전송 속도를 제한한다. TCP가 한 번에 전송할 수 있는 패킷의 수를 제한하며 이를 TCP slow start 라고 한다. 데이터가 성공적으로 전송되어 확인응답을 받으면 더 많은 패킷을 전송할 수 있는 권한을 얻게된다. 이처럼 시간에 따라 전송 속도가 높아지는 것을 튜닝이라고 하며, 새로운 커넥션은 튜닝된 커넥션보다 전송 속도가 당연힌 느리다. 따라서 HTTP는 튜닝된 커넥션을 재사용하는 기능이 있으며 이는 '지속 커넥션'을 통해서 알 수 있다.
TCP는 데이터 스트림 인터페이스라는 것을 통해서 매우 작은 데이터도 TCP 스택으로 전송할 수 있도록 한다. 그러나 앞서 살펴본 TCP 세그먼트는 플래그와 헤더를 포함하여 약 40바이트 정도 된다. 정작 데이터는 매우 작은데(예를 들어 1바이트) 40바이트에 해당하는 플래그와 헤더를 포함한 수 많은 패킷을 전송한다면 이는 네트워크의 성능 저하로 이어진다.
네이글 알고리즘은 세그먼트가 최대 크기가 되지 않으면 전송하지 않는다. 여러 TCP 데이터를 한 개의 덩어리로 만든 후에 패킷으로 전송한다. 모든 패킷이 확인응답을 받은 경우에만 최대 크기가 되지 않아도 패킷을 전송하며, 다른 패킷들이 아직 전송 중인 경우에는 버퍼에 저장했다가 다른 패킷들이 확인응답을 모두 받았거나 패킷이 충분히 쌓인 경우에 버퍼에 저장된 데이터가 전송된다.
네이글 알고리즘은 HTTP 성능과 관련해 문제를 발생시킨다.
이러한 문제를 해결하기 위해 HTTP 스택에 TCP_NODELAY 파라미터를 설정하여 네이글 알고리즘을 비활성화시킬 수 있다.
TCP 커넥션을 끊으면 커넥션의 IP주소와 포트 번호를 메모리의 작은 제어영역(control block)에 저장한다. 그리고 같은 주소와 포트 번호를 사용하는 새로운 TCP커넥션이 일정 시간 동안에는 생성되지 않게 하기 위한 것으로 2MLS(2분) 동안 유지된다.
TIME_WAIT을 두는 이유는 지연 패킷이 있을 경우, 도달하기 전에 다른 연결이 진행되었다면 데이터의 무결성에 영향을 주는 등의 상황이 발생할 수 있기 때문이다.
TIME_WAIT 로 인한 문제는 일반적인 상황보다는 성능 시험을 하는 경우에 주로 발생한다. 테스트를 위한 서버의 포트 80번으로 클라이언트가 TCP 커넥션을 시도하는 경우, 클라이언트 포트 수가 60,000개로 가정하면 초당 500개의 커넥션까지만 가능하다. 따라서 이러한 포트 고갈로 인하여 네트워크 성능 저하가 발생할 수 있다.
HTTP는 Connection 헤더 필드를 통해 특정 커넥션에만 적용될 옵션을 지정할 수 있으며, 다양한 기술들을 통해 커넥션 성능을 향상시킬 수 있다.
Connection 헤더를 통해 두 개의 인접한 HTTP 애플리케이션이 현재 맺고 있는 커넥션에만 적용될 옵션을 지정할 수 있다.
Connection 헤더에 있는 모든 헤더 필드는 메시지를 다른 곳으로 전달하는 시점에 모두 삭제돼야 한다. Connection 헤더에는 hop(서버)별 헤더 명을 기술하는데, 이것을 '헤더 보호하기' 라고 한다.
기본적으로 HTTP 트랜잭션은 요청에 대한 응답이 순차적으로 발생한다. 예컨대 3개의 이미지가 있는 페이지의 경우 총 4번의 트랜잭션이 순차적으로 발생한다. 최초 HTML을 받아오는 트랜잭션과 나머지 3개의 이미지를 순서대로 가져오는 트랜잭션이다. 이 때, 커넥션을 맺는 데 발생하는 지연과 더불어 느린 시작 지연이 발생한다. 클라이언트 입장에서는 하나씩 로딩되는 이미지로 인해 비어있는 공간에 대한 심리적 지연도 느끼게 된다. HTTP 커넥션의 성능을 향상시킬 수 있는 여러 기술들을 통해 이러한 문제점을 해결할 수 있다.
위와 같이 병렬 커넥션은 여러 개의 커넥션을 통해 병렬로 처리된다. 앞서 예로 들었던 이미지 3개는 server2 로부터 병렬로 가져옴으로써, 시간을 단축시킬 수 있다. 커넥션들의 지연 시간을 겹치게 하여 총 지연시간을 줄이고, 단일 커넥션일 때 하나의 대역폭을 단일 커넥션이 모두 사용하는 것과는 다르게 여러 개의 커넥션이 대역폭을 나눠 사용할 수 있게 된다.
그러나 항상 병렬 커넥션이 빠른 것은 아니다. 클라이언트의 네트워크 대역폭이 좁은 경우에는 제한된 대역폭 내에서 여러 객체를 전송받는 것은 느리기 때문에 성능상의 장점은 없어진다. 또한 다수의 커넥션은 메모리 소모가 크다. 서버의 입장에서는 너무 많은 커넥션을 감당하는 경우, 서버의 성능을 크게 떨어뜨리는 문제도 발생할 수 있다. 따라서 브라우저는 적은 수의 병렬 커넥션만 허용하며, 서버는 과도한 수의 커넥션이 맺어진 경우 이를 임의로 끊을 수 있다.
마지막으로 클라이언트 입장에서는 페이지 내의 여러 객체가 동시에 보이므로 실제 속도가 더 빠르지는 않더라도 상대적으로 더 빠르다고 느낄 수 있다. 개발자의 측면에서는 저해상도로 시작하여 점차 해상도를 높여가는 형태의 이미지를 사용하여 그 효과를 극대화시킬 수도 있다.
보통 클라이언트는 같은 사이트에서 여러 개의 커넥션을 맺는다. 그리고 동일 서버에 여러 번의 요청을 보낸다. 이를 사이트 지역성(site locality) 라고 한다.
이러한 사이트 지역성을 활용하기 위해 TCP 커넥션을 유지하는 상태를 지속 커네셕이라고 한다. 이를 통해 커넥션 준비 시간과 TCP 느린 시작으로 인한 지연을 피하고 빠르게 데이터를 전송할 수 있다.
병렬 커넥션은 단일 커넥션에 비해 페이지를 빠르게 전송한다. 반면 다음과 같은 단점을 가진다.
반면, 상대적으로 지속 커넥션은 몇 가지 장점이 있다. 커넥션을 맺기 위한 사전 작업과 지연을 줄여주고, 튜닝된 커넥션을 유지한다. 또한 커넥션의 수를 줄여준다. 하지만 지속 커넥션을 잘못 관리할 경우, 연결된 상태의 수많은 커넥션이 쌓일 수 있다. 이는 결과적으로 클라이언트와 서버의 리소스에 불필요한 소모를 발생시킨다.
지속 커넥션과 병렬 커넥션이 함께 사용될 때 가장 효과적이며, 많은 웹 애플리케이션이 적은 수의 병렬 커넥션만 맺고 이를 유지한다. 지속 커넥션 타입은 HTTP/1.0+ 에 keep-alive 커넥션
과 HTTP/1.1 지속 커넥션
이 있다.
위의 그림은 4개의 HTTP 트랜잭션에 대하여 연속적으로 4개의 커넥션을 생성하여 처리하는 방식과 하나의 지속 커넥션으로 처리하는 방식을 비교한 시간 차이가 나타나있다.
지속 커넥션은 커넥션을 맺고 끊는 데에 필요한 작업이 없기 때문에 시간이 단축된 것을 알 수 있다.
Keep-alive는 사용하지 않게 되어 HTTP/1.1 명세에서는 빠졌으나, 아직까지 keep-alive 핸드셰이크가 널리 사용되고 있으므로 이를 처리할 수 있도록 개발해야 한다. HTTP/1.0 keep-alive 커넥션을 구현한 클라이언트는 Connection 헤더를 포함한다.
해당 요청을 받은 서버는 그 다음 요청도 이 커넥션을 통해 받고 싶은 경우 마찬가지로 Connection 헤더를 담아 응답한다.
Connection:Keep-Alive
헤더를 보내지 않을 경우, 서버는 요청을 처리한 뒤 커넥션을 끊는다.Connection 헤더를 이해하지 못하는 멍청한 프락시가 클라이언트와 서버 사이에 있는 경우에 문제가 발생한다.
Conneciton: Keep-Alive
헤더와 함께 메시지를 보낸다.총체적인 난국이다. 클라이언트는 커넥션 유지가 된다고 착각하고, 서버는 프락시와 커넥션이 유지된다고 또다시 착각한다.
위의 멍청한 프락시 문제를 해결하기 위해 Proxy-Connection 이라는 헤더를 사용한다. 대부분의 현대 브라우저에서 지원하고 있으며, 많은 프락시도 이를 인식한다.
Proxy-Connection 은 비표준 확장 헤더이므로 멍청한 프락시가 이를 무조건 서버에 전달하더라도 서버는 이를 무시한다. 따라서 문제가 생기지 않는다.
반면, 이를 이해하는 영리한 프락시의 경우에는 기존에 원하던 커넥션 유지를 가능케 한다.
영리한 프락시는 Proxy-connection 헤더를 인식하고 Connection 헤더로 바꿔서 전송한다.
영리한 프락시와 서버가 지속 커넥션을 맺고, 클라이언트가 같은 커넥션에 다른 요청을 보내더라도 프락시가 이를 무시하지 않고 서버로 전달한다.
추가로, 멍청한 프락시와 영리한 프락시가 공존하는 구조에서는 여전히 문제가 발생할 수 있다.
HTTP/1.1 에서는 keep-alive 커넥션을 지원하지 않는 대신, 기본적으로 지속 커넥션이 활성화 되어 있다. 별도 설정하지 않는 한, 모든 커넥션을 지속 커넥션으로 취급한다. 커넥션을 끊으려면 Connection: close
헤더를 명시해야 한다.
Connection:close
헤더를 포함해 보냈으면, 그 커넥션으로 추가 요청을 보낼 수 없다.Connection:close
헤더를 보내야 한다.Content-Length
값을 가져야 한다.파이프라인 커넥션은 여러 개의 요청을 응답이 도착하기 전에 큐에 쌓는 것이다. 이를 통해서 대기 시간이 긴 네트워크 상황에서 왕복으로 인한 시간을 줄이고 성능을 향상시킨다.
파이프라인에는 여러 가지 제약 사항이 있다.
커넥션을 언제 끊는 가에 대해서는 명확한 기준이 없다.
어떠한 HTTP 클라이언트, 서버, 프라시든 언제든지 TCP 전송 커넥션을 끊을 수 있다.
각 HTTP 응답은 본문의 정확한 크기 값을 가지는 Content-Length 헤더를 가지고 있어야 한다. 그리고 클라이언트나 프락시가 커넥션이 끊어졌다는 응답을 받은 후, 실제 전달된 데이터와 Content-Length 값이 일치하지 않는 경우 데이터의 정확한 길이를 서버에 물어봐야 한다.
앞서 언급되었듯이, 커넥션은 에러가 없더라도 언제든 끊을 수 있다. 클라이언트는 실제로 서버에서 얼만큼 요청이 처리되었는지 전혀 알 수 없다. 그로 인한 재시도를 했을 때, 데이터의 변동이 있을 만한 경우에는 이를 파이프라인을 통해서 요청해서는 안된다. 실행 횟수와 상관없이 같은 결과를 반환할 때 이를 멱등(idempotent)하다고 한다. 비멱등인 메서드나 순서에 대해 에이전트가 요청을 다시 보낼 수 있도록 기능을 제공한다고 할 지라도, 자동으로 재시도 하면 안된다. 예컨대 브라우저는 캐시된 POST 요청 페이지를 다시 로드하려고 할 때, 요청을 다시 보내기를 원하는지 묻는 대화상자를 보여준다.
connection reset by peer
응답을 받게 되면 제대로 도착한 10개의 요청에 대한 응답 데이터를 삭제시켜버린다.