Socket #

Socket programming di Go dibangun di atas package net yang mengabstraksi TCP, UDP, Unix domain socket, dan protokol jaringan lainnya melalui interface yang konsisten. Yang membuat Go sangat cocok untuk networking adalah goroutine — kamu bisa menangani ribuan koneksi concurrent dengan pola sederhana: satu goroutine per koneksi. Tidak ada callback hell, tidak ada event loop manual, kode sequential yang mudah dibaca.

TCP — Transmission Control Protocol #

TCP menjamin urutan pengiriman dan keandalan data. Ini yang kamu pakai untuk HTTP, database, dan hampir semua protokol yang butuh reliability.

TCP Server #

Pola dasar TCP server: listen → accept → handle per goroutine:

import (
    "bufio"
    "fmt"
    "net"
    "log"
)

func handleConn(conn net.Conn) {
    defer conn.Close()  // pastikan koneksi selalu ditutup

    addr := conn.RemoteAddr().String()
    log.Printf("Koneksi baru dari: %s", addr)

    scanner := bufio.NewScanner(conn)
    for scanner.Scan() {
        msg := scanner.Text()
        log.Printf("[%s] → %s", addr, msg)

        // Echo kembali ke client
        fmt.Fprintf(conn, "Echo: %s\n", msg)
    }

    if err := scanner.Err(); err != nil {
        log.Printf("[%s] error: %v", addr, err)
    }
    log.Printf("Koneksi ditutup: %s", addr)
}

func main() {
    ln, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatal("Gagal listen:", err)
    }
    defer ln.Close()

    log.Println("TCP server listening di :8080")

    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Println("Accept error:", err)
            continue
        }
        go handleConn(conn)  // satu goroutine per koneksi
    }
}

Graceful Shutdown #

Server yang bisa berhenti dengan bersih — menunggu koneksi aktif selesai sebelum exit:

import (
    "context"
    "net"
    "sync"
    "log"
    "os/signal"
    "syscall"
    "os"
)

type Server struct {
    ln      net.Listener
    wg      sync.WaitGroup
    quit    chan struct{}
}

func NewServer(addr string) (*Server, error) {
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return nil, err
    }
    return &Server{ln: ln, quit: make(chan struct{})}, nil
}

func (s *Server) Start() {
    s.wg.Add(1)
    go func() {
        defer s.wg.Done()
        for {
            conn, err := s.ln.Accept()
            if err != nil {
                select {
                case <-s.quit:
                    return  // server sedang shutdown, bukan error
                default:
                    log.Println("Accept error:", err)
                    continue
                }
            }
            s.wg.Add(1)
            go func() {
                defer s.wg.Done()
                handleConn(conn)
            }()
        }
    }()
}

func (s *Server) Stop() {
    close(s.quit)      // sinyal shutdown
    s.ln.Close()       // paksa Accept() return error
    s.wg.Wait()        // tunggu semua goroutine selesai
    log.Println("Server berhenti dengan bersih")
}

func main() {
    srv, err := NewServer(":8080")
    if err != nil {
        log.Fatal(err)
    }
    srv.Start()
    log.Println("Server berjalan di :8080")

    // Tunggu sinyal OS (Ctrl+C atau SIGTERM)
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
    <-sigCh

    log.Println("Menerima sinyal shutdown...")
    srv.Stop()
}

TCP Client #

import (
    "bufio"
    "fmt"
    "net"
    "time"
)

func main() {
    // Koneksi dasar
    conn, err := net.Dial("tcp", "localhost:8080")
    if err != nil {
        log.Fatal("Gagal konek:", err)
    }
    defer conn.Close()

    // Dengan timeout koneksi
    conn2, err := net.DialTimeout("tcp", "localhost:8080", 5*time.Second)
    if err != nil {
        log.Fatal("Timeout konek:", err)
    }
    defer conn2.Close()

    // Kirim pesan
    fmt.Fprintf(conn, "Halo server!\n")

    // Terima respons
    reader := bufio.NewReader(conn)
    resp, err := reader.ReadString('\n')
    if err != nil {
        log.Fatal("Error baca:", err)
    }
    fmt.Print("Server:", resp)
}

Deadline — Timeout pada Koneksi #

Tanpa deadline, operasi Read/Write bisa block selamanya jika client tidak mengirim atau menerima data. Selalu set deadline untuk koneksi produksi:

