Chapter sebelumnya menempatkan IP, port, TCP, dan socket pada peta besar jaringan; sekarang kita menulis kode nyatanya. Antarmuka yang kita pakai adalah POSIX Socket API — keluarga fungsi C yang dibakukan POSIX. Perlu dicatat, API ini bukan bagian dari pustaka standar C++, sehingga cppreference tidak memuatnya; rujukan resmi yang kita pakai adalah Linux man-pages (man7.org). Agar contoh dapat dijalankan dan diperiksa langsung, program utama chapter ini menjalankan echo server dan client dalam satu proses: thread server menerima koneksi dan mengembalikan setiap pesan, thread client mengirim pesan dan mencetak hasilnya. Konsep thread dan lambda kita warisi dari section Concurrency dan Parallelism, dan karena glibc modern (2.34 ke atas) sudah menggabungkan pthread ke dalam libc, program ini dikompilasi tanpa flag -pthread.

Empat Header POSIX yang Dipakai #

Empat header cukup untuk seluruh program chapter ini. Masing-masing punya peran yang tegas:

HeaderIsi yang dipakai
<sys/socket.h>fungsi inti: socket, bind, listen, accept, connect, send, recv; tipe socklen_t
<netinet/in.h>struct sockaddr_in, konstanta AF_INET dan INADDR_LOOPBACK
<arpa/inet.h>konversi urutan byte: htons, htonl, ntohs, ntohl
<unistd.h>close() untuk menutup file descriptor

Konvensi galatnya ala C: fungsi mengembalikan -1 saat gagal dan menulis kode kesalahan ke variabel global errno, yang kita terjemahkan menjadi pesan dengan std::strerror(errno) dari header <cstring> dan <cerrno>. Di kode kita juga ada awalan :: pada pemanggilan seperti ::socket(...) — awalan itu menegaskan kita memanggil fungsi C global, bukan anggota class atau fungsi lain yang namanya bentrok.

Alur Panggilan: Server dan Client #

Alur panggilan server dan client terbelah setelah socket() yang sama. Server menjadi pasif dengan tiga langkah; client langsung aktif dengan satu langkah:

Dari diagram di atas, server memanggil socket() lalu bind() untuk menempati alamat dan port, listen() untuk mengubah socket menjadi pendengar, dan accept() untuk mengambil satu koneksi dari antrean. Client hanya memanggil socket() lalu connect() menuju alamat server — three-way handshake TCP terjadi di dalam connect(). Setelah koneksi berdiri, kedua sisi memakai send() dan recv() pada socket koneksi yang berbeda dari socket pendengar server: accept() mengembalikan descriptor baru untuk koneksi itu, dan socket pendengar tetap bertugas menerima client berikutnya.

struct sockaddr_in dan Urutan Byte Jaringan #

Alamat IPv4 diwakili struct sockaddr_in dari <netinet/in.h> dengan tiga anggota yang kita isi: sin_family (keluarga alamat, AF_INET), sin_port (port), dan sin_addr.s_addr (alamat IP sebagai satu bilangan 32-bit). Fungsi POSIX menerima pointer ke struct generik sockaddr, bukan sockaddr_in, sehingga kita mengisi sockaddr_in lalu mengonversinya dengan reinterpret_cast<sockaddr*>(&alamat) — pola warisan C untuk satu antarmuka dengan banyak keluarga alamat.

Setiap anggota lebih dari satu byte wajib ditulis dalam network byte order (big-endian). Prosesor umum seperti x86-64 adalah little-endian, sehingga nilai mentah akan terbaca terbalik oleh jaringan: port 443 yang tidak melewati htons() bisa diperlakukan sebagai port 47873. Empat fungsi konversinya mudah diingat: htons/htonl (host to network, short/long) saat mengisi struct, ntohs/ntohl (network to host) saat membacanya. htons(0) tetap nol — kita pakai nilai itu sebagai sinyal “pilihkan port bebas” pada pola ephemeral di bawah.

Program Utama: Echo Server dan Client dalam Satu Proses #

