iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
JavaScript

Learn HTTP With JS(2)系列 第 8

Node.js net.Socket 教學:TCP 連線應用

  • 分享至 

  • xImage
  •  

前言

Node.js 的 net.Socket 是一個 TCP (Layer 4) 的抽象 & 封裝,讓開發者不必理解 TCP 的架構,直接使用封裝好的 API 就可以建立 TCP 連線、傳輸資料

所謂的 TCP 架構,包含但不限於以下:

  1. TCP Finite State Machine
  2. TCP Header
  3. TCP Flags
  4. TCP HandShake
  5. TCP Checksum

透過封裝好的 API,就可以不必關注以上細節

TCP socket 這個抽象 & 封裝並非 Node.js 獨有的概念,許多程式語言都有實作

As an HTTP client

身為前端工程師,正常要發起 HTTP request

fetch("http://example.com");

若以 net 模組來達成這件事情

import net from "net";

const socket = net.connect({
  host: "example.com",
  port: 80,
});
socket.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
  • 我們平常用的 HTTP client (curl, fetch, Postman...) 底層也是透過 TCP client socket 來傳輸 raw bytes
  • HTTP client 幫開發者處理好了 HTTP parsing, connection reuse 等等底層細節
  • stream-overview 有提到 net.Socket 繼承 stream.Duplex,故包含 ReadableWritable 的所有 methods

As an HTTP server

正常要用 Node.js 創建一個 HTTP server

import http from "http";

const httpServer = http.createServer();
httpServer.on("request", (req, res) => {
  res.end("ok");
});
httpServer.listen(5000);

用 curl 戳看看,確認真的有收到 HTTP response

curl http://localhost:5000 -v

* Host localhost:5000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:5000...
* Connected to localhost (::1) port 5000
> GET / HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Connection: keep-alive
< Keep-Alive: timeout=5
< Content-Length: 2
<
* Connection #0 to host localhost left intact
ok%

若以 net 模組來達成這件事情

import net from "net";

const server = net.createServer({ allowHalfOpen: true });
server.on("connection", (socket) => {
  socket.on("data", (chunk) => {
    // todo: implement HTTP Parser to parse chunk...
    socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok");
  });
});
server.listen(5000);

用 curl 戳看看,確認真的有收到 HTTP response

curl http://localhost:5000 -v

* Host localhost:5000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:5000...
* Connected to localhost (::1) port 5000
> GET / HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Content-Length: 2
<
* Connection #0 to host localhost left intact
ok%

net.Servernet.createServer

要創建一個 TCP server 的話,可以使用

import net from "net";

const server5000 = new net.Server();
server5000.listen(5000);

const server5001 = net.createServer();
server5001.listen(5001);

這兩者是等效的,其中 createServer 是一個 wrapper function,單純是語意上比較好理解,這也是 Node.js 的一貫風格

https://github.com/nodejs/node/blob/main/lib/net.js

function createServer(options, connectionListener) {
  return new Server(options, connectionListener);
}

net.connectnet.createConnection

要創建一個 TCP client 的話,可以使用

import net from "net";

const socket = net.connect({
  host: "localhost",
  port: 5000,
});

const socket2 = net.createConnection({
  host: "localhost",
  port: 5000,
});

這兩者是完全一樣的 function,只是名稱不一樣

https://github.com/nodejs/node/blob/main/lib/net.js

module.exports = {
  connect,
  createConnection: connect,
};

並且從 Node.js 官方文件 可以得知這是用來創建 net.Socket 的 factory function

A factory function, which creates a new net.Socket, immediately initiates connection with socket.connect(), then returns the net.Socket that starts the connection.

直接看 Node.js 原始碼 的話

function connect(...args) {
  const normalized = normalizeArgs(args);
  const options = normalized[0];
  debug("createConnection", normalized);
  const socket = new Socket(options);

  if (options.timeout) {
    socket.setTimeout(options.timeout);
  }

  return socket.connect(normalized);
}

其實就是幫忙設定 socket.setTimeoutsocket.connect 而已XD

TCP client / server 小結

我們現在學會了創建 TCP client / server 的語法,並且也成功傳輸 HTTP/1.1 plain text。接下來要針對 net.Socket 深入講解

net.Socket 跟 TCP socket 在本篇文章會大量提到,並且代表的是同樣的概念

net.Socket 也有 keepAlive ?!

我在去年寫的 HTTP 文章 Keep-Alive 和 Connection 有提到 keepAlive,但 HTTP 層級跟 TCP socket 層級的 keepAlive 是不同的概念

HTTP 層級的 keepAlive: timeout=5, max=200 代表的是

  • 這條 TCP connection 若 5 秒沒有傳輸資料則關閉
  • 這條 TCP connection 最多只能傳送 200 個 HTTP round trip 就要關閉

而 TCP 層級的 keepAlive 則是一個 "heartbeat" 機制,可由 client 或 server 發出,確認對方是否還活著

