웹서버
브라우저가 말을 걸어 주는 쉘 스크립트 20줄.
웹서버는 요청을 기다렸다가 응답을 보내는 프로그램일 뿐입니다. 그것을 증명하기 위해 bash 와 netcat, 이름 있는 파이프만으로 서버를 만듭니다 — 한 줄도 빠짐없이 읽을 수 있습니다. 그다음 실제 사이트를 HTTPS 까지 붙여 돌리는 Caddy 로 넘어갑니다.
웹서버는 대답하는 프로그램입니다
장비 랙과 클라우드 상표를 걷어 내면, 웹서버는 한 가지 일을 되풀이하는 프로그램 하나입니다. 누가 연결하기를 기다렸다가, 무엇을 달라는지 읽고, 글을 돌려주고, 다시 기다립니다.
"서버"라는 낱말은 두 가지를 가리키고 사람들이 늘 섞어 씁니다. 데이터센터에 놓인 기계도 서버고, 포트에서 귀를 기울이는 프로그램도 서버입니다. 여기서 말하는 것은 프로그램 쪽이고, 그것은 지금 앞에 있는 노트북에서도 똑같이 잘 돕니다.
손님 한 명, 네 걸음
- 귀 기울이기 프로그램이 포트 하나를 차지하고 기다립니다 — 예제에서는 8084 입니다. 포트는 기계에 번호가 붙은 문이라, 두 프로그램이 같은 문을 잡을 수는 없습니다.
- 요청 읽기 브라우저가 연결해 몇 줄의 글을 보냅니다. 첫 줄에 어떤 방법으로 어떤 경로를 원하는지가 담깁니다. 예를 들면 경로 / 를 GET 해 달라는 식입니다.
- 판단하기 무엇을 보낼지 정합니다. 디스크에서 파일을 읽거나, 코드를 돌리거나, 데이터베이스에 묻습니다. 예제는 늘 같은 파일만 보내기 때문에 20줄에 들어갑니다.
- 응답하고 끊기 상태 줄과 몇 개의 헤더, 빈 줄 하나, 그리고 내용을 적어 보낸 뒤 연결을 끊고 다음 손님을 위해 처음으로 돌아갑니다.
여러분이 이름을 들어 본 모든 서버가 — Apache · Nginx · Caddy, 지금 이 사이트를 돌리는 Node 프로세스까지 — 위의 네 걸음을 합니다. 다른 점은 그것을 수천 명에게 동시에, 안전하게, HTTPS 로 한다는 것뿐입니다. 모양은 똑같습니다.
쉘 스크립트 한 장으로 만드는 웹서버
아래가 bash 로 쓴 완전한 웹서버입니다. 포트를 열고, 요청 줄을 읽고, 기록을 남기고, index.html 을 제대로 된 HTTP 응답으로 돌려줍니다. 스무 줄, 라이브러리 없음, 그리고 브라우저는 차이를 알아채지 못합니다.
무거운 일은 두 명령이 맡습니다. netcat(nc) 이 네트워크를 말하고, mkfifo 가 이름 있는 파이프를 만들어 우리가 쓴 답을 nc 가 붙들고 있는 연결로 되돌려 보냅니다.
#!/bin/bash
PORT=8084
PIPE=simple-web-server-pipe
rm -f "$PIPE"
mkfifo "$PIPE"
response() {
read -r method path _
echo "[$(date)] $method $path" >&2
index=$(cat index.html)
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n%s' \
"$index"
}
while true; do
response < "$PIPE" | nc -l "$PORT" > "$PIPE"
done한 줄씩 뜯어보기
PORT=8084귀를 기울일 포트입니다. 1024 보다 큰 번호는 관리자 권한 없이 쓸 수 있고, 8084 는 이미 쓰고 있는 것과 부딪힐 일이 적습니다.PIPE=simple-web-server-pipe파이프 파일의 이름입니다. 폴더에 보통 파일처럼 나타나지만 안에 저장되는 것은 없습니다 — 두 명령 사이로 자료를 지나가게 할 뿐입니다.mkfifo "$PIPE"이름 있는 파이프(FIFO)를 만듭니다. 바로 윗줄이 지난번에 남은 것을 지우는 이유는, mkfifo 가 이미 있는 파일 위에 덮어쓰기를 거부하기 때문입니다.read -r method path _요청의 첫 줄을 읽어 방법 · 경로 · 나머지로 나눕니다. 그 뒤에 오는 브라우저의 헤더들은 이 서버가 그냥 흘려보냅니다.echo "[$(date)] $method $path" >&2기록을 한 줄 남깁니다. 표준 오류로 방향을 돌린 덕분에 이 글이 브라우저로 가지 않고 터미널에만 나타납니다.index=$(cat index.html)지금 폴더의 index.html 을 읽어 변수에 담습니다. 서버를 그 파일이 있는 폴더에서 실행해야 하는 이유가 이것입니다.printf 'HTTP/1.1 200 OK\r\n…'응답을 씁니다. 상태 줄, 내용의 종류, 여기서 연결을 끝낸다는 헤더, 그리고 빈 줄과 페이지. 헤더와 내용을 가르는 것이 그 빈 줄이고, HTTP 는 모든 줄 끝에 캐리지리턴과 줄바꿈을 함께 요구합니다.response < "$PIPE" | nc -l "$PORT" > "$PIPE"이것이 동작하게 만드는 요령입니다. nc 는 파이프에서 우리 응답을 읽고, 브라우저의 요청을 그 파이프에 다시 써 넣습니다. 두 쪽이 서로에게 먹이를 주는 셈입니다.while true; do … done반복문입니다. nc 는 연결 하나를 처리하면 끝나 버리므로, 이 줄이 없으면 서버는 손님 한 명만 받고 멈춥니다.
요청이 함수에 닿아야 함수의 답이 nc 에 닿을 수 있습니다 — 고리 모양입니다. 그런데 쉘의 파이프라인은 한 방향으로만 흐릅니다. 이름 있는 파이프가 그 고리를 닫아 줍니다. 응답은 파이프라인을 타고 nc 로 가고, 요청은 FIFO 를 타고 되돌아옵니다. 두 명령이 양방향으로 말을 주고받게 하는 가장 작은 방법입니다.
직접 띄워 보기
스크립트를 server.sh 로 저장하고 옆에 index.html 을 두고 실행합니다. 그 터미널은 켜 둔 채로, 두 번째 터미널에서 요청을 보내 보세요.
# 1. create an index.html in the same folder
echo '<h1>Hello</h1>' > index.html
# 2. save the script above as server.sh and start it
bash server.sh
# 3. open it in a browser (or check from a second terminal)
open http://localhost:8084
curl http://localhost:8084브라우저를 새로고침하면서 첫 터미널을 보세요. 요청 하나마다 기록이 한 줄씩 쌓입니다. Control-C 로 멈추고, 파이프 파일이 남아 있으면 지웁니다. 리눅스의 nc 는 포트 앞에 -p 를 더 붙여야 할 수 있고, macOS 에 기본으로 들어 있는 판은 적힌 그대로 동작합니다.
선을 타고 실제로 오가는 것
연결을 열어 보면 여러분이 손으로 칠 수도 있었을 평범한 글자가 흐릅니다. 쉘 웹서버가 가능한 이유가 이것이고, 웹에서 생긴 문제를 짐작이 아니라 읽어서 풀 수 있는 이유도 이것입니다.
브라우저가 보내는 요청
GET / HTTP/1.1
Host: localhost:8084
User-Agent: Mozilla/5.0
Accept: text/html첫 줄이 우리 서버가 읽는 유일한 부분입니다. 방법(GET 은 가져오기, POST 는 보내기), 경로, 그리고 프로토콜 판입니다. Host 는 어느 사이트를 원하는지 말합니다. 서버 하나가 한 주소에서 수백 개 도메인을 품을 수 있고, 그것을 구별하는 것이 이 헤더입니다. 나머지는 브라우저의 자기소개입니다. 자신이 어떤 프로그램인지, 어떤 형식을 읽을 수 있는지, 어떤 언어를 좋아하는지. 서버는 쓸 수도 있고 무시할 수도 있습니다.
서버가 보내는 응답
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Connection: close
<h1>Hello</h1>상태 줄입니다. 프로토콜 판과, 일이 어떻게 됐는지 알리는 숫자. 200 은 요청이 잘 됐다는 뜻입니다. Content-Type 은 뒤따라올 것을 어떻게 읽어야 하는지 알려 줍니다. HTML 을 엉뚱한 종류로 보내면 브라우저가 태그를 페이지가 아니라 글자 그대로 보여 줍니다. charset 부분은 실제로는 선택이 아닙니다. 이것이 없으면 한글은 깨진 글자로 도착하기 쉽습니다. 그리고 빈 줄 하나. 그 뒤는 전부 내용입니다. 헤더와 페이지를 가르는 경계는 오직 그 빈 줄뿐입니다.
알아 둘 만한 상태 코드
| 코드 | 뜻 |
|---|---|
| 200 OK | 성공입니다. 요청한 것이 응답에 담겨 있습니다. 200 번대는 모두 이런 종류의 성공을 뜻합니다. |
| 301 Moved Permanently | 주소가 영구히 옮겨졌습니다. 새 주소는 Location 헤더에 있고 브라우저가 알아서 따라갑니다. 사이트가 http 손님을 https 로 보낼 때 쓰는 방법입니다. |
| 404 Not Found | 서버는 멀쩡한데 그 경로가 없습니다. 400 번대는 문제가 요청 쪽에 있다는 뜻입니다. |
| 500 Internal Server Error | 멀쩡한 요청을 처리하다 서버가 넘어졌습니다. 500 번대는 고쳐야 할 사람이 여러분이라는 뜻이고, 자세한 내용은 서버 기록에 있습니다. |
이 서버가 못 하는 일
예제는 정직한 교재이고 형편없는 서비스입니다. 어디서 모자란지 정확히 아는 것이 곧 진짜 서버가 대신 풀어 주는 문제를 아는 것입니다.
한 번에 한 명입니다. nc 는 연결 하나만 다루므로, 그것이 바쁜 동안 나머지는 모두 기다립니다. 경로를 무시합니다. 무엇을 달라고 하든 index.html 만 돌아옵니다 — 길 안내도 없고 두 번째 페이지도 없습니다. 전부 HTML 로 보냅니다. 그림도 스타일 파일도 내려받기도 저마다의 내용 종류가 필요한데, 이 서버는 딱 하나만 압니다. HTTPS 가 없습니다. 모든 것이 맨몸으로 지나가므로, 중간의 어떤 네트워크든 읽거나 바꿔치기할 수 있습니다. 안전장치가 전혀 없습니다. 요청 크기를 재지 않고, 시간 제한도 없으며, 함부로 확장하면 남이 정한 파일 경로를 그대로 읽어 버립니다.
내 컴퓨터에서 돌려 HTTP 를 눈으로 보는 데 쓰고, 남이 찾아올 것에는 진짜 서버를 씁니다. 다음 절이 그 진짜 서버이고, 시작하기는 이것보다 크게 어렵지도 않습니다.
Caddy — 읽을 수 있는 채로 남은 진짜 웹서버
Caddy 는 실행 파일 하나로 된 현대적인 웹서버입니다. 쉘 예제 바로 다음에 배울 값어치가 있는 이유는, 읽을 수 있는 몇 줄이면 돌아간다는 느낌을 그대로 유지하면서 쉘 판이 못 하던 것을 전부 해결하기 때문입니다.
가장 내세우는 기능은 자동 HTTPS 입니다. 진짜 도메인 이름만 주면 인증서를 받아 오고, 설치하고, 만료 전에 갱신하고, http 손님을 https 로 넘겨 줍니다. 다른 서버에서는 몇 달마다 되풀이하는 잡일입니다.
설치하기
# macOS
brew install caddy
# Ubuntu / Debian
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
sudo apt update && sudo apt install caddy
# Docker alone is enough
docker run -p 8080:80 -v "$PWD":/usr/share/caddy caddy명령 한 줄로 폴더 내놓기
설정 파일이 필요 없습니다. index.html 이 있는 폴더로 가서 한 줄만 치면 됩니다 — 같은 네트워크의 누군가에게 페이지를 보여 주는 가장 빠른 방법입니다.
# serve this folder's index.html on port 8080
caddy file-server --listen :8080
# add a directory listing for folders without an index.html
caddy file-server --listen :8080 --browseCaddyfile
오래 둘 것이라면 Caddyfile 을 씁니다. 그냥 글자 파일이고, 사이트 하나가 네 줄로 끝납니다.
example.com {
root * /var/www/html
file_server
encode gzip zstd
}example.com {사이트 주소입니다. 여기에 진짜 도메인을 적으면 Caddy 가 HTTPS 인증서를 알아서 마련하고, localhost 를 적으면 로컬 작업용으로 평범한 http 를 내놓습니다.root * /var/www/html파일이 디스크의 어디에 있는지입니다. 별표는 이 사이트의 모든 경로에 적용한다는 뜻입니다.file_server그 파일들을 실제로 내놓으라는 줄입니다. 이 줄이 없으면 Caddy 는 요청에 아무것도 주지 않습니다.encode gzip zstd보내기 전에 응답을 압축합니다. 한 낱말인데, 느린 회선에서 페이지가 눈에 띄게 빨리 도착합니다.
Caddy 는 Let's Encrypt 에 무료 인증서를 요청합니다. 그러려면 도메인의 DNS 가 내 기계를 가리켜야 하고 80 · 443 포트에 바깥에서 닿을 수 있어야 합니다. 앞에 있는 다른 프록시가 그 포트를 대신 받고 있으면 확인이 실패해 인증서가 끝내 오지 않습니다 — 처음 Caddy 를 세울 때 자물쇠가 초록색이 되지 않는 가장 흔한 이유입니다.
리버스 프록시 — 요청을 넘겨주기
대부분의 사이트는 디스크의 파일이 아니라 어느 로컬 포트에서 귀를 기울이는 응용 프로그램입니다. 리버스 프록시는 바깥의 요청을 받아 그 프로그램에 건네고 답을 되돌려 주는 서버입니다. 한 줄이면 됩니다.
getpes.com {
reverse_proxy pes-web:3000
}
dl.getpes.com {
root * /srv/files
file_server browse
}기계 한 대가 여러 사이트를 품는 방법도 이것입니다. 443 포트는 한 프로그램만 가질 수 있으므로 Caddy 가 그것을 쥐고, 손님이 어느 도메인을 찾았는지 읽어 각각 다른 프로그램으로 넘깁니다.
알아야 할 명령들
| 명령 | 하는 일 |
|---|---|
| caddy run | 앞에서 실행하며 기록을 터미널에 뿌립니다. 설정을 아직 맞춰 가는 동안 원하는 것이 이쪽입니다. |
| caddy start | 뒤로 보내고 프롬프트를 돌려줍니다. 잠깐 보여 줄 때는 괜찮지만, 제대로 된 서버는 시스템 서비스로 띄웁니다. |
| caddy reload | 연결을 하나도 끊지 않고 바뀐 설정을 다시 읽습니다. Caddyfile 을 고쳤다고 재시작할 필요가 없습니다. |
| caddy fmt --overwrite | 파일 서식을 제자리에서 바로잡습니다. 들여쓰기가 보기보다 중요하고, 이 명령이 그 다툼을 끝냅니다. |
| caddy validate | 아무것도 띄우지 않고 설정에 잘못이 있는지 검사합니다. 돌고 있는 서버를 reload 하기 전에 먼저 돌립니다. |
PES 서버에서는 Caddy 하나가 80 · 443 포트를 쥐고 도메인 이름으로 길을 가릅니다. 본 사이트는 응용 프로그램 컨테이너로, 내려받기 서브도메인은 디스크에서 곧바로 내놓습니다. 새 사이트를 더하는 일은 위와 같은 파일 하나를 두고 reload 하는 것이 전부입니다 — 재시작도 없고, 포트를 두고 다투는 두 번째 프로그램도 없습니다.
스스로 표시하는 칸이에요. 여기서 채점하지는 않아요.