Socket #
Semua komunikasi jaringan — HTTP, database, chat, email — pada akhirnya melewati socket. Socket adalah titik ujung dari sebuah koneksi jaringan: sepasang alamat IP dan nomor port yang memungkinkan dua proses bertukar data, baik di mesin yang sama maupun di seluruh penjuru internet. Memahami cara kerja socket memberi kamu fondasi untuk memahami protokol tingkat lebih tinggi dan membangun server sendiri dari awal. Java menyediakan API socket yang lengkap di paket java.net — dari ServerSocket dan Socket untuk TCP, hingga DatagramSocket untuk UDP. Artikel ini membahas cara kerja komunikasi TCP, membangun server yang bisa melayani banyak klien secara bersamaan, mengirim data teks dan biner, mengatur timeout, hingga merancang server yang robust.
Konsep Dasar #
Sebelum menulis kode, ada beberapa konsep jaringan yang perlu dipahami:
| Konsep | Penjelasan |
|---|---|
| IP Address | Alamat unik setiap mesin di jaringan. 127.0.0.1 (localhost) adalah alamat mesin sendiri. |
| Port | Angka 0–65535 yang membedakan aplikasi di mesin yang sama. Port < 1024 biasanya butuh hak admin. |
| TCP | Protokol yang menjamin data sampai secara berurutan dan utuh. Perlu handshake sebelum komunikasi. |
| UDP | Protokol tanpa koneksi yang lebih cepat tapi tidak menjamin urutan atau ketibaan data. |
| Socket | Endpoint komunikasi — gabungan IP + port. Satu koneksi butuh dua socket: satu di klien, satu di server. |
| Backlog | Jumlah koneksi yang bisa mengantre sambil menunggu server memproses accept(). |
sequenceDiagram
participant C as Klien
participant S as Server
S->>S: ServerSocket.bind(port)
S->>S: ServerSocket.accept() ← menunggu
C->>S: Socket.connect(ip, port)
S->>S: accept() kembali dengan Socket baru
Note over C,S: Koneksi TCP terbentuk (3-way handshake)
C->>S: kirim data (OutputStream)
S->>C: balas data (OutputStream)
C->>S: tutup koneksiTCP — Server Sederhana #
ServerSocket mendengarkan koneksi masuk pada port tertentu. Setiap kali klien terhubung, accept() mengembalikan objek Socket baru yang merepresentasikan koneksi ke klien tersebut.
Membuat Server #
import java.io.*;
import java.net.*;
public class ServerSederhana {
public static void main(String[] args) {
int port = 8080;
// try-with-resources: ServerSocket otomatis ditutup
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Server mendengarkan di port " + port);
// accept() memblokir sampai ada klien yang terhubung
try (Socket socket = serverSocket.accept()) {
System.out.println("Klien terhubung: " + socket.getInetAddress());
// Bungkus stream dengan BufferedReader/PrintWriter untuk kemudahan
BufferedReader masuk = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter keluar = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()), true); // autoFlush
// Baca pesan dari klien
String pesan = masuk.readLine();
System.out.println("Diterima: " + pesan);
// Kirim balasan
keluar.println("Echo: " + pesan);
}
} catch (IOException e) {
System.err.println("Server error: " + e.getMessage());
}
}
}
Membuat Klien #
import java.io.*;
import java.net.*;
public class KlienSederhana {
public static void main(String[] args) {
String host = "localhost";
int port = 8080;
try (Socket socket = new Socket(host, port);
PrintWriter keluar = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()), true);
BufferedReader masuk = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
System.out.println("Terhubung ke " + host + ":" + port);
// Kirim pesan ke server
keluar.println("Halo, Server!");
// Baca balasan
String balasan = masuk.readLine();
System.out.println("Balasan: " + balasan);
} catch (ConnectException e) {
System.err.println("Gagal terhubung — server belum berjalan?");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Menjalankan Keduanya #
1. Jalankan ServerSederhana terlebih dahulu
2. Jalankan KlienSederhana di terminal lain
Output server:
Server mendengarkan di port 8080
Klien terhubung: /127.0.0.1
Diterima: Halo, Server!
Output klien:
Terhubung ke localhost:8080
Balasan: Echo: Halo, Server!
Server Multi-Klien #
Server sederhana di atas hanya bisa melayani satu klien — setelah koneksi pertama ditutup, program berakhir. Aplikasi nyata perlu melayani banyak klien secara bersamaan. Solusinya: tangani setiap koneksi di thread terpisah menggunakan ExecutorService.
Server dengan Thread Pool #
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class ServerMultiKlien {
private static final int PORT = 8080;
private static final int THREADS = 10; // maksimal 10 klien bersamaan
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(THREADS);
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
serverSocket.setReuseAddress(true); // izinkan reuse port setelah restart cepat
System.out.println("Server siap di port " + PORT);
while (true) { // loop terus menerima koneksi baru
Socket socket = serverSocket.accept();
System.out.println("Klien baru: " + socket.getInetAddress() + ":" + socket.getPort());
// Delegasikan penanganan ke thread pool
pool.submit(new TanganiKlien(socket));
}
} catch (IOException e) {
System.err.println("Server berhenti: " + e.getMessage());
} finally {
pool.shutdown();
}
}
}
// Handler untuk satu klien — berjalan di thread terpisah
class TanganiKlien implements Runnable {
private final Socket socket;
public TanganiKlien(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
String alamat = socket.getInetAddress().getHostAddress();
try (socket; // Java 9+: bisa pakai try-with-resources untuk variabel yang sudah ada
BufferedReader masuk = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter keluar = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()), true)) {
System.out.println("[" + alamat + "] Terhubung");
String baris;
while ((baris = masuk.readLine()) != null) {
System.out.println("[" + alamat + "] → " + baris);
if ("QUIT".equalsIgnoreCase(baris)) {
keluar.println("Sampai jumpa!");
break;
}
// Echo balik dengan tambahan timestamp
keluar.println("[" + java.time.LocalTime.now() + "] " + baris);
}
} catch (IOException e) {
System.err.println("[" + alamat + "] Koneksi terputus: " + e.getMessage());
} finally {
System.out.println("[" + alamat + "] Terputus");
}
}
}
flowchart TD
A["ServerSocket\n(port 8080)"] -->|"accept()"| B["Koneksi Klien 1"]
A -->|"accept()"| C["Koneksi Klien 2"]
A -->|"accept()"| D["Koneksi Klien N"]
B -->|"submit()"| E["Thread Pool\n(ExecutorService)"]
C -->|"submit()"| E
D -->|"submit()"| E
E --> F["Worker Thread 1\n(TanganiKlien)"]
E --> G["Worker Thread 2\n(TanganiKlien)"]
E --> H["Worker Thread N\n(TanganiKlien)"]Komunikasi Dua Arah — Chat Sederhana #
Skenario chat membutuhkan kedua sisi (klien dan server) bisa mengirim dan menerima pesan secara independen. Ini membutuhkan dua thread di sisi klien: satu untuk membaca input dari keyboard, satu untuk menerima pesan dari server.
Server Chat #
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class ServerChat {
private static final int PORT = 9090;
// Set semua PrintWriter aktif — untuk broadcast ke semua klien
private static final Set<PrintWriter> semuaKlien = new CopyOnWriteArraySet<>();
public static void main(String[] args) throws IOException {
ExecutorService pool = Executors.newCachedThreadPool();
try (ServerSocket server = new ServerSocket(PORT)) {
System.out.println("Server chat di port " + PORT);
while (true) {
Socket socket = server.accept();
pool.submit(() -> tanganiKlienChat(socket));
}
}
}
private static void tanganiKlienChat(Socket socket) {
PrintWriter keluar = null;
try {
BufferedReader masuk = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
keluar = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()), true);
semuaKlien.add(keluar);
// Minta nama pengguna
keluar.println("Masukkan nama kamu:");
String nama = masuk.readLine();
broadcast("[" + nama + " bergabung]", null);
String pesan;
while ((pesan = masuk.readLine()) != null) {
if ("QUIT".equalsIgnoreCase(pesan)) break;
broadcast(nama + ": " + pesan, null);
}
broadcast("[" + nama + " keluar]", null);
} catch (IOException e) {
System.err.println("Klien terputus: " + e.getMessage());
} finally {
if (keluar != null) semuaKlien.remove(keluar);
try { socket.close(); } catch (IOException ignored) {}
}
}
// Kirim pesan ke semua klien yang terhubung
private static void broadcast(String pesan, PrintWriter kecuali) {
System.out.println("Broadcast: " + pesan);
for (PrintWriter pw : semuaKlien) {
if (pw != kecuali) pw.println(pesan);
}
}
}
Klien Chat #
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class KlienChat {
public static void main(String[] args) throws IOException {
try (Socket socket = new Socket("localhost", 9090);
BufferedReader masuk = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter keluar = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()), true)) {
// Thread untuk membaca pesan dari server (berjalan di background)
Thread pembaca = new Thread(() -> {
try {
String baris;
while ((baris = masuk.readLine()) != null) {
System.out.println(baris);
}
} catch (IOException e) {
System.out.println("Koneksi ke server terputus.");
}
});
pembaca.setDaemon(true); // mati saat main thread selesai
pembaca.start();
// Thread utama: baca input dari keyboard dan kirim ke server
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String input = scanner.nextLine();
keluar.println(input);
if ("QUIT".equalsIgnoreCase(input)) break;
}
}
}
}
Timeout dan Opsi Socket #
Tanpa timeout, operasi I/O socket bisa memblokir selamanya — misalnya saat server tidak merespons. Selalu atur timeout yang masuk akal untuk aplikasi yang berhadapan dengan jaringan eksternal.
Mengatur Timeout #
// Timeout saat menunggu accept() — berapa lama server menunggu koneksi baru
ServerSocket serverSocket = new ServerSocket(8080);
serverSocket.setSoTimeout(30_000); // 30 detik, lempar SocketTimeoutException jika habis
// Timeout untuk operasi read() pada socket klien
Socket socket = new Socket();
socket.setSoTimeout(5_000); // 5 detik per operasi read
// Timeout untuk proses koneksi itu sendiri
socket.connect(new InetSocketAddress("example.com", 80), 10_000); // 10 detik
// Contoh dengan penanganan timeout
try (ServerSocket server = new ServerSocket(8080)) {
server.setSoTimeout(60_000); // tunggu klien maksimal 60 detik
try {
Socket klien = server.accept();
// proses klien...
} catch (SocketTimeoutException e) {
System.out.println("Tidak ada klien yang terhubung dalam 60 detik.");
}
}
Opsi Socket Penting #
Socket socket = new Socket("localhost", 8080);
// TCP_NODELAY: nonaktifkan Nagle's algorithm — kirim data segera tanpa buffering
// Penting untuk aplikasi real-time yang butuh latensi rendah (game, chat)
socket.setTcpNoDelay(true);
// SO_KEEPALIVE: kirim keepalive packet secara periodik untuk deteksi koneksi mati
socket.setKeepAlive(true);
// SO_LINGER: tunggu sampai data terkirim saat socket ditutup (dalam detik)
socket.setSoLinger(true, 5); // tunggu maksimal 5 detik
// SO_RCVBUF / SO_SNDBUF: ukuran buffer penerimaan dan pengiriman
socket.setReceiveBufferSize(65536); // 64 KB
socket.setSendBufferSize(65536);
// Info socket yang berguna
System.out.println("IP lokal: " + socket.getLocalAddress());
System.out.println("Port lokal: " + socket.getLocalPort());
System.out.println("IP remote: " + socket.getInetAddress());
System.out.println("Port remote: " + socket.getPort());
System.out.println("Terhubung: " + socket.isConnected());
System.out.println("Ditutup: " + socket.isClosed());
Transfer File Biner #
Selain teks, socket bisa dipakai untuk mentransfer file biner — gambar, dokumen, atau data apapun — dengan langsung membaca dan menulis byte dari stream.
Server Penerima File #
import java.io.*;
import java.net.*;
import java.nio.file.*;
public class ServerTerimaFile {
public static void main(String[] args) throws IOException {
try (ServerSocket server = new ServerSocket(7070);
Socket socket = server.accept()) {
System.out.println("Klien terhubung, menerima file...");
DataInputStream dis = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
// Terima nama file dan ukurannya dulu
String namaFile = dis.readUTF();
long ukuran = dis.readLong();
System.out.printf("Menerima '%s' (%d byte)%n", namaFile, ukuran);
// Terima isi file
Path output = Path.of("terima-" + namaFile);
try (OutputStream fos = Files.newOutputStream(output)) {
byte[] buffer = new byte[8192];
long sisaDiterima = ukuran;
int terbaca;
while (sisaDiterima > 0 &&
(terbaca = dis.read(buffer, 0, (int) Math.min(buffer.length, sisaDiterima))) != -1) {
fos.write(buffer, 0, terbaca);
sisaDiterima -= terbaca;
}
}
System.out.println("File berhasil diterima: " + output.toAbsolutePath());
}
}
}
Klien Pengirim File #
import java.io.*;
import java.net.*;
import java.nio.file.*;
public class KlienKirimFile {
public static void main(String[] args) throws IOException {
Path file = Path.of("dokumen.pdf"); // file yang akan dikirim
try (Socket socket = new Socket("localhost", 7070);
DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(socket.getOutputStream()))) {
// Kirim nama dan ukuran file terlebih dahulu
dos.writeUTF(file.getFileName().toString());
dos.writeLong(Files.size(file));
dos.flush();
// Kirim isi file
Files.copy(file, dos);
dos.flush();
System.out.println("File berhasil dikirim: " + file);
}
}
}
UDP — Komunikasi Tanpa Koneksi #
UDP (User Datagram Protocol) tidak membangun koneksi seperti TCP. Setiap paket (datagram) dikirim secara independen dan mungkin tiba tidak berurutan atau tidak sampai sama sekali. Ini membuatnya lebih cepat tapi kurang andal — cocok untuk streaming video, game online, atau DNS.
Server UDP #
import java.net.*;
public class ServerUDP {
public static void main(String[] args) throws Exception {
try (DatagramSocket socket = new DatagramSocket(9999)) {
System.out.println("Server UDP mendengarkan di port 9999");
byte[] buffer = new byte[1024];
while (true) {
// Terima datagram
DatagramPacket paket = new DatagramPacket(buffer, buffer.length);
socket.receive(paket); // memblokir sampai ada data
String pesan = new String(paket.getData(), 0, paket.getLength());
System.out.println("Diterima dari " + paket.getAddress() + ": " + pesan);
// Kirim balasan ke alamat dan port yang sama
String balasan = "UDP Echo: " + pesan;
byte[] dataBalasan = balasan.getBytes();
DatagramPacket paketBalas = new DatagramPacket(
dataBalasan, dataBalasan.length,
paket.getAddress(), paket.getPort()
);
socket.send(paketBalas);
}
}
}
}
Klien UDP #
import java.net.*;
public class KlienUDP {
public static void main(String[] args) throws Exception {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(3000); // timeout 3 detik untuk receive
InetAddress alamatServer = InetAddress.getByName("localhost");
int portServer = 9999;
// Kirim datagram
String pesan = "Halo UDP!";
byte[] data = pesan.getBytes();
DatagramPacket kirim = new DatagramPacket(
data, data.length, alamatServer, portServer);
socket.send(kirim);
// Terima balasan
byte[] buffer = new byte[1024];
DatagramPacket terima = new DatagramPacket(buffer, buffer.length);
socket.receive(terima);
System.out.println("Balasan: " + new String(terima.getData(), 0, terima.getLength()));
}
}
}
Penanganan Error yang Robust #
Aplikasi jaringan wajib menangani berbagai kondisi error — koneksi terputus tiba-tiba, server tidak merespons, port sudah dipakai.
Reconnect Otomatis di Sisi Klien #
public class KlienTangguh {
private static final int MAKS_COBA = 5;
private static final int JEDA_MS = 2000;
public static Socket hubungDenganRetry(String host, int port)
throws IOException, InterruptedException {
for (int percobaan = 1; percobaan <= MAKS_COBA; percobaan++) {
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(host, port), 5000);
System.out.println("Terhubung setelah " + percobaan + " percobaan");
return socket;
} catch (ConnectException e) {
System.err.printf("Percobaan %d/%d gagal. Coba lagi dalam %d ms...%n",
percobaan, MAKS_COBA, JEDA_MS);
if (percobaan < MAKS_COBA) {
Thread.sleep(JEDA_MS);
}
}
}
throw new IOException("Gagal terhubung setelah " + MAKS_COBA + " percobaan");
}
public static void main(String[] args) {
try {
Socket socket = hubungDenganRetry("localhost", 8080);
// gunakan socket...
socket.close();
} catch (IOException | InterruptedException e) {
System.err.println("Koneksi gagal total: " + e.getMessage());
}
}
}
Penanganan Exception yang Tepat #
// ANTI-PATTERN: tangkap Exception terlalu luas, sembunyikan detail error
try {
Socket socket = new Socket("host", 8080);
// ...
} catch (Exception e) {
e.printStackTrace(); // ✗ tidak informatif, tidak ada recovery
}
// BENAR: tangkap exception spesifik dan tangani dengan tepat
try {
Socket socket = new Socket();
socket.setSoTimeout(5000);
socket.connect(new InetSocketAddress("host", 8080), 10_000);
// gunakan socket...
} catch (ConnectException e) {
System.err.println("Server tidak bisa dihubungi: " + e.getMessage());
// → coba server lain, atau beri tahu pengguna
} catch (SocketTimeoutException e) {
System.err.println("Timeout: " + e.getMessage());
// → retry atau abort
} catch (UnknownHostException e) {
System.err.println("Host tidak ditemukan: " + e.getMessage());
// → periksa nama host
} catch (IOException e) {
System.err.println("Error I/O: " + e.getMessage());
// → log dan handle
}
TCP vs UDP — Kapan Menggunakan Masing-Masing #
Gunakan TCP jika:
✓ Data harus tiba lengkap dan berurutan (transfer file, HTTP, database)
✓ Kehilangan data tidak dapat diterima
✓ Tidak kritis soal latensi beberapa milidetik ekstra
Gunakan UDP jika:
✓ Kecepatan lebih penting dari keandalan (game real-time, streaming video)
✓ Kehilangan sesekali tidak masalah — data sudah usang sebelum bisa di-retry
✓ Butuh broadcast atau multicast ke banyak penerima sekaligus
✓ Membangun protokol kustom di atas UDP (QUIC, WebRTC)
Praktik terbaik server TCP:
✓ Selalu gunakan thread pool (ExecutorService) untuk server multi-klien
✓ Atur SO_TIMEOUT agar tidak ada thread yang terblokir selamanya
✓ Gunakan setReuseAddress(true) agar port bisa dipakai ulang saat restart
✓ Tutup socket di blok finally atau pakai try-with-resources
✓ Log alamat klien untuk debugging dan monitoring
Ringkasan #
ServerSocketmenunggu koneksi,Socketmembuat koneksi —accept()memblokir sampai ada klien yang terhubung dan mengembalikanSocketbaru untuk berkomunikasi.- Satu klien = satu thread — untuk server yang melayani banyak klien, delegasikan setiap koneksi ke thread pool dengan
ExecutorService. Jangan buat thread baru secara manual untuk setiap koneksi.- Selalu atur timeout —
setSoTimeout()mencegah operasiread()memblokir selamanya. Tanpa timeout, satu klien yang lambat bisa memblokir seluruh thread pool.- Gunakan
BufferedReader/PrintWriteruntuk teks — wrapsocket.getInputStream()dansocket.getOutputStream()dengan buffer agar efisien dan mudah dipakai.- Gunakan
DataInputStream/DataOutputStreamuntuk data terstruktur — menyediakanwriteUTF(),readLong(),writeInt()dll. yang memastikan format byte konsisten antar platform.- Tutup socket dengan try-with-resources — socket adalah resource OS yang harus ditutup. Lupa menutup socket akan menyebabkan kebocoran file descriptor.
- TCP untuk keandalan, UDP untuk kecepatan — TCP menjamin urutan dan ketibaan data tapi ada overhead. UDP lebih cepat tapi tidak menjamin apa pun.
setReuseAddress(true)untuk restart cepat — tanpa ini, server mungkin gagal bind ke port yang sama segera setelah di-restart karena OS masih “menahan” port tersebut.