Framing di chapter Desain Protokol dan Framing menjawab pertanyaan “di mana batas pesan”, dan tidak ada protokol yang memakai jawabannya sesering HTTP. Setiap halaman web, panggilan REST API, dan unduhan yang kita buat hari ini berjalan di atas pesan teks yang dipisahkan delimiter dan body yang dibatasi panjang. Chapter ini menempuh dua arah: pertama, kita membongkar format HTTP itu sendiri dengan client mentah berbasis socket TCP yang benar-benar diuji di loopback — request ditulis tangan, response dip-parse baris demi baris. Kedua, kita mengenali libcurl, pustaka yang dipakai program produksi agar tidak perlu menulis semua itu sendiri; API-nya ditampilkan sebagai blok ilustratif berlabel karena pustakanya tidak terinstal di lingkungan praktikum ini.
Format Pesan HTTP: Request dan Response #
Dokumentasi MDN meringkas posisi HTTP dalam tumpukan jaringan:
“HTTP is a protocol for fetching resources such as HTML documents. It is the foundation of any data exchange on the Web and it is a client-server protocol, which means requests are initiated by the recipient, usually the Web browser.” — MDN, “An overview of HTTP”
Dan tentang bentuk komunikasinya:
“Clients and servers communicate by exchanging individual messages (as opposed to a stream of data). The messages sent by the client are called requests and the messages sent by the server as an answer are called responses.” — MDN, “An overview of HTTP”
Pesan itu punya format teks yang ketat. Setiap baris diakhiri CRLF (\r\n, dua byte: carriage return plus line feed), bukan sekadar \n. Spesifikasi lengkapnya ada di RFC 7230 (HTTP/1.1: Message Syntax and Routing); bentuk ringkasnya seperti ini:
-- request (klien ke server) -- -- response (server ke klien) --
GET / HTTP/1.1 HTTP/1.1 200 OK
Host: example.test Content-Type: text/plain
Connection: close Content-Length: 21
(baris kosong)
(baris kosong = akhir header) Halo dari server miniStrukturnya berlapis jelas. Baris pertama request adalah request line — method (GET), path (/), dan versi protokol, dipisah satu spasi. Baris pertama response adalah status line — versi, kode status, dan reason phrase singkat (200 OK). Baris-baris berikutnya adalah header berformat Nama: Nilai, dan baris kosong menandai akhir header. Apa pun yang datang setelah baris kosong adalah body, dan panjangnya diberitahukan oleh header Content-Length — length prefix versi teks yang sudah kita kenal di chapter framing. Header Connection: close meminta server menutup koneksi setelah satu response; bagi klien pembelajaran kita, itu berarti membaca sampai EOF adalah cara sah untuk mengetahui jawaban sudah lengkap.
Program Utama: HTTP Client Mentah lewat Socket TCP #
Program berikut memuat kedua sisi koneksi dalam satu proses. Thread server mini menjawab dengan response tetap; main bertindak sebagai klien yang menulis request HTTP/1.1 lewat socket, membaca response sampai koneksi ditutup, lalu mem-parse status line, header, dan body. Koneksi berjalan di loopback dengan port 0 — pola port ephemeral dari chapter TCP: OS memilih port yang tersedia — dan thread server sengaja tidak mencetak apa pun sehingga output program sepenuhnya ditentukan oleh klien — deterministik penuh.
#include <arpa/inet.h> // htonl, ntohs
#include <netinet/in.h> // sockaddr_in, htons
#include <sys/socket.h> // socket, bind, listen, accept, connect, send, recv
#include <unistd.h> // close
#include <cerrno> // errno, EINTR
#include <cstddef> // size_t
#include <cstdio> // perror
#include <iostream>
#include <string>
#include <thread>
// Mengirim seluruh isi buffer: loop send() sampai tidak ada byte tersisa.
bool kirimSemua(int fd, const std::string& data) {
size_t terkirim = 0;
while (terkirim < data.size()) {
ssize_t n = send(fd, data.data() + terkirim, data.size() - terkirim, 0);
if (n < 0) {
if (errno == EINTR) continue;
return false;
}
terkirim += static_cast<size_t>(n);
}
return true;
}
// Menerima byte sampai koneksi ditutup (recv = 0), kumpulkan dalam string.
std::string terimaSampaiEof(int fd) {
std::string semua;
char buffer[512];
for (;;) {
ssize_t r = recv(fd, buffer, sizeof buffer, 0);
if (r == 0) break; // peer menutup koneksi: EOF
if (r < 0) {
if (errno == EINTR) continue;
std::perror("recv");
break;
}
semua.append(buffer, static_cast<size_t>(r));
}
return semua;
}
// Response tetap server mini: baris status, dua header, baris kosong, body.
std::string buatResponse() {
return "HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 21\r\n"
"\r\n"
"Halo dari server mini";
}
// Server mini in-process: baca request sampai akhir header, jawab, tutup.
// Thread ini sengaja tidak mencetak apa pun agar seluruh output program
// ditentukan oleh thread klien (tetap deterministik).
void jalankanServerMini(int listener) {
int koneksi = accept(listener, nullptr, nullptr);
if (koneksi < 0) {
std::perror("accept");
return;
}
// Baca request sampai penanda akhir header "\r\n\r\n" ditemukan
std::string request;
char buffer[512];
for (;;) {
ssize_t r = recv(koneksi, buffer, sizeof buffer, 0);
if (r <= 0) {
if (r < 0 && errno == EINTR) continue;
close(koneksi);
return; // klien menutup sebelum request lengkap: tidak dijawab
}
request.append(buffer, static_cast<size_t>(r));
if (request.find("\r\n\r\n") != std::string::npos) break;
}
kirimSemua(koneksi, buatResponse());
close(koneksi); // menutup koneksi = client melihat EOF di akhir body
}
int main() {
// Listener di loopback, port 0 = OS memilih port bebas
int listener = socket(AF_INET, SOCK_STREAM, 0);
if (listener < 0) {
std::perror("socket");
return 1;
}
sockaddr_in alamat{};
alamat.sin_family = AF_INET;
alamat.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
alamat.sin_port = 0;
if (bind(listener, reinterpret_cast<sockaddr*>(&alamat), sizeof alamat) < 0) {
std::perror("bind");
return 1;
}
if (listen(listener, 1) < 0) {
std::perror("listen");
return 1;
}
socklen_t panjang = sizeof alamat;
getsockname(listener, reinterpret_cast<sockaddr*>(&alamat), &panjang);
int port = ntohs(alamat.sin_port);
std::thread serverMini(jalankanServerMini, listener);
// ---------- sisi klien ----------
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
std::perror("socket");
return 1;
}
sockaddr_in tujuan{};
tujuan.sin_family = AF_INET;
tujuan.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
tujuan.sin_port = htons(static_cast<uint16_t>(port));
if (connect(fd, reinterpret_cast<sockaddr*>(&tujuan), sizeof tujuan) < 0) {
std::perror("connect");
return 1;
}
// Request GET HTTP/1.1: baris request, dua header, baris kosong
const std::string request =
"GET / HTTP/1.1\r\n"
"Host: example.test\r\n"
"Connection: close\r\n"
"\r\n";
if (!kirimSemua(fd, request)) {
std::cerr << "pengiriman request gagal\n";
return 1;
}
std::cout << "request terkirim (" << request.size() << " byte)\n";
std::string response = terimaSampaiEof(fd);
close(fd);
serverMini.join();
close(listener);
// ---------- parsing response ----------
size_t batas = response.find("\r\n\r\n");
if (batas == std::string::npos) {
std::cerr << "response tidak lengkap\n";
return 1;
}
std::string header = response.substr(0, batas);
std::string body = response.substr(batas + 4);
// Baris pertama header = status line: VERSI-HTTP KODE ALASAN
size_t akhirStatus = header.find("\r\n");
std::string statusLine =
(akhirStatus == std::string::npos) ? header : header.substr(0, akhirStatus);
std::cout << "status line : " << statusLine << "\n";
// Kode status = token kedua dari status line
size_t spasi1 = statusLine.find(' ');
size_t spasi2 = statusLine.find(' ', spasi1 + 1);
std::cout << "kode status : "
<< statusLine.substr(spasi1 + 1, spasi2 - spasi1 - 1) << "\n";
// Header berikutnya dicetak berurutan sesuai urutan di kawat
size_t mulai = akhirStatus + 2;
while (mulai < header.size()) {
size_t akhir = header.find("\r\n", mulai);
if (akhir == std::string::npos) akhir = header.size();
std::cout << "header : " << header.substr(mulai, akhir - mulai) << "\n";
mulai = akhir + 2;
}
std::cout << "body : " << body << "\n";
std::cout << "server mini menutup koneksi setelah satu response\n";
return 0;
}Output yang Dihasilkan:
request terkirim (57 byte)
status line : HTTP/1.1 200 OK
kode status : 200
header : Content-Type: text/plain
header : Content-Length: 21
body : Halo dari server mini
server mini menutup koneksi setelah satu responseMari kita bedah baris demi baris:
- Baris 42-51 (
buatResponse): response tetap disusun sebagai satu string — status line, dua header, baris kosong, lalu bodyHalo dari server miniyang tepat 21 byte sesuai janjiContent-Length: 21. Ketidakcocokan antara janji dan isi adalah cacat protokol klasik yang perlu dihindari sejak contoh pertama. - Baris 56-79 (
jalankanServerMini): server menerima koneksi, lalu membaca request sampai penanda\r\n\r\nditemukan — delimiter CRLF dari chapter framing bekerja persis seperti dirancang. Setelah itu server menjawab dan menutup koneksi, yang membuatrecv()milik klien mengembalikan 0 (EOF). - Baris 104 (thread server in-process):
std::thread serverMinimenjalankan server di listener yang sudahbind()danlisten(); backlog TCP menyimpan koneksi klien walauaccept()baru dipanggil beberapa saat kemudian. - Baris 122-126 (menulis request): request line
GET / HTTP/1.1, headerHostyang wajib, headerConnection: close, lalu baris kosong. Baris kosong itu bukan hiasan — tanpa itu, server masih menunggu sisa header yang tidak pernah datang. - Baris 133-136 (membaca response):
terimaSampaiEof()melakukan looprecv()sampai koneksi ditutup server, kemudianclose(fd),join(), danclose(listener)memastikan semua descriptor ditutup dan thread di-join sebelum parsing dimulai. - Baris 139-145 (memisah header dan body):
find("\r\n\r\n")menemukan batas;substrmemotong header di depan dan body di belakangnya — pasangan delimiter dan length yang sama seperti protokol bingkai kita. - Baris 147-157 (membaca status line): baris pertama header dipecah di spasi: token pertama versi protokol, token kedua kode status, sisanya reason phrase. Program mencetak
200dari token kedua, bukan dengan mencari substring “200” di seluruh response. - Baris 159-168 (header berurutan dan body): loop
find("\r\n")mencetak setiap header sesuai urutan aslinya di kawat, lalu body dicetak terakhir. Urutan keluaran deterministik karena sumbernya adalah byte yang tetap.
libcurl: Client HTTP untuk Proyek Nyata #
Client mentah di atas layak dibangun untuk belajar; untuk produk, detailnya menumpuk cepat: TLS, redirect, cookie, timeout, proxy, dan DNS caching masing-masing punya aturan sendiri. Di titik itu pekerjaan berpindah ke libcurl, pustaka C yang menjadi pilihan standar untuk HTTP di banyak bahasa dan aplikasi. Tutorial resminya memperkenalkan API dasarnya begitu:
“libcurl first introduced the so called easy interface. All operations in the easy interface are prefixed with ‘curl_easy’. The easy interface lets you do single transfers with a synchronous and blocking function call.” — curl.se, libcurl programming tutorial
Pola pakainya berurutan tetap: curl_global_init() sekali sepanjang masa program, curl_easy_init() membuat handle untuk satu transfer, opsi diatur lewat curl_easy_setopt(), curl_easy_perform() menjalankan transfer, dan curl_easy_cleanup() membebaskan handle. URL diset lewat CURLOPT_URL, dan data yang diterima dialirkan lewat callback yang didaftarkan di CURLOPT_WRITEFUNCTION — kontraknya tegas:
“your callback function should return the number of bytes it ’took care of’. If that is not the same amount of bytes that was passed to it, libcurl aborts the operation and returns with an error code.” — curl.se, libcurl programming tutorial
#include <curl/curl.h>
#include <iostream>
#include <string>
// Callback WRITEFUNCTION: libcurl memanggil fungsi ini untuk setiap potongan body.
size_t tulisKeString(char* ptr, size_t ukuran, size_t nmemb, void* pengguna) {
std::string* keluaran = static_cast<std::string*>(pengguna);
size_t total = ukuran * nmemb;
keluaran->append(ptr, total);
return total; // wajib: jumlah byte yang ditangani; beda = transfer dihentikan
}
int main() {
curl_global_init(CURL_GLOBAL_ALL); // sekali sepanjang masa program
CURL* mudah = curl_easy_init(); // handle satu transfer
std::string body;
curl_easy_setopt(mudah, CURLOPT_URL, "https://example.com/");
curl_easy_setopt(mudah, CURLOPT_WRITEFUNCTION, tulisKeString);
curl_easy_setopt(mudah, CURLOPT_WRITEDATA, &body);
CURLcode hasil = curl_easy_perform(mudah); // sinkron dan blocking
if (hasil != CURLE_OK) {
std::cerr << "galat: " << curl_easy_strerror(hasil) << "\n";
}
curl_easy_cleanup(mudah);
curl_global_cleanup();
return 0;
}Struktur kode itu paralel dengan yang sudah kita bangun sendiri: curl_easy_perform() menutupi kombinasi connect() + kirimSemua() + terimaSampaiEof(), callback WRITEFUNCTION menggantikan loop pengumpulan byte, dan parsing header bisa didapat dari opsi tambahan. Pada proyek nyata, program semacam ini dikompilasi dengan penanda -lcurl (misalnya g++ main.cpp -lcurl) setelah pustaka diinstal lewat manajer paket sistem.
Klien HTTP yang kita tulis di chapter ini, baik versi mentah maupun versi libcurl, tetap blocking: satu thread duduk menunggu sampai jawaban datang. Untuk satu koneksi, pilihan itu jujur dan mudah diikuti. Untuk server yang melayani ribuan koneksi bersamaan, membuka satu thread per koneksi bukan lagi jawaban yang sehat — dan itulah masalah yang dijawab Asio, topik chapter terakhir section ini.
Referensi: MDN — An overview of HTTP , curl.se — libcurl-tutorial