Master TCP/UDP, Sockets & Connection Pools in Node.js – From Basics to Advanced
This article walks developers through the fundamentals of the OSI model, TCP three‑way handshake and UDP characteristics, explains socket concepts, demonstrates Node.js server and client implementations with heartbeats and custom protocols, and details how to build and use a socket connection pool with the generic‑pool library.
Introduction
Developers often hear terms such as HTTP, TCP/IP, UDP, Socket, long‑living Socket connections and Socket connection pools, but the relationships, differences and underlying principles are not always clear. This article starts from basic network protocol concepts and gradually explains each topic up to a full Socket connection‑pool implementation.
OSI Seven‑Layer Model
The OSI (Open System Interconnection) model divides network communication into seven layers: Physical, Data Link, Network, Transport, Session, Presentation and Application. The diagram below shows typical protocols and hardware associated with each layer.
From the picture we see that IP belongs to the Network layer, TCP/UDP to the Transport layer, and HTTP to the Application layer. OSI does not define a Socket; the concept will be introduced later with code examples.
TCP and UDP Connections
TCP provides reliable, connection‑oriented communication, while UDP offers an unreliable, connection‑less service with lower overhead. The article first describes the TCP three‑way handshake and four‑step termination, then compares the two protocols.
TCP Three‑Way Handshake
First handshake: Client sends a SYN packet (SYN=1, Sequence Number=x) and waits for server acknowledgment. Second handshake: Server replies with SYN‑ACK (Acknowledgment Number=x+1, SYN=1, Sequence Number=y). Third handshake: Client sends ACK (Acknowledgment Number=y+1). Both sides enter ESTABLISHED state.
TCP Four‑Step Termination
First step: Host1 sends FIN, enters FIN_WAIT_1. Second step: Host2 replies with ACK, Host1 enters FIN_WAIT_2. Third step: Host2 sends FIN, enters LAST_ACK. Fourth step: Host1 replies with ACK, enters TIME_WAIT, then finally CLOSED after 2 MSL.
UDP does not require these handshakes, which is why it is faster but less reliable.
Socket (套接字)
A Socket is an API that abstracts the TCP/IP stack (or other network stacks) and provides functions such as create, listen, accept, connect, read and write. Different languages expose their own Socket libraries.
Node.js Server Example
const net = require('net');
const server = net.createServer();
server.on('connection', (client) => {
client.write('Hi!
');
client.write('Bye!
');
});
server.listen(9000);Testing with curl and telnet shows the server responding with the two messages.
Node.js Client Example
const net = require('net');
const client = new net.Socket();
client.connect(9000, '127.0.0.1', () => {});
client.on('data', (chunk) => {
console.log('data', chunk.toString());
});Long‑Living Connections and Heartbeats
A long‑living connection (persistent TCP connection) allows multiple data packets to be sent without repeatedly establishing new connections. To keep the connection alive, a heartbeat packet is periodically sent from client to server (or vice‑versa) to indicate that the endpoint is still reachable.
Heartbeat Implementation (Server)
const net = require('net');
let clientList = [];
const HEARTBEAT = 'HEARTBEAT';
const server = net.createServer();
server.on('connection', (client) => {
clientList.push(client);
client.on('data', (chunk) => {
const content = chunk.toString();
if (content === HEARTBEAT) {
console.log('Received heartbeat');
} else {
console.log('Received data:', content);
client.write('Server reply:' + content);
}
});
client.on('end', () => {
clientList.splice(clientList.indexOf(client), 1);
});
});
server.listen(9000);
setInterval(() => {
clientList.forEach((c) => {
if (c.writable) c.write(HEARTBEAT);
});
}, 10000);Heartbeat Implementation (Client)
const net = require('net');
const HEARTBEAT = 'HEARTBEAT';
const client = new net.Socket();
client.connect(9000, '127.0.0.1', () => {});
client.on('data', (chunk) => {
const content = chunk.toString();
if (content === HEARTBEAT) {
console.log('Received heartbeat');
} else {
console.log('Received data:', content);
}
});
setInterval(() => client.write(HEARTBEAT), 10000);
setInterval(() => client.write(new Date().toUTCString()), 5000);Defining a Custom Protocol
When the application layer protocol is custom, the following aspects must be defined:
Heartbeat packet format and handling.
Message header that indicates the length of the payload.
Payload serialization format (e.g., JSON).
Example header format: length:00000000000xxxx where the last four digits represent the payload length.
Custom Protocol Server (Node.js)
const net = require('net');
const server = net.createServer();
let clientList = [];
const HEARTBEAT = 'HeartBeat';
function getHeader(num) {
return 'length:' + (Array(13).join('0') + num).slice(-13);
}
server.on('connection', (client) => {
client.name = client.remoteAddress + ':' + client.remotePort;
console.log('Client connected:', client.name);
clientList.push(client);
let chunks = [];
let length = 0;
client.on('data', (chunk) => {
const content = chunk.toString();
if (content === HEARTBEAT) {
console.log('Received heartbeat');
} else {
if (content.indexOf('length:') === 0) {
length = parseInt(content.substring(7, 20));
chunks = [chunk.slice(20)];
} else {
chunks.push(chunk);
}
const heap = Buffer.concat(chunks);
if (heap.length >= length) {
try {
const data = JSON.parse(heap.toString());
console.log('Received data', data);
const resp = 'Server data:' + heap.toString();
const respBuf = Buffer.from(resp);
client.write(getHeader(respBuf.length));
client.write(respBuf);
} catch (e) {
console.log('Parse error');
}
}
}
});
});
server.listen(9000);Custom Protocol Client (Node.js)
const net = require('net');
const HEARTBEAT = 'HeartBeat';
const client = new net.Socket();
function getHeader(num) {
return 'length:' + (Array(13).join('0') + num).slice(-13);
}
client.connect(9000, '127.0.0.1', () => {});
let chunks = [];
let length = 0;
client.on('data', (chunk) => {
const content = chunk.toString();
if (content === HEARTBEAT) {
console.log('Received heartbeat');
} else {
if (content.indexOf('length:') === 0) {
length = parseInt(content.substring(7, 20));
chunks = [chunk.slice(20)];
} else {
chunks.push(chunk);
}
const heap = Buffer.concat(chunks);
if (heap.length >= length) {
try {
console.log('Received data', JSON.parse(heap.toString()));
} catch (e) {
console.log('Parse error');
}
}
}
});
setInterval(() => {
const data = JSON.stringify({msg: new Date().toUTCString()});
const buf = Buffer.from(data);
client.write(getHeader(buf.length));
client.write(buf);
}, 5000);
setInterval(() => client.write(HEARTBEAT), 10000);Socket Connection Pool
A Socket connection pool maintains a set of long‑living Socket objects, automatically validates their health, discards invalid ones, and creates new connections as needed. The pool consists of:
Idle (available) connection queue.
Active (in‑use) connection queue.
Waiting request queue.
Invalid‑connection removal logic.
Configurable pool size.
Connection creation logic.
Typical usage scenario: a request asks the pool for a connection; if an idle connection exists it is handed over, otherwise a new connection is created up to the maximum pool size. When the request finishes, the connection returns to the idle queue.
Pool Initialization (generic‑pool)
'use strict';
const net = require('net');
const genericPool = require('generic-pool');
function createPool(config) {
const options = Object.assign({
fifo: true,
priorityRange: 1,
testOnBorrow: true,
autostart: true,
min: 10,
max: 0,
evictionRunIntervalMillis: 0,
numTestsPerEvictionRun: 3,
softIdleTimeoutMillis: -1,
idleTimeoutMillis: 30000
}, config.options);
const factory = {
create: function () {
return new Promise((resolve, reject) => {
const socket = new net.Socket();
socket.setKeepAlive(true);
socket.connect(config.port, config.host);
socket.on('connect', () => resolve(socket));
socket.on('error', (err) => reject(err));
});
},
destroy: function (socket) {
return new Promise((resolve) => {
socket.destroy();
resolve();
});
},
validate: function (socket) {
return new Promise((resolve) => {
if (socket.destroyed || !socket.readable || !socket.writable) {
resolve(false);
} else {
resolve(true);
}
});
}
};
const pool = genericPool.createPool(factory, options);
pool.on('factoryCreateError', (err) => {
const req = pool._waitingClientsQueue.dequeue();
if (req) req.reject(err);
});
return pool;
}
let pool = createPool({port: 9000, host: '127.0.0.1', options: {min: 0, max: 10}});Using the Connection Pool
async function request(dataBuff) {
const client = await pool.acquire();
return new Promise((resolve, reject) => {
let chunks = [];
let length = 0;
client.setTimeout(10000);
client.once('error', (err) => {
pool.destroy(client);
reject(err);
});
client.once('timeout', () => {
pool.destroy(client);
reject('socket timeout');
});
const header = getHeader(dataBuff.length);
client.write(header);
client.write(dataBuff);
client.on('data', (chunk) => {
const content = chunk.toString();
if (content.indexOf('length:') === 0) {
length = parseInt(content.substring(7, 20));
chunks = [chunk.slice(20)];
} else {
chunks.push(chunk);
}
const heap = Buffer.concat(chunks);
if (heap.length >= length) {
pool.release(client);
try {
resolve(JSON.parse(heap.toString()));
} catch (e) {
reject(e);
}
}
});
});
}
request(Buffer.from(JSON.stringify({a: 'a'})))
.then(data => console.log('Response', data))
.catch(err => console.error(err));Log output shows that the first two requests create new sockets, while subsequent requests reuse existing connections from the pool.
Key Parts of generic‑pool Implementation
The core of the pool resides in lib/Pool.js. Below are excerpts of the most relevant methods.
Constructor
constructor(Evictor, Deque, PriorityQueue, factory, options) {
factoryValidator(factory);
this._config = new PoolOptions(options);
this._factory = factory;
this._draining = false;
this._started = false;
this._waitingClientsQueue = new PriorityQueue(this._config.priorityRange);
this._availableObjects = new Deque();
this._allObjects = new Set();
this._resourceLoans = new Map();
this._evictor = new Evictor();
if (this._config.autostart) this.start();
}acquire()
acquire(priority) {
if (!this._started && !this._config.autostart) this.start();
if (this._draining) return Promise.reject(new Error('pool is draining'));
const request = new ResourceRequest(this._config.acquireTimeoutMillis, this._Promise);
this._waitingClientsQueue.enqueue(request, priority);
this._dispense();
return request.promise;
}_dispense()
_dispense() {
const numWaiting = this._waitingClientsQueue.length;
if (numWaiting < 1) return;
const shortfall = numWaiting - this._potentiallyAllocableResourceCount;
const toCreate = Math.min(this.spareResourceCapacity, shortfall);
for (let i = 0; i < toCreate; i++) this._createResource();
if (this._config.testOnBorrow) {
const needed = numWaiting - this._testOnBorrowResources.size;
const actual = Math.min(this._availableObjects.length, needed);
for (let i = 0; i < actual; i++) this._testOnBorrow();
} else {
const dispatchable = Math.min(this._availableObjects.length, numWaiting);
for (let i = 0; i < dispatchable; i++) this._dispatchResource();
}
}_dispatchResource()
_dispatchResource() {
if (this._availableObjects.length < 1) return false;
const pooled = this._availableObjects.shift();
this._dispatchPooledResourceToNextWaitingClient(pooled);
return true;
}_dispatchPooledResourceToNextWaitingClient()
_dispatchPooledResourceToNextWaitingClient(pooled) {
const req = this._waitingClientsQueue.dequeue();
if (!req || req.state !== Deferred.PENDING) {
this._addPooledResourceToAvailableObjects(pooled);
return false;
}
const loan = new ResourceLoan(pooled, this._Promise);
this._resourceLoans.set(pooled.obj, loan);
pooled.allocate();
req.resolve(pooled.obj);
return true;
}These snippets illustrate how the pool creates resources, validates them, allocates them to waiting clients, and recycles them after use.
Conclusion
The article covered OSI fundamentals, TCP/UDP mechanics, Socket programming in Node.js, heartbeat handling, custom protocol design, and a complete Socket connection‑pool solution using the generic‑pool library, providing a practical reference for building high‑performance, long‑living network services.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Linux
Focused on sharing Linux/Unix content, covering fundamentals, system development, network programming, automation/operations, cloud computing, and related professional knowledge.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
