Socket

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_REUSEADDR hampir selalu diperlukan — tanpanya server tidak bisa langsung di-restart setelah dihentikan karena port masih dalam TIME_WAIT.
  • Setiap klien butuh thread atau non-blocking I/O — server yang menangani satu klien per satu (blocking) hanya cocok untuk demo; gunakan Thread.new per klien atau IO.select untuk multiplexing.
  • Selalu tangani exception koneksiErrno::ECONNRESET, Errno::EPIPE, dan EOFError bisa 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_RCVTIMEO atau Timeout.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 keamananOpenSSL::SSL::SSLSocket bisa di-wrap di atas TCP socket biasa dengan beberapa baris kode.
  • Graceful shutdown sangat penting — tangani SIGTERM dan SIGINT, tutup semua koneksi klien dengan notifikasi sebelum menutup server.
  • IO.select untuk multiplexing ringan — alternatif dari threading untuk server dengan banyak koneksi idle yang tidak butuh overhead thread.

← Sebelumnya: I/O   Berikutnya: Web Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact