Di balik setiap HTTP request, koneksi database, dan pesan antar layanan, ada satu abstraksi yang bekerja tanpa henti: socket. Socket adalah titik akhir (endpoint) komunikasi antar proses — identitasnya adalah kombinasi alamat IP dan port, dan di atasnya mengalir byte mentah yang kita bentuk sendiri. Java memodelkannya dengan class Socket untuk sisi klien dan ServerSocket untuk sisi server, keduanya di package java.net. Memahami socket memberi kita intuisi tentang apa yang sebenarnya dilakukan API tingkat lebih tinggi seperti HttpClient. Chapter ini mengupas konsep endpoint, alur accept() yang memblokir, dan aliran data dua arah pada satu koneksi.

Socket sebagai Titik Akhir #

Sebuah koneksi TCP menghubungkan tepat dua endpoint: klien dan server. Endpoint itu direpresentasikan class InetSocketAddress — pembungkus alamat InetAddress plus nomor port. Ketika klien membuka Socket ke 127.0.0.1:8080, sistem operasi sekaligus memesan port ephemeral untuk sisi klien, sehingga satu koneksi TCP dikenali oleh pasangan alamat-port kedua sisi. Program kecil berikut memetakan anatomi satu endpoint:

java TitikAkhir.java
import java.net.InetAddress;
import java.net.InetSocketAddress;

public class TitikAkhir {
    public static void main(String[] args) {
        // Socket address = alamat IP + port, alamat lengkap sebuah endpoint
        InetSocketAddress alamatServer = new InetSocketAddress(
                InetAddress.getLoopbackAddress(), 8080);

        System.out.println("host    : " + alamatServer.getHostString());
        System.out.println("alamat  : " + alamatServer.getAddress().getHostAddress());
        System.out.println("port    : " + alamatServer.getPort());
        System.out.println("string  : " + alamatServer);
    }
}

Output yang Dihasilkan:

text
host    : localhost
alamat  : 127.0.0.1
port    : 8080
string  : localhost/127.0.0.1:8080

Method getHostString() mengambil nama host tanpa pencarian DNS lanjutan, getAddress() mengembalikan objek InetAddress, dan getPort() mengembalikan port persis seperti yang diberikan saat objek dibuat. Perhatikan bentuk toString() — localhost/127.0.0.1:8080 — yang menampilkan nama, alamat, dan port sekaligus; format inilah yang sering muncul pada pesan galat koneksi sehingga kita perlu membacakannya dengan lancar.

Peran ServerSocket dan Socket #

Sisi server memegang dua class yang berbeda peran. ServerSocket hanya bertugas satu hal: mendengarkan permintaan koneksi pada satu alamat dan port. Method accept() memblokir thread pemanggil sampai seorang klien datang, lalu mengembalikan objek Socket baru yang mewakili koneksi tersebut — satu ServerSocket bisa melayani banyak klien bergilir dengan memanggil accept() berulang kali. Sisi klien cukup membuat satu Socket dengan alamat tujuan; constructor Socket langsung melakukan TCP handshake ke server.

Setelah tersambung, kedua pihak berkomunikasi lewat aliran byte. socket.getInputStream() dan socket.getOutputStream() adalah pintu data biasa — sama seperti aliran berkas yang kita kenal pada Section I/O. Kita membungkusnya dengan InputStreamReader/OutputStreamWriter dan charset eksplisit agar byte terjemahkan menjadi teks dengan benar. Urutan percakapan lengkapnya tampak seperti ini:

Sekarang kita wujudkan diagram tersebut menjadi program nyata dalam satu berkas. Server berjalan di thread terpisah pada port ephemeral loopback, klien menyapa, lalu keduanya menutup koneksi secara rapi:

java SapaSocket.java
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;

public class SapaSocket {
    public static void main(String[] args) throws Exception {
        // Server menunggu di loopback pada port ephemeral pilihan sistem
        ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
        System.out.println("server siap    : " + server.isBound());

        Thread pegawaiServer = new Thread(() -> {
            try (Socket sambungan = server.accept();
                 BufferedReader masuk = new BufferedReader(new InputStreamReader(
                         sambungan.getInputStream(), StandardCharsets.UTF_8));
                 PrintWriter keluar = new PrintWriter(new OutputStreamWriter(
                         sambungan.getOutputStream(), StandardCharsets.UTF_8), true)) {
                String pesan = masuk.readLine();
                keluar.println("gema: " + pesan);
            } catch (IOException e) {
                System.out.println("server galat   : " + e.getMessage());
            }
        });
        pegawaiServer.start();

        Socket klien = new Socket(InetAddress.getLoopbackAddress(), server.getLocalPort());
        try (klien;
             PrintWriter keluar = new PrintWriter(new OutputStreamWriter(
                     klien.getOutputStream(), StandardCharsets.UTF_8), true);
             BufferedReader masuk = new BufferedReader(new InputStreamReader(
                     klien.getInputStream(), StandardCharsets.UTF_8))) {
            keluar.println("halo dari Budi");
            System.out.println("terhubung      : " + klien.isConnected());
            System.out.println("alamat remote  : " + klien.getInetAddress().getHostAddress());
            System.out.println("gema diterima  : " + masuk.readLine());
        }

        pegawaiServer.join();
        System.out.println("client tertutup: " + klien.isClosed());
        server.close();
        System.out.println("server tertutup: " + server.isClosed());
    }
}

