Pada chapter UDP dan Datagram kita mengirim dan menerima datagram: setiap unit yang dikirim pengirim tiba sebagai satu keseluruhan, sehingga batas antarpesan terjaga dengan sendirinya. Saat berpindah ke TCP, jaminan itu hilang. TCP hanya melihat aliran byte (byte stream) — dua pesan yang dikirim berurutan bisa tiba menyatu dalam satu pembacaan, dan satu pesan besar bisa tiba terpotong menjadi beberapa pembacaan. Tugas mengembalikan batas pesan itu berpindah ke aplikasi, dan keputusan desainnya disebut framing. Chapter ini membahas dua strategi framing yang paling umum, cara membaca satu pesan lengkap lewat loop recv(), penanganan partial send/recv, lalu merakit semuanya menjadi protokol biner mini yang benar-benar diuji.
Masalah Framing: TCP Aliran Byte Tanpa Batas Pesan #
Fungsi send() tidak mengenal “pesan”; fungsi itu menyalin byte ke buffer socket, dan recv() menarik byte apa pun yang sudah tersedia. Man page recv(2) menyatakannya gamblang:
“The receive calls normally return any data available, up to the requested amount, rather than waiting for receipt of the full amount requested.” — man7.org,
recv(2)
Terjemahannya: recv() mengembalikan byte yang kebetulan sudah tiba, bukan byte yang sekadar kita minta. Jika pengirim memanggil send() dua kali untuk dua pesan, keempat bentuk penerimaan berikut sama sahnya menurut TCP:
Pengirim menulis dua "pesan" ke satu koneksi TCP:
send(): "HALO" send(): "DUNIA"
Bentuk penerimaan yang mungkin di sisi penerima:
recv(): "HALODUNIA" (dua pesan menyatu dalam satu recv)
recv(): "HALO", recv(): "DUNIA" (kebetulan terpisah rapi)
recv(): "HAL", recv(): "ODUNIA" (terpotong di tengah kata)Di loopback yang cepat, pemisahan rapi sering terjadi karena kebetulan, sehingga program yang salah framing terlihat bekerja saat diuji. Di jaringan nyata, potongan datang dalam ukuran acak mengikuti isi buffer kernel dan jadwal packet. Program yang mencetak setiap hasil recv() sebagai satu pesan akan membaca HAL dan ODUNIA sebagai dua “pesan” yang tidak pernah dikirim siapa pun.
Dua Strategi Framing: Length Prefix dan Delimiter #
Ada dua keluarga jawaban untuk pertanyaan “di mana batas pesan”. Strategi length prefix menuliskan panjang pesan di depan isinya, sehingga penerima membaca header dulu, lalu membaca tepat sebanyak itu. Strategi delimiter mengakhiri setiap pesan dengan penanda khusus yang tidak muncul di isinya — newline adalah pilihan paling populer untuk protokol berbasis teks.
| Aspek | Length prefix | Delimiter (newline) |
|---|---|---|
| Bentuk data | biner, cocok untuk payload apa pun | teks, mudah diinspeksi dengan telnet |
| Syarat pada isi | tidak ada | isi dilarang memuat penanda |
| Yang perlu disepakati | ukuran header dan byte order | karakter penanda dan aturan escaping |
| Dipakai di | banyak protokol biner; HTTP memakai Content-Length untuk body | SMTP, IRC, log streaming |
Program pertama menerapkan strategi delimiter. Pembaca satu baris di chapter TCP Socket dengan POSIX Socket API menarik recv() byte demi byte; varian di sini lebih hemat panggilan sistem: kumpulkan potongan ke satu buffer, lalu potong setiap kali penanda ditemukan. Agar penerima tidak mungkin “kebetulan benar”, pengirim sengaja memindahkan batas: pesan kedua dikirim terpotong di tengah kata.
#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; // terinterupsi sinyal: ulangi
return false;
}
terkirim += static_cast<size_t>(n); // send boleh mengirim sebagian
}
return true;
}
// Thread penerima: mengumpulkan byte sampai newline, lalu cetak tiap baris.
void jalankanPenerima(int listener) {
int koneksi = accept(listener, nullptr, nullptr);
if (koneksi < 0) {
std::perror("accept");
return;
}
std::string sisa; // byte yang belum membentuk baris lengkap
char buffer[64];
for (;;) {
ssize_t r = recv(koneksi, buffer, sizeof buffer, 0);
if (r == 0) break; // pengirim menutup koneksi: EOF
if (r < 0) {
if (errno == EINTR) continue;
std::perror("recv");
break;
}
sisa.append(buffer, static_cast<size_t>(r));
// Pisahkan semua baris lengkap yang sudah tersedia di buffer
size_t posisi;
while ((posisi = sisa.find('\n')) != std::string::npos) {
std::string baris = sisa.substr(0, posisi);
sisa.erase(0, posisi + 1);
std::cout << "diterima baris: " << baris << "\n";
}
}
std::cout << "EOF: sisa buffer \"[" << sisa << "]\" (kosong berarti utuh)\n";
close(koneksi);
}
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 penerima(jalankanPenerima, listener);
// Pengirim: dua panggilan send() dengan batas yang SENGAJA dipindah
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;
}
kirimSemua(fd, "baris satu\nbaris "); // pesan kedua terpotong di sini
kirimSemua(fd, "dua\nbaris tiga\n");
close(fd); // tutup: penerima melihat EOF
penerima.join();
close(listener);
std::cout << "2 panggilan send() direkonstruksi menjadi 3 baris utuh\n";
return 0;
}Output yang Dihasilkan:
diterima baris: baris satu
diterima baris: baris dua
diterima baris: baris tiga
EOF: sisa buffer "[]" (kosong berarti utuh)
2 panggilan send() direkonstruksi menjadi 3 baris utuhMari kita bedah program delimiter ini:
- Baris 14-25 (
kirimSemua): loopsend()berjalan sampai seluruhdata.size()byte keluar — helper yang sama dengan chapter TCP, kini ditambah penangananEINTR. Nilai baliksend()bisa lebih kecil dari yang diminta (partial send), sehingga byte yang sudah keluar dihitung dan sisanya dikirim ulang dari posisi itu. - Baris 37-45: penerima tidak berasumsi tentang ukuran potongan — hasil
recv()sebagian kecil maupun beberapa pesan sekaligus diperlakukan sama: ditambahkan kesisa. - Baris 49-53:
find('\n')mencari penanda di buffer;substrmengambil satu baris lengkap danerasemembuang byte yang sudah dipakai. Loopwhilewajib karena saturecv()bisa membawa beberapa baris sekaligus. - Baris 100-101: pengirim membagi aliran di posisi yang janggal —
"baris satu\nbaris "lalu"dua\nbaris tiga\n"— sehingga pesan kedua terpotong menjadi"baris "dan"dua". Penerima tetap menyusun ulang dengan benar karena batas ditentukan penanda, bukan waktu kirim. - Baris 56: ketika pengirim menutup koneksi, penerima mencetak sisa buffer. Tanda
[]berarti semua byte sudah terpakai; sisa yang tidak kosong menandakan pesan tanpa penanda akhir — bug protokol yang terdeteksi cepat.
Program Utama: Framing Biner Length Prefix #
Format bingkai yang kita rancang sederhana namun lengkap: setiap pesan diawali header 4 byte berisi panjang isinya, ditulis dalam network byte order (big-endian) lewat htonl(), diikuti isi pesan apa adanya. Byte order disepakati karena integer bisa tersusun berbeda antar arsitektur; dengan htonl() di pengirim dan ntohl() di penerima, protokol tetap benar meski kedua sisi berjalan di mesin yang berbeda-endian. Program utama berjalan dalam satu proses dua thread: pengirim menyusun tiga bingkai lalu mengirim semuanya dalam satu panggilan send(), dan penerima menjalankan loop pembaca framing untuk memisahkan kembali ketiganya.
#include <arpa/inet.h> // htonl, ntohl
#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 <cstdint> // uint32_t
#include <cstdio> // perror
#include <cstring> // memcpy
#include <iostream>
#include <string>
#include <string_view>
#include <thread>
constexpr uint32_t BATAS_PANJANG = 1024; // penolakan panjang yang tidak wajar
// 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; // terinterupsi sinyal: ulangi
return false;
}
terkirim += static_cast<size_t>(n); // send boleh mengirim sebagian
}
return true;
}
// Membaca TEPAT n byte dengan loop recv(). Kembalikan 1 bila cukup,
// 0 bila koneksi ditutup pengirim (EOF), -1 bila galat.
int terimaTepat(int fd, char* tujuan, size_t n) {
size_t terkumpul = 0;
while (terkumpul < n) {
ssize_t r = recv(fd, tujuan + terkumpul, n - terkumpul, 0);
if (r == 0) return 0; // EOF sebelum n byte terkumpul
if (r < 0) {
if (errno == EINTR) continue;
return -1;
}
terkumpul += static_cast<size_t>(r); // recv boleh memberi sebagian
}
return 1;
}
// Menyusun satu bingkai: 4 byte panjang (network byte order) + isi pesan.
std::string bingkaiPesan(std::string_view pesan) {
uint32_t panjang = htonl(static_cast<uint32_t>(pesan.size()));
std::string bingkai(reinterpret_cast<const char*>(&panjang), sizeof panjang);
bingkai.append(pesan);
return bingkai;
}
// Thread penerima: loop pembaca framing yang memisahkan kembali pesan.
void jalankanPenerima(int listener) {
int koneksi = accept(listener, nullptr, nullptr);
if (koneksi < 0) {
std::perror("accept");
return;
}
int jumlah = 0;
for (;;) {
// 1. Baca header 4 byte: panjang isi pesan
char header[4];
int hasil = terimaTepat(koneksi, header, sizeof header);
if (hasil <= 0) {
if (hasil < 0) std::perror("recv header");
break; // EOF setelah pesan terakhir = kondisi berhenti normal
}
uint32_t panjangJaringan;
std::memcpy(&panjangJaringan, header, sizeof panjangJaringan);
uint32_t panjang = ntohl(panjangJaringan);
// 2. Panjang dari jaringan tidak boleh dipercaya mentah-mentah
if (panjang > BATAS_PANJANG) {
std::cout << "panjang tidak wajar (" << panjang
<< " byte), koneksi dihentikan\n";
break;
}
// 3. Baca isi pesan persis sepanjang header
std::string isi(panjang, '\0');
if (terimaTepat(koneksi, isi.data(), panjang) != 1) {
std::cout << "aliran berakhir di tengah isi pesan\n";
break;
}
std::cout << "diterima (" << panjang << " byte): " << isi << "\n";
++jumlah;
}
std::cout << "framing loop berhenti setelah " << jumlah << " pesan utuh\n";
close(koneksi);
}
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 penerima(jalankanPenerima, listener);
// Pengirim: 3 pesan length-prefix digabung, dikirim dalam SATU panggilan
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;
}
std::string payload;
for (std::string_view pesan : {"Halo", "pesan kedua", "sampai jumpa"}) {
payload += bingkaiPesan(pesan);
}
kirimSemua(fd, payload);
close(fd); // tutup koneksi: EOF menjadi penanda akhir aliran
penerima.join();
close(listener);
std::cout << "klien: 1 panggilan send() terkirim, semua descriptor ditutup\n";
return 0;
}Output yang Dihasilkan:
diterima (4 byte): Halo
diterima (11 byte): pesan kedua
diterima (12 byte): sampai jumpa
framing loop berhenti setelah 3 pesan utuh
klien: 1 panggilan send() terkirim, semua descriptor ditutupMari kita bedah baris demi baris:
- Baris 16 (
BATAS_PANJANG): konstanta batas panjang pesan. Header datang dari pihak lain di jaringan, dan panjang yang tidak wajar harus ditolak sebelum dipakai mengalokasikan memori. - Baris 34-46 (
terimaTepat): inti cara membaca satu pesan lengkap — looprecv()sampai tepatnbyte terkumpul.recv()boleh memberi sebagian (partial recv), sehingga setiap hasil ditambahkan ke posisiterkumpuldan kebutuhan sisanya (n - terkumpul) menjadi ukuran permintaan berikutnya. Nilai balik memisahkan tiga keadaan: cukup (1), EOF (0), dan galat (-1). - Baris 49-54 (
bingkaiPesan): panjang pesan dikonversihtonl()lalu byte-nya disalin mentah ke depan bingkai denganmemcpy; isi pesan menempel setelah header. - Baris 65-75 (membaca header): loop framing membaca 4 byte header dengan
terimaTepat, lalumemcpymengembalikan byte mentah menjadiuint32_tdanntohl()menerjemahkannya ke byte order mesin penerima. - Baris 78-82 (validasi panjang): panjang di atas
BATAS_PANJANGmenghentikan koneksi. Nilai ini bukan data kepercayaan — penerima yang lalai bisa dibuat mengalokasikan memori raksasa hanya dengan satu header bohong. - Baris 85-90 (membaca isi):
std::string isi(panjang, '\0')menyediakan tempat persis sepanjang header,terimaTepatmengisinya sampai penuh, dan pesan utuh baru dicetak. - Baris 138-143 (sisi pengirim): tiga bingkai digabung ke satu
payload, lalu satukirimSemua()mengirim semuanya.close(fd)setelahnya membuat penerima melihat EOF — penanda aliran berakhir. - Baris 121-147 (urutan aman antar thread): listener sudah
bind()danlisten()sebelum thread penerima berjalan;maintidak mencetak apa pun sampaijoin()selesai sehingga output penerima tidak terselingi; semua descriptor ditutup dan thread di-join sebelummainberakhir.
Menangani Partial Send dan Partial Recv #
Perilaku yang membuat loop terimaTepat() wajib ada di setiap program TCP adalah hak recv() untuk mengembalikan sebagian. Man page recv(2) menegaskan fungsi itu “normally return any data available, up to the requested amount, rather than waiting for receipt of the full amount requested” — mengembalikan data yang tersedia, bukan menunggu jumlah yang diminta terkumpul. Sisi send() punya kebebasan serupa. Man page send(2) menjelaskan: “When the message does not fit into the send buffer of the socket, send() normally blocks, unless the socket has been placed in nonblocking I/O mode.” Pada socket nonblocking, pengiriman besar bisa berhenti setengah jalan dan send() mengembalikan byte yang sudah masuk buffer saja. Pola kirimSemua() dan terimaTepat() menangani keduanya dengan cara yang sama: simpan hitungan yang sudah selesai, ulangi sisa pekerjaannya.
Satu pintu pintas yang sering ditanyakan adalah flag MSG_WAITALL pada recv(). Man page menjelaskan: “This flag requests that the operation block until the full request is satisfied. However, the call may still return less data than requested if a signal is caught, an error or disconnect occurs.” Panggilan tetap bisa kembali lebih sedikit dari yang diminta saat sinyal menginterupsi, galat terjadi, atau koneksi terputus — sehingga loop tetap diperlukan. Itulah alasan terimaTepat() tidak bergantung pada flag apa pun: kebutuhan “baca sampai cukup” dipenuhi oleh kode kita sendiri.
Loop framing juga punya kondisi berhenti yang jelas. Man page recv(2) menyatakan: “When a stream socket peer has performed an orderly shutdown, the return value will be 0 (the traditional “end-of-file” return).” Nilai 0 itulah yang program utama pakai: setelah pengirim menutup koneksi, terimaTepat() melaporkan EOF dan loop framing berhenti dengan rapi. Urutan penutupan di program kita menjaga kebersihan itu pula — koneksi ditutup, thread di-join, listener ditutup, semua sebelum main berakhir.
Kedua strategi di chapter ini menjawab pertanyaan “di mana batas pesan”. Protokol yang kita pakai sehari-hari menjawabnya dengan cara yang sama: HTTP, topik chapter berikutnya, memisahkan headernya dengan delimiter CRLF dan membatasi body-nya dengan header Content-Length — length prefix versi teks. Setelah melihat framing dari sisi protokol, kita siap membongkar format HTTP baris demi baris.
Referensi: man7.org — recv(2) , man7.org — send(2)