Socket #
Socket adalah mekanisme komunikasi dua arah antara dua proses melalui jaringan — fondasi dari hampir semua komunikasi real-time dalam aplikasi modern. TypeScript membawa keunggulan signifikan ke socket programming melalui sistem tipe: tipe pesan yang dibagi antara server dan klien, type-safe event handlers, dan penanganan error yang eksplisit. Tanpa type safety, socket programming rentan terhadap kelas bug yang sulit dilacak — klien mengirim tipe data yang berbeda dari yang diharapkan server, event yang salah dieja, atau field yang tidak ada. Artikel ini membahas tiga lapisan socket programming di TypeScript: TCP socket dengan modul net (untuk komunikasi antar proses), Socket.io (untuk real-time dengan fallback otomatis), dan praktik keamanan yang wajib diterapkan di produksi.
Jenis-Jenis Socket #
Sebelum masuk ke implementasi, penting memahami perbedaan antara jenis socket yang tersedia:
flowchart TD
A[Komunikasi Real-time] --> B{Protokol?}
B -- TCP/IP langsung --> C[modul net\nNode.js]
B -- HTTP upgrade --> D[WebSocket\nprotokol ws://]
B -- Abstraksi di atas WS --> E[Socket.io\nlibrary]
C --> C1[Kasus: komunikasi antar\nservice di jaringan internal\ngame server, proxy, protokol kustom]
D --> D1[Kasus: browser ke server\nreal-time murni, performa tinggi]
E --> E1[Kasus: aplikasi chat, notifikasi\nbutuh fallback & room management]
style C fill:#339af0,color:#fff
style D fill:#51cf66,color:#fff
style E fill:#fcc419,color:#000TCP Socket dengan Modul net
#
Modul net Node.js menyediakan TCP socket tingkat rendah. Tidak ada framing bawaan — data diterima sebagai stream byte yang harus kamu parse sendiri. Ini krusial untuk dipahami agar tidak terjebak bug “partial data”:
Protocol Framing — Menghindari Partial Data #
TCP adalah stream protocol — tidak ada jaminan bahwa satu socket.write() akan diterima dalam satu event data. Data bisa dipecah atau digabung. Solusinya adalah framing: setiap pesan diawali dengan panjang data (length prefix):
// src/protocol.ts — definisi protokol framing dan tipe pesan
import { Buffer } from "buffer";
// Format frame: [4 bytes panjang pesan][N bytes data JSON]
const UKURAN_HEADER = 4; // 4 byte = uint32, maks ~4GB per pesan
export function enkodeFrame(data: object): Buffer {
const json = JSON.stringify(data);
const body = Buffer.from(json, "utf-8");
const header = Buffer.allocUnsafe(UKURAN_HEADER);
header.writeUInt32BE(body.length, 0); // Big-endian unsigned 32-bit
return Buffer.concat([header, body]);
}
// Parser untuk stream yang mungkin berisi partial atau multiple frames
export class StreamParser {
private buffer = Buffer.allocUnsafe(0);
terima(chunk: Buffer): object[] {
this.buffer = Buffer.concat([this.buffer, chunk]);
const pesan: object[] = [];
while (this.buffer.length >= UKURAN_HEADER) {
const panjangBody = this.buffer.readUInt32BE(0);
const totalFrame = UKURAN_HEADER + panjangBody;
if (this.buffer.length < totalFrame) break; // Belum lengkap
const body = this.buffer.slice(UKURAN_HEADER, totalFrame);
this.buffer = this.buffer.slice(totalFrame);
try {
pesan.push(JSON.parse(body.toString("utf-8")));
} catch {
console.error("Frame tidak valid, dilewati");
}
}
return pesan;
}
}
Tipe Pesan yang Dibagi #
// src/types/messages.ts — tipe dibagi antara server dan client
export type PesanServer =
| { tipe: "SAMBUTAN"; serverId: string; waktu: string }
| { tipe: "RESPONS"; requestId: string; data: unknown }
| { tipe: "ERROR"; requestId: string; kode: string; pesan: string }
| { tipe: "PING" };
export type PesanKlien =
| { tipe: "PERMINTAAN"; requestId: string; aksi: string; payload: unknown }
| { tipe: "PONG" }
| { tipe: "PUTUS" };
TCP Server #
// src/server/tcp-server.ts
import * as net from "net";
import { enkodeFrame, StreamParser } from "../protocol";
import type { PesanKlien, PesanServer } from "../types/messages";
interface InfoKoneksi {
id: string;
socket: net.Socket;
parser: StreamParser;
terhubungPada: Date;
}
class TCPServer {
private server: net.Server;
private koneksiAktif = new Map<string, InfoKoneksi>();
private idCounter = 0;
constructor(private readonly port: number) {
this.server = net.createServer((socket) => {
this.tanganiKoneksiBaru(socket);
});
this.server.on("error", (err) => {
console.error("[TCPServer] Error server:", err);
});
}
private tanganiKoneksiBaru(socket: net.Socket): void {
const id = `klien-${++this.idCounter}`;
const parser = new StreamParser();
const info: InfoKoneksi = {
id,
socket,
parser,
terhubungPada: new Date(),
};
this.koneksiAktif.set(id, info);
console.log(`[TCPServer] Klien terhubung: ${id} (${socket.remoteAddress})`);
// Kirim sambutan
const sambutan: PesanServer = {
tipe: "SAMBUTAN",
serverId: "tcp-server-1",
waktu: new Date().toISOString(),
};
this.kirimKe(id, sambutan);
socket.on("data", (chunk: Buffer) => {
const pesanMasuk = parser.terima(chunk);
for (const pesan of pesanMasuk) {
this.prosespesan(id, pesan as PesanKlien);
}
});
socket.on("end", () => {
console.log(`[TCPServer] Klien terputus: ${id}`);
this.koneksiAktif.delete(id);
});
socket.on("error", (err) => {
console.error(`[TCPServer] Error pada klien ${id}:`, err.message);
this.koneksiAktif.delete(id);
});
// Timeout koneksi idle selama 5 menit
socket.setTimeout(5 * 60_000);
socket.on("timeout", () => {
console.log(`[TCPServer] Timeout klien ${id}`);
socket.end();
});
}
private prosespesan(idKlien: string, pesan: PesanKlien): void {
switch (pesan.tipe) {
case "PERMINTAAN": {
console.log(`[TCPServer] Permintaan dari ${idKlien}:`, pesan.aksi);
const respons: PesanServer = {
tipe: "RESPONS",
requestId: pesan.requestId,
data: { status: "ok", aksi: pesan.aksi },
};
this.kirimKe(idKlien, respons);
break;
}
case "PONG":
// Klien merespons PING — koneksi masih hidup
break;
case "PUTUS":
this.koneksiAktif.get(idKlien)?.socket.end();
break;
}
}
kirimKe(idKlien: string, pesan: PesanServer): void {
const info = this.koneksiAktif.get(idKlien);
if (!info || info.socket.destroyed) return;
info.socket.write(enkodeFrame(pesan));
}
siarkan(pesan: PesanServer): void {
for (const info of this.koneksiAktif.values()) {
this.kirimKe(info.id, pesan);
}
}
mulai(): Promise<void> {
return new Promise((resolve, reject) => {
this.server.once("error", reject);
this.server.listen(this.port, () => {
console.log(`[TCPServer] Mendengarkan di port ${this.port}`);
resolve();
});
});
}
berhenti(): Promise<void> {
return new Promise((resolve) => {
for (const info of this.koneksiAktif.values()) {
info.socket.destroy();
}
this.server.close(() => resolve());
});
}
}
// Jalankan server
const server = new TCPServer(8080);
server.mulai().catch(console.error);
TCP Client #
// src/client/tcp-client.ts
import * as net from "net";
import { enkodeFrame, StreamParser } from "../protocol";
import type { PesanKlien, PesanServer } from "../types/messages";
class TCPClient {
private socket: net.Socket | null = null;
private parser = new StreamParser();
private terhubung = false;
async hubungkan(host: string, port: number): Promise<void> {
return new Promise((resolve, reject) => {
this.socket = net.createConnection({ host, port }, () => {
this.terhubung = true;
console.log("[TCPClient] Terhubung ke server");
resolve();
});
this.socket.on("data", (chunk: Buffer) => {
const pesan = this.parser.terima(chunk);
for (const p of pesan) {
this.tanganipesanServer(p as PesanServer);
}
});
this.socket.on("error", (err) => {
console.error("[TCPClient] Error:", err.message);
if (!this.terhubung) reject(err);
});
this.socket.on("close", () => {
this.terhubung = false;
console.log("[TCPClient] Koneksi ditutup");
});
});
}
private tanganipesanServer(pesan: PesanServer): void {
switch (pesan.tipe) {
case "SAMBUTAN":
console.log(`[TCPClient] Disambut oleh ${pesan.serverId}`);
break;
case "RESPONS":
console.log(`[TCPClient] Respons untuk ${pesan.requestId}:`, pesan.data);
break;
case "ERROR":
console.error(`[TCPClient] Error ${pesan.kode}: ${pesan.pesan}`);
break;
case "PING":
this.kirim({ tipe: "PONG" });
break;
}
}
kirim(pesan: PesanKlien): void {
if (!this.socket || !this.terhubung) {
throw new Error("Belum terhubung ke server");
}
this.socket.write(enkodeFrame(pesan));
}
minta(aksi: string, payload: unknown): void {
this.kirim({
tipe: "PERMINTAAN",
requestId: crypto.randomUUID(),
aksi,
payload,
});
}
putuskan(): void {
this.socket?.end();
}
}
// Penggunaan
async function main() {
const klien = new TCPClient();
await klien.hubungkan("localhost", 8080);
klien.minta("AMBIL_DATA", { id: "usr-001" });
}
main().catch(console.error);
Socket.io dengan Typed Events #
Socket.io memberikan abstraksi yang nyaman di atas WebSocket — room management, namespace, auto-reconnect, dan fallback ke HTTP long-polling. TypeScript 4.x+ memungkinkan typed events yang sepenuhnya type-safe:
Definisi Interface Events #
// src/types/socket-events.ts — dibagi antara server dan client
export interface DataPesan {
id: string;
pengirimId: string;
pengirimNama: string;
isi: string;
waktu: string;
roomId: string;
}
export interface InfoPengguna {
id: string;
nama: string;
avatar?: string;
}
// Events dari Client ke Server
export interface ClientToServerEvents {
"pesan:kirim": (data: { roomId: string; isi: string }) => void;
"room:gabung": (roomId: string, callback: (berhasil: boolean) => void) => void;
"room:keluar": (roomId: string) => void;
"pengguna:mengetik": (roomId: string) => void;
"pengguna:berhenti-mengetik": (roomId: string) => void;
}
// Events dari Server ke Client
export interface ServerToClientEvents {
"pesan:baru": (pesan: DataPesan) => void;
"pengguna:bergabung": (pengguna: InfoPengguna, roomId: string) => void;
"pengguna:keluar": (penggunaId: string, roomId: string) => void;
"pengguna:mengetik": (penggunaId: string, roomId: string) => void;
"pengguna:berhenti-mengetik": (penggunaId: string, roomId: string) => void;
"error": (pesan: string) => void;
}
// Data yang disimpan per socket (sisi server)
export interface SocketData {
penggunaId: string;
namaPengguna: string;
roomAktif: Set<string>;
}
Server Socket.io yang Type-safe #
// src/server/socketio-server.ts
import { createServer } from "http";
import { Server, Socket } from "socket.io";
import type {
ClientToServerEvents,
ServerToClientEvents,
SocketData,
DataPesan,
} from "../types/socket-events";
// Tipe server dan socket yang fully typed
type ServerType = Server<ClientToServerEvents, ServerToClientEvents, {}, SocketData>;
type SocketType = Socket<ClientToServerEvents, ServerToClientEvents, {}, SocketData>;
const httpServer = createServer();
const io: ServerType = new Server(httpServer, {
cors: {
origin: process.env.ALLOWED_ORIGIN ?? "http://localhost:3000",
methods: ["GET", "POST"],
credentials: true,
},
});
// Middleware autentikasi — verifikasi JWT sebelum koneksi diterima
io.use(async (socket, next) => {
const token = socket.handshake.auth.token as string | undefined;
if (!token) {
return next(new Error("Token autentikasi diperlukan"));
}
try {
// Simulasi verifikasi JWT
const payload = verifikasiToken(token);
socket.data.penggunaId = payload.sub;
socket.data.namaPengguna = payload.nama;
socket.data.roomAktif = new Set();
next();
} catch {
next(new Error("Token tidak valid atau kedaluwarsa"));
}
});
io.on("connection", (socket: SocketType) => {
const { penggunaId, namaPengguna } = socket.data;
console.log(`[Socket.io] Pengguna terhubung: ${namaPengguna} (${penggunaId})`);
// Handler: gabung ke room
socket.on("room:gabung", async (roomId, callback) => {
try {
await socket.join(roomId);
socket.data.roomAktif.add(roomId);
// Beritahu semua di room (kecuali pengirim) bahwa ada yang bergabung
socket.to(roomId).emit("pengguna:bergabung", {
id: penggunaId,
nama: namaPengguna,
}, roomId);
callback(true);
console.log(`[Socket.io] ${namaPengguna} bergabung ke room: ${roomId}`);
} catch (err) {
console.error("[Socket.io] Gagal bergabung ke room:", err);
callback(false);
}
});
// Handler: kirim pesan
socket.on("pesan:kirim", ({ roomId, isi }) => {
if (!socket.data.roomAktif.has(roomId)) {
socket.emit("error", "Kamu belum bergabung ke room ini");
return;
}
if (isi.trim().length === 0 || isi.length > 2000) {
socket.emit("error", "Pesan tidak valid (1-2000 karakter)");
return;
}
const pesan: DataPesan = {
id: crypto.randomUUID(),
pengirimId: penggunaId,
pengirimNama: namaPengguna,
isi: isi.trim(),
waktu: new Date().toISOString(),
roomId,
};
// Kirim ke semua di room termasuk pengirim
io.to(roomId).emit("pesan:baru", pesan);
});
// Handler: indikator mengetik
socket.on("pengguna:mengetik", (roomId) => {
socket.to(roomId).emit("pengguna:mengetik", penggunaId, roomId);
});
socket.on("pengguna:berhenti-mengetik", (roomId) => {
socket.to(roomId).emit("pengguna:berhenti-mengetik", penggunaId, roomId);
});
// Handler: keluar dari room
socket.on("room:keluar", async (roomId) => {
await socket.leave(roomId);
socket.data.roomAktif.delete(roomId);
socket.to(roomId).emit("pengguna:keluar", penggunaId, roomId);
});
// Cleanup saat putus
socket.on("disconnect", (reason) => {
console.log(`[Socket.io] ${namaPengguna} terputus: ${reason}`);
// Beritahu semua room yang ditinggalkan
for (const roomId of socket.data.roomAktif) {
socket.to(roomId).emit("pengguna:keluar", penggunaId, roomId);
}
});
});
httpServer.listen(3001, () => {
console.log("[Socket.io] Server berjalan di port 3001");
});
// Helper mock JWT
function verifikasiToken(token: string): { sub: string; nama: string } {
if (token === "token-valid") return { sub: "usr-001", nama: "Budi" };
throw new Error("Token tidak valid");
}
Client Socket.io yang Type-safe #
// src/client/socketio-client.ts
import { io, Socket } from "socket.io-client";
import type {
ClientToServerEvents,
ServerToClientEvents,
} from "../types/socket-events";
// Tipe socket client yang fully typed
type SocketClient = Socket<ServerToClientEvents, ClientToServerEvents>;
class KlienChat {
private socket: SocketClient;
private reconnectDelay = 1000; // ms, untuk backoff
constructor(private readonly serverUrl: string, private readonly token: string) {
this.socket = io(serverUrl, {
auth: { token },
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
reconnectionDelayMax: 10_000,
timeout: 10_000,
});
this.daftarkanEventHandlers();
}
private daftarkanEventHandlers(): void {
this.socket.on("connect", () => {
console.log("[KlienChat] Terhubung:", this.socket.id);
this.reconnectDelay = 1000; // Reset backoff
});
this.socket.on("connect_error", (err) => {
console.error("[KlienChat] Gagal terhubung:", err.message);
});
this.socket.on("disconnect", (reason) => {
console.log("[KlienChat] Terputus:", reason);
});
this.socket.on("pesan:baru", (pesan) => {
// TypeScript tahu pesan bertipe DataPesan
console.log(`[${pesan.pengirimNama}] ${pesan.isi}`);
});
this.socket.on("pengguna:bergabung", (pengguna, roomId) => {
console.log(`${pengguna.nama} bergabung ke room ${roomId}`);
});
this.socket.on("error", (pesan) => {
console.error("[KlienChat] Error dari server:", pesan);
});
}
gabungRoom(roomId: string): Promise<boolean> {
return new Promise((resolve) => {
// TypeScript memvalidasi argumen dan callback sesuai definisi
this.socket.emit("room:gabung", roomId, (berhasil) => {
resolve(berhasil);
});
});
}
kirimPesan(roomId: string, isi: string): void {
this.socket.emit("pesan:kirim", { roomId, isi });
}
putuskan(): void {
this.socket.disconnect();
}
}
// Penggunaan
async function main() {
const klien = new KlienChat("http://localhost:3001", "token-valid");
const berhasil = await klien.gabungRoom("room-umum");
if (berhasil) {
klien.kirimPesan("room-umum", "Halo semua! 👋");
}
}
main().catch(console.error);
Keamanan Socket di Produksi #
TLS untuk TCP Socket #
import * as tls from "tls";
import * as fs from "fs";
// Server TLS
const serverTLS = tls.createServer({
key: fs.readFileSync("private-key.pem"),
cert: fs.readFileSync("certificate.pem"),
// Hanya izinkan klien dengan sertifikat yang valid (mutual TLS)
requestCert: true,
rejectUnauthorized: true,
ca: fs.readFileSync("ca-certificate.pem"),
}, (socket) => {
if (!socket.authorized) {
console.error("Klien tidak terotorisasi:", socket.authorizationError);
socket.destroy();
return;
}
console.log("Klien TLS terhubung:", socket.getPeerCertificate().subject.CN);
socket.write("Koneksi aman terjalin\n");
});
serverTLS.listen(8443);
// Client TLS
const klienTLS = tls.connect({
host: "localhost",
port: 8443,
key: fs.readFileSync("client-key.pem"),
cert: fs.readFileSync("client-cert.pem"),
ca: fs.readFileSync("ca-certificate.pem"),
}, () => {
console.log("Terhubung ke server TLS");
});
Validasi Input pada Socket #
// Selalu validasi dan sanitasi data yang diterima dari socket
function validasiPesan(data: unknown): data is { roomId: string; isi: string } {
if (typeof data !== "object" || data === null) return false;
const d = data as Record<string, unknown>;
return (
typeof d.roomId === "string" &&
d.roomId.length > 0 &&
d.roomId.length <= 50 &&
typeof d.isi === "string" &&
d.isi.length > 0 &&
d.isi.length <= 2000
);
}
// Rate limiting per socket — cegah spam
class RateLimiter {
private hitungan = new Map<string, { jumlah: number; resetPada: number }>();
izinkan(socketId: string, maks: number, windowMs: number): boolean {
const sekarang = Date.now();
const entri = this.hitungan.get(socketId);
if (!entri || sekarang > entri.resetPada) {
this.hitungan.set(socketId, { jumlah: 1, resetPada: sekarang + windowMs });
return true;
}
if (entri.jumlah >= maks) return false;
entri.jumlah++;
return true;
}
}
Ringkasan #
- TCP socket (
net) untuk komunikasi antar service di jaringan internal atau protokol kustom; WebSocket/Socket.io untuk browser dan komunikasi klien-server yang melintasi firewall.- TCP adalah stream, bukan message — implementasikan protocol framing (length prefix) untuk memisahkan pesan; tanpanya data bisa tiba sebagai partial chunk atau beberapa pesan sekaligus.
- Definisikan tipe pesan dalam file terpisah yang diimport oleh server dan client — ini adalah satu-satunya cara mendapatkan type safety penuh pada semua
emit()dan event handler.- Socket.io generic types
Server<ClientToServer, ServerToClient, {}, SocketData>danSocket<ClientToServer, ServerToClient, {}, SocketData>memberikan autocomplete dan validasi tipe pada semua event handler dan emit.- Selalu autentikasi di middleware, bukan di event handler — gunakan
io.use()di Socket.io atau cek token di awal koneksi di TCP; jangan tunggu pesan pertama untuk validasi.- Konfigurasi reconnection dengan backoff — Socket.io mendukung
reconnectionDelaydanreconnectionDelayMax; untuk TCP kustom, implementasikan backoff eksponensial manual.- Rate limiting per socket mencegah satu klien membanjiri server dengan pesan — simpan hitungan per
socket.iddan tolak jika melampaui batas.- Gunakan TLS (
wss://atautls.createServer()) di produksi — socket tanpa enkripsi mudah disadap di jaringan yang tidak dipercaya.- Bersihkan resource saat socket disconnect — hapus dari Map koneksi, tinggalkan semua room, dan hentikan timer heartbeat untuk mencegah memory leak.