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:

text
-- 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 mini

Strukturnya 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.

cpp
#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:

text
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 response

Mari kita bedah baris demi baris:

  1. Baris 42-51 (buatResponse): response tetap disusun sebagai satu string — status line, dua header, baris kosong, lalu body Halo dari server mini yang tepat 21 byte sesuai janji Content-Length: 21. Ketidakcocokan antara janji dan isi adalah cacat protokol klasik yang perlu dihindari sejak contoh pertama.
  2. Baris 56-79 (jalankanServerMini): server menerima koneksi, lalu membaca request sampai penanda \r\n\r\n ditemukan — delimiter CRLF dari chapter framing bekerja persis seperti dirancang. Setelah itu server menjawab dan menutup koneksi, yang membuat recv() milik klien mengembalikan 0 (EOF).
  3. Baris 104 (thread server in-process): std::thread serverMini menjalankan server di listener yang sudah bind() dan listen(); backlog TCP menyimpan koneksi klien walau accept() baru dipanggil beberapa saat kemudian.
  4. Baris 122-126 (menulis request): request line GET / HTTP/1.1, header Host yang wajib, header Connection: close, lalu baris kosong. Baris kosong itu bukan hiasan — tanpa itu, server masih menunggu sisa header yang tidak pernah datang.
  5. Baris 133-136 (membaca response): terimaSampaiEof() melakukan loop recv() sampai koneksi ditutup server, kemudian close(fd), join(), dan close(listener) memastikan semua descriptor ditutup dan thread di-join sebelum parsing dimulai.
  6. Baris 139-145 (memisah header dan body): find("\r\n\r\n") menemukan batas; substr memotong header di depan dan body di belakangnya — pasangan delimiter dan length yang sama seperti protokol bingkai kita.
  7. 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 200 dari token kedua, bukan dengan mencari substring “200” di seluruh response.
  8. 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

cpp Kerangka libcurl easy interface — ILUSTRATIF (libcurl tidak terinstal di sandbox; dipakai di proyek nyata)
#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