TCP dan UDP

Memahami TCP: Tulang Punggung Komunikasi Internet yang Andal

Di dunia jaringan komputer, Transmission Control Protocol (TCP) adalah salah-satu protokol inti yang menjadi fondasi utama cara data dikirim dan diterima melalui internet. Anggaplah TCP sebagai layanan pos yang sangat teliti dan andal. Ia tidak hanya mengirimkan paket data, tetapi juga memastikan setiap paket tiba di tujuan dengan selamat, dalam urutan yang benar, dan tanpa kerusakan.

TCP bersifat connection-oriented, yang berarti sebelum data apa pun dikirim, sebuah "sesi" atau koneksi yang stabil harus dibuat terlebih dahulu antara pengirim dan penerima. Proses pembentukan koneksi ini dikenal sebagai Three-Way Handshake.

Fitur Utama TCP

  • Handal (Reliable): TCP menggunakan mekanisme seperti nomor urut (sequence number) dan acknowledgment (ACK). Setiap kali sebuah segmen data dikirim, ia diberi nomor. Penerima akan mengirimkan ACK untuk memberitahu pengirim bahwa data telah diterima. Jika pengirim tidak menerima ACK dalam rentang waktu tertentu, ia akan mengirim ulang data tersebut. Inilah yang menjamin pengiriman data.

  • Koneksi Terurut (Ordered): Karena setiap segmen data memiliki nomor urut, penerima dapat menyusun kembali data sesuai urutan yang benar, bahkan jika paket-paket data tersebut tiba di tujuan secara acak.

  • Kontrol Aliran (Flow Control): TCP memastikan pengirim tidak "membanjiri" penerima dengan data. Penerima secara berkala memberitahu pengirim berapa banyak data (disebut window size) yang sanggup ia terima pada satu waktu. Ini mencegah data hilang karena buffer penerima penuh.

  • Pemeriksaan Kesalahan (Error Checking): Setiap segmen TCP menyertakan nilai checksum. Penerima akan menghitung checksum dari data yang diterima dan membandingkannya dengan checksum yang dikirim. Jika nilainya tidak cocok, data dianggap rusak dan akan diabaikan (dan akan dikirim ulang oleh pengirim karena tidak menerima ACK).

Proses Three-Way Handshake

Proses ini adalah "jabat tangan" digital untuk memulai koneksi TCP yang andal:

  1. SYN (Synchronize): Klien (pengirim) mengirimkan segmen dengan flag SYN ke server (penerima) untuk memulai koneksi dan menyinkronkan nomor urut.

  2. SYN-ACK (Synchronize-Acknowledge): Server merespons dengan segmen yang memiliki flag SYN dan ACK. Ini menandakan bahwa server setuju untuk berkomunikasi dan mengakui permintaan dari klien.

  3. ACK (Acknowledge): Klien mengirimkan segmen ACK kembali ke server. Setelah ini diterima, koneksi pun terbentuk dan stabil, siap untuk transfer data.


Praktik: Membuat TCP Listener Manual

Sekarang, mari kita lihat bagaimana cara membuat server sederhana yang "mendengarkan" (listen) koneksi TCP yang masuk menggunakan Bash dan Go. Server ini tidak akan menjadi server web HTTP, melainkan listener TCP murni pada level yang lebih rendah.

1. TCP Listener dengan Bash

Di lingkungan Linux atau macOS, Bash dapat memanfaatkan fitur internal atau utilitas umum seperti nc (netcat) untuk membuat listener TCP dengan cepat. Ini sangat berguna untuk debugging atau tugas sederhana.

Menggunakan /dev/tcp

Bash memiliki fitur virtual device yang memungkinkan kita membuka koneksi TCP seolah-olah itu adalah sebuah file.

Kode:

#!/bin/bash

# Tentukan port yang akan didengarkan
PORT=8080

echo "Menjalankan TCP listener di port $PORT..."
echo "Gunakan 'nc localhost $PORT' atau 'telnet localhost $PORT' dari terminal lain untuk terhubung."

