Node.js 的 net.Socket 是一個 TCP (Layer 4) 的抽象 & 封裝,讓開發者不必理解 TCP 的架構,直接使用封裝好的 API 就可以建立 TCP 連線、傳輸資料
所謂的 TCP 架構,包含但不限於以下:
透過封裝好的 API,就可以不必關注以上細節
TCP socket 這個抽象 & 封裝並非 Node.js 獨有的概念,許多程式語言都有實作
身為前端工程師,正常要發起 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");
net.Socket 繼承 stream.Duplex,故包含 Readable 跟 Writable 的所有 methods正常要用 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.Server、net.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.connect、net.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.setTimeout 跟 socket.connect 而已XD
我們現在學會了創建 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 層級的 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

keepAliveInitialDelay: 3000 的語意是指 TCP 三方交握,過了 3 秒都沒傳輸資料的話,server 就會發出 "heartbeat"在 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
透過以上觀察,我們可以發現:
allowHalfOpen,所以連線還是會維持半開on("connection"),因為 BlockList 已經擋掉了這個連線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

透過以上觀察,我們可以發現第二個連線,其行為與觸發 blocklist 幾乎是一樣的:
allowHalfOpen,所以連線還是會維持半開on("connection"),而是會觸發 on("drop")
在這篇文章,我們學到了
net.Socket 發送 HTTP requestnet.createServer 接收 HTTP request,並且回傳 HTTP responseBlocklist 跟 maxConnections 這兩個控制 TCP 連線 "來源" 跟 "上限" 的參數接下來,會進到 net.Socket 的生命週期~