Membuka koneksi TCP itu mahal: sistem operasi menjalankan handshake tiga arah, server mengalokasikan memori untuk sesi baru, dan jaringan ditempuh dua kali sebelum satu byte data terkirim. Bayangkan aplikasi yang memproses ribuan permintaan per detik — jika setiap permintaan membuka koneksi baru, sebagian besar tenaga habis untuk seremoni pembukaan, bukan untuk data. Solusinya sudah lama menjadi standar: keep-alive membuat koneksi bertahan setelah satu permintaan selesai, dan pool menyimpannya untuk dipakai ulang. HttpClient modern menjalankan keduanya secara otomatis di balik layar, dengan satu syarat sederhana namun sering dilanggar: satu objek client dipakai untuk banyak permintaan. Chapter ini membuktikan efeknya lewat server penghitung yang kita tulis sendiri, lalu membahas kerelaan HttpClient terhadap pemakaian bersama antar thread.
Keep-Alive di Balik Layar #
Protokol HTTP/1.1 menganggap koneksi persisten secara default: setelah respons lengkap terkirim, koneksi boleh dipakai untuk permintaan berikutnya tanpa pembukaan ulang. Penanda selesai respons adalah header Content-Length yang tepat — klien tahu persis di mana body berakhir dan kapan socket boleh dikembalikan ke pool. Header Connection: close (dari klien maupun server) menyatakan kebalikannya: setelah respons ini, koneksi ditutup. Pada praktiknya kita hampir tidak pernah menyentuh header ini secara manual; HttpClient mengelolanya, dan keputusan kita yang berpengaruh hanyalah apakah objek client yang sama dipakai lagi atau dibuang.
Membuktikan keep-alive membutuhkan server yang bisa menghitung dua hal berbeda: berapa kali koneksi TCP dibuka, dan berapa permintaan HTTP yang lewat. Kita tulis server HTTP mini secara manual di atas ServerSocket — tanpa framework — sehingga koneksi dan permintaan terlihat telanjang:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class HitungKoneksi {
public static void main(String[] args) throws Exception {
// Server HTTP tulisan tangan untuk menghitung koneksi vs permintaan
AtomicInteger koneksi = new AtomicInteger();
AtomicInteger permintaan = new AtomicInteger();
AtomicReference<Socket> sambunganAktif = new AtomicReference<>();
ServerSocket pintu = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
Thread pegawai = new Thread(() -> {
try {
while (true) {
Socket s = pintu.accept();
sambunganAktif.set(s);
koneksi.incrementAndGet();
try (s;
BufferedReader masuk = new BufferedReader(new InputStreamReader(
s.getInputStream(), StandardCharsets.ISO_8859_1));
PrintWriter keluar = new PrintWriter(new OutputStreamWriter(
s.getOutputStream(), StandardCharsets.ISO_8859_1))) {
while (true) {
String baris = masuk.readLine();
if (baris == null) {
break; // klien menutup koneksi
}
if (!baris.isEmpty()) {
continue; // lewati baris header permintaan
}
permintaan.incrementAndGet();
// Content-Length yang tepat membuat koneksi layak dipakai ulang
keluar.print("HTTP/1.1 200 OK\r\n");
keluar.print("Content-Type: text/plain\r\n");
keluar.print("Content-Length: 3\r\n\r\n");
keluar.print("hai");
keluar.flush();
}
} catch (IOException e) {
// koneksi ditutup dari luar
}
}
} catch (IOException e) {
// ServerSocket ditutup — pegawai selesai
}
});
pegawai.start();
try {
HttpClient klien = HttpClient.newHttpClient();
String url = "http://127.0.0.1:" + pintu.getLocalPort() + "/";
for (int i = 1; i <= 3; i++) {
HttpResponse<String> res = klien.send(
HttpRequest.newBuilder(URI.create(url)).GET().build(),
HttpResponse.BodyHandlers.ofString());
System.out.println("permintaan " + i + " : status " + res.statusCode()
+ ", body " + res.body());
}
System.out.println("koneksi dibuka : " + koneksi.get());
System.out.println("permintaan total: " + permintaan.get());
} finally {
pintu.close();
Socket s = sambunganAktif.get();
if (s != null) {
s.close();
}
pegawai.join();
}
}
}Output yang Dihasilkan:
permintaan 1 : status 200, body hai
permintaan 2 : status 200, body hai
permintaan 3 : status 200, body hai
koneksi dibuka : 1
permintaan total: 3Mari kita telaah bagian-bagian pentingnya:
- Baris 33-37 (
pintu.accept()di dalamwhile (true)): loop luar mewakili siklus hidup koneksi — setiapaccept()berarti satu koneksi TCP baru, dan penghitungkoneksidinaikkan di sana. Loop dalam membaca permintaan HTTP satu per satu pada koneksi yang sama. - Baris 46-50 (
baris.isEmpty()): satu permintaan HTTP berakhir pada baris kosong — batas antara head dan body. Setiap baris kosong berarti satu permintaan selesai dibaca, sehinggapermintaanbertambah. - Baris 53-57 (balasan manual): respons ditulis mentah sesuai format HTTP — baris status, header, baris kosong, lalu body
haisepanjang tepat 3 byte sesuaiContent-Length: 3. Angka yang cocok inilah kunci keep-alive: klien tahu body selesai tanpa perlu koneksi ditutup. - Baris 66-74 (
HttpClient.newHttpClient()lalu tigasend()): satu client mengirim tiga permintaan berurutan ke origin yang sama. Hasil akhirnya bukti yang kita cari:koneksi dibuka: 1,permintaan total: 3— ketiga permintaan menumpang satu koneksi yang sama.
Bila setiap permintaan memakai client baru, jumlah koneksi akan sama dengan jumlah permintaan — itulah biaya tersembunyi dari kebiasaan “buat client di dalam method”. Pada beban tinggi, kebiasaan tersebut berlipat menjadi ribuan handshake, ribuan socket TIME_WAIT di sistem operasi, dan tundaan yang terasa oleh pengguna.
Satu Client untuk Banyak Thread #
HttpClient dirancang untuk dipakai bersama. Seluruh operasinya — sinkron maupun asinkron — aman dipanggil dari banyak thread sekaligus, dan pool koneksi internalnya dibagi secara otomatis: dua thread yang memanggil origin yang sama bisa menumpang satu koneksi bergilir atau memakai dua koneksi paralel sesuai kebutuhan. Program berikut membagikan satu client ke lima permintaan berurutan lalu tiga thread yang berjalan bersamaan:
import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class KlienDipakaiUlang {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/sapa/", exchange -> {
String nama = exchange.getRequestURI().getPath().substring("/sapa/".length());
byte[] body = ("halo " + nama).getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
try {
HttpClient klien = HttpClient.newHttpClient(); // SATU instance untuk semuanya
// Lima permintaan berurutan lewat client yang sama
List<String> nama = List.of("Budi", "Siti", "Dewi", "Rina", "Ali");
List<String> hasilUrut = new ArrayList<>();
for (String n : nama) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:" + server.getAddress().getPort()
+ "/sapa/" + n))
.GET().build();
HttpResponse<String> res = klien.send(req, HttpResponse.BodyHandlers.ofString());
hasilUrut.add(res.body());
}
for (int i = 0; i < hasilUrut.size(); i++) {
System.out.println("urutan " + (i + 1) + " : " + hasilUrut.get(i));
}
// Tiga thread memakai client yang sama secara bersamaan
List<String> tugas = List.of("Budi", "Siti", "Dewi");
List<String> hasilParalel =
new ArrayList<>(Collections.nCopies(tugas.size(), null));
List<Thread> pegawai = new ArrayList<>();
for (int i = 0; i < tugas.size(); i++) {
int idx = i;
Thread t = new Thread(() -> {
try {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:"
+ server.getAddress().getPort()
+ "/sapa/" + tugas.get(idx)))
.GET().build();
HttpResponse<String> res = klien.send(req,
HttpResponse.BodyHandlers.ofString());
hasilParalel.set(idx, res.body());
} catch (Exception e) {
hasilParalel.set(idx, "galat " + e.getClass().getSimpleName());
}
});
pegawai.add(t);
t.start();
}
for (Thread t : pegawai) {
t.join();
}
for (int i = 0; i < hasilParalel.size(); i++) {
System.out.println("thread " + (i + 1) + " : " + hasilParalel.get(i));
}
} finally {
server.stop(0);
}
}
}Output yang Dihasilkan:
urutan 1 : halo Budi
urutan 2 : halo Siti
urutan 3 : halo Dewi
urutan 4 : halo Rina
urutan 5 : halo Ali
thread 1 : halo Budi
thread 2 : halo Siti
thread 3 : halo DewiDua detail desain pada program ini menjamin hasil yang terurut. Kelompok berurutan menyimpan setiap body ke hasilUrut lebih dulu, lalu mencetak semuanya setelah lima respons diterima. Kelompok paralel memakai pola yang sama ditambah Collections.nCopies sebagai kerangka list berukuran tetap — setiap thread menulis pada slot indeksnya sendiri, join() menunggu semua selesai, dan pencetakan terakhir mengikuti urutan indeks, bukan urutan kedatangan thread. Kedua pola ini adalah cara standar menjaga determinisme output pada kode konkuren.
Koneksi yang efisien kini terkelola. Satu lapisan lagi menentukan aman atau tidaknya percakapan di jaringan terbuka: enkripsi. Chapter berikutnya masuk ke TLS — handshake, sertifikat, dan truststore — beserta cara HttpClient memvalidasi identitas server sebelum percaya pada jawabannya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
HitungKoneksi.java
Unduh berkasimport java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class HitungKoneksi {
public static void main(String[] args) throws Exception {
// Server HTTP tulisan tangan untuk menghitung koneksi vs permintaan
AtomicInteger koneksi = new AtomicInteger();
AtomicInteger permintaan = new AtomicInteger();
AtomicReference<Socket> sambunganAktif = new AtomicReference<>();
ServerSocket pintu = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
Thread pegawai = new Thread(() -> {
try {
while (true) {
Socket s = pintu.accept();
sambunganAktif.set(s);
koneksi.incrementAndGet();
try (s;
BufferedReader masuk = new BufferedReader(new InputStreamReader(
s.getInputStream(), StandardCharsets.ISO_8859_1));
PrintWriter keluar = new PrintWriter(new OutputStreamWriter(
s.getOutputStream(), StandardCharsets.ISO_8859_1))) {
while (true) {
String baris = masuk.readLine();
if (baris == null) {
break; // klien menutup koneksi
}
if (!baris.isEmpty()) {
continue; // lewati baris header permintaan
}
permintaan.incrementAndGet();
// Content-Length yang tepat membuat koneksi layak dipakai ulang
keluar.print("HTTP/1.1 200 OK\r\n");
keluar.print("Content-Type: text/plain\r\n");
keluar.print("Content-Length: 3\r\n\r\n");
keluar.print("hai");
keluar.flush();
}
} catch (IOException e) {
// koneksi ditutup dari luar
}
}
} catch (IOException e) {
// ServerSocket ditutup — pegawai selesai
}
});
pegawai.start();
try {
HttpClient klien = HttpClient.newHttpClient();
String url = "http://127.0.0.1:" + pintu.getLocalPort() + "/";
for (int i = 1; i <= 3; i++) {
HttpResponse<String> res = klien.send(
HttpRequest.newBuilder(URI.create(url)).GET().build(),
HttpResponse.BodyHandlers.ofString());
System.out.println("permintaan " + i + " : status " + res.statusCode()
+ ", body " + res.body());
}
System.out.println("koneksi dibuka : " + koneksi.get());
System.out.println("permintaan total: " + permintaan.get());
} finally {
pintu.close();
Socket s = sambunganAktif.get();
if (s != null) {
s.close();
}
pegawai.join();
}
}
}
KlienDipakaiUlang.java
Unduh berkasimport com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class KlienDipakaiUlang {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/sapa/", exchange -> {
String nama = exchange.getRequestURI().getPath().substring("/sapa/".length());
byte[] body = ("halo " + nama).getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
try {
HttpClient klien = HttpClient.newHttpClient(); // SATU instance untuk semuanya
// Lima permintaan berurutan lewat client yang sama
List<String> nama = List.of("Budi", "Siti", "Dewi", "Rina", "Ali");
List<String> hasilUrut = new ArrayList<>();
for (String n : nama) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:" + server.getAddress().getPort()
+ "/sapa/" + n))
.GET().build();
HttpResponse<String> res = klien.send(req, HttpResponse.BodyHandlers.ofString());
hasilUrut.add(res.body());
}
for (int i = 0; i < hasilUrut.size(); i++) {
System.out.println("urutan " + (i + 1) + " : " + hasilUrut.get(i));
}
// Tiga thread memakai client yang sama secara bersamaan
List<String> tugas = List.of("Budi", "Siti", "Dewi");
List<String> hasilParalel =
new ArrayList<>(Collections.nCopies(tugas.size(), null));
List<Thread> pegawai = new ArrayList<>();
for (int i = 0; i < tugas.size(); i++) {
int idx = i;
Thread t = new Thread(() -> {
try {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:"
+ server.getAddress().getPort()
+ "/sapa/" + tugas.get(idx)))
.GET().build();
HttpResponse<String> res = klien.send(req,
HttpResponse.BodyHandlers.ofString());
hasilParalel.set(idx, res.body());
} catch (Exception e) {
hasilParalel.set(idx, "galat " + e.getClass().getSimpleName());
}
});
pegawai.add(t);
t.start();
}
for (Thread t : pegawai) {
t.join();
}
for (int i = 0; i < hasilParalel.size(); i++) {
System.out.println("thread " + (i + 1) + " : " + hasilParalel.get(i));
}
} finally {
server.stop(0);
}
}
}