func handleConn(conn net.Conn) {
    defer conn.Close()

    // Set deadline untuk seluruh koneksi (dari sekarang)
    conn.SetDeadline(time.Now().Add(30 * time.Second))

    // Atau set per-operasi:
    // Read deadline — berapa lama tunggu data datang
    conn.SetReadDeadline(time.Now().Add(10 * time.Second))
    // Write deadline — berapa lama tunggu write selesai
    conn.SetWriteDeadline(time.Now().Add(5 * time.Second))

    scanner := bufio.NewScanner(conn)
    for scanner.Scan() {
        // Reset read deadline setelah terima data
        conn.SetReadDeadline(time.Now().Add(10 * time.Second))

        msg := scanner.Text()
        fmt.Fprintf(conn, "OK: %s\n", msg)
    }

    if err := scanner.Err(); err != nil {
        if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
            log.Println("Koneksi timeout")
        }
    }
}

Protocol Design — Membaca Data dengan Benar #

TCP adalah stream of bytes — tidak ada batas “message” bawaan. Kamu perlu protocol sendiri untuk menentukan di mana satu pesan berakhir dan berikutnya dimulai.

Delimiter-Based (Newline Protocol) #

// Cocok untuk teks sederhana — pisahkan pesan dengan '\n'
// Kelemahan: pesan tidak boleh mengandung newline

// Kirim
fmt.Fprintf(conn, "pesan tanpa newline di tengah\n")

// Terima
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
    pesan := scanner.Text()  // tanpa '\n'
    proses(pesan)
}

Length-Prefix Protocol #

import "encoding/binary"

// Lebih robust — kirim panjang pesan (4 byte) diikuti isi pesan
// Mendukung pesan biner dan pesan dengan newline di dalamnya

// Kirim
func sendMessage(conn net.Conn, msg []byte) error {
    // Tulis panjang pesan sebagai uint32 big-endian (4 byte)
    length := uint32(len(msg))
    if err := binary.Write(conn, binary.BigEndian, length); err != nil {
        return fmt.Errorf("kirim panjang: %w", err)
    }
    // Tulis isi pesan
    _, err := conn.Write(msg)
    return err
}

// Terima
func receiveMessage(conn net.Conn) ([]byte, error) {
    // Baca 4 byte pertama untuk mendapat panjang
    var length uint32
    if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
        return nil, fmt.Errorf("baca panjang: %w", err)
    }

    // Validasi — cegah alokasi memori raksasa dari client jahat
    if length > 10*1024*1024 {  // max 10MB
        return nil, fmt.Errorf("pesan terlalu besar: %d bytes", length)
    }

    // Baca tepat sejumlah byte yang diperlukan
    msg := make([]byte, length)
    if _, err := io.ReadFull(conn, msg); err != nil {
        return nil, fmt.Errorf("baca pesan: %w", err)
    }
    return msg, nil
}
io.ReadFull sangat penting untuk length-prefix protocol. conn.Read() biasa tidak menjamin membaca sejumlah byte yang diminta — ia bisa saja mengembalikan lebih sedikit. io.ReadFull terus membaca sampai buffer penuh atau terjadi error.

UDP — User Datagram Protocol #

UDP tidak ada koneksi, tidak ada jaminan urutan atau pengiriman. Cocok untuk: game realtime, live streaming, DNS, DHCP — situasi di mana kecepatan lebih penting dari keandalan.

UDP Server #

func main() {
    addr, _ := net.ResolveUDPAddr("udp", ":9090")
    conn, err := net.ListenUDP("udp", addr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    log.Println("UDP server di :9090")

    buf := make([]byte, 1024)
    for {
        n, remoteAddr, err := conn.ReadFromUDP(buf)
        if err != nil {
            log.Println("Error:", err)
            continue
        }

        msg := string(buf[:n])
        log.Printf("Dari %s: %s", remoteAddr, msg)

        // Kirim balasan
        reply := fmt.Sprintf("Echo: %s", msg)
        conn.WriteToUDP([]byte(reply), remoteAddr)
    }
}

UDP Client #

func main() {
    serverAddr, _ := net.ResolveUDPAddr("udp", "localhost:9090")
    conn, err := net.DialUDP("udp", nil, serverAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // Kirim tanpa menunggu koneksi
    conn.Write([]byte("Halo UDP!"))

    // Baca respons (dengan timeout karena UDP bisa hilang)
    conn.SetReadDeadline(time.Now().Add(2 * time.Second))
    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    if err != nil {
        log.Println("Timeout atau error:", err)
        return
    }
    fmt.Println(string(buf[:n]))
}

Unix Domain Socket #

Unix domain socket untuk komunikasi antar proses di mesin yang sama — lebih cepat dari TCP loopback karena tidak melalui network stack:

// Server Unix socket
func main() {
    socketPath := "/tmp/myapp.sock"
    os.Remove(socketPath)  // hapus socket lama jika ada

    ln, err := net.Listen("unix", socketPath)
    if err != nil {
        log.Fatal(err)
    }
    defer ln.Close()
    defer os.Remove(socketPath)

    log.Println("Unix socket server:", socketPath)

    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Println("Accept error:", err)
            continue
        }
        go handleConn(conn)
    }
}

