namalzi

← 글 목록

nginx 소스 파헤치기 8: HTTP 요청 읽기 — 연결이 ngx_http_request_t가 되기까지

nginx · 게시

accept한 소켓 하나가 nginx 안에서 ngx_http_request_t가 되기까지를 소스로 따라갑니다. 데이터가 올 때까지 요청 구조체를 만들지 않는 이유, switch 상태 기계로 된 요청 줄·헤더 파서, client_header_buffer_size와 large_client_header_buffers가 쓰이는 자리, Host 헤더로 server 블록을 고르는 순서, keep-alive와 파이프라인을 디버그 로그·gdb·nc로 직접 확인합니다.

이 글에서 다루는 것

"nginx 소스 파헤치기" 연재의 여덟 번째 글입니다. 7편에서는 워커의 이벤트 루프가 epoll_wait로 깨어나 accept4로 연결을 받고, 그 연결의 읽기 이벤트에 걸린 핸들러 함수를 불러 주는 데까지 봤습니다. 이번 글은 그 핸들러 쪽, 곧 HTTP 모듈로 넘어옵니다. 소켓으로 들어온 바이트가 어떻게 GET /hello?x=1 HTTP/1.1 한 줄과 헤더 몇 줄로 나뉘어 ngx_http_request_t 라는 구조체가 되는지 따라갑니다.

2편의 지도에서 이 글은 "C. HTTP 요청 하나" 줄의 첫 상자, 요청 읽기를 확대합니다.

연재 지도에서 이 글의 위치. A 준비(기동·cycle 4편, 모듈·디렉티브 5편, 메모리 풀·자료구조 3편), B 프로세스와 이벤트(마스터·워커 6편, 이벤트 루프 7편), C HTTP 요청 하나(요청 읽기 8편에서 phase·location 9편으로, 거기서 필터·정적 파일 10편 또는 upstream·proxy 11편으로, upstream·proxy에서 부하 분산·keepalive·stream 12편으로) 가운데 8편 요청 읽기 상자만 초록색으로 칠해져 있다. 초록 상자가 이 글입니다. 요청을 다 읽고 나면 9편의 phase 엔진으로 넘깁니다.

답하려는 질문은 네 가지입니다.

항목 내용
대상 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;

src/http/ngx_http.c 1882행

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()이 하는 일은 세 가지입니다.

  1. 연결 풀에서 ngx_http_connection_t를 하나 받아 c->data에 매답니다(226–232행).
  2. 이 연결이 들어온 주소:포트를 보고 hc->addr_conf를 고르고, 그 주소의 기본 서버(default_server) 설정을 hc->conf_ctx로 잡아 둡니다(236–309행). Host 헤더를 보기 전이므로 일단 기본 서버입니다. 진짜 server 블록은 6절에서 고릅니다.
  3. 읽기 핸들러를 걸고 기다립니다. 함수의 뒷부분입니다.
    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_init_connection이 ngx_http_wait_request_handler를 걸고, 데이터가 오면 요청 구조체를 만들고 ngx_http_process_request_line으로, 요청 줄이 끝나면 ngx_http_process_request_headers로, 빈 줄을 만나면 ngx_http_process_request를 거쳐 ngx_http_request_handler로 바뀌어 phase 처리와 응답(9~11편)을 한다. 그다음 keep-alive면 ngx_http_set_keepalive가 ngx_http_keepalive_handler를 걸어 다음 요청을 기다리고, 데이터가 오면 새 요청 구조체를 만들어 다시 ngx_http_process_request_line으로 간다. 버퍼에 다음 요청이 이미 있으면(파이프라인) 바로 ngx_http_process_request_line으로 간다. 닫아야 하는데 입력이 남아 있으면 ngx_http_lingering_close_handler가 남은 입력을 버리고 닫는다. keepalive_timeout이 지나면 연결을 닫는다. 상자 하나가 그 순간 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행

