Percakapan satu baris pada Chapter sebelumnya adalah kerangka paling kecil dari aplikasi jaringan. Aplikasi nyata membutuhkan lebih dari itu: server yang menerima banyak pesan berturut-turut, protokol yang jelas untuk mengakhiri percakapan, dan jaminan bahwa data tiba utuh dan berurutan. Untungnya, jaminan terakhir itu sudah dibawa oleh TCP sendiri — checksum, nomor urut, dan retransmisi otomatis bekerja di lapisan transport tanpa campur tangan kode kita. Tugas kita adalah merancang protokol aplikasi di atas aliran byte tersebut. Chapter ini membangun server echo multi-baris lengkap, lalu membahas blocking I/O dan pengaturan SO_TIMEOUT agar penantian tidak berlangsung tanpa batas.

Merancang Protokol Baris #

Aliran TCP tidak mengenal “pesan”; TCP hanya mengirim urutan byte. Batas pesan karena itu harus kita sepakati sendiri, dan cara paling sederhana adalah protokol baris: satu baris teks sama dengan satu pesan, ditandai karakter baris baru. Protokol pada contoh ini punya tiga aturan — klien mengirim baris apa pun dan server membalas dengan gema: <isi>; klien mengirim kata SELESAI dan server membalas sampai jumpa lalu menutup sisi server; penutupan koneksi oleh klien membuat readLine() di sisi server mengembalikan null.

Program berikut menjalankan server dan klien dalam satu berkas sehingga bisa diuji tanpa jaringan eksternal. Server berjalan di thread terpisah, melayani satu klien sampai percakapan berakhir, kemudian menutup semua resource secara rapi:

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

public class ServerGema {
    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());

        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 baris;
                while ((baris = masuk.readLine()) != null) {
                    if (baris.equals("SELESAI")) {
                        keluar.println("sampai jumpa");
                        break;
                    }
                    keluar.println("gema: " + baris);
                }
            } catch (IOException e) {
                System.out.println("server galat : " + e.getMessage());
            }
        });
        pegawaiServer.start();

        try (Socket klien = new Socket(InetAddress.getLoopbackAddress(), server.getLocalPort());
             PrintWriter keluar = new PrintWriter(new OutputStreamWriter(
                     klien.getOutputStream(), StandardCharsets.UTF_8), true);
             BufferedReader masuk = new BufferedReader(new InputStreamReader(
                     klien.getInputStream(), StandardCharsets.UTF_8))) {

            for (String pesan : new String[]{"Budi", "Siti", "Dewi"}) {
                keluar.println(pesan);
                System.out.println("klien kirim  : " + pesan);
                System.out.println("server balas : " + masuk.readLine());
            }
            keluar.println("SELESAI");
            System.out.println("klien kirim  : SELESAI");
            System.out.println("server balas : " + masuk.readLine());
        }
        pegawaiServer.join();
        server.close();
    }
}

Output yang Dihasilkan:

text
klien kirim  : Budi
server balas : gema: Budi
klien kirim  : Siti
server balas : gema: Siti
klien kirim  : Dewi
server balas : gema: Dewi
klien kirim  : SELESAI
server balas : sampai jumpa

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 10 (new ServerSocket(0, 1, InetAddress.getLoopbackAddress())): server bind ke loopback dengan port ephemeral. Port 0 meminta sistem operasi memilih port yang sedang bebas, sehingga program tidak pernah bentrok dengan layanan lain di mesin yang sama.
  2. Baris 13-34 (while ((baris = masuk.readLine()) != null)): inti server echo. Setiap baris yang terbaca dibalas dengan gema: <isi>; baris SELESAI memicu balasan penutup lalu break keluar dari loop. Ketika klien menutup koneksi, readLine() mengembalikan null dan loop berhenti dengan sendirinya — dua jalur keluar yang sama-sama rapi.
  3. Baris 18 (new PrintWriter(..., true)): argumen true mengaktifkan auto-flush sehingga setiap println langsung dikirim. Tanpa auto-flush, data bisa tersimpan di penyangga internal dan klien menunggu data yang sebenarnya sudah siap tetapi belum terkirim.
  4. Baris 37-44 (blok try klien): klien mengikuti pola kirim-satu-terima-satu secara bergantian. Pola bergantian ini membuat urutan output program dapat diprediksi: setiap println selalu diikuti readLine sebelum baris berikutnya dikirim.
  5. Baris 49 (pegawaiServer.join()): main menunggu thread server tuntas sebelum menutup ServerSocket, memastikan seluruh balasan sudah diproses saat program berakhir.

Blocking I/O dan SO_TIMEOUT #