Program berikut merangkai semuanya. Thread utama menyiapkan socket pendengar; thread server menerima satu koneksi, mengirim sapaan, lalu mengembalikan setiap baris pesan; thread client mengirim dua pesan beserta perintah kontrol selesai, dan mencatat bahwa recv() mengembalikan 0 saat server menutup koneksi. Semua pencetakan dilakukan thread client dan thread utama secara berurutan, sehingga keluaran pasti sama setiap dijalankan.

cpp
// echo-tcp.cpp — server dan client TCP dalam satu proses: thread server
// menerima koneksi lalu mengembalikan (echo) setiap baris pesan, thread
// client mengirim pesan dan mencetak hasil round-trip-nya.
#include <arpa/inet.h>   // htons, htonl
#include <netinet/in.h>  // struct sockaddr_in, INADDR_LOOPBACK
#include <sys/socket.h>  // socket, bind, listen, accept, connect, send, recv
#include <unistd.h>      // close
#include <cerrno>        // errno
#include <cstdint>       // uint16_t
#include <cstring>       // strerror
#include <iostream>      // std::cout, std::cerr
#include <string>
#include <thread>        // std::thread, join

namespace {

// Mengirim seluruh byte pesan. send() boleh berhenti di tengah buffer,
// sehingga kita mengulang untuk sisa byte yang belum terkirim.
bool kirimSemua(int fd, const std::string& pesan) {
    std::size_t terkirim = 0;
    while (terkirim < pesan.size()) {
        ssize_t n = ::send(fd, pesan.data() + terkirim, pesan.size() - terkirim, 0);
        if (n < 0) {
            return false;
        }
        terkirim += static_cast<std::size_t>(n);
    }
    return true;
}

// Membaca satu baris (sampai '\n'). Mengembalikan false saat koneksi
// ditutup (recv() mengembalikan 0) atau saat galat.
bool terimaBaris(int fd, std::string& baris) {
    baris.clear();
    char c = 0;
    while (true) {
        ssize_t n = ::recv(fd, &c, 1, 0);
        if (n <= 0) {
            return false;
        }
        if (c == '\n') {
            return true;
        }
        baris.push_back(c);
    }
}

}  // namespace