연결을 맺은 뒤 요청 구조체가 생기기까지의 순서. 클라이언트가 데이터 없이 TCP 연결만 맺으면 이벤트 루프가 ngx_http_init_connection을 부르고, 이 함수는 512바이트 연결 풀에 ngx_http_connection_t만 만들고 읽기 핸들러를 wait_request_handler로, 타이머를 60초로 건다. 이때는 버퍼도 요청 구조체도 없다. 첫 바이트가 도착하면 ngx_http_wait_request_handler가 불려 c->buffer 1024바이트를 malloc하고 c->recv로 읽은 뒤, 4096바이트 요청 풀과 ngx_http_request_t를 만들고 읽기 핸들러를 process_request_line으로 바꿔 바로 부른다. 연결을 맺은 순간과 첫 바이트가 온 순간 사이에는 작은 구조체 하나만 있습니다.

왜 이렇게 미룰까요. 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행

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로 가느냐가 갈린 것으로 보입니다. 읽을 것은 이렇습니다.

각 줄을 찍는 소스는 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이 지나는 길만 그리면 이렇습니다.

요청 줄 파서의 주요 상태. sw_start에서 빈 줄을 건너뛰고 대문자를 만나면 sw_method로, 공백에서 메서드를 확정하고 sw_spaces_before_uri로 간다. 슬래시면 sw_after_slash_in_uri로, 알파벳이면 http://host:port/ 꼴의 절대 URI를 읽는 sw_schema부터 sw_port까지를 거쳐 sw_after_slash_in_uri로 간다. sw_after_slash_in_uri에서 보통 글자면 sw_check_uri로 가고, sw_check_uri는 다음 슬래시에서 다시 sw_after_slash_in_uri로 돌아온다. 점, 슬래시, 퍼센트, 물음표를 보면 sw_uri로 간다. 세 상태 모두 공백을 만나면 uri_end를 적고 sw_http_09로 가고, H를 만나면 sw_http_H부터 sw_minor_digit까지 HTTP/1.1을 읽고, CR 다음 LF에서 NGX_OK로 끝난다. 상자 하나가 case 하나입니다(여러 개를 묶은 상자는 "…"로 표시했습니다). 화살표 옆이 그 단계에서 다음으로 넘어가게 하는 글자입니다.

몇 가지 눈여겨볼 기법이 있습니다.

예제 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"

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를 넘길 때 씁니다.

