Сокеты и модуль net в Node.js¶
Источник: theNodeBook — Sockets and net
Модуль net в Node.js открывает сырые потоко-ориентированные сокеты. Ниже — net.Server, net.Socket, TCP-серверы и клиенты, запись, завершение, таймауты и локальные IPC endpoint'ы. net.createServer() создаёт объект сервера в JavaScript. server.listen() просит ОС привязаться и слушать. Принятые соединения становятся объектами net.Socket на handles libuv.
Модуль net в Node.js¶
Чтение приходит как данные stream. Запись попадает в локальные буферы и движется через libuv и сокетный слой ядра. end() запускает упорядоченное завершение с локальной стороны. destroy() срывает локальное состояние stream. Закрытие сервера прекращает приём новых соединений; у уже принятых сокетов свой жизненный цикл.
net.createServer() даёт объект сервера в JavaScript до захвата порта. Порт принадлежит ОС после успешного listen(). Это разделение — откуда берётся смысл большей части поведения node:net: один объект в JavaScript, один сокет ниже.
1 2 3 4 5 6 7 | |
net.createServer() создаёт net.Server. Сохраняет колбэк соединения. Настраивает EventEmitter. Готовит внутренние поля для нативного состояния. Порт ещё не занят. Таблица сокетов ядра не меняется.
server.listen() переносит операцию ниже JavaScript. Node создаёт нативное TCP-состояние, отдаёт libuv, просит ОС о TCP-сокете, привязывает к 127.0.0.1:3000 и переводит в listening. После успеха процесс владеет слушающим endpoint.
Маленький API. Много состояния.
Трасса объектов:
1 2 3 4 | |
Имена важны: каждый слой владеет своей частью. net.Server — API JavaScript. TCPWrap — нативная обёртка Node вокруг TCP handle. Handle libuv связывает нативный объект с готовностью event loop. ОС владеет состоянием сокета, дескриптором или platform handle, привязанным адресом, listen и accept.
Сокет — endpoint процесса для сетевого или локального IPC-разговора. Здесь чаще TCP-сокет: семейство адресов, состояние протокола, буферы ядра, локальные и удалённые endpoint-данные. Node оборачивает нижний объект в JavaScript: события, streams, методы вместо syscalls.
Полезное разделение:
1 2 3 4 5 | |
net.Server оборачивает слушающий сокет. net.Socket обычно — подключённый. Сервер принимает новых пиров. Сокет говорит с одним пиром.
Это объясняет большинство багов node:net.
node:net владеет сырыми потоками¶
node:net — встроенный модуль Node для потоковых TCP и локальных сокетных endpoint'ов: TCP-серверы и клиенты, UNIX domain sockets на Unix-подобных системах, именованные pipe в Windows. Ниже HTTP, TLS, WebSocket, клиентов БД, Redis и большинства бинарных протоколов.
Используйте, когда нужны байты. Сырые чтения и записи. Подключённые потоки байтов и события жизненного цикла сокета. Парсер, заголовки и кадрирование сообщений — ваш код выше.
1 2 3 4 5 6 | |
Колбэк получает net.Socket. Это Duplex stream — словарь из основ потоков. Readable отдаёт данные пира. Writable принимает байты к пиру. Backpressure есть и в очередях Node, и ниже в буферах ядра и TCP flow control.
net.Socket также EventEmitter — словарь из EventEmitter. connect, data, end, error, timeout, close — события поверх нативных переходов.
Нижнее состояние держите в голове.
1 2 3 4 | |
Для TCP handle libuv — uv_tcp_t: состояние TCP и регистрация в event loop. На Unix — привязка к file descriptor. На Windows — socket handle и Windows I/O. Публичный API Node выровнен на обеих платформах.
TCPWrap — внутренний binding Node. Реализация может сдвигаться между версиями Node; роль стабильна для отладки: нативный объект между net.Socket/net.Server и TCP handle libuv. JavaScript вызывает методы → libuv → колбэки → события и колбэки пользователя.
Для локальных IPC Node использует pipe-ориентированные handles вместо TCP. API по-прежнему net.Server и net.Socket для многих операций. Меняется формат endpoint и нижний примитив ОС. Контракт stream знаком.
Два частых пути создания:
1 2 | |
net.createConnection() и net.connect() — синонимы для клиента. Создают net.Socket и вызывают socket.connect(). Сервер обычно с net.createServer(). Клиент — с net.connect(), когда host и port известны.
Путь нативного handle¶
Глубина — между вызовом метода JavaScript и событием, пришедшим позже.
net.Server и net.Socket — объекты JavaScript с видимым состоянием, слушателями, stream-механикой и ссылкой на нативный handle. Нативный handle — место, где Node покидает JavaScript и идёт в libuv. Для TCP — libuv TCP handle. Для локальных pipe — pipe handle.
TCP handle — объект event loop для TCP. В терминах libuv: инициализация, bind, connect, read, write, stop, close и жизненный цикл: initialized, active, closing, closed. У объекта JavaScript свои флаги stream — два слоя состояния нужно согласовать.
Здесь живут странные баги.
При server.listen() Node просит libuv bind и listen. libuv делает платформенную работу. На Unix нижний сокет — descriptor и polling backend (epoll на Linux через libuv, kqueue на macOS/BSD, IOCP на Windows). Событие JavaScript остаётся connection на всех платформах.
Polling backend сообщает готовность к accept. Node выполняет нативный accept. Принятый сокет получает свой handle. Node создаёт новый net.Socket и связывает с handle.
Владение:
1 2 3 4 5 | |
Handle сервера и принятого сокета разделены. server.close() и socket.end() делают разную работу.
Чтение: готовность connected сокета → libuv → read в память через allocation callbacks → Buffer → readable сторона net.Socket → data.
Запись: чанк в writable → нативные write requests → libuv → ОС → позже completion, колбэки, drain.
Write request живёт своей жизнью: может пережить вызов socket.write(), завершиться после return стека, упасть после reset пира, быть отброшенным локальным destroy().
Закрытие в два слоя. socket.end() — упорядоченное завершение writable. socket.destroy() — destroyed в JS и teardown handle. Колбэки close libuv после закрытия нижнего handle → close в JavaScript.
Поэтому close иногда «запаздывает»: JS уже destroyed, handle ещё закрывается; close — позднее наблюдение завершения teardown с точки зрения Node.
TCPWrap — ориентир отладки, не публичный API. Логика приложения — на net.Server, net.Socket, событиях, stream-методах и документированных опциях.
Нюанс: net.Socket может существовать до полезного handle. new net.Socket() — stream-состояние сразу; connect после connect(). Accept сервера — уже connected. net.connect() — создание и connect одним вызовом. Один класс, разные точки входа в жизненный цикл.
Практическая модель состояний net.Socket:
1 2 3 4 5 6 | |
Имена — чтение для отладки, не гарантия внутренних полей. Публично: connecting, pending, destroyed, closed, readyState, connect, end, close.
readyState — opening, open, readOnly, writeOnly, closed. Для диагностики; в приложении чище явное состояние протокола.
Ошибка сокета может прийти после операции, которая её вызвала. Запись в JS → submit write → позже сбой в ядре → completion в libuv → error. Функция давно вернулась — нормальный async I/O; на сокетах видно, потому что пир меняет состояние в любой момент.
Одно правило: net.Socket — фасад stream вокруг handle, состояние которого независимо от стека вызовов JavaScript.
До и после listen()¶
net.Server — объект сервера JavaScript: слушатели соединений, состояние close, адрес после bind, нативный handle после появления нижнего сокета.
1 2 3 4 5 6 | |
До завершения bind server.address() возвращает null. После listening — AddressInfo для TCP-серверов.
1 2 3 | |
AddressInfo — { address, family, port }. Для IPv4 loopback, например { address: '127.0.0.1', family: 'IPv4', port: 41891 }. Порт 0 — ОС выбирает порт; server.address().port нужен клиенту.
server.listen() привязывает endpoint и начинает accept. Для TCP — порт и опциональный host или объект опций. Вызов может быть асинхронным: Node уходит в нативный код, ОС может отклонить bind.
1 2 3 4 | |
Ошибки bind — событие error сервера. EADDRINUSE — конфликт локального endpoint. EADDRNOTAVAIL — адрес недоступен в namespace хоста/сети. Ошибки прав — привилегированные порты и политика платформы.
Успех — событие listening.
1 2 3 4 | |
Колбэк listen — разовый слушатель listening. Слушающий сокет есть ниже JavaScript. ОС заняла endpoint. Входящие handshake могут завершаться и ждать accept в Node.
Слушающий сокет — состояние ОС, принимающее новые попытки соединения на локальный endpoint: протокол, семейство, адрес, порт, listen, очереди ядра до accept в user space. Backlog — в опциях сокетов и backlog; разделение здесь: объект сервера — JS, слушающий сокет — ОС.
Путь listen примерно:
1 2 3 4 5 6 | |
Syscalls скрыты одним методом. Границы просачиваются через ошибки, поля адреса и тайминг.
Перегрузки server.listen() сходятся к одной нижней операции. Числовая форма:
1 | |
Объектная — удобнее расширять:
1 2 3 4 | |
Путь для локальных endpoint:
1 | |
TCP даёт AddressInfo после bind. Путь — строку из server.address(). Ветвите код по типу endpoint при публикации адреса.
1 2 3 4 | |
В библиотеках с TCP и IPC трактовать server.address() всегда с .port ломает UNIX domain.
Колбэк listen привязан к успешному setup, не к здоровью будущих соединений. Сервер может час работать и принимать сокеты, которые сразу reset. Колбэк значит только: listening endpoint создан.
error до listening — обычно провал старта; процесс должен упасть быстро или явно сообщить об ошибке старта.
1 2 3 | |
После listening error на сервере — сбои уровня сервера. Сбои соединения — на каждом net.Socket. Ошибка: ждать, что server.on('error') поймает все сбои сокетов.
1 2 3 4 5 | |
Два обработчика — два emitter'а. Сервер — listen и handle сервера. Сокет — одно соединение.
Привязка к host меняет, какие локальные адреса принимают трафик:
1 | |
IPv4 loopback. Клиенты на той же машине — 127.0.0.1:3000. Удалённые машины нуждаются в не-loopback адресе хоста.
1 | |
Все подходящие IPv4 адреса namespace. У принятого сокета socket.localAddress — конкретный адрес, до которого дошёл клиент. server.address() может показывать wildcard bind.
Без host Node и ОС выбирают умолчание; возможно поведение IPv6 wildcard. При отладке указывайте host явно. Числовые адреса убирают DNS из эксперимента.
Путь accept¶
Клиент подключается. Ядро делает TCP handshake. Слушающий сокет готов к accept. libuv видит готовность. Node accept и создаёт JavaScript-сокет.
1 2 3 4 5 6 | |
Событие connection — входящий пир принят и обёрнут в net.Socket. Колбэк net.createServer() — слушатель этого события.
1 2 3 4 5 | |
К моменту listener подключённый сокет существует. TCP established. Есть локальный и удалённый endpoint. Node создал обёртку и stream-состояние.
Подключённый сокет — сокет с peer endpoint. Для TCP — одно established-соединение с полями адресов, readable/writable, буферами ядра и TCP-состоянием ниже Node.
Один слушающий сервер — много подключённых сокетов:
1 2 3 4 5 6 | |
Set отслеживает обёртки JS. Ядро — состояние каждого сокета. Каждый accept — свой handle; на Unix ещё один file descriptor. Busy-сервер может исчерпать дескрипторы при одном net.Server.
Принятый сокет несёт endpoint:
1 2 3 4 | |
local* — ваша сторона. remote* — пир по TCP. Прокси/NAT описывают immediate TCP peer; идентичность выше — в других главах.
Данные — чанки stream:
1 2 3 4 5 | |
Echo-сервер. Чанк data — байты из приёмного пути сокета. TCP держит порядок; границы сообщений — выше. Одна запись клиента — несколько data или наоборот. Поведение потока байтов — в TCP: поток и сбои; node:net отдаёт его напрямую.
Сырым TCP нужно кадрирование поверх net.Socket: где кончается одно логическое сообщение. Префиксы длины, разделители, фиксированные записи, state machine парсера. Сокет даёт байты; протокол — границы.
Минимальный парсер по разделителю:
1 2 3 4 5 6 7 | |
Неполон: границы кодировки и неограниченный рост памяти. Механизм: хвост между data, потому что границы чанков ≠ границы протокола.
Для бинарных протоколов чаще безопаснее Buffer:
1 2 3 4 5 | |
Buffer.concat() копирует. Высокая пропускная способность — список чанков и смещения; копия только для целого кадра. См. Buffer; на горячем пути node:net это заметно.
socket.pause() — поведение stream с сетевым следствием. Readable в JS перестаёт течь. Данные могут оставаться в буферах Node и receive buffer ядра. Долгая пауза при продолжающейся отправке пира — TCP flow control через receive window.
1 2 3 | |
Пауза меняет только локальное чтение. Пир видит замедление или блокировку записей через TCP, не кастомное сообщение.
pause() — когда локальному парсеру или downstream нужно время. Сообщения протокола — когда пиру нужна семантическая обратная связь.
Принятый сокет может читать сразу. Если перед чтением нужен setup — вешайте обработчики рано и пауза осознанно.
1 2 3 4 5 6 7 | |
Сокет есть, пока идёт setup. Пир уже может слать байты. Пауза не эмитирует flowing data до готовности; нижние буферы конечны. Долгий setup — таймауты или ранний отказ.
Принятые сокеты могут жить дольше слушающего сокета сервера.
1 2 3 4 5 | |
После server.close() принятые сокеты могут читать и писать до end, error или destroy(). Жизненный цикл сервера и соединения разделены — разные нижние сокеты.
Клиентские сокеты¶
Клиент начинается с net.Socket до привязки к удалённому пиру. socket.connect() — исходящая попытка.
1 2 3 4 5 | |
socket.connect() просит Node подключить сокет к удалённому endpoint. Для TCP — разрешение host при необходимости, TCP handle, connect в ОС, connect после established.
Чаще net.connect():
1 2 3 | |
Колбэк — разовый connect. Записи до connect ставятся в очередь Node и сбрасываются после успеха, с учётом ошибок и writable.
Форма опций — когда важен выбор адреса:
1 2 3 4 5 | |
host/port — удалённый запрос. localAddress ограничивает источник. ОС проверяет адрес по интерфейсам и маршрутам. DNS — в разрешении DNS; hostname должен стать адресом до TCP connect.
Также family и lookup:
1 2 3 4 5 | |
Запрос IPv4. Полезно, когда localhost даёт и IPv6, и IPv4, а сервер слушает одно семейство.
Кастомный lookup — острый инструмент:
1 2 3 4 5 | |
Пример явно передаёт стандартный lookup. Реальные функции добавляют кэш, метрики, overrides, service discovery. Контракт жёсткий: connect ждёт адрес и family для TCP.
Node может пробовать кандидатов по логике lookup и connect. Гонки и полный путь клиента — в пути запроса от клиента. На уровне net.Socket — connect или error.
1 2 3 4 | |
После connect поля endpoint заполнены. Локальный порт часто эфемерный. Локальный адрес — из маршрутизации, если не задан. Удалённый — часто числовой адрес после resolve, не исходная строка host.
Сбои connect — ошибки:
1 2 3 | |
ECONNREFUSED — отказ на транспорте. ETIMEDOUT — таймаут нижней сети. ENOTFOUND — DNS до TCP. Один канал error несёт слои — логируйте code и поля endpoint.
Запись до connect легальна — Node ставит в очередь:
1 2 3 | |
Удобно для маленьких клиентов. Скрывает ошибки порядка. При провале connect очередь некуда деть — ошибка сокета. Для протоколов с setup-state ждите connect.
1 2 3 | |
Протокол после транспорта. Пир может сразу закрыться, но локальная последовательность ясна.
Для багов connect полезны readyState и pending:
1 | |
pending — ждёт ли connect. readyState — readable/writable на слое stream. Для логов и assert; состояние протокола — в своём автомате.
Клиент эмитирует close после закрытия. Аргумент close может быть boolean «был ли error до close».
1 2 3 | |
error и close разные. error — сбой. close — сокет закрыт. Cleanup, который должен всегда выполниться — в close; диагностика — в error.
Запись — локальное обязательство¶
socket.write() принимает байты в writable-путь. Строка, Buffer, TypedArray, DataView. Строки кодируются перед нижним путём.
1 2 | |
Возвращаемое значение — сигнал backpressure stream. true — ниже порога. false — буферизовано достаточно; ждите drain.
1 2 3 | |
Сигнал локального stream, близко к сокету, но всё же локальный. true — чанк принят в writable-путь Node. Колбэк записи — чанк ушёл из очереди записи Node в нижний слой. Ноль байтов обработано приложением пира.
Цепочка удержания:
1 2 3 4 5 | |
Send buffer ядра ниже Node. TCP может ждать окно пира, congestion, ретрансмиссию, маршрут. socket.write() — локальный сигнал; сеть продолжается дальше.
Для протокола это важно.
1 2 3 | |
Колбэк упорядочивает локальные операции: end после прохождения очереди Node. Это прогресс локальной очереди, не ACK пира. Нужен ответ протокола — читайте байты обратно.
Node может батчить чанки перед libuv. Публичный контракт прост: вы пишете чанки, stream решает staging для нативных writes.
cork() / uncork() Writable работают и здесь:
1 2 3 4 | |
Батч в слое stream до uncork(). Ниже — socket options и ядро. Nagle и TCP_NODELAY — в опциях сокетов. Узкое утверждение: cork меняет буферизацию Node writable до ухода ниже.
Порядок колбэков записи следует порядку чанков в stream. Полезно освобождать память на чанк или двигать локальную send-очередь.
1 2 3 4 | |
Между enqueue и completion сокет может упасть — обрабатывайте err в колбэке.
Большие записи: socket.write() на 100 MiB Buffer принимает аллокацию в stream-путь. Backpressure после вызова не отменяет уже сделанную аллокацию. Стримите чанками.
1 2 3 4 | |
Старый паттерн. stream.pipeline() чище при двух stream, но сырые TCP-протоколы часто требуют парсера между read и write.
socket.write() может синхронно упасть на неверных аргументах или локальном состоянии. Сетевые сбои — error или колбэки после изменения нижнего состояния.
1 2 3 4 | |
EPIPE — запись после закрытия пути записи. ECONNRESET — reset. Тайминг решает, где увидите сбой. Пир reset — следующая запись может его обнаружить.
Backpressure на net.Socket — и stream, и сеть. Игнорирование false раздувает память в user space. Пир не читает — сжимается receive window, локальная отправка встаёт. Producer в JS может обогнать и очередь Node, и сеть.
Loopback в тестах быстрый, буферы щедрые — давление не видно. Тот же код падает на медленном клиенте или congested пути.
Безопасная форма — обычный stream-код:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Упрощённый пример: стоп при false, resume на drain. Очередь в closure, потому что drain вызывает pump без аргументов. В продакшене — cleanup, ошибки, состояние протокола.
end() передаёт намерение¶
socket.end() завершает writable-сторону. Можно передать финальный чанк.
1 | |
Для TCP — путь FIN из TCP: поток и сбои. Node ставит опциональные данные в очередь и закрывает запись. Пир может ещё слать. При allowHalfOpen: false по умолчанию Node обычно закрывает запись после конца readable — привычное полное закрытие.
Readable и writable — разные события:
1 2 3 4 5 6 7 | |
end — пир закончил отправку вам. close — handle закрыт. Между ними могут быть pending данные.
Для простых request-response часто достаточно end():
1 2 | |
Отправить байты и завершить нашу сторону записи. Очередь сохраняется через stream. TCP получает шанс на упорядоченное закрытие.
Half-open — где node:net показывает TCP. Сервер с allowHalfOpen: true оставляет writable открытым после end пира.
1 2 3 4 5 6 | |
end — пир прислал FIN. С allowHalfOpen: true вы сами шлёте FIN через socket.end(). Half-open как TCP-состояние — в TCP: поток и сбои; здесь — опция Node.
Держите half-open только с причиной в протоколе. Иначе дескрипторы, таймеры и состояние живут дольше, чем нужно.
finish — writable stream закончил и сбросил данные из реализации stream.
1 2 | |
finish — локальное завершение записи. end — конец чтения с пира. close — закрытие handle. Имена легко перепутать рядом с shutdown.
Простой сервер часто слушает все три:
1 2 3 | |
Разные рёбра. В баг-репорте «сокет ended» уточняйте, какое событие.
destroy() срывает локальное состояние¶
socket.destroy() немедленно закрывает сокет с точки зрения Node: stream, handle, отбрасывает queued writes в Node.
1 | |
Сбои, нарушения протокола, жёсткий shutdown, cleanup после таймаута.
С ошибкой:
1 | |
Ошибка уходит в teardown сокета, затем close. Парсер нашёл невалидный ввод — downstream получает причину.
destroy() и TCP RST связаны на границе API; пакеты решает ОС по состоянию, unread data, pending writes, платформе, опциям. Практически: queued userland writes могут пропасть, handle закрывается, read/write прекращаются.
Не для нормального завершения протокола:
1 2 | |
Запись может остаться в очереди Node при destroy(). Пир может ничего не получить, часть байтов или данные и reset — по таймингу. Нужна доставка через локальный путь — end() или колбэк записи до teardown.
Пути сбоя:
1 2 3 | |
Резкий отказ: прекращает read/write для пира, освобождает ресурсы после close.
Повторный destroy() безвреден. После destroyed дополнительные вызовы — no-op. Флаг destroyed.
1 2 3 | |
closed и destroyed для lifecycle-багов; чище смотреть порядок событий: error, end, timeout, close.
Есть destroySoon() — legacy: end writable после drain очереди, затем destroy. Новый код читает яснее с явным end() и destroy().
resetAndDestroy() для TCP — reset, когда возможно, затем destroy stream.
1 2 3 | |
Только когда reset — желаемое поведение протокола. Сильнее обычного destroy(). На pipe — ERR_INVALID_HANDLE_TYPE. Для TCP — connecting/connected; закрытый TCP — ERR_SOCKET_CLOSED и destroy. Обычным серверам редко нужен.
Шкала событий¶
Счастливый путь сервера:
1 2 3 4 5 6 | |
Реальность ветвится: reset клиента, destroy парсера, timeout, broken pipe на write, server.close() при живых соединениях.
Клиент:
1 2 3 4 5 | |
Ошибки до/после connect, на write, read, close. Один error handler на сокет. Необработанный error на EventEmitter всё ещё валит процесс.
1 2 3 | |
После ошибки сокета обычно следует close. Release logic — в close. Диагностика — в error.
data — один режим чтения. Есть stream-методы, pipe(), async iteration.
1 2 3 | |
Цикл до конца readable или ошибки. Async iteration из async-итераторов. Тот же нижний приёмный путь.
События сервера:
1 2 3 | |
listening — слушающий сокет активен. close — handle сервера закрыт. error — сбой уровня сервера, часто bind/listen.
События — наблюдения JS, переведённые из нижнего состояния. При странном тайминге смотрите оба: логи сокета и таблицу сокетов ОС.
Таймауты сообщают о неактивности¶
Таймаут сокета в node:net — таймер неактивности: за интервал не было активности сокета.
1 2 3 4 5 | |
timeout — уведомление. Node сокет не закрывает. Серверы часто уничтожают idle raw-сокеты: память, handle, состояние протокола.
Таймер сбрасывается при активности. Read и write считаются. Точный учёт — в реализации stream/socket Node; трактуйте как детектор idle, не дедлайн протокола.
1 2 3 4 | |
Финальное сообщение и graceful end. Может зависнуть, если пир не читает и последняя запись не продвигается. Жёсткая политика idle — destroy() после timeout. Вежливый протокол — end() и второй таймер на destroy.
Таймауты исходящего connect — отдельный дизайн. setTimeout() может ловить idle при connecting, но retry, AbortController и дедлайны запросов — в других главах. Для raw net.Socket: событие timeout приходит; политику закрытия пишете вы.
Типичный баг — handler только логирует:
1 2 | |
Idle записан, дескриптор открыт. Под нагрузкой сокеты копятся. Нужен cleanup в handler.
Локальные socket endpoint'ы¶
node:net поддерживает локальные endpoint'ы. Unix — UNIX domain sockets. Windows — named pipes.
UNIX domain socket — локальный IPC по пути в ФС. Socket API и stream-семантика, трафик на том же хосте. Адрес — путь, не IP:port.
1 2 3 4 5 6 7 | |
Для UNIX server server.address() — строка пути. AddressInfo — форма TCP; путь возвращается напрямую.
Клиент:
1 2 3 4 5 | |
Тот же Duplex stream. Ниже — локальный IPC. Частые сбои: права, cleanup пути, лимиты ОС.
После падения сервера файл пути может остаться — следующий listen(path) падает.
1 2 3 4 5 6 | |
В примерах часто; в продакшене не unlink'айте путь, которым владеет другой живой процесс. Безопаснее проверить, слушает ли кто-то endpoint, перед удалением stale state.
Windows named pipe — имя pipe вместо пути:
1 2 3 4 5 | |
Именованный локальный IPC endpoint. Те же net.Server/net.Socket где возможно. Правила имён и безопасность — специфичны для Windows.
Локальные endpoint'ы — same-host сервисы, sidecar, тестовые фикстуры, supervisor-managed демоны. Без TCP-портов и удалённой экспозиции. Нужны lifecycle, права и timeout policy.
TCP — когда endpoint достижим по IP и порту. Локальный сокет или pipe — оба пира на хосте и нужен OS-local endpoint. Передача handle IPC — отдельная тема (в nodebook — позже).
Закрытие сервера¶
server.close() прекращает accept новых соединений. Принятые сокеты живут своим циклом.
1 2 3 4 5 6 7 | |
После server.close() слушающий сокет закрывается или закрывается. Новые клиенты через этот сервер не подключаются. Уже принятые продолжают до end/error/destroy.
Колбэк close:
1 2 3 4 | |
Если сервер не был открыт — ошибка в колбэке. Событие close без аргумента ошибки. Колбэк — когда нужно отличить нормальное закрытие от «close при уже закрытом/не стартовавшем».
Учёт сокетов — ваша задача, если shutdown требует cleanup соединений:
1 2 3 4 5 6 | |
Set — способ вызвать end()/destroy() на принятых после закрытия listener.
1 2 3 4 5 | |
Эскиз локального shutdown. Продакшен draining — дедлайны, readiness, контракты supervisor, семантика HTTP, деплой (в nodebook — отдельные главы). Факт node:net: close сервера ≠ close соединений.
Владение соединением в raw net-сервере должно быть явным. Фреймворки прячут lifecycle запроса. node:net отдаёт сокет и уходит.
Правило: кто принял сокет, тот вешает terminal handlers.
1 2 3 4 5 | |
error сразу — сокет может упасть до конца setup. close сразу — любой путь должен снять tracking.
Per-socket state рядом с сокетом:
1 2 3 4 5 | |
Состояние умирает с сокетом. Общая map — удаляйте в close. Таймер — clear в close. Парсерные буферы — отпустите в close. Handle уже нет — приложение не должно держать память пира.
Таймеры легко утекают:
1 2 3 | |
Интервал держит ссылку на сокет через колбэк. Без cleanup в close — попытки писать в мёртвый сокет и удержание state.
Backpressure state тоже нужен terminal path. Producer на паузе из-за false при ошибке сокета должен resume или destroy источник — иначе вечное ожидание drain.
1 2 3 | |
Паттерн приложенческий, идея общая: связывайте lifecycle сокета с тем, что его кормит и потребляет. В raw TCP меньше абстракций — утечки Buffer, дескрипторов, застрявших producer и процесс, живой из-за сокетов.
Наблюдение:
1 2 3 4 | |
Принятый сокет ещё пишет — connected handle отдельно от закрываемого listener.
Повторные close могут давать ошибки по таймингу. Close — переход состояния; флаг, если shutdown запрашивают из нескольких мест.
1 2 3 4 5 6 7 | |
Меньше шума в логах shutdown. Документирует владение: один путь из accepting в closing.
Один локальный trace¶
Компактный сервер и клиент показывают путь объектов без DNS и удалённой маршрутизации.
1 2 3 4 5 | |
Создан объект сервера, привязан TCP listener на IPv4 loopback с портом от ОС. Порт неизвестен до завершения listen.
1 2 3 4 | |
Connect клиента после listening на точный порт ОС. Числовой адрес убирает DNS из пути.
Обычный порядок событий:
1 2 3 4 5 6 7 | |
Точное чередование connection и client connect зависит от планировщика. Оба означают: состояние уже прошло ниже JavaScript. Сервер accept connected socket. Клиент установил свой connected socket.
Логи для нижних идентичностей:
1 2 3 4 | |
У принятого сокета local port равен listening port. remote port — эфемерный порт клиента.
Зеркало на клиенте:
1 2 3 4 | |
Local клиента — эфемерный. Remote — listening port сервера. Одно TCP-соединение, разные локальные представления.
Cleanup:
1 2 3 | |
Клиент закрылся после pong\n и FIN пира. Затем сервер закрывает listener. Принятый сокет уже прошёл свой close. server.close() закрывает только listener.
Три сокетных объекта на разных слоях:
1 2 3 | |
В JS — один net.Server и два net.Socket. В ОС — один listening socket и одно TCP-соединение с двух сторон на loopback. Форма та же, что у удалённого соединения; путь локальный.
Почему в raw TCP-тестах порт 0: ОС выбирает свободный порт, тест читает после listening, клиент бьёт в это значение. Параллельные прогоны не делят один глобальный порт.
Версия с видимым сбоем connect:
1 2 3 4 5 | |
Порт 9 обычно закрыт локально — вероятен ECONNREFUSED. Ошибка этапа установления. Accept не было. data от этой попытки не последует.
Смените только семейство адресов — результат может измениться:
1 2 3 | |
Сервер на 127.0.0.1, клиент на IPv6 loopback — сбой при «здоровом» IPv4 listener. Порт совпал, адрес сокета — нет. Семейство — часть endpoint.
Тот же trace для UNIX domain с другой формой endpoint:
1 2 3 4 5 6 | |
server.address() — строка пути. Объект всё ещё net.Socket. Ниже — pipe/socket endpoint вместо TCP. Код, которому нужны только stream read/write, почти не меняется. Логи endpoint должны учитывать другую форму адреса.
Отладка границы объектов¶
Быстрее всего логировать переход, который сменил владение.
Сервер — результат bind и endpoint принятого сокета:
1 2 3 4 | |
Клиент — connect и ошибки:
1 2 3 | |
socket.address() для TCP — локальный AddressInfo, пара server.address(). Удалённые поля — remoteAddress, remotePort, remoteFamily.
Нет connection — сначала listener: семейство адреса клиента, bind на loopback при клиенте вне namespace, error на listen(), порт в таблице сокетов хоста.
Нет connect у клиента — разделите lookup, маршрут и TCP setup. Числовой адрес убирает DNS. Логируйте err.code, err.address, err.port. Сервер слушает то же семейство и адрес?
«Записи исчезают» — смотрите close path. write + destroy() может отбросить очередь. Колбэк записи — прогресс локальной очереди, не обработка пиром. Подтверждение протокола — байты, прочитанные от пира.
Копятся idle-сокеты — handlers таймаута. setTimeout() эмитирует событие; политику закрытия пишете вы. Handler только с логом — утечка под стабильным трафиком.
Код node:net маленький, потому что Node уже сделал обёртку. Сложность — помнить, какой объект чем владеет. net.Server — accept. net.Socket — один разговор. TCPWrap и libuv — мост к event loop. ОС — реальное состояние сокета. Когда разделение ясно, API перестаёт казаться магией и становится проверяемым.
Связанное чтение¶
- Предыдущая: TCP в Node.js: поток данных и сбои
- Далее: UDP и модуль dgram