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:#000

TCP 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> dan Socket<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 reconnectionDelay dan reconnectionDelayMax; untuk TCP kustom, implementasikan backoff eksponensial manual.
  • Rate limiting per socket mencegah satu klien membanjiri server dengan pesan — simpan hitungan per socket.id dan tolak jika melampaui batas.
  • Gunakan TLS (wss:// atau tls.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.

← Sebelumnya: I/O   Berikutnya: Web Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact