本文 AI 產出,尚未審核
Golang TCP 伺服器與客戶端(net 套件)
簡介
在現代分散式系統中,TCP 仍是最常見、最可靠的傳輸層協定之一。無論是即時聊天、日誌收集,或是自訂的 RPC 協議,都會直接或間接依賴 TCP 連線。Go 語言的標準庫 net 為 TCP 提供了簡潔且功能完整的 API,使得開發者可以在幾行程式碼內完成伺服器與客戶端的實作。
本篇文章將從 概念、程式碼範例、常見陷阱 與 最佳實踐 逐層剖析,讓 初學者 能快速上手,中級開發者 也能在實務專案中安全、有效地使用 TCP。
核心概念
1. TCP 與 net 套件的基本概念
- TCP(Transmission Control Protocol)提供可靠、順序且無錯誤的資料傳輸。
- Go 的
net套件將底層的 socket 操作抽象為net.Conn介面,所有的讀寫都透過Read、Write方法完成。 net.Listen用於建立 監聽端點(Listener),而net.Dial則是建立 客戶端連線(Conn)。
重點:
net.Conn同時實作了io.Reader、io.Writer、io.Closer,因此可以直接與bufio、encoding/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產生一個Listener,Accept會阻塞等待新連線。- 每個連線使用
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,同時支援Read與Write。- 使用
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.Context 與 os/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 |
若客戶端斷線或傳送緩慢,呼叫會無限等待。 | 使用 deadline(SetReadDeadline、SetWriteDeadline)或 context 取消。 |
忘記 Close |
連線資源未釋放,導致檔案描述符耗盡(FD Exhaustion)。 | defer conn.Close() 必須放在取得 net.Conn 後第一行。 |
| 資料分割錯誤 | TCP 為串流協定,訊息可能被拆成多段或合併。 | 使用 緩衝讀取(bufio.Reader)+ 明確的分隔符(\n、長度前置)或 自訂協議。 |
| 過度建立 goroutine | 每個連線直接 go 可能在大量客戶端時產生成千上萬的 goroutine,耗盡記憶體。 |
工作池、限制同時連線數(semaphore、sync.WaitGroup)。 |
| 未處理錯誤 | Accept、Read、Write 的錯誤被忽略,會導致不易偵測的問題。 |
每一次 I/O 都檢查 error,並記錄日誌。 |
| 硬編碼 IP/Port | 部署環境不同,需要重新編譯。 | 使用 環境變數、設定檔 或 flag 參數。 |
其他最佳實踐
- 使用
bufio:減少系統呼叫次數,提高吞吐量。 - 結合
encoding/json/protobuf:在傳輸層上層加入結構化編碼,方便後端服務解析。 - 限制單一連線的讀寫速率:透過
io.LimitReader或自行實作 token bucket,防止 DoS。 - 日誌與度量:使用
log、zap或zerolog記錄連線建立、關閉、錯誤;配合 Prometheus 暴露connections_total、bytes_sent等指標。 - TLS 加密:若資料敏感,使用
tls.Listen、tls.Dial為 TCP 加上安全層。
實際應用場景
| 場景 | 為何選擇 TCP | 可能的實作方式 |
|---|---|---|
| 即時聊天系統 | 需要低延遲、持久連線 | 使用 長連線 + 心跳,配合自訂訊息框架(長度前置)。 |
| 日誌聚合服務 | 大量小訊息、可靠傳送 | 客戶端使用 非阻塞寫入 + 批次緩衝,伺服器端使用 多工作池 處理寫入磁碟或 Kafka。 |
| 微服務間的自訂 RPC | 需要高效、可控的序列化 | 在 TCP 上實作 Protobuf 或 FlatBuffers,搭配 deadline 防止卡住。 |
| 代理伺服器(Proxy) | 需要透明轉發、雙向流量 | 讀取來源連線後直接 io.Copy 到目標連線,使用 net.Pipe 或 bufio 進行緩衝。 |
| IoT 裝置管理 | 裝置可能在不穩定網路環境 | 使用 重連機制 + KeepAlive,在斷線時自動重試。 |
總結
- TCP 為可靠的資料傳輸基礎,Go 的
net套件讓它變得 簡潔且易於測試。 - 透過
net.Listen+Accept建立伺服器,net.Dial建立客戶端,配合bufio、deadline、context可以快速完成功能完整的網路服務。 - 同時處理多連線 時,務必使用 goroutine + 工作池、信號量 或 限制器,避免資源耗盡。
- Graceful Shutdown、錯誤處理、資源釋放(
Close)是部署到正式環境的必備要素。 - 在實務上,從 聊天系統、日誌收集 到 自訂 RPC,TCP 都是可靠且可擴充的選擇,只要遵守上述 最佳實踐,就能寫出 高效、穩定 的 Go 網路程式。
祝開發順利,期待看到你用 Go 打造的各式 TCP 應用! 🚀