Socket #
Socket adalah abstraksi fundamental untuk komunikasi jaringan — titik akhir dari sebuah koneksi, di mana dua program bisa bertukar data melewati jaringan atau bahkan dalam satu mesin yang sama. Ruby menyediakan library socket di standard library yang membungkus system call socket POSIX dengan antarmuka yang lebih Ruby-like. Tapi lebih dari sekadar sintaks, membangun server socket yang benar memerlukan pemahaman tentang bagaimana menangani banyak klien bersamaan, bagaimana memframe pesan agar tidak terpotong, bagaimana menangani koneksi yang putus dengan bersih, dan bagaimana mengamankan komunikasi dengan SSL/TLS. Artikel ini membahas semua ini dari TCP dasar hingga server yang siap untuk beban nyata.
TCP vs UDP — Memilih Protokol yang Tepat #
Sebelum menulis kode, memahami perbedaan mendasar antara TCP dan UDP adalah fondasi yang tidak bisa dilewati:
TCP (Transmission Control Protocol):
✓ Connection-oriented — ada handshake sebelum data mengalir
✓ Reliable — data dijamin tiba dan dalam urutan yang benar
✓ Flow control dan congestion control bawaan
✓ Stream-based — data mengalir seperti sungai, bukan paket terpisah
✗ Overhead lebih tinggi karena mekanisme reliability
✗ Latency lebih tinggi karena handshake dan retransmit
Cocok untuk: web server, database, file transfer, email, chat
UDP (User Datagram Protocol):
✓ Connectionless — langsung kirim tanpa handshake
✓ Lebih cepat dan latency lebih rendah
✓ Datagram-based — setiap send adalah paket terpisah
✗ Tidak reliable — paket bisa hilang, terduplikat, atau tidak urut
✗ Aplikasi harus tangani reliability sendiri jika dibutuhkan
Cocok untuk: streaming video/audio, game online, DNS, VoIP
sequenceDiagram
participant C as Client
participant S as Server
note over C,S: TCP — Three-way Handshake
C->>S: SYN
S->>C: SYN-ACK
C->>S: ACK
note over C,S: Koneksi terbentuk
C->>S: Data
S->>C: ACK
S->>C: Response
C->>S: ACK
C->>S: FIN
S->>C: FIN-ACK
note over C,S: UDP — Tanpa Handshake
C->>S: Datagram (langsung)
S->>C: Datagram (langsung, atau tidak — tidak ada jaminan)TCP Server Dasar #
TCPServer adalah wrapper yang paling mudah untuk membuat server TCP di Ruby:
require 'socket'
# Buat server yang mendengarkan di port 4000
server = TCPServer.new("localhost", 4000)
puts "Server berjalan di localhost:4000"
# Loop menerima koneksi (blocking — menunggu sampai ada klien)
loop do
client = server.accept # tunggu koneksi masuk
puts "Klien terhubung dari #{client.peeraddr[2]}:#{client.peeraddr[1]}"
# Kirim respons
client.puts "Selamat datang! Waktu sekarang: #{Time.now}"
client.puts "Ketik sesuatu dan tekan Enter:"
# Terima pesan dari klien
pesan = client.gets&.chomp
client.puts "Kamu mengetik: #{pesan}"
client.close # tutup koneksi dengan klien ini
puts "Klien terputus"
end
Untuk menguji server ini tanpa menulis klien terlebih dahulu, gunakan nc (netcat) dari terminal:
# Terminal 1: jalankan server
ruby server.rb
# Terminal 2: hubungkan menggunakan netcat
nc localhost 4000
TCP Klien #
require 'socket'
begin
# Buat koneksi ke server
socket = TCPSocket.new("localhost", 4000)
puts "Terhubung ke server"
# Terima pesan sambutan
sambutan = socket.gets
puts "Server: #{sambutan.chomp}"
prompt = socket.gets
puts "Server: #{prompt.chomp}"
# Kirim pesan
socket.puts "Halo dari klien!"
# Terima balasan
balasan = socket.gets
puts "Server: #{balasan.chomp}"
rescue Errno::ECONNREFUSED => e
puts "Gagal terhubung: server mungkin belum berjalan"
rescue Errno::ETIMEDOUT => e
puts "Koneksi timeout"
ensure
socket&.close
puts "Koneksi ditutup"
end
Server Multi-Client dengan Thread #
Server dasar di atas hanya bisa melayani satu klien pada satu waktu — klien berikutnya harus menunggu sampai yang pertama selesai. Untuk server yang nyata, setiap klien harus ditangani di thread terpisah:
require 'socket'
class EchoServer
def initialize(host, port)
@server = TCPServer.new(host, port)
@klien_aktif = []
@mutex = Mutex.new
puts "Echo server berjalan di #{host}:#{port}"
end
def jalankan
loop do
client = @server.accept
Thread.new(client) { |conn| tangani_klien(conn) }
end
rescue Interrupt
bersihkan
end
private
def tangani_klien(conn)
addr = conn.peeraddr
puts "Klien terhubung: #{addr[2]}:#{addr[1]}"
@mutex.synchronize { @klien_aktif << conn }
conn.puts "Selamat datang di Echo Server!"
conn.puts "Ketik 'keluar' untuk disconnect."
loop do
baris = conn.gets
break if baris.nil? # koneksi ditutup klien
baris.chomp!
break if baris.downcase == "keluar"
puts "[#{addr[2]}] #{baris}"
conn.puts "Echo: #{baris}"
end
rescue Errno::ECONNRESET, Errno::EPIPE => e
# Klien memutus koneksi secara paksa
puts "Koneksi reset oleh klien: #{addr[2]}"
ensure
@mutex.synchronize { @klien_aktif.delete(conn) }
conn.close rescue nil
puts "Klien terputus: #{addr[2]}"
end
def bersihkan
puts "\nMematikan server..."
@mutex.synchronize do
@klien_aktif.each do |conn|
conn.puts "Server sedang dimatikan..."
conn.close rescue nil
end
end
@server.close
puts "Server selesai."
end
end
server = EchoServer.new("0.0.0.0", 4000)
server.jalankan
SO_REUSEADDR — Restart Server Tanpa Tunggu #
Ketika server dihentikan dan dijalankan ulang dengan cepat, port mungkin masih dalam status TIME_WAIT dari koneksi sebelumnya. SO_REUSEADDR mengatasi ini:
require 'socket'
server = TCPServer.new("localhost", 4000)
# Aktifkan SO_REUSEADDR — izinkan reuse port yang baru saja digunakan
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
# Atau cara lebih idiomatik:
server = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
server.bind(Addrinfo.tcp("localhost", 4000))
server.listen(Socket::SOMAXCONN)
Framing Pesan — Masalah yang Sering Diabaikan #
TCP adalah stream protocol — tidak ada batas alami antara satu “pesan” dan pesan berikutnya. Data yang dikirim dengan satu write belum tentu terbaca dengan satu read di sisi lain. Ini disebut framing problem dan merupakan sumber bug yang umum:
# ANTI-PATTERN: menganggap satu send = satu recv
# Server
client.write("pesan pertama") # bisa terpotong di tengah!
# Klien
data = client.read(1024) # bisa mendapat "pesan per" saja
# atau "pesan pertamapesan kedua" sekaligus!
Ada beberapa strategi untuk mengatasi framing problem:
Strategi 1: Delimiter — Pisahkan dengan Karakter Khusus #
# Gunakan \n sebagai delimiter (paling sederhana)
# Server — kirim dengan newline di akhir
client.puts "pesan pertama" # puts otomatis tambahkan \n
client.puts "pesan kedua"
# Klien — baca sampai newline
pesan1 = socket.gets.chomp # baca sampai \n, hapus \n
pesan2 = socket.gets.chomp
# Protokol berbasis JSON dengan delimiter \n (JSON Lines)
require 'json'
# Server — kirim objek JSON satu per baris
def kirim_json(conn, data)
conn.puts JSON.generate(data)
end
# Klien — baca dan parse JSON satu baris per satu
def terima_json(conn)
baris = conn.gets
return nil if baris.nil?
JSON.parse(baris.chomp)
end
kirim_json(client, { tipe: "respons", data: "berhasil", kode: 200 })
pesan = terima_json(socket)
puts pesan["tipe"] # => "respons"
Strategi 2: Length-Prefix — Awali dengan Panjang Pesan #
# Protokol: 4 byte panjang (big-endian) + data pesan
# Lebih kuat dari delimiter karena bisa mengirim data biner
def kirim_pesan(conn, data)
data_bytes = data.encode("UTF-8")
panjang = [data_bytes.bytesize].pack("N") # 4 byte, big-endian
conn.write(panjang + data_bytes)
end
def terima_pesan(conn)
# Baca tepat 4 byte untuk panjang
header = conn.read(4)
return nil if header.nil? || header.bytesize < 4
panjang = header.unpack1("N") # parse 4 byte menjadi Integer
# Baca tepat 'panjang' byte untuk data
data = conn.read(panjang)
return nil if data.nil? || data.bytesize < panjang
data.force_encoding("UTF-8")
end
# Penggunaan
kirim_pesan(client, "Halo dari server!")
pesan = terima_pesan(socket)
puts pesan
Timeout pada Socket #
Tanpa timeout, operasi socket bisa memblokir selamanya jika jaringan bermasalah atau klien tidak responsif:
require 'socket'
require 'timeout'
# Cara 1: Timeout::timeout — paling sederhana
begin
Timeout.timeout(5) do
socket = TCPSocket.new("api.example.com", 80)
socket.puts "GET / HTTP/1.0\r\nHost: api.example.com\r\n\r\n"
response = socket.read
puts response
socket.close
end
rescue Timeout::Error
puts "Koneksi timeout setelah 5 detik"
end
# Cara 2: setsockopt — timeout di level OS (lebih tepat)
socket = TCPSocket.new("api.example.com", 80)
# Set timeout 5 detik untuk receive
timeout = [5, 0].pack("l_2") # struct timeval: [detik, microsecond]
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeout)
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, timeout)
begin
socket.puts "GET / HTTP/1.0\r\n\r\n"
response = socket.read # raise Errno::EAGAIN jika timeout
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
puts "Operasi timeout"
ensure
socket.close
end
# Cara 3: IO.select — polling dengan timeout
ready = IO.select([socket], nil, nil, 5) # tunggu max 5 detik
if ready
data = socket.read_nonblock(4096)
else
puts "Tidak ada data dalam 5 detik (timeout)"
end
IO.select — Multiplexing Tanpa Thread #
IO.select memungkinkan satu thread memantau banyak socket sekaligus — berguna untuk server sederhana yang tidak mau overhead thread:
require 'socket'
server = TCPServer.new("localhost", 4000)
puts "Server berjalan..."
klien_list = []
loop do
# Pantau server socket DAN semua klien yang terhubung
semua_socket = [server] + klien_list
readable, _, error = IO.select(semua_socket, nil, semua_socket, 1.0)
next unless readable
readable.each do |sock|
if sock == server
# Ada koneksi masuk
klien = server.accept_nonblock
klien_list << klien
puts "Klien baru: #{klien.peeraddr[2]}"
klien.puts "Selamat datang!"
else
# Data masuk dari klien yang sudah terhubung
begin
baris = sock.gets_nonblock
if baris
puts "Pesan: #{baris.chomp}"
sock.puts "Echo: #{baris.chomp}"
end
rescue EOFError, Errno::ECONNRESET
# Klien disconnect
puts "Klien terputus"
klien_list.delete(sock)
sock.close
rescue IO::WaitReadable
# Belum ada data (non-blocking)
end
end
end
end
UDP Socket #
UDP cocok untuk aplikasi yang memprioritaskan kecepatan dan bisa mentoleransi paket yang hilang:
require 'socket'
# UDP Server
server = UDPSocket.new
server.bind("localhost", 5000)
puts "UDP server berjalan di port 5000"
loop do
# recvfrom mengembalikan [data, [family, port, hostname, ip]]
data, pengirim = server.recvfrom(1024)
ip = pengirim[3]
port = pengirim[1]
puts "Pesan dari #{ip}:#{port}: #{data}"
# Kirim balasan ke pengirim
server.send("Pesan diterima: '#{data}'", 0, ip, port)
end
require 'socket'
# UDP Klien
klien = UDPSocket.new
5.times do |i|
pesan = "Ping #{i + 1}"
klien.send(pesan, 0, "localhost", 5000)
puts "Terkirim: #{pesan}"
# Tunggu balasan dengan timeout
ready = IO.select([klien], nil, nil, 2)
if ready
balasan, _ = klien.recvfrom(1024)
puts "Diterima: #{balasan}"
else
puts "Timeout — tidak ada balasan"
end
sleep 0.5
end
klien.close
Unix Domain Socket #
Unix Domain Socket bekerja seperti TCP tapi menggunakan file sistem sebagai alamat — hanya bisa digunakan antar proses di mesin yang sama, tapi jauh lebih cepat dari TCP lokal:
require 'socket'
SOCKET_PATH = "/tmp/app.sock"
# Server
File.delete(SOCKET_PATH) if File.exist?(SOCKET_PATH) # hapus socket lama
server = UNIXServer.new(SOCKET_PATH)
puts "Unix socket server di #{SOCKET_PATH}"
loop do
client = server.accept
Thread.new(client) do |conn|
pesan = conn.gets&.chomp
conn.puts "Diproses: #{pesan}"
conn.close
end
end
# Klien
socket = UNIXSocket.new(SOCKET_PATH)
socket.puts "Halo via Unix socket!"
puts socket.gets.chomp
socket.close
HTTP Client Manual dengan Socket #
Memahami bagaimana HTTP bekerja di atas TCP sangat berguna — dan bisa diimplementasikan hanya dengan socket dasar:
require 'socket'
def http_get(host, path = "/", port = 80)
socket = TCPSocket.new(host, port)
# HTTP request
request = [
"GET #{path} HTTP/1.1",
"Host: #{host}",
"Connection: close",
"User-Agent: Ruby-Socket/1.0",
"", # baris kosong = akhir header
"" # baris kosong lagi untuk memastikan \r\n\r\n
].join("\r\n")
socket.write(request)
# Baca respons
response = socket.read
socket.close
# Parse status line dan headers
baris = response.split("\r\n")
status_line = baris.shift
puts "Status: #{status_line}"
# Pisahkan header dan body
separator = response.index("\r\n\r\n")
headers = response[0...separator]
body = response[(separator + 4)..]
{ status: status_line, headers: headers, body: body }
end
# Penggunaan
hasil = http_get("example.com", "/")
puts "Body (100 karakter pertama):"
puts hasil[:body][0..100]
SSL/TLS dengan OpenSSL #
Untuk komunikasi yang aman, Ruby menyediakan openssl library yang bisa di-wrap di atas socket biasa:
require 'socket'
require 'openssl'
# HTTPS client dengan SSL/TLS
host = "www.google.com"
port = 443
# Buat TCP socket biasa
tcp_socket = TCPSocket.new(host, port)
# Wrap dengan SSL context
ssl_context = OpenSSL::SSL::SSLContext.new
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER # verifikasi sertifikat
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
ssl_socket.hostname = host # SNI (Server Name Indication)
ssl_socket.connect # lakukan TLS handshake
puts "Koneksi SSL berhasil"
puts "Protokol: #{ssl_socket.ssl_version}"
puts "Cipher: #{ssl_socket.cipher[0]}"
# Sekarang bisa berkomunikasi melalui koneksi terenkripsi
ssl_socket.write("GET / HTTP/1.1\r\nHost: #{host}\r\nConnection: close\r\n\r\n")
# Baca respons
response = ssl_socket.read
puts response[0..200]
ssl_socket.close
tcp_socket.close
# Server dengan SSL
require 'socket'
require 'openssl'
ssl_context = OpenSSL::SSL::SSLContext.new
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read("server.crt"))
ssl_context.key = OpenSSL::PKey::RSA.new(File.read("server.key"))
tcp_server = TCPServer.new("0.0.0.0", 4433)
ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ssl_context)
puts "SSL server berjalan di port 4433"
loop do
ssl_client = ssl_server.accept
Thread.new(ssl_client) do |conn|
puts "SSL klien dari #{conn.io.peeraddr[2]}"
conn.puts "Halo via SSL!"
conn.close
end
end
Pola Server yang Robust #
Menggabungkan semua konsep di atas menjadi server TCP yang benar-benar siap untuk produksi:
require 'socket'
require 'logger'
class RobustServer
MAX_KLIEN = 100
BATAS_PESAN = 64 * 1024 # 64 KB per pesan
def initialize(host, port)
@host = host
@port = port
@log = Logger.new($stdout)
@log.formatter = proc { |level, _, _, msg| "[#{level}] #{msg}\n" }
@berjalan = false
@mutex = Mutex.new
@klien = {} # socket => thread
end
def mulai
@server = TCPServer.new(@host, @port)
@server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
@berjalan = true
@log.info "Server berjalan di #{@host}:#{@port}"
# Tangani SIGTERM dan SIGINT untuk graceful shutdown
Signal.trap("TERM") { hentikan }
Signal.trap("INT") { hentikan }
loop do
break unless @berjalan
begin
klien = @server.accept_nonblock
if jumlah_klien >= MAX_KLIEN
klien.puts "ERROR: Server penuh, coba lagi nanti"
klien.close
next
end
daftarkan_klien(klien)
rescue IO::WaitReadable
# Tidak ada klien masuk, coba lagi
IO.select([@server], nil, nil, 0.1)
rescue Errno::EBADF
break # server socket sudah ditutup
end
end
end
def hentikan
@log.info "Mematikan server..."
@berjalan = false
@mutex.synchronize do
@klien.each_key do |sock|
sock.puts "Server sedang dimatikan, koneksi akan ditutup."
sock.close rescue nil
end
@klien.clear
end
@server.close rescue nil
@log.info "Server selesai."
end
private
def jumlah_klien
@mutex.synchronize { @klien.size }
end
def daftarkan_klien(sock)
thread = Thread.new { tangani_klien(sock) }
@mutex.synchronize { @klien[sock] = thread }
end
def tangani_klien(sock)
addr = sock.peeraddr[2]
@log.info "Klien terhubung: #{addr}"
loop do
baris = sock.gets
break if baris.nil?
baris.chomp!
@log.info "[#{addr}] #{baris}"
sock.puts "OK: #{baris}"
end
rescue Errno::ECONNRESET, Errno::EPIPE, IOError
@log.info "Koneksi terputus: #{sock.peeraddr[2] rescue 'unknown'}"
ensure
@mutex.synchronize { @klien.delete(sock) }
sock.close rescue nil
@log.info "Klien terlepas: total aktif #{jumlah_klien}"
end
end
server = RobustServer.new("0.0.0.0", 4000)
server.mulai
Ringkasan #
- TCP untuk reliability, UDP untuk kecepatan — pilih berdasarkan kebutuhan: TCP untuk data yang tidak boleh hilang, UDP untuk streaming yang memprioritaskan latency rendah.
- Framing adalah masalah paling umum di socket programming — TCP adalah stream, bukan paket; gunakan delimiter (
\n) atau length-prefix (4 byte panjang) untuk membedakan batas pesan.SO_REUSEADDRhampir selalu diperlukan — tanpanya server tidak bisa langsung di-restart setelah dihentikan karena port masih dalamTIME_WAIT.- Setiap klien butuh thread atau non-blocking I/O — server yang menangani satu klien per satu (blocking) hanya cocok untuk demo; gunakan
Thread.newper klien atauIO.selectuntuk multiplexing.- Selalu tangani exception koneksi —
Errno::ECONNRESET,Errno::EPIPE, danEOFErrorbisa terjadi kapan saja; server harus bisa recover tanpa crash.- Timeout wajib untuk socket — koneksi tanpa timeout bisa hang selamanya jika jaringan bermasalah; gunakan
setsockopt SO_RCVTIMEOatauTimeout.timeout.- Unix Domain Socket lebih cepat dari TCP lokal — untuk komunikasi antar proses di mesin yang sama, gunakan
UNIXServer/UNIXSocket.- Wrap dengan SSL/TLS untuk keamanan —
OpenSSL::SSL::SSLSocketbisa di-wrap di atas TCP socket biasa dengan beberapa baris kode.- Graceful shutdown sangat penting — tangani
SIGTERMdanSIGINT, tutup semua koneksi klien dengan notifikasi sebelum menutup server.IO.selectuntuk multiplexing ringan — alternatif dari threading untuk server dengan banyak koneksi idle yang tidak butuh overhead thread.