本文 AI 產出,尚未審核

Golang TCP 伺服器與客戶端(net 套件)


簡介

在現代分散式系統中,TCP 仍是最常見、最可靠的傳輸層協定之一。無論是即時聊天、日誌收集,或是自訂的 RPC 協議,都會直接或間接依賴 TCP 連線。Go 語言的標準庫 net 為 TCP 提供了簡潔且功能完整的 API,使得開發者可以在幾行程式碼內完成伺服器與客戶端的實作。

本篇文章將從 概念程式碼範例常見陷阱最佳實踐 逐層剖析,讓 初學者 能快速上手,中級開發者 也能在實務專案中安全、有效地使用 TCP。


核心概念

1. TCP 與 net 套件的基本概念

  • TCP(Transmission Control Protocol)提供可靠、順序且無錯誤的資料傳輸。
  • Go 的 net 套件將底層的 socket 操作抽象為 net.Conn 介面,所有的讀寫都透過 ReadWrite 方法完成。
  • net.Listen 用於建立 監聽端點(Listener),而 net.Dial 則是建立 客戶端連線(Conn)。

重點net.Conn 同時實作了 io.Readerio.Writerio.Closer,因此可以直接與 bufioencoding/json 等標準庫結合使用。


2. 建立簡易的 TCP 伺服器

以下範例示範最基本的 Echo Server,接收到的資料會原樣回傳給客戶端。

package main

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

func main() {
	// 監聽本機 8080 埠口
	ln, err := net.Listen("tcp", ":8080")
	if err != nil {
		panic(err)
	}
	defer ln.Close()
	fmt.Println("Server listening on :8080")

	for {
		// 接受新連線(阻塞)
		conn, err := ln.Accept()
		if err != nil {
			fmt.Println("Accept error:", err)
			continue
		}
		// 每個連線交給獨立 goroutine 處理
		go handleConn(conn)
	}
}

func handleConn(c net.Conn) {
	defer c.Close()
	reader := bufio.NewReader(c)
	for {
		// 讀取一行(以換行符號結尾)
		msg, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("Read error:", err)
			return
		}
		// 原樣回傳
		_, err = c.Write([]byte(msg))
		if err != nil {
			fmt.Println("Write error:", err)
			return
		}
	}
}

說明

  • net.Listen 產生一個 ListenerAccept 會阻塞等待新連線。
  • 每個連線使用 go handleConn(conn) 交給 獨立 goroutine,避免單一客戶端阻塞整個伺服器。
  • bufio.Reader 能有效緩衝讀取,減少系統呼叫次數。

3. 建立 TCP 客戶端

下面的程式碼示範如何連接上述 Echo Server,並傳送與接收訊息。

package main

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

func main() {
	// 建立連線
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	fmt.Println("Connected to server")

	// 讀取使用者輸入
	scanner := bufio.NewScanner(os.Stdin)
	for {
		fmt.Print("Enter message: ")
		if !scanner.Scan() {
			break
		}
		text := scanner.Text() + "\n" // 加上換行符號,讓伺服器能辨識

		// 寫入伺服器
		_, err = conn.Write([]byte(text))
		if err != nil {
			fmt.Println("Write error:", err)
			return
		}

		// 讀取回傳的 echo
		reply, err := bufio.NewReader(conn).ReadString('\n')
		if err != nil {
			fmt.Println("Read error:", err)
			return
		}
		fmt.Printf("Server replied: %s", reply)
	}
}

說明

  • net.Dial 會返回一個 net.Conn,同時支援 ReadWrite
  • 使用 bufio.NewScanner 讀取終端機輸入,並將訊息加上 \n,讓伺服器的 ReadString('\n') 能正確分割。

4. 同時處理多個連線的進階寫法

在高併發環境下,僅僅使用 go handleConn 仍可能因 資源泄漏過多 goroutine 而造成系統不穩。以下示範 工作池(worker pool) 的概念,限制同時執行的 goroutine 數量。

package main

import (
	"bufio"
	"fmt"
	"net"
	"sync"
)

const (
	maxWorkers = 100 // 同時最多處理的連線數
)

func main() {
	ln, _ := net.Listen("tcp", ":9090")
	defer ln.Close()
	fmt.Println("Server listening on :9090")

	// 建立工作池
	var wg sync.WaitGroup
	sem := make(chan struct{}, maxWorkers)

	for {
		conn, err := ln.Accept()
		if err != nil {
			fmt.Println("Accept error:", err)
			continue
		}
		sem <- struct{}{} // 取得工作槽位
		wg.Add(1)
		go func(c net.Conn) {
			defer wg.Done()
			handleConn(c)
			<-sem // 釋放槽位
		}(conn)
	}
}

// 與前面的 handleConn 相同,只是加入了錯誤日誌
func handleConn(c net.Conn) {
	defer c.Close()
	reader := bufio.NewReader(c)
	for {
		msg, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("Read error:", err)
			return
		}
		_, err = c.Write([]byte(msg))
		if err != nil {
			fmt.Println("Write error:", err)
			return
		}
	}
}

說明

  • sem(有緩衝的 channel)作為 信號量,限制同時啟動的 goroutine 數量。
  • sync.WaitGroup 用於優雅關閉(在實作 graceful shutdown 時會派上用場)。

5. Graceful Shutdown 與 Context

在正式服務中,平滑關閉(Graceful Shutdown)是必備需求。以下示範如何結合 context.Contextos/signal 讓伺服器在收到 SIGINT 時,先停止接受新連線,然後等所有既有連線結束後才退出。

package main