Seluruh pemanggilan accept(), read(), dan write() pada pola ini bersifat blocking: thread berhenti sampai peristiwa yang ditunggu terjadi. Blocking I/O mudah dipahami dan jarang salah, tetapi punya satu risiko — penantian tanpa batas. Klien yang tersambung lalu diam selamanya akan mengunci thread server pada satu read(). Pada server dengan satu thread per klien, satu klien macet berarti satu thread terbuang.

Java menyediakan opsi socket SO_TIMEOUT sebagai jaring pengaman. Opsi ini membatasi lama operasi pembacaan (dan accept() pada ServerSocket) sebelum SocketTimeoutException dilempar; thread mendapat kembali kendali dan bisa memutuskan menutup koneksi atau mencoba lagi. Contoh berikut sengaja tidak mengirim klien apa pun agar penantian accept() habis:

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.SocketTimeoutException;

public class BatasWaktuServer {
    public static void main(String[] args) throws IOException {
        // SO_TIMEOUT membatasi lama accept() menunggu klien
        ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
        server.setSoTimeout(300);

        try {
            server.accept(); // tidak ada klien yang datang
            System.out.println("klien diterima");
        } catch (SocketTimeoutException e) {
            System.out.println("jenis galat  : " + e.getClass().getSimpleName());
            System.out.println("pesan galat  : " + e.getMessage());
        } finally {
            server.close();
        }
    }
}

Output yang Dihasilkan:

text
jenis galat  : SocketTimeoutException
pesan galat  : Accept timed out

Method setSoTimeout(300) mengatur batas 300 milidetik untuk accept(). Karena tidak ada klien yang tersambung, penantian berakhir dengan SocketTimeoutException bertipe checked — kode harus menangkapnya secara eksplisit, dan blok finally memastikan ServerSocket tertutup apa pun hasilnya. Opsi yang sama pada objek Socket (bukan ServerSocket) membatasi lama read(); pola penanganannya identik dan akan kita pakai lagi pada Chapter Timeouts dan Retries.

Pola Server yang Siap Diperluas #

Struktur server echo di atas adalah cetak biru server TCP pada umumnya. Loop luar memanggil accept() berulang kali — setiap pemanggilan menghasilkan Socket baru untuk satu klien; loop dalam memproses protokol baris sampai selesai; try-with-resources menutup semua aliran secara berurutan. Dari cetak biru ini, aplikasi nyata menambahkan hal-hal yang bersifat kebijakan: format pesan yang lebih kaya (misalnya JSON), autentikasi sebelum percakapan, dan pembatasan ukuran baris agar klien jahat tidak mengirim baris sepanjang memori.

TCP memberi kita keandalan pengiriman, tetapi tetap menyisakan satu kelemahan struktural: setiap pesan butuh koneksi dan protokol sendiri. Ketika kebutuhan hanyalah melempar data singkat tanpa jaminan tiba — misalnya detak jantung layanan — protokol UDP lebih ringan. Chapter berikutnya beralih ke DatagramSocket dan pola komunikasi tanpa koneksi tersebut.

Referensi #

Berkas Sumber

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

BatasWaktuServer.java

Unduh berkas
java
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.SocketTimeoutException;

public class BatasWaktuServer {
    public static void main(String[] args) throws IOException {
        // SO_TIMEOUT membatasi lama accept() menunggu klien
        ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
        server.setSoTimeout(300);

        try {
            server.accept(); // tidak ada klien yang datang
            System.out.println("klien diterima");
        } catch (SocketTimeoutException e) {
            System.out.println("jenis galat  : " + e.getClass().getSimpleName());
            System.out.println("pesan galat  : " + e.getMessage());
        } finally {
            server.close();
        }
    }
}

ServerGema.java

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

public class ServerGema {
    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());

        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 baris;
                while ((baris = masuk.readLine()) != null) {
                    if (baris.equals("SELESAI")) {
                        keluar.println("sampai jumpa");
                        break;
                    }
                    keluar.println("gema: " + baris);
                }
            } catch (IOException e) {
                System.out.println("server galat : " + e.getMessage());
            }
        });
        pegawaiServer.start();

        try (Socket klien = new Socket(InetAddress.getLoopbackAddress(), server.getLocalPort());
             PrintWriter keluar = new PrintWriter(new OutputStreamWriter(
                     klien.getOutputStream(), StandardCharsets.UTF_8), true);
             BufferedReader masuk = new BufferedReader(new InputStreamReader(
                     klien.getInputStream(), StandardCharsets.UTF_8))) {

            for (String pesan : new String[]{"Budi", "Siti", "Dewi"}) {
                keluar.println(pesan);
                System.out.println("klien kirim  : " + pesan);
                System.out.println("server balas : " + masuk.readLine());
            }
            keluar.println("SELESAI");
            System.out.println("klien kirim  : SELESAI");
            System.out.println("server balas : " + masuk.readLine());
        }
        pegawaiServer.join();
        server.close();
    }
}