[요약] HTTP 완벽 가이드 - 1부 HTTP: 웹의 기초
1장) HTTP 개관
HTTP: 인터넷의 멀티미디어 배달부
- 신뢰성 있는 데이터 전송 프로토콜
웹 클라이언트와 서버
- 가장 흔한 클라이언트는 웹브라우저
리소스
- 정적 파일 & 동적 파일
- 꼭 파일이 아니라 검색엔진, 웹 게이트웨이 등도 리소스로 볼 수 있다
미디어 타입
MIME (Multipurpose Internet Mail Extensions) 은 원래 각기 다른 전자메일 시스템 사이에서 메시지가 오갈 때 겪는 문제점을 해결하기 위해 설계되었다. HTTP에서도 멀티미디어 콘텐츠를 기술하고 라벨을 붙이기 위해 채택. 웹 서버는 모든 HTTP 객체 데이터에 MIME 타입을 붙인다.
- 주타입과 부타입을 기술하는데
/
로 구분한다text/html
,image/jpeg
,video/quicktime
(애플 퀵타임 동영상),application/vnd.ms-powerpoint
(마이크로소프트 ppt)
URI
Uniform Resource Identifier. URL과 URN으로 나뉜다.
- URL: Uniform Resource Locator.
- 스킴, 주소, 리소스를 포함한다.
- URN: Uniform Resource Name. 리소스의 위치에 영향을 받지 않는 유일무이한 이름. 리소스가 그 이름을 변하지 않게 유지하는 한, 여러 종류의 네트워크 접속 프로토콜로 접근해도 문제가 없다.
- 아직 실험 중인 단계이며 리소스 위치 분석을 위한 인프라 지원이 필요한 상태
트랜잭션
HTTP 트랜잭션은 요청 명령
과 응답 결과
로 구성되어 있다.
메서드
서버에게 어떤 동작이 취해져야 하는지 말해준다. 흔히 알려진 것들로는 GET
, PUT
, DELETE
, POST
, HEAD
상태 코드
클라이언트에게 요청이 성공했는지, 추가 조치가 필요한지 알려주는 세 자리 숫자. 대표적으로 200, 302, 404 등이 있다.
메시지
사실상 HTTP 메시지는 줄 단위로 나누어진 단순 문자열에 불과하다. 구성을 살펴보면 이렇다.
- 시작줄: 어떤 응답과 요청인지 알 수 있다.
- 헤더: 0개 이상의 필드가 있고 이름과 값을 쌍점(
:
)으로 구분한다. 빈 줄로 끝낸다. - 본문: 어떤 종류의 데이터든 들어갈 수 있다.
TCP 커넥션
TCP/IP
HTTP는 애플리케이션 계층 프로토콜이라 네트워크 통신의 핵심적인 세부사항에 대해서는 신경쓰지 않는다. 이는 TCP/IP가 담당한다. TCP는 다음을 제공한다.
- 오류 없는 데이터 전송
- 순서에 맞는 전달
- 조각나지 않는 데이터 스트리
TCP/IP는 패킷 교환 네트워크 프로토콜의 집합이고 신뢰성 있는 의사소통을 하게 해준다.
접속, IP 주소 그리고 포트번호
URL을 참고하여 IP주소와 포트번호를 파악한 뒤 TCP 커넥션을 맺게 된다. 이렇게 커넥션이 맺어지면 HTTP 통신을 시작할 수 있다.
- telnet으로 HTTP 통신을 확인해볼 수도 있고, 저자는 netcat을 추천한다 (HTTP, UDP, TCP 기반의 트래픽을 조작하고 스크립트 할 수 있게 해준다)
웹의 구성요소
프락시
클라이언트와 서버 사이에 위치한 HTTP 중개자. 주로 보안을 위해 사용하고 요청과 응답을 필터링한다.
캐시
자주 찾는 리소스의 사본을 저장해두는 HTTP 프락시 서버.
게이트웨이
다른 서버들의 중개자로 동작하는 특별한 서버로 주로 HTTP 트래픽을 다른 프로토콜로 변환하기 위해 사용.
터널
두 커넥션 사이에서 raw data를 그대로 전달해주는 HTTP 애플리케이션. 터널 활용의 대표적인 예로 암호화된 SSL 트래픽을 HTTP 커넥션으로 전송함으로써 웹 트래픽만 허용하는 사내 방화벽을 통과시키는 것.
에이전트
자동화된 HTTP 요청을 만드는 준지능적(semi-intelligent) 웹클라이언트
추가 정보
HTTP - Hypertext Transfer Protocol
→ HTTP 프로토콜에 관한 여러 링크 포함
→ 1.1 의 현재 버전에 대한 공식 명세서
RFC 1945 - Hypertext Transfer Protocol -- HTTP/1.0
→ 1.0을 서술한 정보성 RFC
2장) URL과 리소스
URL 문법
<스킴>://<사용자이름>:<비밀번호>@<호스트>:<포트>/<경로>;<파라미터>?<질의>#<프래그먼트>
🤔 생각해볼 것
RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax
최신화된 RFC 3986 문서를 보면 세미콜론으로 구분하는 파라미터는 나오지 않는다. 실제 사용 사례는 거의 본 적이 없고, 이제는 허용되지 않는다는 이야기도 있어서 사용에 주의가 필요해보인다.
단축 URL
상대 URL
상대 URL은 모든 정보를 담고 있지 않고, base URL을 알아야 한다. URL을 짧게 표기하기 위한 방법. 상대 참조를 절대 경로로 변환하는 알고리즘은 RFC 1808, 이후 RFC 2396에 기술되었다.
URL 확장
- 호스트명 확장: 주소창에
naver
만 치면 알아서www
,com
등이 붙는다. 다만 이런 기능은 프락시와 같은 다른 HTTP 애플리케이션에 문제를 발생시킬 수도 있다고 한다. - 히스토리 확장: 과거 방문 기록을 저장
안전하지 않은 문자
안전한 전송: 정보가 유실될 위험 없이 URL을 전송. (SMTP 같은 프로토콜은 7비트 인코딩이라 8비트 이상으로 인코딩 되면 특정 문제가 제거될 수도 있다.)
이런 문제를 피하기 위해 URL은 상대적으로 작고 일반적으로 안전한 알파벳 문자만 포함하도록 허락.
이스케이프 기능을 추가하여 안전한지 않은 문자를 안전한 문자로 인코딩할 수도 있다.
URL 문자 집합
- US-ASCII는 7비트
문자 제한
몇몇 문자들은 특수한 의미를 가지기 때문에 일반적인 문자로 사용하기 위해서는 반드시 인코딩을 거쳐야한다.
URL Encoding of Special Characters
스킴의 바다
3장) HTTP 메시지
메시지의 흐름
- 인바운드: 메시지가 원 서버로 향하는 것
- 아웃바운드: 모든 처리가 끝난 뒤에 메시지가 사용자 에이전트로 돌아오는 것
HTTP 메시지는 모두 다운스트림으로 흐른다. 메시지의 발송자는 수신자의 업스트림이다. (항상 한 쪽에서 보내고 한쪽이 받는 형태이기 때문에)
메시지의 각 부분
HTTP/1.0 200 OK
Content-type: text/plain
Content-length: 19
Hello World!
- 각 줄은 캐리지 리턴 (ASCII 13)과 개행문자(ASCII 10)으로 구성된 두 글자의 줄바꿈 문자열. 이걸
CRLF
라고 부른다. (CR은 커서만 앞으로 옮기기, LF는 커서는 그대로 두고 아래줄로 이동하기, 즉 합쳐서 개행이 된다. 타자기 시절의 잔재라고 함) 견고한 애플리케이션이라면 일반적인 개행 문자도 받아들일 수 있어야 한다.
메시지의 문법
// 요청
<메서드><요청 URL><버전>
<헤더>
<엔터티 본문>
// 응답
<버전><상태 코드><사유 구절>
<헤더>
<엔터티 본문>
시작줄
- 요청줄: 매서드, 요청 URL, HTTP 버전. 모든 필드는 공백으로 구분한다
- 응답줄: HTTP 버전, 상태 코드, 수행 상태. 모든 필드는 공백으로 구분한다
메서드
상태 코드
상태 코드
사유 구절
엄격한 규칙은 없다. 사유에 대한 설명을 첨부하면 된다.
버전 번호
어떤 애플리케이션이 지원하는 가장 높은 버전
참고로 HTTP/2.22가 HTTP/2.3보다 크다 (22가 3보다 큰 맥락)
헤더
- 일반: 요청 응답 양 쪽 모두 가능
- 요청: 요청에 대한 부가정보
- 응답: 응답에 대한 부가정보
- Entity: 본문 크기와 콘텐츠, 혹은 리소스 그 자체를 서술
- 확장: 명세에 정의되지 않은 새로운 헤더
헤더를 여러 줄로 나누려면 추가 줄 앞에 최소 하나의 스페이스 혹은 탭 문자가 와야 한다.
엔터티 본문
과거 0.9버전 시절에는 응답은 오직 엔터티로만 구성되어 있었다고 한다 (버전, 상태 코드, 사유 구절 아무것도 없다!). 지나친 단순함은 이후 개편되었다.
메서드
안전한 메서드
GET, HEAD 등. 요청의 결과로 서버에 어떤 작용도 없음을 의미. 물론 서버가 어떻게 구현되어있는가는 전적으로 개발자에게 달려있으니 이 메서드 하나만으로 보장되는 것은 없다.
GET
주로 리소스 요청을 위해 사용.
HEAD
GET
과 유사하나 응답으로 헤더만을 돌려준다.
- 리소스를 가져오지 않고도 그에 대해 무엇인가 알아낼 수 있다
- 응답의 상태 코드를 통해, 개체가 존재하는지 확인
- 헤더를 확인하여 리소스가 변경되었는지 검사
PUT
서버에 문서를 쓴다.
POST
서버에 입력 데이터를 전송하기 위해 설계. 실제로 HTML 폼을 지원하기 위해 흔히 사용된다.
TRACE
클라이언트에게 자신의 요청이 서버에 도달했을 때 어떻게 보이게 되는지 알려준다. 주로 진단을 위해 사용한다. 단, 모든 메서드를 일관되게 다룬다고 가정한다 (GET
은 웹 캐시로 갈 수도 있으나 이런 걸 무시한다)
어떠한 본문도 보낼 수 없으며 응답 본문에는 서버가 받은 요청이 그대로 들어있다.
OPTIONS
서버에게 특정 리소스에 대해 어떤 메서드가 지원되는지 응답을 받아올 수 있다. Allow
헤더 안에 지원하는 메서드의 목록을 반환한다.
DELETE
리소스 삭제 요청. 하지만 이 또한 보장되는 것은 없고 HTTP 명세상 서버가 클라이언트에게 알리지 않고 요청을 무시하는 것도 허용하기 때문
확장 메서드
- WebDav HTTP 확장의 일부로는
LOCK
,MKCOL
,COPY
,MOVE
- 엄격하게 보내고 관대하게 받아들여라
상태 코드
100번대
- 100:
Continue
- 요청의 시작 부분 일부가 받아들여졌으며, 클라이언트는 나머지를 계속 이어서 보내야함을 의미
- MDN 설명에 의하면, 응답에 100이 있으면 클라이언트의 요청이 정상적으로 처리되고 있는 중임을 의미한다. 클라이언트는 서버에게 응답이 돌아오면 엔터티를 보내겠다는 의미로
Expect
헤더를 보내야 한다. - 프락시의 입장에서는 다음 홉 서버가 다른 HTTP 버전을 지원하는 경우 417
Expectation Failed
에러로 응답해야 한다.
- 101
Switching Protocols
200번대
성공 상태 코드
- 201
Created
- 202
Accepted
- 203
Non-Authorative Information
- 204
No Content
- 205
Reset Content
: 주로 브라우저를 위해 사용되며 현재 페이지에 있는 HTML 폼에 채워진 모든 값을 비우라고 안내 - 206
Partial Content
300번대
리다이렉션 상태 코드
- 300
Multiple Choices
- 여러 리소스를 가리키는 URL을 요청한 경우 목록과 함께 반환 - 301
Moved Permanently
- 응답은 Location 헤더에 현재 리소스가 존재하고 있는 URL을 포함해야 한다 - 302
Found
- 303
See Other
- 304
Not Modified
- 305
Use Proxy
- 307
Temporary Redirect
비슷한 목적으로 쓰이는 것들이 많은데, 이는 HTTP 버전이 변화하면서 생긴 현상이다. 결국 서버는 리다이렉트 응답에 들어갈 가장 적절한 상태 코드를 선택하기 위해 클라이언트의 HTTP 버전을 검사할 필요가 있다
400번대
클라이언트 에러 상태 코드
- 400
Bad Request
- 401
Unauthorized
- 402
Payment Required
- 아직 웹 결제 자체가 실험적인 단계라 안 씀 - 403
Forbidden
- 404
Not Found
- 405
Method Not Allowed
- 406
Not Acceptable
- 407
Proxy Authentication Required
- 408
Request Timeout
등등.
500번대
서버 에러 상태 코드
- 500
Internal Server Error
- 501
Not Implelemented
- 502
Bad Gateway
- 503
Service Unavailable
- 504
Gateway Timeout
- 505
HTTP Version Not Supported
헤더
- 일반 헤더: 클라이언트, 서버 모두 사용. 메시지에 대한 아주 기본적인 정보.
- 일반 캐시 헤더:
Cache-Control
,Pragma
등
- 일반 캐시 헤더:
- 요청 헤더: 요청자, 클라이언트의 정보와 선호, 능력 등
Accept
관련 헤더: 클라이언트가 선호하는 데이터에 대한 정의- 조건부 요청 헤더: 요청에 제약을 추가
- 요청 보안 헤더: 클라이언트가 리소스에 접근하기 전에 자신을 인증하기 위한 용도
- 프락시 요청 헤더: 프락시 기능을 돕기 위한 헤더
- 응답 헤더: 응답에 대한 부가적인 정보
- 협상 헤더: 여러가지 표현이 가능한 문서라면 클라이언트가 어떤 표현을 택할지 협상을 할 수 있도록 정보를 제공
- 응답 보안 헤더:
Proxy-Authenticate
,Set-Cookie
등 - 엔터티 헤더: 엔터티에 대한 광범위한 정보
- 콘텐츠 헤더: 엔터티의 콘텐츠에 대하나 구체적인 정보
- 엔터티 캐싱 헤더
4장) 커넥션 관리
TCP 커넥션
전 세계 모든 HTTP 통신은 TCP/IP (프로토콜들의 계층화된 집합)를 통해 이루어진다. 웹 브라우저가 TCP 커넥션을 통해 웹 서버에 요청을 보낸다면 이렇다
- 브라우저가 호스트를 기반으로 IP, 포트번호를 조회한다
- 1에서 얻어온 곳으로 브라우저가 TCP 커넥션 생성
- 브라우저가 서버로 HTTP 요청 메시지 전송
- 브라우저가 서버에서 온 HTTP 응답 메시지 수령
- 브라우저가 커넥션을 해제
TCP 통신의 특징
- 신뢰할 만한 통신 방식을 제공하고, 순서를 보장
- IP 패킷이라는 작은 조각을 통해 데이터를 전송 (TLS, SSL 같은 보안은 TCP와 HTTP 사이에 위치)
TCP 유일성 식별 기준
- 발신지 IP 주소
- 발신지 포트
- 수신지 IP 주소
- 수신지 포트
TCP 소켓 프로그래밍
소켓 API를 사용하면, TCP 종단(endpoint)데이터 구조를 생성하고, 원격 서버의 TCP 종단에 그 종단 데이터 구조를 연결하여 데이터 스트림을 읽고 쓸 수 있다.
TCP 성능에 대한 고려
HTTP에 있어 작은 데이터의 지연의 경우 실제 데이터 전송보다 TCP 네트워크 지연 때문에 발생한다. 여기엔 여러 이유가 있다.
- DNS 조회
- TCP 커넥션 요청
- 실제 인터넷을 통해 전달되는 과정
- 웹 서버의 대응 속도
일반적인 TCP 지연들은 다음과 같다.
- 핸드셰이크:
SYN
→SYN
+ACK
→ 통신 완료의 구조 때문에 크기가 작은 HTTP 트랜잭션은 50% 이상의 시간을 TCP를 구성하는 데 사용한다. - 확인응답 지연
- 순번을 보장하기 때문에 전송되지 않은 패킷이 있다면 재전송을 하게 된다. 이를 확인하기 위해 확인응답 패킷을 송신자에게 반환한다.
- 송신패킷은 작기 때문에 같은 방향으로 송출되는 데이터 패킷에 확인응답을 편승(piggyback)시킨다. 그러나 HTTP는 요청, 응답 두 가지만 존재해서 막상 편승할 패킷을 찾으려고 하면 해당 방향으로 송출될 패키지가 많지 않아 지연이 자주 발생한다.
- TCP 느린 시작
- TCP 커넥션은 처음에는 커넥션의 최대 속도를 제한하다가 성공적으로 전송되면 조금 씩 속도 제한을 높여가는 ‘튜닝' 로직이 있다. 이렇게 응답을 받았을 때 보낼 수 있는 패킷 수를 점진적으로 늘려가는 것을 ‘혼잡 윈도를 연다' (opening the congestion window)라고 표현한다.
- 네이글(Nagle) 알고리즘
- 패킷을 전송하기 전에 많은 양의 TCP 데이터를 한 개의 덩어리로 합치는 알고리즘인데, 최대 크기가 아니면 전송을 하지 않아서 추가적인 데이터를 기다리며 지연되는 경우가 있다.
- 이를 비활성화 하려면 HTTP 스택에
TCP_NODELAY
파라미터 값을 설정하자.
TIME_WAIT
의 누적과 포트 고갈- 성능 측정 시에는 항상 심각한 성능 저하를 발생시키니 오해하지는 말자 (늘 그런 건 아님)
- 동일한 TCP 커넥션을 일정시간 동안은 살려두어 재활용할 수 있게 해준다. 단, 커넥션을 너무 많이 맺거나 대기 상태로 있는 제어 블록이 너무 많아지는 상황은 주의하자.
HTTP 커넥션 관리
Connection 헤더
connection
헤더 필드는 커넥션 토큰을 쉽표로 구분하여 가지고 있으며, 이 값들은 다른 커넥션에 전달되지 않는다 (중개 서버의 상황일 때). 다음 메시지를 보낸 다음 끊어져야 할 커넥션은 Connection:close
라고 명시할 수 있다. 즉, 홉(hop: 각 서버)이 메시지를 전달하기 전에 Connection
헤더와 Connection
헤더에 기술되어 있던 모든 헤더를 삭제.
순차적인 트랜잭션 처리에 의한 지연
각각의 트랜잭션이 모두 커넥션을 구축해야한다면 순차적인 처리가 발생하면서 지연이 생길 수 있다. 이를 향상시키기 위한 네 가지 방법이 있다.
병렬(parallel) 커넥션
여러 개의 TCP 커넥션을 통한 동시 HTTP 요청
단, 대역폭이 좁을 때는 여러 커넥션이 생기면서 부하가 발생해 순차적 처리보다 느릴 수 있으며, 다수의 커넥션이 메모리를 많이 소모한다. 또한, 각각의 커넥션은 TCP 느린 시작이 발생하고, 병렬 커넥션의 수에는 제한이 있다.
지속(persistent) 커넥션
커넥션을 맺고 끊는 데서 발생하는 지연을 제거하기 위한 TCP 커넥션 재활용
병렬 커넥션의 단점을 개선한다 - 사전 작업과 지연 감소, 튜닝된 커넥션 유지, 커넥션 수 감소.
하지만 잘못 관리한다면 계속 연결된 채로 남아 불필요한 리소스 소모를 발생시킨다.
➡️ Keep-Alive 옵션
Connection: Kepp-Alive
헤더를 포함시키면 응답 메시지를 전송하고도 계속 커넥션을 유지하기를 바란다고 요청을 보낼 수 있다. timeout
, max
파라미터를 통해 커넥션의 유지 시간, 최대 몇개의 트랜잭션을 처리할지를 정할 수 있다.
- HTTP/1.0에선 기본적으로 사용되지는 않는다
- HTTP/1.1 은 지속 커넥션이 기본으로 활성화
- 커넥션이 끊어지기 전에 엔터티 본문의 길이를 알 수 있어야 커넥션을 유지할 수 있기 때문에
Content-Length
값과 함께 멀티파트 미디어 형식을 가지거나 청크 전송 인코딩으로 인코드 되어야 한다. - 프락시와 게이트웨이는 메시지를 전달하거나 캐시에 넣기 전에
Connection
헤더에 명시된 모든 헤더 필드와Connection
헤더를 제거해야 한다.
➡️ 멍청한 프락시
프락시가 keep-alive
헤더를 이해하지 못하면, 서버는 프락시와 커넥션을 끊지 않은 채로 계속 데이터를 전달하는데 프락시는 첫 요청에 대한 응답 외에는 모두 무시하게 된다. 이렇게 되면 클라이언트는 프락시와 연결해둔 커넥션으로 계속 요청을 보내는데 정작 이 요청이 모두 무시되면서 행에 걸리게 된다.
이를 해결하기 위해 비표준인 Proxy-Connection
확장 헤더를 프락시에게 전달한다. 영리한 프락시는 이를 Connection
헤더로 바꾸어 원하던 효과를 얻을 수 있다. 다만 이 해결 방법은 프락시가 중간에 하나만 있을 때만 가능하다 (아니라면 멍청한 프락시와 영리한 프락시가 서로 keep-alive
시도를 하면서 위에서 언급한 문제가 되풀이된다.)
파이프라인(pipeline) 커넥션
공유 TCP 커넥션을 통한 병렬 HTTP 요청. 여러 개의 요청을 응답이 도착하기 전까지 큐에 쌓고, 한 요청이 서버로 전달되면 그 다음 요청이 이어서 전달된다. 파이프라인에는 다음 제약사항들이 있다.
- 클라이언트는 커넥션이 지속 커넥션인지 확인하기 전까지는 파이프라인을 이어서는 안 된다
- 응답은 요청 순서와 같게 와야 한다.
- 완료되지 않은 요청이 파이프라인에 있으면 언제든 다시 보낼 준비가 필요하다
- 반복해서 보낼 경우 문제가 생기는 요청은 파이프라인을 통해서 보내면 안 된다 (비멱등 nonidempotent 요청은 문제가 된다. 예를 들어 POST)
다중(multiplexed) 커넥션
요청과 응답들에 대한 중재 (실험적)
커넥션 끊기
- 전체 끊기: 입력, 출력 채널의 커넥션 모두 끊기
- 절반 끊기: 입력, 출력 둘 중 하나만 끊기
TCP 끊기와 리셋 에러
- 보통은 커넥션의 출력 채널을 끊는 것이 안전하다. 입력 채널이 끊긴 채로 클라이언트가 데이터를 전송하면 서버의 운영체제는
TCP connection reset py peer
메시지를 보내는데, 이는 대부분의 운영체제에서는 심각한 에러로 취급하여 버퍼에 저장된, 아직 읽히지 않은 데이터를 모두 삭제한다. - 일반적으로 우하하게 커넥션을 끊는다면 애플리케이션 자신의 출력 채널을 먼저 끊고 다른 쪽에 있는 기기의 출력 채널이 끊기는 것을 기다린다.