Speak HTTP by hand
한국어 원문으로 표시합니다.
목표
HTTP 는 어렵지 않습니다. TCP 위에 약속된 글자를 주고받는 것뿐입니다. 그런데 라이브러리가 그 약속을 대신 지켜 주기 때문에, 어긋났을 때 무엇이 어긋났는지 읽지 못하게 됩니다.
여기서는 그 약속을 손으로 지켜 보고, 라이브러리가 감춘 것들을 하나씩 봅니다.
재료
python3 /opt/lab/netplan/httpsrv.py 8080 &
경로마다 일부러 다르게 대답하는 작은 서버입니다.
/ Content-Length 로 길이를 알려 준다
/chunked 길이를 모른 채 조각으로 나눠 보낸다
/slow 첫 조각을 보내고 잠시 쉰다
/nolength 길이도 조각도 없이, 연결을 닫아 끝을 알린다
/moved /seeother /keepme 301 · 303 · 307
남길 것
01-raw.txt 손으로 친 요청과 그 응답
02-nohost.txt Host 를 뺐을 때와 1.0 으로 보냈을 때
03-length.txt 본문의 끝을 알리는 세 방법
04-keepalive.txt 한 연결에 요청 두 번
05-redirect.txt 301 · 303 · 307 의 차이
06-timing.txt 구간별 시간
07-notes.md 다음에 읽을 사람에게
요청을 손으로 친다
라이브러리 없이 TCP 연결에 직접 HTTP 요청 문자열을 써서 / 의 응답을 받아 01-raw.txt 에 남기세요. 보낸 것과 받은 것을 모두 남깁니다.
bash 만으로 됩니다.
exec 3<>/dev/tcp/127.0.0.1/8080
printf 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3
timeout 3 cat <&3
줄 끝은 반드시 \r\n 이고, 헤더 끝에 빈 줄이 하나 더 있어야 합니다. 그 빈 줄이 "헤더는 여기까지" 라는 신호입니다.
Host 를 빼 본다
Host 헤더 없이 HTTP/1.1 요청을 보내고, 같은 요청을 HTTP/1.0 으로도 보내 결과를 02-nohost.txt 에 남기세요. 왜 1.1 부터 필수가 되었는지 한 줄 적습니다.
1.1 에서는 400 Bad Request 가 규칙입니다. 서버 취향이 아니라 명세입니다.
한 주소에 여러 사이트를 올릴 수 있게 되면서 필수가 되었습니다. 서버는 어느 사이트로 온 요청인지 Host 로만 알 수 있습니다.
본문은 어디서 끝나나
/ · /chunked · /nolength 를 각각 받아 본문의 끝을 알리는 세 가지 방법을 03-length.txt 에 남기세요. chunked 는 가공하지 말고 원본 바이트 그대로 남깁니다.
curl 은 chunked 를 알아서 합쳐 주므로 원본이 안 보입니다. /dev/tcp 로 직접 받으세요.
조각마다 16진수 크기 줄이 앞에 붙고, 마지막에 크기 0인 조각이 끝을 알립니다. 그 0이 없으면 받는 쪽은 계속 기다립니다.
한 연결에 두 번
연결 하나를 열어 두고 요청을 두 번 보내 응답 두 개를 받아 04-keepalive.txt 에 남기세요. 연결을 재사용하면 무엇이 절약되는지 한 줄 적습니다.
Connection: close 를 빼면 서버가 연결을 유지합니다. 첫 응답을 Content-Length 만큼 읽고 나서 두 번째 요청을 같은 fd 로 보냅니다.
파이썬으로 하면 길이만큼 정확히 읽기가 쉽습니다.
301 · 303 · 307 은 무엇이 다른가
세 경로를 각각 받아 응답을 05-redirect.txt 에 남기고, 셋의 차이를 적으세요. 특히 다시 요청할 때 메서드를 바꿔도 되는지를 씁니다.
셋 다 Location 을 주지만 다시 요청하는 방법이 다릅니다.
- 301 · 302 는 역사적으로 브라우저가 POST 를 GET 으로 바꿔 다시 보냅니다
- 303 은 바꾸라는 것이 명세입니다
- 307 · 308 은 메서드와 본문을 그대로 두라는 뜻입니다
결제 요청이 리다이렉트를 만났을 때 어느 쪽인지가 곧 사고와 정상을 가릅니다.
느린 구간을 숫자로 가리킨다
/ 와 /slow 를 각각 curl 의 시간 표시자로 재서 06-timing.txt 에 남기고, 어느 구간이 늘어났는지 적으세요. 이 값들이 누적이라는 것도 함께 씁니다.
curl -sS -o /dev/null -w 'dns=%{time_namelookup} tcp=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' http://127.0.0.1:8080/
숫자는 누적입니다. time_connect 가 0.32 라면 연결까지 0.32초가 걸렸다는 뜻이지 연결에 0.32초가 걸렸다는 뜻이 아닙니다. 구간별 시간은 빼서 구합니다.
/slow 는 첫 바이트를 보낸 뒤에 쉽니다. 그러면 ttfb 는 빠른데 total 만 커집니다.
다음에 읽을 사람에게
여기서 본 것 중 넷 이상을 골라 07-notes.md 에 정리하세요. 무엇을 했는지가 아니라 왜 그런지를 적습니다.
몇 달 뒤의 자신이 읽는다고 생각하고 쓰세요. "/chunked 를 받았다" 는 도움이 안 되고, "길이를 미리 모를 때 조각으로 보내고 크기 0인 조각으로 끝을 알린다 — 그 0이 없으면 받는 쪽이 계속 기다린다" 는 도움이 됩니다.