// Client Unix socket
func connectUnix() {
    conn, err := net.Dial("unix", "/tmp/myapp.sock")
    if err != nil {
        log.Fatal("Gagal konek ke unix socket:", err)
    }
    defer conn.Close()

    fmt.Fprintf(conn, "Halo via unix socket!\n")
}

TLS — Enkripsi Koneksi #

Untuk production, semua koneksi harus dienkripsi dengan TLS:

import "crypto/tls"

// TLS Server
func tlsServer() {
    // Load sertifikat dan private key
    cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
    if err != nil {
        log.Fatal("Gagal load sertifikat:", err)
    }

    config := &tls.Config{
        Certificates: []tls.Certificate{cert},
        MinVersion:   tls.VersionTLS13,  // gunakan TLS 1.3 minimum
    }

    ln, err := tls.Listen("tcp", ":8443", config)
    if err != nil {
        log.Fatal("Gagal listen TLS:", err)
    }
    defer ln.Close()

    log.Println("TLS server di :8443")
    for {
        conn, err := ln.Accept()
        if err != nil {
            continue
        }
        go handleConn(conn)  // conn adalah tls.Conn, tapi implements net.Conn
    }
}

// TLS Client
func tlsClient() {
    config := &tls.Config{
        InsecureSkipVerify: false,  // JANGAN set true di production!
        MinVersion:         tls.VersionTLS13,
    }

    conn, err := tls.Dial("tcp", "localhost:8443", config)
    if err != nil {
        log.Fatal("Gagal konek TLS:", err)
    }
    defer conn.Close()

    fmt.Fprintf(conn, "Pesan rahasia!\n")
}

Contoh Program Lengkap — Chat Server Multi-Client #

Program berikut membangun chat server lengkap dengan broadcast ke semua klien:

package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "strings"
    "sync"
    "time"
)

// Message merepresentasikan pesan chat
type Message struct {
    From    string
    Content string
    Time    time.Time
}

func (m Message) String() string {
    return fmt.Sprintf("[%s] %s: %s",
        m.Time.Format("15:04:05"), m.From, m.Content)
}

// Hub mengelola semua klien yang terhubung
type Hub struct {
    mu      sync.RWMutex
    clients map[string]net.Conn   // username → koneksi
    msgCh   chan Message
}

func NewHub() *Hub {
    h := &Hub{
        clients: make(map[string]net.Conn),
        msgCh:   make(chan Message, 256),
    }
    go h.broadcastLoop()
    return h
}

func (h *Hub) Register(username string, conn net.Conn) bool {
    h.mu.Lock()
    defer h.mu.Unlock()
    if _, exists := h.clients[username]; exists {
        return false  // username sudah dipakai
    }
    h.clients[username] = conn
    return true
}

func (h *Hub) Unregister(username string) {
    h.mu.Lock()
    defer h.mu.Unlock()
    delete(h.clients, username)
}

func (h *Hub) Broadcast(msg Message) {
    h.msgCh <- msg
}

func (h *Hub) broadcastLoop() {
    for msg := range h.msgCh {
        text := msg.String() + "\n"
        h.mu.RLock()
        for username, conn := range h.clients {
            if username == msg.From {
                continue  // jangan kirim ke pengirim sendiri
            }
            conn.SetWriteDeadline(time.Now().Add(3 * time.Second))
            if _, err := fmt.Fprint(conn, text); err != nil {
                log.Printf("Gagal kirim ke %s: %v", username, err)
            }
        }
        h.mu.RUnlock()
    }
}

func (h *Hub) UserList() []string {
    h.mu.RLock()
    defer h.mu.RUnlock()
    users := make([]string, 0, len(h.clients))
    for u := range h.clients {
        users = append(users, u)
    }
    return users
}