# Loop tak terbatas untuk menerima banyak koneksi (satu per satu)
while true; do
  # exec 3<>/dev/tcp/0/$PORT membuat file descriptor 3 mendengarkan di port yang ditentukan
  # cat <&3 akan membaca data yang masuk dari koneksi tersebut
  # Koneksi akan ditutup setelah klien mengirimkan EOF (misalnya, dengan Ctrl+D)
  coproc { cat; } < <(exec 3<>/dev/tcp/0/$PORT; cat <&3)
  
  # Kode di bawah ini akan dijalankan untuk setiap koneksi yang masuk
  echo -e "HTTP/1.1 200 OK\n\nHello from Bash TCP Listener!" >&${COPROC[1]}
  
  # Tutup file descriptor
  exec 3<&-
  exec 3>&-
  
  echo "Koneksi diterima dan ditutup."
done

Cara Menjalankannya:

  1. Simpan kode di atas ke dalam file, misalnya listener.sh.

  2. Berikan izin eksekusi: chmod +x listener.sh.

  3. Jalankan skrip: ./listener.sh.

  4. Buka terminal lain dan hubungkan ke listener tersebut menggunakan netcat atau telnet:

    nc localhost 8080

    Atau:

    telnet localhost 8080

Setelah terhubung, server Bash akan mengirimkan balasan "Hello from Bash TCP Listener!" dan koneksi akan ditutup. Skrip akan siap menerima koneksi berikutnya.

2. TCP Listener dengan Go (Golang)

Go, dengan pustaka standarnya (net), sangat cocok untuk membangun aplikasi jaringan berperforma tinggi. Membuat listener TCP manual sangatlah mudah dan transparan.

Kode:

package main

import (
	"fmt"
	"io"
	"net"
	"os"
)

const (
	HOST = "localhost"
	PORT = "8080"
	TYPE = "tcp"
)

func main() {
	// Mulai mendengarkan koneksi TCP yang masuk di alamat dan port yang ditentukan.
	// net.Listen() mengembalikan sebuah listener object atau error.
	listener, err := net.Listen(TYPE, HOST+":"+PORT)
	if err != nil {
		fmt.Println("Error listening:", err.Error())
		os.Exit(1)
	}
	// Pastikan listener ditutup saat program berakhir.
	defer listener.Close()

	fmt.Printf("TCP Listener berjalan di %s:%s\n", HOST, PORT)
	fmt.Println("Gunakan 'nc localhost 8080' atau 'telnet localhost 8080' untuk terhubung.")

	// Loop tak terbatas untuk menerima koneksi baru.
	for {
		// listener.Accept() akan memblokir eksekusi sampai ada koneksi baru masuk.
		// Ia mengembalikan object koneksi (net.Conn) yang merepresentasikan koneksi TCP.
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Error accepting connection:", err.Error())
			os.Exit(1)
		}
		
		fmt.Printf("Koneksi diterima dari: %s\n", conn.RemoteAddr().String())

		// Jalankan fungsi untuk menangani koneksi dalam sebuah goroutine baru.
		// Ini memungkinkan server untuk menangani banyak koneksi secara bersamaan.
		go handleConnection(conn)
	}
}

// handleConnection menangani logika untuk setiap koneksi yang masuk.
func handleConnection(conn net.Conn) {
	// Pastikan koneksi ditutup setelah fungsi ini selesai.
	defer conn.Close()

	// Buat buffer untuk membaca data yang masuk.
	buffer := make([]byte, 1024)

	for {
		// Baca data dari koneksi ke dalam buffer.
		n, err := conn.Read(buffer)
		if err != nil {
			// Jika error adalah End-Of-File (EOF), berarti klien telah menutup koneksi.
			if err == io.EOF {
				fmt.Println("Koneksi ditutup oleh klien.")
				return
			}
			fmt.Println("Error reading:", err.Error())
			return
		}

		// Tampilkan data yang diterima ke konsol server.
		fmt.Printf("Menerima data: %s", string(buffer[:n]))

		// Kirim balasan kembali ke klien.
		responseText := "Pesanmu diterima!\n"
		_, err = conn.Write([]byte(responseText))
		if err != nil {
			fmt.Println("Error writing:", err.Error())
			return
		}
	}
}