int main() {
    // 1) Socket pendengar: socket -> bind -> listen.
    int fdListen = ::socket(AF_INET, SOCK_STREAM, 0);
    if (fdListen < 0) {
        std::cerr << "socket() gagal: " << std::strerror(errno) << "\n";
        return 1;
    }

    sockaddr_in alamat{};
    alamat.sin_family = AF_INET;
    alamat.sin_port = htons(0);                       // 0 = OS memilih port bebas
    alamat.sin_addr.s_addr = htonl(INADDR_LOOPBACK);  // 127.0.0.1
    if (::bind(fdListen, reinterpret_cast<const sockaddr*>(&alamat), sizeof(alamat)) < 0) {
        std::cerr << "bind() gagal: " << std::strerror(errno) << "\n";
        ::close(fdListen);
        return 1;
    }

    if (::listen(fdListen, 8) < 0) {
        std::cerr << "listen() gagal: " << std::strerror(errno) << "\n";
        ::close(fdListen);
        return 1;
    }

    // 2) getsockname membaca port yang benar-benar dipilih OS.
    sockaddr_in terikat{};
    socklen_t panjangTerikat = sizeof(terikat);
    if (::getsockname(fdListen, reinterpret_cast<sockaddr*>(&terikat), &panjangTerikat) < 0) {
        std::cerr << "getsockname() gagal: " << std::strerror(errno) << "\n";
        ::close(fdListen);
        return 1;
    }
    const int port = ntohs(terikat.sin_port);  // dipakai client, tidak dicetak
    std::cout << "[server] mendengarkan di 127.0.0.1 (port ephemeral dari getsockname)\n";

    // Statistik yang diisi thread server dan dibaca main() setelah join.
    int jumlahEcho = 0;
    long totalByte = 0;

    // 3) Thread server: accept satu koneksi, kirim sapaan, lalu echo.
    std::thread threadServer([&fdListen, &jumlahEcho, &totalByte]() {
        int fdKoneksi = ::accept(fdListen, nullptr, nullptr);
        if (fdKoneksi < 0) {
            return;  // kasus galat tidak terjadi pada contoh loopback ini
        }

        if (!kirimSemua(fdKoneksi, "Selamat datang di echo server!\n")) {
            ::close(fdKoneksi);
            return;
        }

        std::string masuk;
        while (terimaBaris(fdKoneksi, masuk)) {
            if (masuk == "selesai") {
                break;  // pesan kontrol: server menutup koneksi lebih dulu
            }
            if (!kirimSemua(fdKoneksi, masuk + "\n")) {
                break;
            }
            ++jumlahEcho;
            totalByte += static_cast<long>(masuk.size());
        }
        ::close(fdKoneksi);
    });

    // 4) Thread client: socket -> connect, kirim pesan, cetak round-trip.
    std::thread threadClient([port]() {
        int fdClient = ::socket(AF_INET, SOCK_STREAM, 0);
        if (fdClient < 0) {
            std::cerr << "socket() client gagal: " << std::strerror(errno) << "\n";
            return;
        }

        sockaddr_in tujuan{};
        tujuan.sin_family = AF_INET;
        tujuan.sin_port = htons(static_cast<uint16_t>(port));
        tujuan.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
        if (::connect(fdClient, reinterpret_cast<const sockaddr*>(&tujuan), sizeof(tujuan)) < 0) {
            std::cerr << "connect() gagal: " << std::strerror(errno) << "\n";
            ::close(fdClient);
            return;
        }
        std::cout << "[client] connect() berhasil ke 127.0.0.1, koneksi TCP terbentuk\n";

        std::string baris;
        if (terimaBaris(fdClient, baris)) {
            std::cout << "[client] server menyapa: \"" << baris << "\"\n";
        }

        for (const char* pesan : {"Pertama dari client", "Kedua dari client"}) {
            if (!kirimSemua(fdClient, std::string(pesan) + "\n")) {
                break;
            }
            std::cout << "[client] kirim : \"" << pesan << "\"\n";
            if (terimaBaris(fdClient, baris)) {
                std::cout << "[client] echo  : \"" << baris << "\"\n";
            }
        }

        if (kirimSemua(fdClient, "selesai\n")) {
            std::cout << "[client] kirim perintah \"selesai\", menunggu server menutup koneksi\n";
            char sisa = 0;
            ssize_t akhir = ::recv(fdClient, &sisa, 1, 0);
            std::cout << "[client] recv() terakhir mengembalikan " << akhir
                      << " (0 berarti koneksi ditutup dari sisi server)\n";
        }
        ::close(fdClient);
    });

    // 5) Join semua thread, baru tutup socket pendengar.
    threadClient.join();
    threadServer.join();
    ::close(fdListen);

    std::cout << "[ringkasan] server menangani " << jumlahEcho
              << " pesan di-echo (total " << totalByte << " byte payload)\n";
    std::cout << "[ringkasan] kedua thread telah di-join dan semua socket ditutup\n";
    return 0;
}

Output yang Dihasilkan:

text
[server] mendengarkan di 127.0.0.1 (port ephemeral dari getsockname)
[client] connect() berhasil ke 127.0.0.1, koneksi TCP terbentuk
[client] server menyapa: "Selamat datang di echo server!"
[client] kirim : "Pertama dari client"
[client] echo  : "Pertama dari client"
[client] kirim : "Kedua dari client"
[client] echo  : "Kedua dari client"
[client] kirim perintah "selesai", menunggu server menutup koneksi
[client] recv() terakhir mengembalikan 0 (0 berarti koneksi ditutup dari sisi server)
[ringkasan] server menangani 2 pesan di-echo (total 36 byte payload)
[ringkasan] kedua thread telah di-join dan semua socket ditutup