import (
	"bufio"
	"context"
	"fmt"
	"net"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"
)

func main() {
	ln, _ := net.Listen("tcp", ":7070")
	defer ln.Close()
	fmt.Println("Server listening on :7070")

	// 建立可取消的 context
	ctx, cancel := context.WithCancel(context.Background())
	var wg sync.WaitGroup

	// 捕捉系統訊號
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

	// 監聽訊號的 goroutine
	go func() {
		<-sigCh
		fmt.Println("\nReceived shutdown signal")
		cancel()          // 觸發 context 取消
		ln.Close()        // 關閉 Listener,Accept 會返回錯誤
	}()

	// 主 Accept 迴圈
	for {
		conn, err := ln.Accept()
		if err != nil {
			// 若因 context 取消而關閉 Listener,直接跳出迴圈
			select {
			case <-ctx.Done():
				fmt.Println("Stop accepting new connections")
				goto wait
			default:
				fmt.Println("Accept error:", err)
				continue
			}
		}
		wg.Add(1)
		go func(c net.Conn) {
			defer wg.Done()
			handleConn(ctx, c)
		}(conn)
	}

wait:
	// 等待所有連線結束,最多等 30 秒
	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()
	select {
	case <-done:
		fmt.Println("All connections closed")
	case <-time.After(30 * time.Second):
		fmt.Println("Timeout waiting for connections")
	}
}

// 讀寫時檢查 context 是否已取消
func handleConn(ctx context.Context, c net.Conn) {
	defer c.Close()
	reader := bufio.NewReader(c)
	for {
		// 先檢查是否需要關閉
		select {
		case <-ctx.Done():
			fmt.Fprintln(c, "Server is shutting down")
			return
		default:
		}
		c.SetReadDeadline(time.Now().Add(5 * time.Second)) // 防止無限阻塞
		msg, err := reader.ReadString('\n')
		if err != nil {
			return
		}
		c.Write([]byte(msg))
	}
}

說明

  • context.WithCancel 為所有連線提供 全域的關閉訊號
  • SetReadDeadline 防止單一連線因客戶端不回應而永遠阻塞。
  • select 檢查 ctx.Done(),在關閉時立即回應客戶端。

常見陷阱與最佳實踐

常見陷阱 為何會發生 最佳實踐
阻塞的 Read/Write 若客戶端斷線或傳送緩慢,呼叫會無限等待。 使用 deadlineSetReadDeadlineSetWriteDeadline)或 context 取消。
忘記 Close 連線資源未釋放,導致檔案描述符耗盡(FD Exhaustion)。 defer conn.Close() 必須放在取得 net.Conn 後第一行。
資料分割錯誤 TCP 為串流協定,訊息可能被拆成多段或合併。 使用 緩衝讀取bufio.Reader)+ 明確的分隔符\n、長度前置)或 自訂協議
過度建立 goroutine 每個連線直接 go 可能在大量客戶端時產生成千上萬的 goroutine,耗盡記憶體。 工作池限制同時連線數semaphoresync.WaitGroup)。
未處理錯誤 AcceptReadWrite 的錯誤被忽略,會導致不易偵測的問題。 每一次 I/O 都檢查 error,並記錄日誌。
硬編碼 IP/Port 部署環境不同,需要重新編譯。 使用 環境變數設定檔flag 參數。

其他最佳實踐

  1. 使用 bufio:減少系統呼叫次數,提高吞吐量。
  2. 結合 encoding/json / protobuf:在傳輸層上層加入結構化編碼,方便後端服務解析。
  3. 限制單一連線的讀寫速率:透過 io.LimitReader 或自行實作 token bucket,防止 DoS。
  4. 日誌與度量:使用 logzapzerolog 記錄連線建立、關閉、錯誤;配合 Prometheus 暴露 connections_totalbytes_sent 等指標。
  5. TLS 加密:若資料敏感,使用 tls.Listentls.Dial 為 TCP 加上安全層。

實際應用場景

場景 為何選擇 TCP 可能的實作方式
即時聊天系統 需要低延遲、持久連線 使用 長連線 + 心跳,配合自訂訊息框架(長度前置)。
日誌聚合服務 大量小訊息、可靠傳送 客戶端使用 非阻塞寫入 + 批次緩衝,伺服器端使用 多工作池 處理寫入磁碟或 Kafka。
微服務間的自訂 RPC 需要高效、可控的序列化 在 TCP 上實作 ProtobufFlatBuffers,搭配 deadline 防止卡住。
代理伺服器(Proxy) 需要透明轉發、雙向流量 讀取來源連線後直接 io.Copy 到目標連線,使用 net.Pipebufio 進行緩衝。
IoT 裝置管理 裝置可能在不穩定網路環境 使用 重連機制 + KeepAlive,在斷線時自動重試。

總結

  • TCP 為可靠的資料傳輸基礎,Go 的 net 套件讓它變得 簡潔且易於測試
  • 透過 net.Listen + Accept 建立伺服器,net.Dial 建立客戶端,配合 bufiodeadlinecontext 可以快速完成功能完整的網路服務。
  • 同時處理多連線 時,務必使用 goroutine + 工作池信號量限制器,避免資源耗盡。
  • Graceful Shutdown錯誤處理資源釋放Close)是部署到正式環境的必備要素。
  • 在實務上,從 聊天系統日誌收集自訂 RPC,TCP 都是可靠且可擴充的選擇,只要遵守上述 最佳實踐,就能寫出 高效、穩定 的 Go 網路程式。

祝開發順利,期待看到你用 Go 打造的各式 TCP 應用! 🚀