以 server 發出 keepAlive "heartbeat" 為例

import net from "net";

const server = net.createServer({
  keepAlive: true,
  keepAliveInitialDelay: 3000,
});
server.listen(5000);

const socket = net.connect({
  host: "localhost",
  port: 5000,
  keepAlive: false,
});

Wireshark 抓 Loopback: lo0,加上篩選 tcp.port == 5000

tcp-keep-alive

  • 可以看到每 3 秒,由 TCP server 發出 TCP Keep-Alive 封包,client 回應 TCP Keep-Alive ACK 封包
  • 雖說 keepAliveInitialDelay: 3000 的語意是指 TCP 三方交握,過了 3 秒都沒傳輸資料的話,server 就會發出 "heartbeat"
  • 但實際上我用 Node.js v24.13.0 (LTS) + macOS 15.6.1 測試的結果,每 3 秒就會傳送一次 TCP Keep-Alive,這邊我沒深入研究原因

BlockList

net.createServer 有個參數是 blockList,可以阻擋特定 IP addresses, ranges, 或 subnets 的連線

使用方式也很簡單,先創建一個 BlockList

import { BlockList } from "net";

const blockList = new BlockList();
blockList.addAddress("127.0.0.1", "ipv4");

再來創建一個最小 TCP server

import net from "net";

const server = net.createServer({ blockList });
server.listen(5000);
server.on("connection", (serverSocket) => console.log("connection"));

最後創建一個 TCP client 連過去

import net from "net";

const clientSocket = net.createConnection({
  host: "localhost",
  port: 5000,
  family: 4,
  allowHalfOpen: true
});
clientSocket.on("connectionAttempt", () => console.log("connectionAttempt"));
clientSocket.on("connect", () => console.log("connect"));
clientSocket.on("connectionAttemptFailed", () => console.log("connectionAttemptFailed"));
clientSocket.on("connectionAttemptTimeout", () => console.log("connectionAttemptTimeout"));
clientSocket.on("end", () => console.log("end"));
clientSocket.on("error", () => console.log("err"));
clientSocket.on("close", () => console.log("close"));

// Prints
// connectionAttempt
// connect
// end

若用 Wireshark 抓 Loopback: lo0,加上篩選 tcp.port == 5000
wireshark-blocklist

透過以上觀察,我們可以發現:

  • TCP 有成功三次交握,並且 server 後續立即發送 FIN (Finish) 封包
  • 由於 TCP client 有設定 allowHalfOpen,所以連線還是會維持半開
  • 對 TCP server 來說,不會觸發 on("connection"),因為 BlockList 已經擋掉了這個連線

maxConnections

Node.js 的 net.Server 有個 maxconnections 可以控制,我們來測測看

啟動 TCP server,將 maxConnections 設成 1

import net from "net";

const server = net.createServer();
server.maxConnections = 1;
server.listen(5000);
server.on("connection", () => console.log("connection"));
server.on("drop", console.log);

建立 2 個 TCP client,依序連過去

import net from "net";

const clientSocket1 = net.createConnection({
  host: "localhost",
  port: 5000,
  family: 4,
  allowHalfOpen: true,
});
clientSocket1.on("connect", () => {
  const clientSocket2 = net.createConnection({
    host: "localhost",
    port: 5000,
    family: 4,
    allowHalfOpen: true,
  });
});

最終結果,第 1 個成功建立連線,第 2 個觸發 on("drop")

connection
[Object: null prototype] {
  localAddress: '::ffff:127.0.0.1',
  localPort: 5000,
  localFamily: 'IPv6',
  remoteAddress: '::ffff:127.0.0.1',
  remotePort: 7278,
  remoteFamily: 'IPv6'
}

若用 Wireshark 抓 Loopback: lo0,加上篩選 tcp.port == 5000

wireshark-maxconnections

透過以上觀察,我們可以發現第二個連線,其行為與觸發 blocklist 幾乎是一樣的:

  • TCP 有成功三次交握,並且 server 後續立即發送 FIN (Finish) 封包
  • 由於 TCP client 有設定 allowHalfOpen,所以連線還是會維持半開
  • 對 TCP server 來說,不會觸發 on("connection"),而是會觸發 on("drop")

小結

在這篇文章,我們學到了

  • 從 HTTP/1.1 client 的視角,用 net.Socket 發送 HTTP request
  • 從 HTTP/1.1 server 的視角,用 net.createServer 接收 HTTP request,並且回傳 HTTP response
  • TCP Keep-Alive 跟 HTTP Keep-Alive 的差異
  • BlocklistmaxConnections 這兩個控制 TCP 連線 "來源""上限" 的參數

接下來,會進到 net.Socket 的生命週期~

參考資料


上一篇
stream objectMode 與 pipe 實戰教學
下一篇
Node.js TCP Client Socket 生命週期:lookup、connect 事件
系列文
Learn HTTP With JS(2)9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言