Mari kita bedah baris per baris:

  1. Baris 4-7 (#include <arpa/inet.h> dst.): empat header POSIX dari tabel awal chapter — fungsi inti socket, struct alamat, konversi byte, dan close().
  2. Baris 19-28 (kirimSemua): pembungkus send() yang mengulang sampai seluruh byte terkirim. send() boleh berhenti di tengah buffer, jadi nilai kembaliannya (ssize_t n) dijumlahkan ke terkirim hingga habis.
  3. Baris 33-46 (terimaBaris): pembaca satu baris byte-per-byte sampai '\n'. recv() mengembalikan jumlah byte; 0 berarti koneksi ditutup lawan, negatif berarti galat — keduanya mengembalikan false.
  4. Baris 52 (::socket(AF_INET, SOCK_STREAM, 0)): meminta socket TCP IPv4; SOCK_STREAM adalah tipe untuk TCP, dan protokol 0 memilih bawaannya.
  5. Baris 58-61 (pengisian sockaddr_in): sin_port = htons(0) menyuruh OS memilih port bebas, sin_addr.s_addr = htonl(INADDR_LOOPBACK) mengikat ke 127.0.0.1.
  6. Baris 62 dan 68 (::bind, ::listen(fdListen, 8)): socket pendengar menempati alamat lalu aktif mendengarkan dengan antrean koneksi hingga 8.
  7. Baris 75-83 (getsockname dan ntohs): port yang dipilih OS dibaca lewat getsockname(), dikonversi ke urutan byte host dengan ntohs(), lalu disimpan di port untuk dipakai client.
  8. Baris 90-91 (thread server): lambda menangkap referensi socket pendengar dan dua variabel statistik, lalu langsung memanggil accept() yang menunggu hingga ada client.
  9. Baris 96-111 (sapaan dan loop echo): server mengirim sapaan, membaca baris demi baris, dan mengembalikan setiap pesan kecuali selesai — pesan kontrol yang menghentikan loop — sambil menambah jumlahEcho dan totalByte.
  10. Baris 116-132 (thread client): client membuat socket sendiri dan memanggil connect() ke 127.0.0.1 pada port yang sama; keberhasilan connect() ditandai tercetaknya baris handshake.
  11. Baris 135-146 (round-trip): client menerima sapaan, lalu untuk setiap pesan mengirim dan menunggu balasannya — pasangan kirim dan echo itulah round-trip.
  12. Baris 149-156 (penutupan dari sisi server): setelah mengirim selesai, client memanggil recv() sekali lagi dan mendapat 0 — bukti server sudah menutup koneksi; close(fdClient) melepas descriptor.
  13. Baris 160-166 (join dan ringkasan): thread utama men-join kedua thread, menutup socket pendengar, lalu mencetak statistik server. Angka 2 dan 36 bisa dibaca dengan aman karena join menjamin pekerjaan thread server sudah selesai.

Pola Port Ephemeral: bind ke Port 0 lalu getsockname #

Pada program nyata, server biasanya mengikat port tetap seperti 5000. Contoh pembelajaran justru sebaiknya tidak: port tetap bisa sedang dipakai program lain, dan angkanya pun tercetak di keluaran sehingga sulit dijadikan acuan yang stabil. Pola port ephemeral menyelesaikan keduanya — isi sin_port dengan htons(0), dan kernel memilih port bebas saat bind(). Karena kita tetap perlu tahu port mana yang terpilih (client harus mengarah ke sana), getsockname() membaca pasangan alamat-port aktual dari socket yang sudah terikat, dan ntohs() mengubahnya ke urutan byte host.

Server dan client TCP kini sudah berdiri utuh dalam satu proses. Sebelum lanjut ke framing, kita lihat dulu pasangannya yang jauh lebih ringkas: UDP, yang tanpa koneksi dan tanpa jaminan — tetapi persis karena itu dipakai di tempat-tempat yang tepat.

Rujukan resmi: socket(2) — Linux man-pages , accept(2) — Linux man-pages , getsockname(2) — Linux man-pages , cppreference — std::thread .