Output yang Dihasilkan:

text
server siap    : true
terhubung      : true
alamat remote  : 127.0.0.1
gema diterima  : gema: halo dari Budi
client tertutup: true
server tertutup: true

Mari kita telaah baris-baris kode di atas:

  1. Baris 9 (new ServerSocket(0, 1, InetAddress.getLoopbackAddress())): tiga argumen berarti port 0 (minta sistem memilih port ephemeral bebas), backlog 1 (jumlah koneksi menunggu), dan alamat bind loopback. Port pilihan sistem dapat dibaca lewat server.getLocalPort() — nilai inilah yang diberikan ke klien, bukan angka tetap yang rawan bentrok.
  2. Baris 12-25 (Thread pegawaiServer = new Thread(...)): server dijalankan di thread terpisah agar method main bisa berperan sebagai klien dalam program yang sama. Di dalam thread, server.accept() memblokir sampai klien tiba, lalu percakapan dilakukan lewat BufferedReader dan PrintWriter dengan StandardCharsets.UTF_8.
  3. Baris 27 (Socket klien = new Socket(...)): constructor langsung menjalankan TCP handshake ke alamat dan port server. Pemanggilan berikutnya (klien.getOutputStream()) baru bisa dilakukan setelah koneksi terbentuk.
  4. Baris 34 (try (klien; ...)): klien dideklarasikan di luar blok supaya statusnya masih bisa diperiksa setelah try-with-resources menutupnya — isClosed() mengembalikan true karena close() sudah dipanggil otomatis.
  5. Baris 41 (pegawaiServer.join()): main menunggu thread server menyelesaikan tugasnya sebelum mencetak status akhir, sehingga urutan baris output selalu sama.

Koneksi sebagai Aliran Byte #

Tidak ada pesan bawaan pada socket — hanya aliran byte yang mengalir dua arah. Protokol sederhana seperti di atas memakai baris teks: klien mengirim satu baris, server membalas satu baris. Batas pesan ditentukan oleh karakter baris baru yang kita sepakati sendiri. Ketika salah satu pihak memanggil close(), pihak lainnya melihat readLine() mengembalikan null — sinyal end of stream bahwa lawan bicara telah selesai.

Percakapan satu baris tentu masih jauh dari aplikasi nyata. Chapter berikutnya memperluas pola ini menjadi server echo yang menerima banyak baris sampai klien selesai, lengkap dengan protokol tutup yang rapi di kedua sisi.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

SapaSocket.java

Unduh berkas
java
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;

public class SapaSocket {
    public static void main(String[] args) throws Exception {
        // Server menunggu di loopback pada port ephemeral pilihan sistem
        ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
        System.out.println("server siap    : " + server.isBound());

        Thread pegawaiServer = new Thread(() -> {
            try (Socket sambungan = server.accept();
                 BufferedReader masuk = new BufferedReader(new InputStreamReader(
                         sambungan.getInputStream(), StandardCharsets.UTF_8));
                 PrintWriter keluar = new PrintWriter(new OutputStreamWriter(
                         sambungan.getOutputStream(), StandardCharsets.UTF_8), true)) {
                String pesan = masuk.readLine();
                keluar.println("gema: " + pesan);
            } catch (IOException e) {
                System.out.println("server galat   : " + e.getMessage());
            }
        });
        pegawaiServer.start();

        Socket klien = new Socket(InetAddress.getLoopbackAddress(), server.getLocalPort());
        try (klien;
             PrintWriter keluar = new PrintWriter(new OutputStreamWriter(
                     klien.getOutputStream(), StandardCharsets.UTF_8), true);
             BufferedReader masuk = new BufferedReader(new InputStreamReader(
                     klien.getInputStream(), StandardCharsets.UTF_8))) {
            keluar.println("halo dari Budi");
            System.out.println("terhubung      : " + klien.isConnected());
            System.out.println("alamat remote  : " + klien.getInetAddress().getHostAddress());
            System.out.println("gema diterima  : " + masuk.readLine());
        }

        pegawaiServer.join();
        System.out.println("client tertutup: " + klien.isClosed());
        server.close();
        System.out.println("server tertutup: " + server.isClosed());
    }
}

TitikAkhir.java

Unduh berkas
java
import java.net.InetAddress;
import java.net.InetSocketAddress;

public class TitikAkhir {
    public static void main(String[] args) {
        // Socket address = alamat IP + port, alamat lengkap sebuah endpoint
        InetSocketAddress alamatServer = new InetSocketAddress(
                InetAddress.getLoopbackAddress(), 8080);

        System.out.println("host    : " + alamatServer.getHostString());
        System.out.println("alamat  : " + alamatServer.getAddress().getHostAddress());
        System.out.println("port    : " + alamatServer.getPort());
        System.out.println("string  : " + alamatServer);
    }
}