// handleClient menangani satu klien
func handleClient(conn net.Conn, hub *Hub) {
    defer conn.Close()
    remote := conn.RemoteAddr().String()

    // Minta username
    fmt.Fprint(conn, "Masukkan username: ")
    conn.SetReadDeadline(time.Now().Add(30 * time.Second))

    reader := bufio.NewReader(conn)
    username, err := reader.ReadString('\n')
    if err != nil {
        log.Printf("[%s] Gagal baca username: %v", remote, err)
        return
    }
    username = strings.TrimSpace(username)
    if username == "" || len(username) > 20 {
        fmt.Fprint(conn, "Username tidak valid. Koneksi ditutup.\n")
        return
    }

    // Register ke hub
    if !hub.Register(username, conn) {
        fmt.Fprintf(conn, "Username '%s' sudah dipakai. Coba lagi.\n", username)
        return
    }
    defer hub.Unregister(username)

    // Sambut pengguna baru
    fmt.Fprintf(conn, "Selamat datang, %s! Pengguna aktif: %s\n",
        username, strings.Join(hub.UserList(), ", "))

    // Umumkan ke semua
    hub.Broadcast(Message{
        From:    "SYSTEM",
        Content: fmt.Sprintf("%s bergabung ke chat", username),
        Time:    time.Now(),
    })

    log.Printf("%s terhubung dari %s", username, remote)

    // Loop baca pesan dari klien ini
    for {
        conn.SetReadDeadline(time.Now().Add(5 * time.Minute))

        line, err := reader.ReadString('\n')
        if err != nil {
            break
        }

        text := strings.TrimSpace(line)
        if text == "" {
            continue
        }

        // Perintah khusus
        switch {
        case text == "/quit":
            fmt.Fprint(conn, "Sampai jumpa!\n")
            goto done

        case text == "/users":
            users := hub.UserList()
            fmt.Fprintf(conn, "Pengguna aktif (%d): %s\n",
                len(users), strings.Join(users, ", "))

        case strings.HasPrefix(text, "/whisper "):
            // Pesan privat: /whisper username pesan
            parts := strings.SplitN(text[9:], " ", 2)
            if len(parts) != 2 {
                fmt.Fprint(conn, "Format: /whisper <username> <pesan>\n")
                continue
            }
            target, msg := parts[0], parts[1]

            hub.mu.RLock()
            targetConn, exists := hub.clients[target]
            hub.mu.RUnlock()

            if !exists {
                fmt.Fprintf(conn, "User '%s' tidak ditemukan\n", target)
                continue
            }
            fmt.Fprintf(targetConn, "[PRIVAT dari %s]: %s\n", username, msg)
            fmt.Fprintf(conn, "[PRIVAT ke %s]: %s\n", target, msg)

        default:
            // Broadcast ke semua
            hub.Broadcast(Message{
                From:    username,
                Content: text,
                Time:    time.Now(),
            })
        }
    }

done:
    hub.Broadcast(Message{
        From:    "SYSTEM",
        Content: fmt.Sprintf("%s meninggalkan chat", username),
        Time:    time.Now(),
    })
    log.Printf("%s memutus koneksi", username)
}

func main() {
    hub := NewHub()

    ln, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatal("Gagal listen:", err)
    }
    defer ln.Close()

    log.Println("Chat server berjalan di :8080")
    log.Println("Hubungkan dengan: nc localhost 8080")

    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Println("Accept error:", err)
            continue
        }
        go handleClient(conn, hub)
    }
}

Ringkasan #

  • net.Listen + ln.Accept + go handleConn adalah pola dasar TCP server di Go — satu goroutine per koneksi.
  • Selalu defer conn.Close() di awal handler untuk menjamin koneksi ditutup meski terjadi panic.
  • Graceful shutdown: tutup listener untuk membuat Accept() return error, lalu tunggu semua goroutine dengan WaitGroup.
  • TCP adalah stream — tidak ada batas message bawaan; gunakan delimiter (\n) atau length-prefix protocol.
  • io.ReadFull untuk membaca tepat N byte — conn.Read() biasa bisa mengembalikan kurang dari yang diminta.
  • Selalu set deadline (SetReadDeadline, SetWriteDeadline) untuk mencegah goroutine leak karena koneksi yang hang.
  • UDP untuk komunikasi cepat tanpa jaminan pengiriman — game, DNS, live streaming.
  • Unix domain socket lebih cepat dari TCP loopback untuk komunikasi antar proses di mesin yang sama.
  • TLS wajib untuk production — gunakan tls.Listen dan tls.Dial, set MinVersion: tls.VersionTLS13.
  • sync.RWMutex untuk melindungi shared state (daftar klien) — RLock untuk baca concurrent, Lock untuk write eksklusif.

← Sebelumnya: I/O   Berikutnya: Web Socket →

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