Cara Menjalankannya:

  1. Simpan kode di atas ke dalam file main.go.

  2. Jalankan dari terminal: go run main.go.

  3. Buka terminal lain dan hubungkan menggunakan netcat:

    nc localhost 8080
  4. Sekarang, ketikkan pesan apa pun di terminal nc dan tekan Enter. Pesan tersebut akan muncul di konsol server, dan server akan mengirimkan balasan "Pesanmu diterima!" kembali ke nc.

Perbedaan utama dalam contoh Go adalah kemampuannya menangani banyak koneksi secara bersamaan (konkuren) berkat penggunaan goroutine (go handleConnection(conn)). Ini adalah pendekatan yang jauh lebih skalabel dan kuat dibandingkan skrip Bash sederhana.


Melanjutkan pembahasan dari TCP, sekarang mari kita selami UDP, "saudaranya" yang lebih cepat dan simpel, beserta contoh praktis dan sebuah studi kasus lengkap menggunakan Go.


Memahami UDP: Protokol Cepat untuk Dunia Real-Time

Jika TCP adalah layanan pos terdaftar yang teliti, maka User Datagram Protocol (UDP) adalah seperti mengirim kartu pos. Anda menulis pesan, menempelkan alamat, dan mengirimkannya. Prosesnya sangat cepat dan efisien, tetapi tidak ada jaminan bahwa kartu pos itu akan sampai, tidak ada pemberitahuan pengiriman, dan jika Anda mengirim beberapa kartu pos, mereka bisa saja tiba tidak berurutan.

UDP bersifat connectionless. Artinya, tidak ada "jabat tangan" (handshake) yang diperlukan sebelum data dikirim. Pengirim langsung menembakkan datagram (paket) ke penerima, dengan harapan paket tersebut akan sampai. Inilah yang membuatnya mendapat julukan protokol "fire-and-forget".

Fitur Utama UDP

  • Connectionless (Tanpa Koneksi): Tidak ada proses handshake. Aplikasi dapat langsung mengirim data, mengurangi latensi awal secara signifikan.

  • Tidak Andal (Unreliable): UDP tidak memiliki mekanisme acknowledgment (ACK) atau pengiriman ulang. Jika sebuah paket hilang di tengah jalan, UDP tidak akan mengetahuinya dan tidak akan mengirimnya kembali.

  • Tidak Terurut (Unordered): Tidak ada nomor urut. Jika paket A dikirim sebelum paket B, tidak ada jaminan paket A akan tiba lebih dulu.

  • Ringan (Lightweight): Karena tidak ada mekanisme untuk keandalan dan urutan, header UDP jauh lebih kecil dan sederhana dibandingkan TCP. Ini berarti overhead (data tambahan selain data inti) lebih sedikit, membuat transmisi lebih efisien.

  • Sangat Cepat: Kombinasi dari semua fitur di atas membuat UDP memiliki latensi yang sangat rendah, ideal untuk aplikasi yang sensitif terhadap waktu.

Kapan Sebaiknya Menggunakan UDP?

Meskipun terdengar "buruk" karena tidak andal, fitur-fitur ini justru menjadi kekuatan UDP dalam skenario yang tepat. UDP unggul ketika kecepatan lebih penting daripada keandalan 100%.

  • Streaming Video dan Audio: Kehilangan beberapa frame video atau milidetik audio (yang sering tidak disadari pengguna) jauh lebih baik daripada menghentikan seluruh stream untuk menunggu paket yang hilang (buffering).

  • Game Online: Dalam game multipemain yang cepat, data posisi pemain terbaru jauh lebih berharga daripada data posisi yang lama tapi terjamin. Menunggu paket lama yang hilang hanya akan menyebabkan lag.

  • DNS (Domain Name System): Proses query DNS sangat sederhana (tanya nama, dapatkan alamat IP). Jika permintaan gagal, jauh lebih efisien bagi aplikasi untuk mengirim ulang permintaan daripada membangun koneksi TCP yang berat.

  • Monitoring dan Metrik: Mengirim data telemetri atau log dari ribuan perangkat. Jika beberapa paket metrik hilang, biasanya tidak menjadi masalah besar dalam gambaran keseluruhan.


