iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
JavaScript

Learn HTTP With JS(2)系列 第 10

深入 net.Socket:4-way handshake、socket.end vs destroy 差異

  • 分享至 

  • xImage
  •  

前言

承接上一篇 Node.js TCP Client Socket 生命週期,接下來要介紹的是 TCP client socket 跟 server socket 共同的生命週期

TCP socket 生命週期 3:讀寫資料

net.Socket 是繼承 stream.Duplex,這是一個可讀可寫的資料流

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

function Socket(options) {
  // ...
  stream.Duplex.call(this, options);
  // ...
}
  • 讀資料,基本上就是繼承 stream.Readable 的 methods 跟 events
  • 寫資料,基本上就是繼承 stream.Writable 的 methods 跟 events

主要多了

import net from "net";

// localhost:5000 架一個簡易的 TCP server
const server = net.createServer();
server.listen(5000, "localhost");
server.on("connection", (socket) => {
  const cb = () => console.log({ bytesWritten: socket.bytesWritten });
  // server 依序寫入 123, 456 給 client
  socket.write("123", cb);
  setTimeout(() => socket.write("456", cb));
});

// 連線到 TCP server,讀取資料
const socket = net.connect({ host: "localhost", port: 5000 });
socket.setEncoding("utf8");
socket.on("data", (data) => console.log(data, socket.bytesRead));

// Prints
// { bytesWritten: 3 }
// { data: '123', bytesRead: 3 }
// { bytesWritten: 6 }
// { data: '456', bytesRead: 6 }

TCP socket 生命週期 4:關閉連線

TCP 的 4-way-Handshake 用來關閉連線,client 跟 server 皆可以主動發起這個流程

FIN ACK
Full name Finish Acknowledgement
Semantics I'm done writing data — half-close writable side Confirms receipt of a TCP packet (including FIN)
Trigger Via socket.end() Node.js provides no API to control sending of ACK packets
Receipt Via socket.on("end") Node.js provides no event to monitor the arrival of ACK packets

client 透過 socket.end() 發起關閉連線

import net from "net";
import assert from "assert";

// TCP server
const server = net.createServer({ allowHalfOpen: true });
server.listen(5000, "localhost");
server.on("connection", (serverSocket) => {
  serverSocket.on("end", () => {
    assert(serverSocket.readableEnded === true); 
    console.log(`Step 2: TCP server received client's FIN via socket.on("end")`);
    console.log("Step 3: TCP server initiates FIN via socket.end()");
    serverSocket.end();
    assert(serverSocket.writableEnded === true);
  });
});

// TCP client
const clientSocket = net.connect({ host: "localhost", port: 5000, allowHalfOpen: true });
clientSocket.on("connect", () => {
  console.log("Step 1: TCP client initiates FIN via socket.end()");
  clientSocket.end();
  assert(clientSocket.writableEnded === true);
});
clientSocket.on("end", () => {
  assert(clientSocket.readableEnded === true);
  console.log(`Step 4: TCP client received server's FIN via socket.on("end")`);
});

// Prints
// Step 1: TCP client initiates FIN via socket.end()
// Step 2: TCP server received client's FIN via socket.on("end")
// Step 3: TCP server initiates FIN via socket.end()
// Step 4: TCP client received server's FIN via socket.on("end")

Wireshark 抓 Loopback: lo0,加上篩選 tcp.port == 5000,觀察由 TCP client 主動發起的關閉連線

tcp-client-4-way

流程如下:

tcp-client-server-socket-end

server 透過 socket.end() 發起關閉連線

import net from "net";
import assert from "assert";

// TCP server
const server = net.createServer();
server.listen(5000, "localhost");
server.on("connection", (serverSocket) => {
  console.log("Step 1: TCP server initiates FIN via socket.end()");
  serverSocket.end();
  assert(serverSocket.writableEnded === true);
  serverSocket.on("end", () => {
    assert(serverSocket.readableEnded === true);
    console.log(`Step 4: TCP server received client's FIN via socket.on("end")`);
  });
});

// TCP client
const clientSocket = net.connect({ host: "localhost", port: 5000, allowHalfOpen: true });
clientSocket.on("end", () => {
  assert(clientSocket.readableEnded === true);
  console.log(`Step 2: TCP client received server's FIN via socket.on("end")`,);
  console.log("Step 3: TCP client initiates FIN via socket.end()");
  clientSocket.end();
  assert(clientSocket.writableEnded === true);
});

// Prints
// Step 1: TCP server initiates FIN via socket.end()
// Step 2: TCP client received server's FIN via socket.on("end")
// Step 3: TCP client initiates FIN via socket.end()
// Step 4: TCP server received client's FIN via socket.on("end")

Wireshark 抓 Loopback: lo0,加上篩選 tcp.port == 5000,觀察由 TCP server 主動發起的關閉連線

tcp-server-4-way

流程如下:

tcp-server-client-socket-end

TCP socket 生命週期 4-1:強制關閉連線

正常情況走的是 TCP 4-way-Handshake 優雅的關閉連線,但也有情況必須強制關閉連線,這時候就需要用到 destroy 開頭的 methods

以上是 Node.js 官方文件的描述,我翻了 Node.js 原始碼,發現這三個 method,最後都會走到 destroy_destroy

socket-destroy-methods

具體差異是,destroySoon 會等 writableFinished 再呼叫 destroy

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

Socket.prototype.destroySoon = function () {
  if (this.writable) this.end();

  if (this.writableFinished) this.destroy();
  else this.once("finish", this.destroy);
};

resetAndDestroy 則是會設定 resetAndClosingtrue 再呼叫 destroy,TCP 層級會發送 RST (Reset) 封包

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

Socket.prototype.resetAndDestroy = function () {
  if (this._handle) {
    if (!(this._handle instanceof TCP)) throw new ERR_INVALID_HANDLE_TYPE();
    if (this.connecting) {
      debug("reset wait for connection");
      this.once("connect", () => this._reset());
    } else {
      this._reset();
    }
  } else {
    this.destroy(new ERR_SOCKET_CLOSED());
  }
  return this;
};

Socket.prototype._reset = function () {
  debug("reset connection");
  this.resetAndClosing = true;
  return this.destroy();
};

Socket.prototype._destroy = function (exception, cb) {
  // ...
  if (this.resetAndClosing) {
    this.resetAndClosing = false;
    const err = this._handle.reset(() => {
      debug("emit close");
      this.emit("close", isException);
    });
    if (err) this.emit("error", new ErrnoException(err, "reset"));
  }
  // ...
};

resetAndDestroy 的簡化流程:

socket-reset-and-destroy-flow

P.S. 這裡的 _handle 應是指 JavaScript 跟底層 C/C++ 介接的橋樑

socket.end() vs socket.destroy()

兩者都是關閉連線,但行為上卻不一樣

socket.end() socket.destroy()
TCP Flag FIN FIN or RST
socket.writable false false
socket.readable true false

小結

net.Socket 說穿了,就是把 stream.Duplex 這個抽象的讀寫流,加上 TCP 的功能具象化

stream.Duplex net.Socket (TCP)
on("data") emits a chunk emits a chunk received over TCP
on("end") readable = false readable = false, received TCP FIN
write buffers a chunk sends a chunk over TCP
end writable = false writable = false, sends TCP FIN
destroy writable = falsereadable = false writable = falsereadable = false,closes TCP handle (FIN or RST)

參考資料


上一篇
Node.js TCP Client Socket 生命週期:lookup、connect 事件
下一篇
Node.js http.Agent:TCP 連線池管理教學
系列文
Learn HTTP With JS(2)21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言