node.js 분해하기
Hermaeus Mora ·
nodejs는 내부적으로 v8엔진과 libuv를 사용한다. v8 엔진은 ECMAScript를 인터프리트 및 컴파일하는 과정에서 파일 및 네트워크 I/O와 같은 무거운 작업의 처리를 libuv에 위임한다. libuv는 asynchronous non-blocking을 구현하기 위한 구조체와 메서드를 제공한다(handle, request).

다음 node.js 서버가 어떻게 동작하는지 살펴보자.
import fs from \"fs/promises\";
import http from \"http\";
async function sleep(ms: number) {
return new Promise((res, rej) => {
setTimeout(() => {
res(true);
}, ms);
});
}
const server = http.createServer();
server.on(\"request\", async (req, res) => {
await sleep(1000);
const foo = await fs.readFile(\"./foo.txt\");
res.write(foo);
res.end();
});
server.listen(3000);- v8 엔진은 전역 이벤트 루프(uv_default_loop)에 서버 핸들 및 uv_tcp_init(), uv_tcp_bind(), uv_listen() 요청을 등록한다. 서버 소켓이 생성되고, 서버 소켓으로 들어오는 요청에 대한 콜백 함수를 uv_listen()에 매개변수로 전달한다.
uv_tcp_init(loop, uv_tcp_t *server);
uv_tcp_bind(uv_tcp_t *server, const struct sockaddr *addr, unsigned int flags);
int r = uv_listen(uv_stream_t *server, int backlog, uv_connection_cb cb);
if r then exit- uv_listen() 콜백 함수는 클라이언트 핸들, uv_accept(), uv_read_start()(accept 성공 시), uv_close(accept 실패 시) 요청을 등록한다.
void cb(uv_stream_t *server, int status)
if status is -1 then return
uv_tcp_init(loop, uv_tcp_t *client);
if uv_accept(server, uv_tcp_t *client) is 0
then uv_read_start(uv_stream_t *client, uv_alloc_cb alloc_cb, uv_read_cb read_cb)
else uv_close(uv_handle_t *client, uv_close_cb close_cb)server.on()의 콜백 함수는 v8 엔진에 의해 컴파일되어 아래와 같이 read_cb 콜백 함수로써 실행된다.
void read_cb(uv_stream_t *client, ssize_t nread, uv_buf_t buf)
if nread is -1
then uv_close(uv_handle_t *client, uv_close_cb close_cb)
// compiled code by v8 engine
}
- 이벤트 루프의 poll for i/o 단계에서 클라이언트 요청을 감지하면 read_cb 콜백 함수가 실행된다. 이때 setTimeout 콜백과, fs.readFile 콜백(res.write() 포함)가 (v8 엔진의)microtask queue의 promise queue에 등록되며, uv_timer_init(), uv_timer_start()으로 타이머 핸들 및 요청이 이벤트 루프에 등록된다.
uv_timer_init(loop, uv_timer_t *handle)
uv_timer_start(uv_timer_t *handle, uv_timer_cb timer_cb, uint64_t timeout, uint64_t repeat)
void timer_cb(uv_timer_t *handle)
uv_fs_open(uv_loop_t *loop, uv_fs_t *req, const char *path, int flags, int mode, uv_fs_cb fs_open_cb)- 이벤트 루프의 run due timers 단계에서 타이머 콜백이 실행된다. 타이머 콜백 함수의 uv_fs_open() 함수는 블로킹이지만, 스레드 풀을 사용하므로 fs_open_cb 콜백 함수를 전달할 수 있다. 그 다음 단계로 넘어가기 전 promise queue의 setTimeout 콜백이 실행되어 true를 반환한다. 이벤트 루프의 poll for i/0 단계에서 워커 스레드가 fs_open_cb(), fs_read_cb(), fs_close_cb 콜백을 실행하므로 이벤트 루프는 3번 반복된다. 마지막 poll for i/0 단계에서 fs_close_cb 콜백이 실행되고 uv_write 핸들 및 요청이 이벤트 루프에 등록된다. 다음 run check handles 단계로 넘어가기 전 promise queue의 fs.readFile 콜백이 실행되어 파일 내용을 반환한다. 그 다음 poll for i/o 단계에서 uv_write 요청이 완료된다. v8 엔진은 uv_close() 요청으로 모든 핸들을 이벤트 루프에서 제거하고 할당된 메모리를 릴리즈한다. 서버 응답이 완료된다.
void fs_open_cb(uv_fs_t *req)
uv_fs_read(loop, req, uv_file file, const uv_buf_t bufs[], unsigned int nbufs, int64_t offset, uv_fs_cb fs_read_cb)
uv_fs_req_cleanup(req);
void fs_read_cb(uv_fs_t *req)
uv_fs_close(uv_loop_t *loop, uv_fs_t *req, uv_file file, uv_fs_cb fs_close_cb)
void fs_close_cb(uv_fs_t *req)
uv_write(uv_write_t *handle, uv_stream_t *client, const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb write_cb)