Praktik: Server dan Klien UDP dengan Go

Go menyediakan dukungan kelas satu untuk jaringan UDP melalui package net. Berbeda dengan TCP yang menggunakan net.Listener dan net.Conn, UDP biasanya menggunakan net.PacketConn.

1. Server/Listener UDP Sederhana

Server UDP tidak "menerima koneksi" seperti TCP. Ia hanya mengikat (bind) dirinya ke sebuah port dan mendengarkan datagram apa pun yang masuk ke port tersebut dari alamat mana pun.

Kode (server.go):

package main

import (
	"fmt"
	"net"
	"os"
)

func main() {
	// Membuat "koneksi" UDP untuk mendengarkan.
	// net.ListenPacket berbeda dari net.Listen (TCP). Ia menciptakan PacketConn.
	conn, err := net.ListenPacket("udp", ":8080")
	if err != nil {
		fmt.Println("Error listening:", err.Error())
		os.Exit(1)
	}
	// Pastikan koneksi ditutup saat program selesai.
	defer conn.Close()

	fmt.Println("UDP server mendengarkan di port 8080...")
	fmt.Println("Kirim pesan menggunakan 'nc -u localhost 8080'")

	// Buffer untuk menampung data yang masuk.
	// Ukuran 1024 byte biasanya cukup untuk banyak kasus penggunaan UDP.
	buffer := make([]byte, 1024)

	// Loop tak terbatas untuk membaca datagram yang masuk.
	for {
		// ReadFrom akan memblokir sampai sebuah datagram diterima.
		// Ia mengembalikan jumlah byte yang dibaca, alamat pengirim (addr), dan error.
		n, addr, err := conn.ReadFrom(buffer)
		if err != nil {
			fmt.Println("Error reading from client:", err.Error())
			continue // Lanjutkan ke datagram berikutnya
		}

		// Tampilkan pesan yang diterima dan dari siapa.
		fmt.Printf("Menerima '%s' dari %s\n", string(buffer[:n]), addr.String())

		// Kirim balasan kembali ke alamat pengirim.
		response := []byte("Pesan diterima oleh server!")
		_, err = conn.WriteTo(response, addr)
		if err != nil {
			fmt.Println("Error writing response:", err.Error())
		}
	}
}

2. Klien/Pengirim UDP Sederhana

Klien UDP hanya perlu mengetahui alamat server tujuan untuk mengirim datagram.

Kode (client.go):

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
)

func main() {
	// net.Dial untuk UDP tidak membuat koneksi stateful seperti TCP.
	// Ia hanya menyiapkan tujuan default untuk pengiriman dan penerimaan paket.
	conn, err := net.Dial("udp", "localhost:8080")
	if err != nil {
		fmt.Println("Error connecting to server:", err.Error())
		os.Exit(1)
	}
	defer conn.Close()

	// Kirim pesan ke server.
	message := "Halo Server UDP dari Klien Go!"
	_, err = conn.Write([]byte(message))
	if err != nil {
		fmt.Println("Error sending message:", err.Error())
		return
	}
	fmt.Printf("Mengirim pesan: %s\n", message)

	// Buffer untuk membaca balasan dari server.
	buffer := make([]byte, 1024)
	n, err := bufio.NewReader(conn).Read(buffer)
	if err != nil {
		fmt.Println("Error reading response:", err.Error())
		return
	}

	fmt.Printf("Menerima balasan: %s\n", string(buffer[:n]))
}

Cara Menjalankannya:

  1. Buka terminal pertama, jalankan server: go run server.go

  2. Buka terminal kedua, jalankan klien: go run client.go

  3. Anda akan melihat server mencatat pesan dari klien, dan klien mencatat balasan dari server. Anda juga bisa menggunakan netcat untuk berinteraksi dengan server: nc -u localhost 8080.


Studi Kasus Lengkap: Sistem Pengumpul Metrik Sederhana

Mari kita buat kasus penggunaan yang lebih realistis untuk UDP: sebuah layanan pengumpul metrik. Bayangkan Anda memiliki banyak server atau perangkat (agent) yang perlu mengirim metrik (misalnya, penggunaan CPU, memori) secara berkala ke satu server pusat (collector).