요청 줄과 헤더가 버퍼를 가리키는 모습. r->header_in은 c->buffer 1024바이트이고 GET /hello?x=1 HTTP/1.1 CR LF Host: a.test CR LF가 들어 있다. 파서는 request_start, method_end, uri_start, args_start, uri_end, request_end 포인터를 남기고, ngx_str_t인 r->uri, r->args, r->request_line, r->method_name은 버퍼 안을 가리킬 뿐 복사하지 않는다. 헤더 한 줄마다 header_name_start, header_name_end, header_start, header_end를 남기고, headers_in.headers 목록의 ngx_table_elt_t의 key와 value도 버퍼 안을 가리키며 끝에 NUL을 직접 써 넣는다. URI가 복잡하면(/./, /../, //, %XX) ngx_http_parse_complex_uri가 요청 풀에 고친 사본을 만들고 r->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행

  1. 헤더는 전부 목록에 들어갑니다. r->headers_in.headers(ngx_list_t)에 ngx_table_elt_t를 하나 붙이고, key·value는 버퍼 안을 가리킵니다. 그리고 이름과 값 바로 뒤의 버퍼 바이트(:, CR, 공백 등)를 '\0'으로 덮어씁니다. 복사하는 대신 버퍼를 고쳐 써서, C 문자열 함수에 그대로 넘길 수 있게 된 것입니다.
  2. 해시는 파서가 읽으면서 이미 계산했습니다. 헤더 이름의 글자를 하나 읽을 때마다 소문자로 바꾸고 해시를 갱신하므로(hash = ngx_hash(hash, c);, ngx_http_parse.c 963–971행), 줄이 끝난 순간 r->header_hash에 답이 있습니다. 이름을 다시 훑지 않습니다.
  3. 알려진 헤더면 처리 함수를 부릅니다. 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_parse_header_line() 이 헤더 한 줄을 읽으면서 이름을 소문자로 바꾸고 r->header_hash 를 계산한다. ngx_list_push(&r->headers_in.headers) 로 ngx_table_elt_t(hash, key, value)를 붙이는데, key 와 value 는 버퍼 안을 가리키고 바로 뒤 바이트는 NUL(0) 로 덮어쓴다. 그다음 ngx_hash_find(&cmcf->headers_in_hash, h->hash, h->lowcase_key, len) 로 처리 함수를 찾는다. 표에 없는 헤더는 목록에만 남는다. Host 는 { Host, offsetof(host), ngx_http_process_host } 로 r->headers_in.host 칸에 들어간다. Referer 등은 ngx_http_process_header_line() 이 (char *) &r->headers_in + offset 칸에 매달고, 같은 이름이 또 오면 next 로 잇는다. Content-Length 등은 ngx_http_process_unique_header_line() 이 맡아 두 번째가 오면 duplicate header line 으로 400 이 된다. 헤더는 모두 목록에 들어가고, 표에 있는 헤더만 처리 함수를 거칩니다. 세 예는 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행

큰 헤더 버퍼를 잡는 순서. header_in이 꽉 찼는데 줄이 끝나지 않았으면 ngx_http_alloc_large_header_buffer를 부른다. 지금 줄이 이미 8k 이상이면 실패다. 아니면 hc->free에 쓰던 큰 버퍼가 있으면 꺼내고, 없으면 nbusy가 4보다 작을 때만 8k 버퍼를 연결 풀에서 malloc으로 새로 만든다. 그리고 덜 읽은 줄만 새 버퍼 앞으로 복사하고 파서 포인터를 모두 옮긴 뒤 r->header_in을 새 버퍼로 바꾼다. 실패하면 요청 줄일 때는 414, 헤더일 때는 494이고 화면에는 400 Request Header Or Cookie Too Large로 나간다. 8k와 4는 기본값입니다. 빨간 상자로 가는 길이 둘 — 줄 하나가 너무 길 때와, 버퍼 4개를 다 썼을 때 — 이라는 것이 요점입니다.

실패했을 때 두 함수가 고르는 상태 코드가 다릅니다. 요청 줄 쪽은 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>

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_init_connection이 hc->addr_conf를 고르고, 처음에는 그 주소의 default_server 설정을 r->srv_conf와 r->loc_conf로 쓴다. Host 헤더나 요청 줄의 절대 URI를 ngx_http_validate_host가 소문자로 바꾸고 포트와 끝 점을 뗀 뒤, 1 정확한 이름 해시, 2 앞 와일드카드, 3 뒤 와일드카드, 4 설정에 적은 순서대로 정규식을 차례로 본다. 찾았으면 r->srv_conf와 r->loc_conf를 그 server의 것으로 바꾸고, 못 찾으면 default_server 그대로 둔다. 노란 상자 셋은 해시 조회라 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=_

요청 읽기를 마치며 — 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행

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행).

ngx_http_set_keepalive() 가 set http keepalive handler 를 찍고, ngx_http_free_request() 로 요청 풀을 해제한 뒤 c->data 를 hc 로 되돌린다. 버퍼에 다음 요청 바이트가 이미 있으면(b->pos < b->last, 파이프라인) pipelined request 를 찍고 ngx_http_create_request(c) 로 새 요청을 만들어 r->pipeline = 1 로 두고, rev->handler 를 ngx_http_process_request_line 으로 바꿔 ngx_post_event(rev, &ngx_posted_events) 로 큐에 넣는다. 예제 6의 *10 이 소켓을 기다리지 않고 GET /p2 를 읽은 길이다. 버퍼가 비어 있으면 1k 버퍼를 ngx_pfree() 하고 큰 헤더 버퍼(hc->free, hc->busy)를 해제하고, rev->handler 를 ngx_http_keepalive_handler 로 바꾸고 keepalive_timeout 타이머를 건다. 다음 요청 바이트가 오면 http keepalive handler 가 버퍼를 다시 잡고 ngx_http_create_request(c) 로 새 요청을 만들어 ngx_http_process_request_line 으로 간다. 예제 6의 *9 가 1초 뒤 GET /second 를 읽은 길이다. 타이머가 만료되면 연결을 닫는다. 요청은 지우고 연결은 남긴 뒤, 버퍼에 무엇이 남았느냐로 두 길이 갈립니다. 초록 칸은 아래 예제 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

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행이 정합니다.

이 글에서 뺀 것

정리

개념 한 줄 요약
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()이 단계 번호를 들고 다니던 것과 같은 해법입니다 — 기다리지 않는 읽기를 하려면 누구든 이렇게 해야 합니다.

다른 글