이 글에서 다루는 것
"nginx 소스 파헤치기" 연재의 여덟 번째 글입니다. 7편에서는 워커의 이벤트 루프가 epoll_wait로 깨어나 accept4로 연결을 받고, 그 연결의 읽기 이벤트에 걸린 핸들러 함수를 불러 주는 데까지 봤습니다. 이번 글은 그 핸들러 쪽, 곧 HTTP 모듈로 넘어옵니다. 소켓으로 들어온 바이트가 어떻게 GET /hello?x=1 HTTP/1.1 한 줄과 헤더 몇 줄로 나뉘어 ngx_http_request_t 라는 구조체가 되는지 따라갑니다.
2편의 지도에서 이 글은 "C. HTTP 요청 하나" 줄의 첫 상자, 요청 읽기를 확대합니다.
초록 상자가 이 글입니다. 요청을 다 읽고 나면 9편의 phase 엔진으로 넘깁니다.
답하려는 질문은 네 가지입니다.
- 연결을 받은 직후 nginx는 무엇을 만들고, 무엇을 일부러 만들지 않는가 — 왜 요청 구조체는 첫 바이트가 온 뒤에야 생기나
- 요청 줄과 헤더는 어떻게 나뉘나 — 데이터가 반쯤만 왔을 때 파서는 어떻게 이어서 읽나
client_header_buffer_size·large_client_header_buffers는 어디서 쓰이고, 넘으면 왜 414나 "400 Request Header Or Cookie Too Large"가 되나- 한 포트에
server {}블록이 여럿일 때Host헤더로 어느 블록을 고르나, 그리고 keep-alive 연결에서 다음 요청은 어떻게 읽나
| 항목 | 내용 |
|---|---|
| 대상 | nginx |
| 버전 | nginx 1.31.9 (master, 개발판) |
| 커밋 | 9a7439fd79 (2026-10-23) |
| 주로 읽는 파일 | src/http/ngx_http_request.c, src/http/ngx_http_parse.c, src/http/ngx_http_request.h |
글에 나오는 줄 번호는 모두 이 커밋 기준입니다. 같은 커밋의 소스를 받고 예제용 도커 이미지를 만드는 방법은 1편에 있습니다.
ngx_http_request.c는 4,200줄이 넘고 ngx_http_parse.c도 2,400줄이 넘습니다. 이 글은 HTTP/1.x 요청 하나를 읽는 큰 줄기만 골랐습니다. TLS·HTTP/2·요청 본문처럼 같은 파일에 있지만 뺀 것은 글 끝의 "이 글에서 뺀 것"에 이름과 위치만 적어 두었습니다.
먼저 알아 둘 말
이벤트 핸들러는 "이 연결에 읽을 것이 생기면 불러 줄 함수"입니다. nginx의 연결 구조체 ngx_connection_t에는 읽기 이벤트 c->read와 쓰기 이벤트 c->write가 있고, 각 이벤트의 handler 필드가 함수 포인터입니다. 이벤트 루프는 소켓이 읽을 수 있게 되면 rev->handler(rev)를 부를 뿐, 그 함수가 무엇인지는 모릅니다(7편). 이 글의 줄거리는 HTTP 모듈이 이 함수 포인터를 단계마다 바꿔 끼우는 이야기입니다.
ngx_http_connection_t와 ngx_http_request_t 는 수명이 다른 두 구조체입니다. 앞의 것은 TCP 연결 하나에 하나, 연결이 살아 있는 동안 남습니다(어느 주소:포트로 들어왔는지, 큰 헤더 버퍼 목록). 뒤의 것은 HTTP 요청 하나에 하나, keep-alive 연결이면 요청마다 새로 만들고 지웁니다.
메모리 풀(ngx_pool_t)은 작은 할당을 모아 두었다가 한 번에 해제하는 nginx의 할당기입니다. 연결마다 연결 풀, 요청마다 요청 풀이 있어서 요청이 끝나면 요청 풀만 통째로 지웁니다. 풀이 한 번에 잘라 줄 수 있는 크기보다 큰 할당은 따로 malloc해 "large" 목록에 달아 두는데, 이 글에서 그것이 중요한 역할을 합니다. 풀의 구조는 3편에서 봤습니다.
상태 기계(state machine)는 "지금 어느 단계에 있는가"를 변수 하나(state)에 담고, 글자 하나를 읽을 때마다 switch (state)로 다음 단계를 정하는 코드 모양입니다. nginx의 HTTP 파서는 전부 이 모양이고, state를 구조체에 저장해 두기 때문에 데이터가 끊긴 자리에서 나중에 이어서 읽을 수 있습니다.
ngx_str_t 는 len과 data 두 필드로 된 nginx의 문자열입니다. 끝이 NUL('\0')이 아니어도 되므로, 버퍼 한가운데를 복사 없이 가리킬 수 있습니다(3편).
keep-alive와 파이프라인. keep-alive는 응답을 보낸 뒤 연결을 닫지 않고 다음 요청을 기다리는 것, 파이프라인은 클라이언트가 응답을 기다리지 않고 요청 여러 개를 한꺼번에 보내는 것입니다. 둘 다 "연결 하나에 요청 여러 개"이고, nginx에서는 마지막 7절에서 같은 함수(ngx_http_set_keepalive())가 다룹니다.
1. 연결이 HTTP를 만나는 곳 — ngx_http_init_connection()
nginx.conf의 listen 8080; 한 줄은 설정을 읽을 때 리스닝 소켓 하나가 되고, 그 소켓에 "연결이 들어오면 부를 함수"로 ngx_http_init_connection()이 등록됩니다.
ls->handler = ngx_http_init_connection;
7편에서 본 ngx_event_accept()는 accept4()로 새 소켓을 받고 연결 구조체와 연결 풀을 준비한 뒤 이 함수를 부릅니다(src/event/ngx_event_accept.c 312행의 ls->handler(c);). 이벤트 코어는 HTTP를 모르고, HTTP 모듈은 accept를 모릅니다. 둘은 이 함수 포인터 하나로 이어져 있습니다. stream {} 블록의 listen이면 같은 자리에 ngx_stream_init_connection()이 들어갑니다(12편).
ngx_http_init_connection()이 하는 일은 세 가지입니다.
- 연결 풀에서
ngx_http_connection_t를 하나 받아c->data에 매답니다(226–232행). - 이 연결이 들어온 주소:포트를 보고
hc->addr_conf를 고르고, 그 주소의 기본 서버(default_server) 설정을hc->conf_ctx로 잡아 둡니다(236–309행).Host헤더를 보기 전이므로 일단 기본 서버입니다. 진짜 server 블록은 6절에서 고릅니다. - 읽기 핸들러를 걸고 기다립니다. 함수의 뒷부분입니다.
rev = c->read;
rev->handler = ngx_http_wait_request_handler;
c->write->handler = ngx_http_empty_handler;
/* ... HTTP/3 이면 다른 길로, ssl 이면 rev->handler = ngx_http_ssl_handshake ... */
if (rev->ready) {
/* the deferred accept() */
/* ... */
rev->handler(rev);
return;
}
cscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_core_module);
ngx_add_timer(rev, cscf->client_header_timeout);
ngx_reusable_connection(c, 1);
if (ngx_handle_read_event(rev, 0) != NGX_OK) {
ngx_http_close_connection(c);
return;
}
}
src/http/ngx_http_request.c 328–373행
- 읽기 핸들러는
ngx_http_wait_request_handler입니다.listen 443 ssl이면 그 앞에ngx_http_ssl_handshake가 먼저 끼어, 핸드셰이크가 끝난 뒤 같은 대기 함수로 넘어갑니다(ALPN으로h2를 고른 연결은 대기 함수 대신 HTTP/2 쪽ngx_http_v2_init()으로 갑니다). TLS는 "요청보다 앞에 끼는 한 단계"로만 보고 넘어갑니다. client_header_timeout(기본 60초) 타이머를 겁니다. 연결만 맺고 아무것도 보내지 않는 클라이언트는 60초 뒤 끊깁니다. 기본값은ngx_http_core_merge_srv_conf()의ngx_conf_merge_msec_value(conf->client_header_timeout, prev->client_header_timeout, 60000)입니다(src/http/ngx_http_core_module.c 3626–3627행).ngx_reusable_connection(c, 1)은 이 연결을 "아직 아무 일도 안 하는, 급하면 닫아도 되는 연결" 목록에 넣습니다.worker_connections가 바닥나면 nginx는 이 목록의 연결부터 닫아 자리를 만듭니다(src/core/ngx_connection.c 1374행부터, 7편).
이 연결이 앞으로 거칠 읽기 핸들러를 한 장에 모으면 이렇습니다. 이 글 전체의 지도입니다.
상자 하나가 그 순간 c->read->handler에 들어 있는 함수입니다. 1~7절이 이 상자들을 위에서 아래로 따라갑니다.
헷갈리기 쉬운 곳: c->data의 정체가 바뀝니다
ngx_connection_t의 data는 void *라 무엇이든 담을 수 있습니다. HTTP 모듈은 이 칸에 처음에는 ngx_http_connection_t(232행 c->data = hc;)를, 요청이 생기면 ngx_http_request_t(527행 c->data = ngx_http_create_request(c);)를, keep-alive로 요청이 끝나면 다시 ngx_http_connection_t(3424행 c->data = hc;)를 넣습니다. 그래서 ngx_http_wait_request_handler()는 hc = c->data;(405행)로, ngx_http_process_request_line()은 r = c->data;(1125행)로 읽습니다. 같은 칸을 지금 어느 핸들러가 읽느냐에 따라 다른 타입으로 해석하는 것이고, 핸들러를 바꾸는 순간과 c->data를 바꾸는 순간이 늘 짝을 이룹니다. 요청 쪽에서 연결 구조체가 필요하면 r->http_connection으로 찾아갑니다.
2. 데이터가 올 때까지 요청을 만들지 않는다 — ngx_http_wait_request_handler()
첫 바이트가 도착하면 이벤트 루프가 ngx_http_wait_request_handler()를 부릅니다. 이 함수가 비로소 헤더를 담을 버퍼를 만듭니다.
hc = c->data;
cscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_core_module);
size = cscf->client_header_buffer_size;
b = c->buffer;
if (b == NULL) {
b = ngx_create_temp_buf(c->pool, size);
/* ... */
c->buffer = b;
} else if (b->start == NULL) {
/* ... */
}
size = b->end - b->last;
n = c->recv(c, b->last, size);
if (n == NGX_AGAIN) {
/* ... 타이머와 읽기 이벤트를 다시 건다 ... */
if (b->pos == b->last) {
/*
* We are trying to not hold c->buffer's memory for an
* idle connection.
*/
if (ngx_pfree(c->pool, b->start) == NGX_OK) {
b->start = NULL;
}
}
return;
}
src/http/ngx_http_request.c 405–463행
버퍼 크기는 client_header_buffer_size, 기본 1024바이트입니다(ngx_http_core_module.c 3628–3629행). c->recv()는 논블로킹 소켓에서 recv()를 부르는 함수 포인터이고, 읽을 것이 없으면 기다리지 않고 NGX_AGAIN을 돌려줍니다.
NGX_AGAIN 갈래의 주석이 이 함수의 설계를 말해 줍니다. "놀고 있는 연결에 버퍼 메모리를 쥐여 두지 않으려 한다." 1024바이트 버퍼는 연결 풀(기본 connection_pool_size는 64 * sizeof(void *), 64비트에서 512바이트, 3622–3623행)이 한 번에 잘라 줄 수 있는 크기보다 크므로 ngx_palloc()이 따로 malloc해 large 목록에 답니다(src/core/ngx_palloc.c 123–132행). 그리고 ngx_pfree()는 large 할당만 돌려줄 수 있습니다(278–294행). 버퍼가 풀의 일반 블록이 아니라 large라서, 연결을 닫지 않고도 버퍼만 반납할 수 있는 것입니다.
데이터를 읽는 데 성공하면 그때 요청 구조체를 만들고, 읽기 핸들러를 요청 줄 함수로 바꿔 바로 부릅니다.
c->log->action = "reading client request line";
ngx_reusable_connection(c, 0);
c->data = ngx_http_create_request(c);
if (c->data == NULL) {
ngx_http_close_connection(c);
return;
}
rev->handler = ngx_http_process_request_line;
ngx_http_process_request_line(rev);
}
src/http/ngx_http_request.c 523–535행
연결을 맺은 순간과 첫 바이트가 온 순간 사이에는 작은 구조체 하나만 있습니다.
왜 이렇게 미룰까요. nginx 워커 하나는 수천~수만 개의 연결을 동시에 들고 있습니다. 그중 상당수는 연결만 맺어 두고 아직 아무것도 보내지 않았거나, keep-alive로 다음 요청을 기다리는 중입니다. 그런 연결마다 1KB 버퍼와 4KB 요청 풀을 쥐여 주면 연결 1만 개에 50MB가 그냥 묶입니다. 그래서 "할 일이 생긴 연결에만 메모리를 준다" 는 것이 이 함수의 원칙입니다. 7절의 keep-alive에서도 같은 원칙이 다시 나옵니다.
요청 구조체가 처음 받는 것들
ngx_http_create_request()는 ngx_http_alloc_request()로 구조체를 만든 뒤 c->requests++로 이 연결의 요청 수를 셉니다(550행). 만드는 쪽의 핵심입니다.
pool = ngx_create_pool(cscf->request_pool_size, c->log);
/* ... */
r = ngx_pcalloc(pool, sizeof(ngx_http_request_t));
/* ... */
r->pool = pool;
r->http_connection = hc;
r->signature = NGX_HTTP_MODULE;
r->connection = c;
r->main_conf = hc->conf_ctx->main_conf;
r->srv_conf = hc->conf_ctx->srv_conf;
r->loc_conf = hc->conf_ctx->loc_conf;
r->read_event_handler = ngx_http_block_reading;
r->header_in = hc->busy ? hc->busy->buf : c->buffer;
/* ... headers_out 목록, 모듈별 ctx 배열, 변수 값 배열을 만든다 ... */
r->main = r;
r->count = 1;
src/http/ngx_http_request.c 584–647행
- 요청 풀은
request_pool_size, 기본 4096바이트입니다(3624–3625행). 구조체 자신도 이 풀 안에 있으므로, 요청이 끝날 때 풀을 지우면 구조체까지 한꺼번에 사라집니다. main_conf/srv_conf/loc_conf는 "모듈 번호로 찾는 설정 구조체 포인터 배열" 세 개입니다(5편). 지금은 1절에서 잡아 둔 기본 서버의 것을 그대로 받습니다.ngx_http_get_module_srv_conf(r, ngx_http_core_module)같은 매크로가 이 배열을 읽으므로, 배열 포인터를 바꾸면 그 요청이 보는 설정 전체가 바뀝니다. 6절이 그 일을 합니다.header_in은 요청 줄과 헤더를 읽을 버퍼입니다. 보통은 2절에서 만든c->buffer를 그대로 씁니다. 복사하지 않습니다.signature는 디버깅용 표시이고, 값NGX_HTTP_MODULE은"HTTP"네 글자를 정수로 읽은 것입니다(2편의 모듈 종류 값과 같은 방식).
ngx_http_request_s(ngx_http_request.h 386–614행)는 정의만 230줄 가까이 되는 큰 구조체입니다. 맨 앞에 connection, 모듈별 ctx, 위의 설정 배열 세 개, 그리고 read_event_handler/write_event_handler가 옵니다(389–397행). 이 둘은 연결의 rev->handler와는 다른, 요청 단계의 핸들러입니다. 요청을 다 읽고 나면 연결의 핸들러는 둘 다 ngx_http_request_handler로 고정되고, 이 함수가 이벤트 종류에 따라 요청의 두 핸들러를 대신 불러 줍니다(6절 끝). 그 뒤로 pool, header_in, headers_in(407–411행), 요청 줄의 결과인 method·http_version·request_line·uri·args·exten(419–425행)이 이어지고, 구조체 끝에는 파서가 쓰는 칸들 — state, header_hash, uri_start, uri_end, args_start 같은 포인터 — 이 모여 있습니다(584–613행). 3절에서 씁니다.
예제 1: 연결만 맺고 2초 뒤에 보내기
정말 데이터가 와야 요청이 생기는지 디버그 로그로 확인했습니다. 이 예제는 기준 커밋(9a7439fd79)을 그대로 빌드한 nginx 1.31.9 로 도커 컨테이너(Debian bookworm, --with-debug)에서 실행했습니다. 설정은 location / { return 200 "uri=$uri args=$args\n"; } 하나에 error_log ... debug;이고, ex/u08-read-request/run.sh의 핵심은 nc로 연결한 뒤 2초 쉬고 요청을 보내는 것입니다.
( sleep 2; printf 'GET /hello?x=1 HTTP/1.1\r\nHost: a.test\r\nUser-Agent: nc\r\nConnection: close\r\n\r\n' ) | nc -q1 127.0.0.1 8080
error.log에서 관련 줄만 골랐습니다(날짜는 지웠습니다).
02:54:26 [debug] 11#11: posix_memalign: 0000558749165BC0:512 @16
02:54:26 [debug] 11#11: *2 accept: 127.0.0.1:37958 fd:3
02:54:26 [debug] 11#11: *2 event timer add: 3: 60000:2346000
02:54:26 [debug] 11#11: *2 reusable connection: 1
02:54:28 [debug] 11#11: *2 http wait request handler
02:54:28 [debug] 11#11: *2 malloc: 0000558749168410:1024
02:54:28 [debug] 11#11: *2 recv: eof:0, avail:-1
02:54:28 [debug] 11#11: *2 recv: fd:3 76 of 1024
02:54:28 [debug] 11#11: *2 reusable connection: 0
02:54:28 [debug] 11#11: *2 posix_memalign: 0000558749162700:4096 @16
02:54:28 [debug] 11#11: *2 http process request line
02:54:28 [debug] 11#11: *2 http request line: "GET /hello?x=1 HTTP/1.1"
02:54:28 [debug] 11#11: *2 http uri: "/hello"
02:54:28 [debug] 11#11: *2 http args: "x=1"
02:54:28 [debug] 11#11: *2 http exten: ""
02:54:28 [debug] 11#11: *2 posix_memalign: 000055874918F320:4096 @16
02:54:28 [debug] 11#11: *2 http process request header line
02:54:28 [debug] 11#11: *2 http header: "Host: a.test"
02:54:28 [debug] 11#11: *2 http header: "User-Agent: nc"
02:54:28 [debug] 11#11: *2 http header: "Connection: close"
02:54:28 [debug] 11#11: *2 http header done
02:54:28 [debug] 11#11: *2 http finalize request: 0, "/hello?x=1" a:1, c:1
02:54:28 [debug] 11#11: *2 free: 0000558749162700, unused: 8
02:54:28 [debug] 11#11: *2 free: 000055874918F320, unused: 2640
02:54:28 [debug] 11#11: *2 reusable connection: 0
02:54:28 [debug] 11#11: *2 free: 0000558749168410
02:54:28 [debug] 11#11: *2 free: 0000558749165BC0, unused: 128
시각·주소·연결 번호(*2)는 실행마다 다릅니다. recv: eof: 값도 실행마다 달라서, 다시 돌렸을 때는 eof:1이 찍히고 http finalize request 뒤에 lingering close 줄(reusable connection: 1, event timer add: 3: 5000:..., recv: fd:3 0 of 4096)이 더 나왔습니다. 클라이언트의 FIN이 언제 도착했는지에 따라 7절의 lingering close로 가느냐가 갈린 것으로 보입니다. 읽을 것은 이렇습니다.
- 02:54:26 — 연결 직후: 512바이트 연결 풀(
posix_memalign ...:512), accept, 60000ms 타이머, reusable 표시. 그게 전부입니다. 아직 1024 버퍼도 4096 요청 풀도 없습니다. - 02:54:28 — 2초 뒤 데이터 도착: 그제야
http wait request handler(392행)가 찍히고, 1024바이트 버퍼가malloc(large 할당이라malloc으로 찍힙니다), 76바이트를 읽은 뒤 4096바이트 요청 풀이 생깁니다. - 요청 풀의 두 번째 4096 블록은 요청 줄을 다 읽고 헤더 목록을 준비하는 자리(1219행
ngx_list_init(&r->headers_in.headers, ...))에서 생겼습니다. 첫 블록을 구조체와 출력 헤더 목록 따위가 이미 거의 다 써서, 풀이 블록 하나를 더 이은 것입니다(3편의 블록 사슬). - 끝: 요청 풀의 두 블록이 지워지고(
unused: 8,unused: 2640), 연결을 닫으면서 1024 버퍼(free: ...8410)와 연결 풀이 지워집니다.
각 줄을 찍는 소스는 http process request line 1127–1128행, http request line 1159–1160행, http uri/args/exten 1387–1394행, http header 1548–1550행입니다.
예제 2: gdb로 본 호출 시각과 스택
같은 일을 gdb로도 봤습니다(ex/u08-gdb-stack). gdb 아래에서 쉽게 보려고 master_process off; daemon off;로 프로세스 하나에서 돌렸습니다. 세 함수에 중단점을 걸고, 걸릴 때마다 시각을 찍은 뒤 계속 가게 했습니다. 클라이언트는 연결한 뒤 2초 쉬고 요청을 보냅니다.
[time] ngx_http_init_connection 03:24:59.514
[time] ngx_http_create_request 03:25:01.394
[time] ngx_http_parse_request_line 03:25:01.399
#0 ngx_http_parse_request_line at src/http/ngx_http_parse.c:141
#1 0x00005555555fadf4 in ngx_http_process_request_line at src/http/ngx_http_request.c:1149
#2 0x00005555555f9765 in ngx_http_wait_request_handler at src/http/ngx_http_request.c:534
#3 0x00005555555cea51 in ngx_epoll_process_events at src/event/modules/ngx_epoll_module.c:901
#4 0x00005555555bb0b9 in ngx_process_events_and_timers at src/event/ngx_event.c:248
#5 0x00005555555ca62b in ngx_single_process_cycle at src/os/unix/ngx_process_cycle.c:322
#6 0x000055555557f1b2 in main at src/core/nginx.c:400
(인자 목록은 줄이 길어 지웠습니다. 주소와 시각은 실행마다 다릅니다.) init_connection과 create_request 사이가 약 2초(클라이언트가 쉰 시간)입니다. 스택은 아래에서 위로 읽으면 main → 프로세스 하나짜리 사이클(워커로 돌리면 이 자리가 ngx_worker_process_cycle입니다, 6편) → 7편의 이벤트 루프 → epoll_module.c 901행의 rev->handler(rev); → 대기 핸들러 → 534행에서 곧바로 부른 요청 줄 함수 → 파서입니다. 첫 요청 줄은 이벤트 루프를 한 바퀴 더 돌지 않고 같은 호출 안에서 읽힙니다.
막혔던 곳. 처음에는 클라이언트를 nginx 기동 2초 뒤에 연결하게 했는데, 두 시각의 차이가 2초가 아니라 0.5초로 나왔습니다. gdb 아래의 nginx가 기동을 마치기 전에 연결이 커널의 대기열(backlog)에 먼저 들어가 있었고, accept가 늦게 일어나 init_connection 시각이 밀린 것으로 보입니다. 연결을 5초 뒤로 미루자 위처럼 2초가 나왔습니다.
3. 요청 줄 — 상태 기계 ngx_http_parse_request_line()
ngx_http_process_request_line()은 "읽기 → 파싱"을 되풀이하는 루프입니다. 줄인 뼈대입니다.
rc = NGX_AGAIN;
for ( ;; ) {
if (rc == NGX_AGAIN) {
n = ngx_http_read_request_header(r);
if (n == NGX_AGAIN || n == NGX_ERROR) {
break;
}
}
rc = ngx_http_parse_request_line(r, r->header_in);
if (rc == NGX_OK) {
/* ... 요청 줄을 r 의 필드로 옮기고 URI 를 정리한다 ... */
rev->handler = ngx_http_process_request_headers;
ngx_http_process_request_headers(rev);
break;
}
if (rc != NGX_AGAIN) {
/* there was error while a request line parsing */
ngx_log_error(NGX_LOG_INFO, c->log, 0,
ngx_http_client_errors[rc - NGX_HTTP_CLIENT_ERROR]);
if (rc == NGX_HTTP_PARSE_INVALID_VERSION) {
ngx_http_finalize_request(r, NGX_HTTP_VERSION_NOT_SUPPORTED);
} else {
ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
}
break;
}
/* NGX_AGAIN: a request line parsing is still incomplete */
if (r->header_in->pos == r->header_in->end) {
rv = ngx_http_alloc_large_header_buffer(r, 1);
/* ... 실패하면 500, 더 늘릴 수 없으면 414 ... */
}
}
src/http/ngx_http_request.c 1137–1273행
파서가 돌려주는 값은 셋 중 하나입니다. NGX_OK(줄 끝까지 읽었다), NGX_AGAIN(버퍼를 다 봤는데 줄이 안 끝났다), 오류 코드(NGX_HTTP_PARSE_INVALID_METHOD 등). 오류 코드는 10부터 시작하는 번호라서 rc - NGX_HTTP_CLIENT_ERROR가 곧 ngx_http_client_errors[] 배열의 칸 번호가 되고, 거기서 "client sent invalid method" 같은 로그 문장을 꺼냅니다(65–78행).
NGX_AGAIN이면 루프 맨 위의 ngx_http_read_request_header()로 돌아가는데, 이 함수는 소켓을 무작정 읽지 않습니다. 버퍼에 아직 파서가 안 본 바이트가 있으면 그것부터 쓰고, 소켓이 읽을 수 있다고 표시된(rev->ready) 경우에만 recv()합니다(1610–1621행). 읽을 것이 없으면 NGX_AGAIN을 돌려주고, 바깥 루프도 break로 빠져나와 이벤트 루프로 돌아갑니다. 다음에 바이트가 오면 이벤트 루프가 rev->handler, 곧 이 ngx_http_process_request_line()을 다시 부릅니다. 그때 파서는 처음부터가 아니라 멈춘 상태에서 이어 갑니다.
파서의 뼈대: state를 구조체에 둔다
state = r->state;
for (p = b->pos; p < b->last; p++) {
ch = *p;
switch (state) {
/* HTTP methods: GET, HEAD, POST */
case sw_start:
r->request_start = p;
if (ch == CR || ch == LF) {
break;
}
if ((ch < 'A' || ch > 'Z') && ch != '_' && ch != '-') {
return NGX_HTTP_PARSE_INVALID_METHOD;
}
state = sw_method;
break;
case sw_method:
if (ch == ' ') {
r->method_end = p - 1;
m = r->request_start;
state = sw_spaces_before_uri;
switch (p - m) {
case 3:
if (ngx_str3_cmp(m, 'G', 'E', 'T', ' ')) {
r->method = NGX_HTTP_GET;
break;
}
/* ... */
src/http/ngx_http_parse.c 141–175행
함수 시작에서 state = r->state;로 저장된 단계를 꺼내고, 버퍼가 바닥나면 끝에서 다시 저장합니다.
}
}
b->pos = p;
r->state = state;
return NGX_AGAIN;
done:
b->pos = p + 1;
/* ... */
r->http_version = r->http_major * 1000 + r->http_minor;
r->state = sw_start;
src/http/ngx_http_parse.c 843–860행
b->pos도 "여기까지 봤다"로 옮겨 두므로, 다음 호출은 새로 들어온 바이트부터 봅니다. 줄을 다 읽으면 state를 sw_start로 되돌립니다. 이 r->state 한 칸이 헤더 파서와도 공유되어, 헤더 줄 하나가 끝날 때마다 다시 0이 됩니다. 5절에서 이 "0이냐 아니냐"가 중요해집니다.
enum은 27개의 상태(sw_start부터 sw_almost_done까지, 111–139행)로 되어 있습니다. 평범한 GET /path HTTP/1.1이 지나는 길만 그리면 이렇습니다.
상자 하나가 case 하나입니다(여러 개를 묶은 상자는 "…"로 표시했습니다). 화살표 옆이 그 단계에서 다음으로 넘어가게 하는 글자입니다.
몇 가지 눈여겨볼 기법이 있습니다.
-
메서드는 길이로 먼저 나눕니다. 공백을 만나면
p - m(메서드 길이)으로switch해, 3글자면 GET/PUT, 4글자면 POST/HEAD/COPY… 순으로만 비교합니다. 비교도 한 글자씩이 아닙니다. x86처럼 리틀 엔디언이고 정렬되지 않은 읽기를 허용하는 CPU에서는ngx_str3_cmp가 네 바이트를uint32_t하나로 읽어 한 번에 비교합니다.#define ngx_str3_cmp(m, c0, c1, c2, c3) \ *(uint32_t *) m == ((c3 << 24) | (c2 << 16) | (c1 << 8) | c0)src/http/ngx_http_parse.c 42–43행 (40행의
#if (NGX_HAVE_LITTLE_ENDIAN && NGX_HAVE_NONALIGNED)안. 아니면 72행 아래의 한 글자씩 비교로 바뀝니다) -
URI 글자는 비트맵으로 거릅니다.
usual[]은 256글자마다 비트 하나씩, 32비트 정수 8개짜리 표입니다(17–37행).usual[ch >> 5] & (1U << (ch & 0x1f))가 참이면 "특별할 것 없는 글자"로 보고sw_check_uri에서는 상태를 그대로 둡니다(585행./바로 뒤인sw_after_slash_in_uri에서는 525행에서sw_check_uri로 넘어갑니다)./,.,%,?,#같은 글자만switch로 내려가 따로 처리됩니다. -
복잡한 URI는 표시만 해 둡니다.
/바로 뒤의.나/,%를 보면r->complex_uri나r->quoted_uri비트를 켜고(544–555행) 넘어갑니다. 정리는 줄을 다 읽은 뒤 따로 합니다(아래). -
HTTP 메이저 버전은 1까지만 받습니다.
HTTP/2.0이라고 적힌 요청 줄은http_major > 1에서 바로NGX_HTTP_PARSE_INVALID_VERSION이 되어(757–758행) 505로 끝납니다. HTTP/2는 요청 줄로 시작하지 않습니다. 평문 HTTP/2는 연결 첫머리의 프리페이스(preface)로 알아보고 2절 함수에서 따로 넘깁니다(500–521행).
예제 3: 요청 줄을 두 조각으로 보내기
상태를 들고 다니는 것을 눈으로 보려고, 요청 줄을 HT와 TP/1.1 사이에서 잘라 1초 간격으로 보냈습니다(ex/u08-read-request 둘째 부분).
( printf 'GET /a/./b/../c.html?q=%%41 HT'; sleep 1; printf 'TP/1.1\r\nHost: a.test\r\nConnection: close\r\n\r\n' ) | nc -q1 127.0.0.1 8080
02:54:29 [debug] 11#11: *3 recv: eof:0, avail:-1
02:54:29 [debug] 11#11: *3 recv: fd:3 29 of 1024
02:54:29 [debug] 11#11: *3 http process request line
02:54:30 [debug] 11#11: *3 http process request line
02:54:30 [debug] 11#11: *3 recv: eof:0, avail:-1
02:54:30 [debug] 11#11: *3 recv: fd:3 43 of 995
02:54:30 [debug] 11#11: *3 http request line: "GET /a/./b/../c.html?q=%41 HTTP/1.1"
02:54:30 [debug] 11#11: *3 http uri: "/a/c.html"
02:54:30 [debug] 11#11: *3 http args: "q=%41"
02:54:30 [debug] 11#11: *3 http exten: "html"
- 첫 29바이트(
GET /a/./b/../c.html?q=%41 HT)를 읽고http process request line이 한 번 찍힌 뒤, 1초 동안 아무 일도 없습니다. 파서는sw_http_HT상태를r->state에 적어 두고 이벤트 루프로 돌아갔습니다. - 1초 뒤 같은 함수가 다시 불려(두 번째
http process request line), 버퍼의 남은 자리 995바이트(1024 − 29)에 이어서 43바이트를 읽고 줄을 끝냈습니다. 같은 버퍼 뒤에 이어 붙였다는 뜻입니다. r->uri는/a/c.html,r->args는q=%41그대로입니다../와b/../는 지워졌고, 쿼리 문자열의%41은 풀지 않았습니다.
URI 정리 — 복사는 필요할 때만
요청 줄을 다 읽으면 ngx_http_process_request_uri()가 r->uri를 채웁니다.
if (r->complex_uri || r->quoted_uri || r->empty_path_in_uri) {
if (r->empty_path_in_uri) {
r->uri.len++;
}
r->uri.data = ngx_pnalloc(r->pool, r->uri.len);
if (r->uri.data == NULL) {
ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
return NGX_ERROR;
}
cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
if (ngx_http_parse_complex_uri(r, cscf->merge_slashes) != NGX_OK) {
r->uri.len = 0;
ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
"client sent invalid request");
ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
return NGX_ERROR;
}
} else {
r->uri.data = r->uri_start;
}
src/http/ngx_http_request.c 1290–1315행
평범한 URI라면 r->uri.data = r->uri_start; 한 줄, 버퍼 안을 그대로 가리킵니다. 복잡한 URI일 때만 요청 풀에 같은 길이의 자리를 잡고, 두 번째 상태 기계 ngx_http_parse_complex_uri()가 /./·/../를 접고 %XX를 풀고, merge_slashes(기본 on, 3646행)면 //를 하나로 합쳐 사본에 씁니다. 원본(r->unparsed_uri)은 버퍼에 그대로 남아 프록시가 원래 URI를 넘길 때 씁니다.
노란 상자만 실제 메모리입니다. 나머지는 그 메모리 안의 위치를 적은 포인터와 ngx_str_t입니다.
이 "가리키기만 하는" 설계는 Tomcat 6편의 MessageBytes와 같은 발상입니다. Tomcat도 바이트 버퍼의 위치와 길이만 들고 있다가 toString()할 때 처음 String을 만들었습니다. C인 nginx는 문자열 객체를 만들 필요조차 없이 ngx_str_t가 곧 최종 형태입니다.
헷갈리기 쉬운 곳: ..이 루트 위로 올라가면 400입니다
/a/../../etc/passwd처럼 ..이 루트보다 위로 가면, ngx_http_parse_complex_uri()가 앞쪽 /를 찾아 뒤로 가다가 사본의 시작(r->uri.data)을 넘어서고 그 자리에서 NGX_HTTP_PARSE_INVALID_REQUEST를 돌려줍니다(src/http/ngx_http_parse.c 1512–1519행). 그래서 이 요청은 location을 찾기도 전에, 요청 줄 단계에서 400으로 끝납니다(5절 예제의 3번). root 밖의 파일을 열 길이 파서에서 먼저 막히는 셈입니다. 로그 문장은 요청 줄 파서의 오류와 같은 "client sent invalid request"라서, 로그만 보고는 둘을 구별하기 어렵습니다.
4. 헤더 — 한 줄씩, 해시로 처리 함수 찾기
요청 줄이 끝나면 읽기 핸들러가 ngx_http_process_request_headers()로 바뀝니다. 구조는 요청 줄과 같은 "읽기 → 파싱" 루프이고, 파싱은 ngx_http_parse_header_line()이 헤더 한 줄씩 합니다. 한 줄이 끝날 때마다(NGX_OK) 이런 일을 합니다.
/* the host header could change the server configuration context */
cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
rc = ngx_http_parse_header_line(r, r->header_in,
cscf->underscores_in_headers);
if (rc == NGX_OK) {
r->request_length += r->header_in->pos - r->header_name_start;
if (r->invalid_header && cscf->ignore_invalid_headers) {
/* ... "client sent invalid header line" 로그만 남기고 건너뛴다 ... */
continue;
}
/* ... max_headers 검사 ... */
h = ngx_list_push(&r->headers_in.headers);
/* ... */
h->hash = r->header_hash;
h->key.len = r->header_name_end - r->header_name_start;
h->key.data = r->header_name_start;
h->key.data[h->key.len] = '\0';
h->value.len = r->header_end - r->header_start;
h->value.data = r->header_start;
h->value.data[h->value.len] = '\0';
/* ... 소문자 이름 h->lowcase_key 를 준비한다 ... */
hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash,
h->lowcase_key, h->key.len);
if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
break;
}
src/http/ngx_http_request.c 1480–1546행
- 헤더는 전부 목록에 들어갑니다.
r->headers_in.headers(ngx_list_t)에ngx_table_elt_t를 하나 붙이고,key·value는 버퍼 안을 가리킵니다. 그리고 이름과 값 바로 뒤의 버퍼 바이트(:, CR, 공백 등)를'\0'으로 덮어씁니다. 복사하는 대신 버퍼를 고쳐 써서, C 문자열 함수에 그대로 넘길 수 있게 된 것입니다. - 해시는 파서가 읽으면서 이미 계산했습니다. 헤더 이름의 글자를 하나 읽을 때마다 소문자로 바꾸고 해시를 갱신하므로(
hash = ngx_hash(hash, c);, ngx_http_parse.c 963–971행), 줄이 끝난 순간r->header_hash에 답이 있습니다. 이름을 다시 훑지 않습니다. - 알려진 헤더면 처리 함수를 부릅니다.
headers_in_hash는 설정을 읽을 때ngx_http_headers_in[]표로 만든 해시입니다(src/http/ngx_http.c 427–446행). 표는 이렇게 생겼습니다.
ngx_http_header_t ngx_http_headers_in[] = {
{ ngx_string("Host"), offsetof(ngx_http_headers_in_t, host),
ngx_http_process_host },
{ ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),
ngx_http_process_connection },
src/http/ngx_http_request.c 81–86행
한 칸이 { 이름, ngx_http_headers_in_t 안의 오프셋, 처리 함수 }입니다(ngx_http_request.h 173–177행). ngx_http_headers_in_t에는 host, connection, content_length, user_agent처럼 자주 쓰는 헤더마다 ngx_table_elt_t * 칸이 있어서(186–260행), 다른 모듈은 목록을 뒤지지 않고 r->headers_in.host로 바로 꺼냅니다.
처리 함수는 대부분 같은 몇 개를 돌려 씁니다. 가장 흔한 ngx_http_process_header_line()은 오프셋으로 칸을 찾아 매달기만 합니다.
ph = (ngx_table_elt_t **) ((char *) &r->headers_in + offset);
while (*ph) { ph = &(*ph)->next; }
*ph = h;
h->next = NULL;
src/http/ngx_http_request.c 1830–1835행
(char *) &r->headers_in + offset은 "구조체 시작 주소에서 오프셋만큼 떨어진 칸"입니다. 함수 하나가 오프셋만 바꿔 Referer, Content-Type, Cookie 등 여러 헤더를 맡습니다. 같은 이름이 또 오면 next로 잇습니다. 반면 Content-Length·Transfer-Encoding·Authorization처럼 두 번 오면 안 되는 헤더는 ngx_http_process_unique_header_line()이 맡아, 두 번째가 오면 "client sent duplicate header line"을 남기고 400으로 끝냅니다(1841–1863행). Host는 전용 함수 ngx_http_process_host()가 맡는데, 6절에서 봅니다.
헤더는 모두 목록에 들어가고, 표에 있는 헤더만 처리 함수를 거칩니다. 세 예는 ngx_http_headers_in[] 표(ngx_http_request.c 81행부터)에서 골랐습니다.
빈 줄 — 헤더가 끝났을 때의 검사
빈 줄(CR LF)을 만나면 파서가 NGX_HTTP_PARSE_HEADER_DONE을 돌려주고, ngx_http_process_request_header()가 헤더를 모아 놓고 따져 봅니다. HTTP/1.1인데 Host가 없으면 "client sent HTTP/1.1 request without "Host" header"로 400입니다(src/http/ngx_http_request.c 2035–2040행). 이어서 Content-Length 숫자 변환, Content-Length와 Transfer-Encoding: chunked가 함께 오면 400, 모르는 Transfer-Encoding이면 501, TRACE 메서드면 405 같은 검사가 2122행까지 이어집니다. 요청 밀반입(request smuggling)으로 악용될 수 있는 모호한 조합을 여기서 끊어 냅니다.
헷갈리기 쉬운 곳: 밑줄이 든 헤더는 조용히 사라집니다
X_Custom_Id: 1처럼 이름에 _가 든 헤더는 underscores_in_headers(기본 off, 3648–3649행)가 꺼져 있으면 파서가 r->invalid_header = 1로 표시합니다(ngx_http_parse.c 973–983행). 그리고 ignore_invalid_headers(기본 on, 3643–3644행)가 켜져 있으면 위 코드의 continue로 목록에 넣지도 않고 건너뜁니다. 오류도 아니고 응답도 정상이라, 백엔드가 "헤더가 안 왔다"고 할 때 찾기 어려운 원인입니다. 로그는 info 수준의 "client sent invalid header line" 한 줄뿐입니다. 헤더 줄 개수 한도 max_headers(기본 1000, 3641행)는 1.30.6에도 있는 디렉티브입니다.
5. 버퍼가 모자랄 때 — client_header_buffer_size와 large_client_header_buffers
헤더가 1024바이트 버퍼에 다 들어가지 않으면 어떻게 될까요. 설정 두 개의 기본값부터 봅니다.
ngx_conf_merge_size_value(conf->client_header_buffer_size,
prev->client_header_buffer_size, 1024);
ngx_conf_merge_bufs_value(conf->large_client_header_buffers,
prev->large_client_header_buffers,
4, 8192);
src/http/ngx_http_core_module.c 3628–3632행
처음엔 1k 버퍼 하나로 시작하고, 모자라면 8k짜리 "큰 버퍼"를 최대 4개까지 더 씁니다. 3절과 4절의 루프에서 r->header_in->pos == r->header_in->end(버퍼를 끝까지 봤는데 줄이 안 끝남)일 때 불리는 ngx_http_alloc_large_header_buffer()가 그 일을 합니다.
old = request_line ? r->request_start : r->header_name_start;
cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
if (r->state != 0
&& (size_t) (r->header_in->pos - old)
>= cscf->large_client_header_buffers.size)
{
return NGX_DECLINED;
}
hc = r->http_connection;
if (hc->free) {
cl = hc->free;
hc->free = cl->next;
/* ... */
} else if (hc->nbusy < cscf->large_client_header_buffers.num) {
b = ngx_create_temp_buf(r->connection->pool,
cscf->large_client_header_buffers.size);
/* ... */
} else {
return NGX_DECLINED;
}
cl->next = hc->busy;
hc->busy = cl;
hc->nbusy++;
src/http/ngx_http_request.c 1679–1727행
8k와 4는 기본값입니다. 빨간 상자로 가는 길이 둘 — 줄 하나가 너무 길 때와, 버퍼 4개를 다 썼을 때 — 이라는 것이 요점입니다.
- 한도는 "줄 하나"와 "버퍼 개수" 두 가지입니다. 줄 하나(요청 줄, 또는 헤더 한 줄)가 큰 버퍼 하나(8k)를 넘으면 실패합니다. 줄은 짧아도 헤더 전체가 버퍼 4개를 다 채우면 역시 실패합니다. 그래서 "헤더 전체 한도"는 대략 1k + 8k×4지만, 줄이 버퍼 사이에 걸치면 남은 자리를 버리고 옮기므로 정확히 그만큼은 아닙니다.
- 덜 읽은 줄만 옮깁니다. 새 버퍼를 잡으면
old(지금 읽던 줄의 시작)부터pos까지만 새 버퍼 앞으로memcpy하고,request_start·uri_start·header_name_start같은 포인터를 전부 새 주소로 다시 계산합니다(1750–1816행). 이미 끝난 앞줄들은 옛 버퍼에 그대로 두고,ngx_table_elt_t들이 계속 거기를 가리킵니다. 옛 버퍼를 지우지 않는 이유입니다. r->state == 0이면 복사하지 않습니다. 줄이 막 끝난 자리에서 버퍼가 꽉 찼다면 옮길 것이 없으니 새 버퍼로 갈아타기만 합니다(1729–1739행). 3절에서 말한 "state가 0이냐"가 여기서 쓰입니다. 요청 줄인데state가 0이면, 클라이언트가 빈 줄(CR LF)만 잔뜩 보내 버퍼를 채운 것이므로 버퍼를 처음으로 되감기만 합니다(1669–1677행).- 큰 버퍼는 연결에 딸려 있습니다.
hc->busy/hc->free가ngx_http_connection_t에 있는 이유는 파이프라인 때문입니다. 큰 버퍼 끝에 다음 요청이 이미 들어와 있을 수 있어서, 요청이 끝나도 버퍼를 바로 버리지 못합니다(7절).
실패했을 때 두 함수가 고르는 상태 코드가 다릅니다. 요청 줄 쪽은 NGX_HTTP_REQUEST_URI_TOO_LARGE(414, 1263–1270행), 헤더 쪽은 NGX_HTTP_REQUEST_HEADER_TOO_LARGE(1444–1469행)입니다. 뒤의 것은 HTTP 표준에 없는 nginx 고유 코드 494입니다(ngx_http_request.h 117–119행). 클라이언트에게 494를 그대로 보내지는 않습니다. 오류 페이지를 만드는 ngx_http_special_response_handler()가 494(그리고 497 같은 nginx 고유 코드)를 만나면 r->err_status = NGX_HTTP_BAD_REQUEST;로 상태를 400으로 바꿉니다(src/http/ngx_http_special_response.c 513–524행). 본문은 494 전용 페이지(ngx_http_error_494_page, 252–258행)를 쓰므로, 상태는 400인데 제목은 "Request Header Or Cookie Too Large"가 됩니다. 일부러 따로 코드를 둔 덕에 error_page 494 ...;로 이 경우만 골라 다른 페이지를 보여 줄 수 있습니다.
예제 4: 잘못된 요청과 큰 헤더
nc로 날것의 요청을 보내 응답의 첫 줄과 error.log의 [info] 줄을 찍었습니다(ex/u08-bad-requests, 버퍼 설정은 기본값 그대로). 요청 하나마다 이렇게 보냅니다.
printf 'get / HTTP/1.1\r\nHost: a\r\n\r\n' | nc -q1 127.0.0.1 8080 | head -1
로그 줄의 날짜·시각과 , client: ... 뒤는 지웠고, 긴 줄은 150글자에서 잘랐습니다.
=== 1. lowercase method ===
HTTP/1.1 400 Bad Request
[info] 11#11: *2 client sent invalid method while reading client request line
=== 2. HTTP/2.0 in request line ===
HTTP/1.1 505 HTTP Version Not Supported
[info] 11#11: *3 client sent invalid version while reading client request line
=== 3. URI goes above root ===
HTTP/1.1 400 Bad Request
[info] 11#11: *4 client sent invalid request while reading client request line
=== 4. HTTP/1.1 without Host ===
HTTP/1.1 400 Bad Request
[info] 11#11: *5 client sent HTTP/1.1 request without "Host" header while reading client request headers
=== 5. duplicate Host ===
HTTP/1.1 400 Bad Request
[info] 11#11: *6 client sent duplicate host header: "Host: b", previous value: "Host: a" while reading client request headers
=== 6. 9000-byte header line ===
HTTP/1.1 400 Bad Request
[info] 11#11: *7 client sent too long header line: "X-Long: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
=== 7. 9000-byte URI ===
HTTP/1.1 414 Request-URI Too Large
[info] 11#11: *8 client sent too long URI while reading client request line
=== 8. 2000-byte header line (fits in a large buffer) ===
HTTP/1.1 200 OK
[debug] 11#11: *9 recv: fd:3 1024 of 1024
[debug] 11#11: *9 http alloc large header buffer
[debug] 11#11: *9 http large header alloc: 000055A4B2BEF330 8192
[debug] 11#11: *9 http large header copy: 999
[debug] 11#11: *9 recv: fd:3 1031 of 7193
=== 9. 494 page body ===
<html>
<head><title>400 Request Header Or Cookie Too Large</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>Request Header Or Cookie Too Large</center>
- 1~3은 요청 줄 단계(로그 끝의 "while reading client request line"), 4~6은 헤더 단계입니다. 이 꼬리말은
c->log->action에 적어 둔 문자열로, 2절 523행과 3절 1227행에서 바꿔 끼운 것입니다. 어느 단계에서 실패했는지가 로그에 저절로 남습니다. - 2는 505입니다. 메이저 버전 2를 파서가 거절했습니다(3절).
- 6과 7은 같은 9000바이트인데 헤더는 400(속은 494), URI는 414입니다. 9번이 6과 같은 요청의 본문입니다.
- 8은 성공입니다. 첫
recv가 1024바이트 버퍼를 꽉 채웠고,X-Mid:줄이 아직 안 끝났으므로 8192바이트 큰 버퍼를 새로 잡아(large header alloc) 덜 읽은 999바이트를 옮긴 뒤(large header copy: 999), 남은 7193바이트(8192 − 999) 자리에 이어 읽었습니다. 999는 1024에서GET / HTTP/1.1\r\n(16바이트)와Host: a\r\n(9바이트)를 뺀 값, 곧 이미 끝난 앞의 두 줄은 옮기지 않았다는 증거입니다. 디버그 문자열은 1666–1667행, 1717–1719행, 1741–1742행에서 찍습니다.
httpd와 비교하면, httpd는 요청 줄(LimitRequestLine)과 헤더 한 줄(LimitRequestFieldSize)을 각각 8190바이트로 따로 재고 요청 줄이 넘으면 414였습니다(httpd 9편). nginx도 줄 하나 기준 8k에 요청 줄은 414로 비슷하지만, 버퍼 개수라는 두 번째 한도가 있다는 점이 다릅니다. Tomcat은 요청 줄과 헤더를 한 한도(maxHttpHeaderSize)로 재고 넘으면 URI든 헤더든 400이었습니다(Tomcat 6편).
6. 어느 server 블록인가 — 가상 서버 고르기
한 포트에 server {} 블록이 여럿이면, 같은 주소:포트로 들어온 요청을 Host 헤더로 나눠야 합니다. 1절에서 연결은 일단 기본 서버를 받았습니다. Host 헤더 줄을 읽는 순간 처리 함수 ngx_http_process_host()가 진짜 서버를 고릅니다(1866–1915행). 먼저 ngx_http_validate_host()가 값을 다듬습니다. 대문자가 있으면 요청 풀에 소문자 사본을 만들고, :8080 같은 포트를 떼어 r->port로, 끝의 점 하나(example.com.)도 떼어 냅니다(2276–2471행). 그다음 ngx_http_set_virtual_server()를 부르는데, 요청 줄이 GET http://host/ HTTP/1.1 같은 절대 URI여서 이미 서버를 골랐다면(1178–1203행) Host 헤더로는 다시 고르지 않습니다(1903–1905행).
ngx_http_set_virtual_server()는 찾기를 ngx_http_find_virtual_server()에 맡기고, 찾았으면 설정 배열 포인터 두 개를 바꿉니다.
if (rc == NGX_DECLINED) {
return NGX_OK;
}
r->srv_conf = cscf->ctx->srv_conf;
r->loc_conf = cscf->ctx->loc_conf;
src/http/ngx_http_request.c 2542–2547행
server 블록을 "고른다"는 것이 소스에서는 이 두 줄입니다. 2절에서 본 대로 모든 모듈이 이 배열을 통해 설정을 읽으므로, 이후 그 요청에서 ngx_http_get_module_loc_conf(r, ...)가 돌려주는 값은 모두 고른 server 블록의 것입니다. 못 찾으면(NGX_DECLINED) 아무것도 바꾸지 않으니 기본 서버가 그대로 남습니다.
찾는 순서는 ngx_http_find_virtual_server()가 부르는 ngx_hash_find_combined()가 정합니다 — 정확한 이름 해시, 앞 와일드카드(*.example.com), 뒤 와일드카드(www.example.*) 순입니다(src/core/ngx_hash.c 211–245행, 부르는 자리는 ngx_http_request.c 2568–2575행). 셋 다 없으면 정규식을 설정에 적은 순서대로 하나씩 돌립니다(2620–2634행).
노란 상자 셋은 해시 조회라 server 블록이 많아도 빠르고, 회색 정규식만 하나씩 돌립니다.
이름 해시는 설정을 읽을 때 ngx_http_server_names()가 만드는데, 그 주소:포트에 server 블록이 둘 이상일 때만(또는 기본 서버의 정규식 server_name에 캡처가 있을 때) 만듭니다(src/http/ngx_http.c 1572–1581행). 하나뿐이면 고를 것이 없으니 virtual_names가 NULL이고(1973–1984행), ngx_http_find_virtual_server()는 첫 줄에서 바로 돌아갑니다.
예제 5: Host에 따라 답하는 server 블록
한 포트(8080)에 server 블록 다섯 개를 두었습니다(ex/u08-keepalive-vhost).
server { listen 8080 default_server; server_name _; return 200 "default host=$host\n"; }
server { listen 8080; server_name www.example.com; return 200 "exact host=$host\n"; }
server { listen 8080; server_name *.example.com; return 200 "wc-head host=$host\n"; }
server { listen 8080; server_name www.example.*; return 200 "wc-tail host=$host\n"; }
server { listen 8080; server_name ~^api(?<n>\d+)\.example\.org$;
return 200 "regex n=$n host=$host\n"; }
(글에서는 한 줄씩으로 줄였습니다. 실제 파일은 블록마다 여러 줄입니다.) curl -H "Host: ..."로 보낸 결과입니다.
www.example.com -> exact host=www.example.com
WWW.Example.COM:8080 -> exact host=www.example.com
a.example.com -> wc-head host=a.example.com
www.example.net -> wc-tail host=www.example.net
api42.example.org -> regex n=42 host=api42.example.org
unknown.test -> default host=unknown.test
(HTTP/1.0, no Host) -> default host=_
- 대문자와 포트가 붙은
WWW.Example.COM:8080도 정확한 이름으로 찾았습니다.validate_host가 소문자로 바꾸고 포트를 뗀 결과입니다.$host도 다듬어진 값입니다. www.example.com은 앞 와일드카드*.example.com에도 맞지만 정확한 이름이 먼저입니다.- 정규식의 이름 붙은 캡처
(?<n>...)가$n변수가 되었습니다. 정규식 서버를 고를 때 캡처를 변수로 저장하는 일은ngx_http_regex_exec()가 합니다. Host가 없는 HTTP/1.0 요청은 기본 서버로 갔고, 이때$host는 기본 서버의server_name첫 값(_)입니다. HTTP/1.1이었다면 4절의 검사에서 400이었습니다.
요청 읽기를 마치며 — ngx_http_process_request()
빈 줄까지 읽고 검사를 통과하면 ngx_http_process_request()가 불립니다. TLS 클라이언트 인증서 검사(ssl_verify_client)가 이 함수 앞쪽에 있고, 끝은 이렇습니다.
if (c->read->timer_set) {
ngx_del_timer(c->read);
}
#if (NGX_STAT_STUB)
(void) ngx_atomic_fetch_add(ngx_stat_reading, -1);
/* ... */
(void) ngx_atomic_fetch_add(ngx_stat_writing, 1);
/* ... */
#endif
c->read->handler = ngx_http_request_handler;
c->write->handler = ngx_http_request_handler;
r->read_event_handler = ngx_http_block_reading;
if (ngx_http_read_early_body(r) != NGX_OK) {
return;
}
ngx_http_handler(r);
}
src/http/ngx_http_request.c 2198–2218행
client_header_timeout타이머를 지웁니다. 이 타이머는 "헤더를 다 읽을 때까지"의 시간입니다. 응답을 만드는 동안에는 다른 타이머(send_timeout, upstream의 타이머)가 맡습니다.stub_status의 Reading이 하나 줄고 Writing이 하나 늡니다.stub_status페이지의 "Reading: N"은 이 글의 1~6절을 지나는 중인 요청 수였던 것입니다.- 연결의 두 핸들러가
ngx_http_request_handler로 고정됩니다. 이 함수는 이벤트가 쓰기면r->write_event_handler(r), 읽기면r->read_event_handler(r)를 부르는 중계자입니다(2643–2677행). 이제부터 단계를 바꿀 때는 연결이 아니라 요청 쪽 포인터를 바꿉니다. 읽기 쪽ngx_http_block_reading은 "지금은 읽을 생각이 없다"는 뜻입니다. ngx_http_read_early_body()는client_body_early_read디렉티브가 켜져 있을 때 phase에 들어가기 전에 요청 본문을 미리 읽는 1.31.x의 새 기능입니다(1.30.6의ngx_http_core_module.c에는 이 디렉티브가 없습니다). 기본값은 꺼짐(NULL, 3651–3652행)이라 보통은 바로NGX_OK입니다.ngx_http_handler(r)가 phase 엔진의 입구입니다.r->write_event_handler = ngx_http_core_run_phases;로 걸고 phase를 돌리기 시작하는데, 그 이야기는 9편입니다.
7. 다음 요청 — keep-alive와 파이프라인
keep-alive는 누가 정하나
요청을 다 읽은 직후, ngx_http_handler()가 r->keepalive의 첫 값을 정합니다(src/http/ngx_http_core_module.c 856–870행). 4절의 Connection 헤더 처리 함수가 값에 close나 keep-alive가 들었는지 보고 connection_type을 정해 두었으면 그대로 따르고(1918–1934행), 헤더가 없으면 r->http_version > NGX_HTTP_VERSION_10, 곧 HTTP/1.1은 켬, 1.0은 끔입니다. 그 뒤 location이 정해지면 keepalive_timeout 0, 한 연결의 요청 수가 keepalive_requests(기본 1000)에 이르렀을 때, 연결이 keepalive_time(기본 1시간)보다 오래됐을 때 끕니다(1390–1400행, 기본값은 3986–3995행). 2절의 c->requests++가 여기서 쓰입니다. 400·414·500 같은 일부 오류 응답은 r->keepalive가 켜져 있어도 끕니다(ngx_http_special_response.c 436–447행). 5절 예제처럼 요청을 다 읽기 전에 난 오류는 ngx_http_handler()까지 가지 않으므로 r->keepalive가 처음부터 0입니다. 소스에 이유가 적혀 있지는 않지만, 망가진 요청 뒤에 남은 바이트를 다음 요청의 시작으로 믿기 어렵기 때문으로 보입니다.
응답이 끝나면 ngx_http_finalize_connection()이 r->keepalive를 보고 ngx_http_set_keepalive()로 갈지, lingering close로 갈지, 그냥 닫을지 고릅니다(2999–3075행).
ngx_http_set_keepalive() — 요청은 지우고 연결은 남긴다
이 함수의 핵심은 요청 구조체를 지우고(ngx_http_free_request() → 요청 풀 해제, 3422행) c->data를 hc로 되돌리는 것(3424행)입니다. 그다음은 두 갈래입니다.
버퍼에 이미 다음 요청의 바이트가 있으면(파이프라인), 기다리지 않고 바로 새 요청을 만듭니다.
if (b->pos < b->last) {
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "pipelined request");
c->log->action = "reading client pipelined request line";
r = ngx_http_create_request(c);
if (r == NULL) {
ngx_http_close_connection(c);
return;
}
r->pipeline = 1;
c->data = r;
/* ... */
rev->handler = ngx_http_process_request_line;
ngx_post_event(rev, &ngx_posted_events);
return;
}
src/http/ngx_http_request.c 3434–3461행
ngx_post_event()는 "이번 이벤트 루프 바퀴가 끝나기 전에 이 핸들러를 불러 달라"고 posted 큐에 넣는 것입니다(7편). 지금 콜 스택에서 곧바로 부르지 않고 큐로 미루어, 응답을 마무리하던 스택이 다 풀린 뒤 깨끗하게 다음 요청을 시작합니다.
버퍼가 비어 있으면 2절과 같은 원칙으로 메모리를 돌려줍니다. 1k 버퍼를 ngx_pfree()하고, 큰 헤더 버퍼(hc->free, hc->busy)도 전부 해제한 뒤(3470–3513행) 읽기 핸들러를 ngx_http_keepalive_handler로 바꾸고 keepalive_timeout(기본 75초) 타이머를 겁니다(3521–3571행). keep-alive로 기다리는 연결은 결국 512바이트 연결 풀과 ngx_http_connection_t만 쥐고 있게 됩니다.
다음 요청의 바이트가 오면 ngx_http_keepalive_handler()가 버퍼를 다시 잡고(3636–3653행) 읽은 뒤, 2절 끝과 똑같이 c->data = ngx_http_create_request(c);로 새 요청을 만들고 rev->handler = ngx_http_process_request_line;으로 바꿔 바로 부릅니다(3711–3724행).
요청은 지우고 연결은 남긴 뒤, 버퍼에 무엇이 남았느냐로 두 길이 갈립니다. 초록 칸은 아래 예제 6의 두 연결입니다.
예제 6: 한 연결에 요청 두 개 — keep-alive와 파이프라인
같은 예제 폴더에서, 한 연결로 요청을 두 번 보냈습니다. 첫째는 요청 하나를 보내고 1초 쉰 뒤 둘째 요청(Connection: close)을 보낸 것, 둘째는 두 요청을 printf 한 번에 이어 보낸 것입니다.
=== 2. keep-alive: two requests, 1s apart, one connection ===
HTTP/1.1 200 OK
exact host=www.example.com
HTTP/1.1 200 OK
wc-head host=a.example.com
--- error.log (filtered) ---
03:24:43 [debug] 11#11: *9 accept: 127.0.0.1:37362 fd:3
03:24:43 [debug] 11#11: *9 http wait request handler
03:24:43 [debug] 11#11: *9 http request line: "GET /first HTTP/1.1"
03:24:43 [debug] 11#11: *9 set http keepalive handler
03:24:43 [debug] 11#11: *9 hc free: 0000000000000000
03:24:43 [debug] 11#11: *9 hc busy: 0000000000000000 0
03:24:44 [debug] 11#11: *9 http keepalive handler
03:24:44 [debug] 11#11: *9 http request line: "GET /second HTTP/1.1"
03:24:44 [debug] 11#11: *9 close http connection: 3
=== 3. pipelining: two requests in one write ===
HTTP/1.1 200 OK
exact host=www.example.com
HTTP/1.1 200 OK
exact host=www.example.com
--- error.log (filtered) ---
03:24:47 [debug] 11#11: *10 accept: 127.0.0.1:33142 fd:3
03:24:47 [debug] 11#11: *10 http wait request handler
03:24:47 [debug] 11#11: *10 http request line: "GET /p1 HTTP/1.1"
03:24:47 [debug] 11#11: *10 set http keepalive handler
03:24:47 [debug] 11#11: *10 pipelined request
03:24:47 [debug] 11#11: *10 http request line: "GET /p2 HTTP/1.1"
03:24:47 [debug] 11#11: *10 close http connection: 3
- 연결 번호
*9가 두 요청 내내 같습니다.accept는 한 번, 요청 줄은 두 번입니다. 연결 하나에 요청 구조체 두 개가 차례로 만들어졌다 지워졌습니다. - keep-alive 쪽은 첫 응답 뒤
set http keepalive handler(3367행) → 큰 버퍼가 없었으므로hc free/hc busy가 비어 있음(3486–3501행) → 1초 뒤 둘째 요청이 오자http keepalive handler(3591행)가 새 요청을 만들었습니다. 둘째 요청은Host: a.example.com이라 같은 연결인데도 다른 server 블록(wc-head)이 답했습니다. server 블록은 연결이 아니라 요청마다 고른다는 뜻입니다. - 파이프라인 쪽은
set http keepalive handler바로 다음에pipelined request(3436행)가 찍히고,http wait request handler도http keepalive handler도 없이GET /p2를 읽었습니다. 둘째 요청의 바이트가 이미 버퍼에 있었으므로 소켓을 기다릴 필요가 없었습니다.
lingering close는 짧게
keep-alive가 아닌데 클라이언트가 아직 보내는 중일 수 있으면(본문을 다 읽지 않은 오류 응답 등), nginx는 소켓을 바로 닫지 않습니다. 받을 데이터가 남은 채 close()하면 커널이 RST를 보내 클라이언트가 아직 받지 못한 응답까지 버릴 수 있기 때문입니다(TCP의 일반적인 동작이고, 이 판의 소스 주석에 적힌 설명은 아닙니다). 그래서 ngx_http_set_lingering_close()가 쓰기 쪽만 닫고(shutdown(SHUT_WR), 3780행), lingering_timeout 동안 들어오는 입력을 읽어 버린 뒤 닫습니다(3727–3795행). 어느 경우에 lingering close로 가는지는 lingering_close 디렉티브와 위의 ngx_http_finalize_connection() 3063–3072행이 정합니다.
이 글에서 뺀 것
- TLS 핸드셰이크와 SNI로 서버 고르기(
ngx_http_ssl_handshake,ngx_http_ssl_servername): src/http/ngx_http_request.c 675행, 886행부터 - PROXY protocol 머리 읽기: 479–498행,
src/core/ngx_proxy_protocol.c - HTTP/2 프리페이스를 보고
ngx_http_v2_init()으로 넘기기: 500–521행,src/http/v2/. HTTP/3:src/http/v3/ ngx_http_parse_complex_uri()의 상태 전체: src/http/ngx_http_parse.c 1300행부터CONNECT메서드와allow_connect(1.30.6의ngx_http_core_module.c에는 없는 디렉티브): 파서의sw_spaces_before_host, ngx_http_request.c 2098–2112행- 요청 본문 읽기(
ngx_http_read_client_request_body):src/http/ngx_http_request_body.c— 11편에서 짧게 keepalive_min_timeout(1.30.x에도 있는 디렉티브): 3556–3571행- 요청 종료와 해제(
ngx_http_finalize_request,ngx_http_free_request): 2743행, 3981행부터
정리
| 개념 | 한 줄 요약 |
|---|---|
ngx_http_init_connection() |
listen의 ls->handler. 기본 서버를 잡고 대기 핸들러와 client_header_timeout(60초)만 건다 |
ngx_http_wait_request_handler() |
첫 바이트가 와야 1k 버퍼와 요청 풀(4k)·ngx_http_request_t를 만든다. 놀면 버퍼를 돌려준다 |
c->data |
같은 칸에 ngx_http_connection_t → ngx_http_request_t → 다시 hc |
ngx_http_parse_request_line() |
27개 상태의 switch. r->state를 저장해 반쯤 온 데이터에서 이어 읽는다 |
r->uri |
평범하면 버퍼를 가리키기만, 복잡하면(/./, %XX, //) 요청 풀에 고친 사본 |
| 헤더 | 모두 headers_in.headers 목록에. 알려진 헤더는 해시로 처리 함수를 찾아 오프셋 칸에 매단다 |
| 큰 헤더 버퍼 | 줄 하나가 8k를 넘거나 버퍼 4개를 다 쓰면 실패. 요청 줄은 414, 헤더는 494(화면엔 400) |
| server 블록 고르기 | 정확한 이름 → *.앞 → 뒤.* → 정규식(적은 순서) → 기본 서버. 결과는 r->srv_conf·r->loc_conf 교체 |
ngx_http_process_request() |
헤더 타이머 해제, 핸들러를 ngx_http_request_handler로 고정, ngx_http_handler()(9편)로 |
| keep-alive | 요청 구조체·풀은 지우고 연결만 남긴다. 버퍼에 다음 요청이 있으면 파이프라인으로 바로 |
다음에 볼 것
요청을 다 읽었습니다. ngx_http_request_t에는 메서드와 URI, 헤더, 고른 server 블록의 설정이 들어 있고, ngx_http_handler()가 phase 엔진을 돌리기 시작했습니다. 다음 글은 그 안으로 들어갑니다. 요청이 NGX_HTTP_POST_READ_PHASE부터 NGX_HTTP_LOG_PHASE까지 11단계를 어떤 순서로 지나는지, 그 사이 location은 어떤 규칙으로 골라지고 rewrite ... last는 어떻게 단계를 되돌리는지 봅니다. 9편: 11단계 phase와 location 찾기 — 요청은 어떤 순서로 처리되나에서 이어집니다.
같은 일을 앞 연재에서도 봤습니다. httpd 9편에서 httpd는 요청 줄과 헤더를 필터가 읽었고, event MPM은 요청과 요청 사이에는 스레드를 놓아 주지만 요청 하나를 읽고 처리하는 동안은 작업 스레드 하나가 그 연결을 맡았습니다. nginx는 스레드 없이 파서의 state를 구조체에 저장해 두고 이벤트 루프로 돌아가므로, 반쯤 온 요청이 있어도 워커는 다른 연결을 계속 돌립니다. 이 "멈춘 자리를 기억하는 파서"는 Tomcat 6편의 parseRequestLine()이 단계 번호를 들고 다니던 것과 같은 해법입니다 — 기다리지 않는 읽기를 하려면 누구든 이렇게 해야 합니다.