UDP sangat cocok di sini karena:

  • Mengirim metrik harus cepat dan ber-overhead rendah.

  • Kehilangan satu atau dua data metrik biasanya tidak masalah.

  • Tidak perlu membangun dan memelihara koneksi TCP yang mahal dari setiap agent.

Kode Collector (collector.go)

Server ini hanya akan mendengarkan metrik yang masuk dan mencetaknya ke konsol.

// collector.go
package main

import (
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	addr, err := net.ResolveUDPAddr("udp", ":9090")
	if err != nil {
		fmt.Println("Error resolving address:", err)
		os.Exit(1)
	}

	conn, err := net.ListenUDP("udp", addr)
	if err != nil {
		fmt.Println("Error listening:", err)
		os.Exit(1)
	}
	defer conn.Close()

	fmt.Println("Metric Collector berjalan di port 9090...")

	buffer := make([]byte, 1024)

	for {
		n, remoteAddr, err := conn.ReadFromUDP(buffer)
		if err != nil {
			fmt.Printf("Error reading UDP packet: %v\n", err)
			continue
		}

		metric := strings.TrimSpace(string(buffer[:n]))
		fmt.Printf("METRIC | Dari: %s | Data: %s\n", remoteAddr, metric)
	}
}

Kode Agent (agent.go)

Agent ini akan secara berkala mengirim metrik palsu (sebagai simulasi) ke collector.

// agent.go
package main

import (
	"fmt"
	"math/rand"
	"net"
	"os"
	"time"
)

func main() {
	collectorAddr := "localhost:9090"
	conn, err := net.Dial("udp", collectorAddr)
	if err != nil {
		fmt.Println("Could not connect to collector:", err)
		os.Exit(1)
	}
	defer conn.Close()

	fmt.Printf("Agent mengirim metrik ke %s\n", collectorAddr)

	// Loop untuk mengirim metrik setiap 2 detik
	for {
		// Simulasikan metrik
		cpuUsage := 15.0 + rand.Float64()*10 // CPU usage between 15% and 25%
		memUsage := 40.0 + rand.Float64()*15 // Memory usage between 40% and 55%

		// Format pesan metrik
		cpuMetric := fmt.Sprintf("cpu.usage:%f", cpuUsage)
		memMetric := fmt.Sprintf("memory.usage:%f", memUsage)

		// Kirim metrik (fire-and-forget)
		_, err := conn.Write([]byte(cpuMetric))
		if err != nil {
			fmt.Printf("Error sending cpu metric: %v\n", err)
		} else {
			fmt.Printf("Sent: %s\n", cpuMetric)
		}

		_, err = conn.Write([]byte(memMetric))
		if err != nil {
			fmt.Printf("Error sending memory metric: %v\n", err)
		} else {
			fmt.Printf("Sent: %s\n", memMetric)
		}

		time.Sleep(2 * time.Second)
	}
}

Cara Menjalankan Studi Kasus:

  1. Terminal 1 (Collector):

    go run collector.go
    # Output:
    # Metric Collector berjalan di port 9090...
  2. Terminal 2 (Agent):

    go run agent.go
    # Output:
    # Agent mengirim metrik ke localhost:9090
    # Sent: cpu.usage:19.823...
    # Sent: memory.usage:51.123...
    # Sent: cpu.usage:22.456...
    # Sent: memory.usage:45.789...
  3. Kembali ke Terminal 1:

    Anda akan melihat output dari collector yang menerima metrik dari agent.

    # Output di Collector:
    # METRIC | Dari: 127.0.0.1:54321 | Data: cpu.usage:19.823...
    # METRIC | Dari: 127.0.0.1:54321 | Data: memory.usage:51.123...
    # METRIC | Dari: 127.0.0.1:54321 | Data: cpu.usage:22.456...
    # METRIC | Dari: 127.0.0.1:54321 | Data: memory.usage:45.789...

Studi kasus ini secara sempurna mendemonstrasikan kekuatan UDP: komunikasi yang efisien, ber-overhead rendah, dan stateless untuk kasus di mana keandalan absolut bukanlah prioritas utama.

Last updated