Golang 作爲廣泛用於服務端和雲計算領域的編程語言,tcp socket 是其中至關重要的功能。無論是 WEB 服務器還是各類中間件都離不開 tcp socket 的支持。
與早期的每個線程持有一個 socket 的 block IO 模型不同, 多路IO複用模型使用單個線程監聽多個 socket, 當某個 socket 準備好數據後再進行響應。在邏輯上與使用 select 語句監聽多個 channel 的模式相同。
目前主要的多路IO複用實現主要包括: SELECT, POLL 和 EPOLL。 爲了提高開發效率社區也出現很多封裝庫, 如Netty(Java), Tornado(Python) 和 libev(C)等。
Golang Runtime 封裝了各操作系統平臺上的多路IO複用接口, 並允許使用 goroutine 快速開發高性能的 tcp 服務器。
Echo 服務器
作爲開始,我們來實現一個簡單的 Echo 服務器。它會接受客戶端連接並將客戶端發送的內容原樣傳回客戶端。
package main
import (
"fmt"
"net"
"io"
"log"
"bufio"
)
func ListenAndServe(address string) {
// 綁定監聽地址
listener, err := net.Listen("tcp", address)
if err != nil {
log.Fatal(fmt.Sprintf("listen err: %v", err))
}
defer listener.Close()
log.Println(fmt.Sprintf("bind: %s, start listening...", address))
for {
// Accept 會一直阻塞直到有新的連接建立或者listen中斷纔會返回
conn, err := listener.Accept()
if err != nil {
// 通常是由於listener被關閉無法繼續監聽導致的錯誤
log.Fatal(fmt.Sprintf("accept err: %v", err))
}
// 開啓新的 goroutine 處理該連接
go Handle(conn)
}
}
func Handle(conn net.Conn) {
// 使用 bufio 標準庫提供的緩衝區功能
reader := bufio.NewReader(conn)
for {
// ReadString 會一直阻塞直到遇到分隔符 '\n'
// 遇到分隔符後會返回上次遇到分隔符或連接建立後收到的所有數據, 包括分隔符本身
// 若在遇到分隔符之前遇到異常, ReadString 會返回已收到的數據和錯誤信息
msg, err := reader.ReadString('\n')
if err != nil {
// 通常遇到的錯誤是連接中斷或被關閉,用io.EOF表示
if err == io.EOF {
log.Println("connection close")
} else {
log.Println(err)
}
return
}
b := []byte(msg)
// 將收到的信息發送給客戶端
conn.Write(b)
}
}
func main() {
ListenAndServe(":8000")
}
使用 telnet 工具測試我們編寫的 Echo 服務器:
$ telnet 127.0.0.1 8000
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
> a
a
> b
b
Connection closed by foreign host.
拆包與粘包
HTTP 等應用層協議只有收到一條完整的消息後才能進行處理,而工作在傳輸層的TCP協議並不瞭解應用層消息的結構。
因此,可能遇到一條應用層消息分爲兩個TCP包發送或者一個TCP包中含有兩條應用層消息片段的情況,前者稱爲拆包後者稱爲粘包。
在 Echo 服務器的示例中,我們定義用\n
表示消息結束。我們可能遇到下列幾種情況:
- 收到兩個 tcp 包: "abc", "def\n", 應發出一條響應 "abcdef\n", 這是拆包的情況
- 收到一個 tcp 包: "abc\ndef\n", 應發出兩條響應 "abc\n", "def\n", 這是粘包的情況
當我們使用 tcp socket 開發應用層程序時必須正確處理拆包和粘包。
bufio 標準庫會緩存收到的數據直到遇到分隔符纔會返回,它可以正確處理拆包和粘包。
上層協議通常採用下列幾種思路之一來定義消息,以保證完整地進行讀取:
- 定長消息
- 在消息尾部添加特殊分隔符,如示例中的Echo協議和FTP控制協議
- 將消息分爲header 和 body, 並在 header 提供消息總長度。這是應用最廣泛的策略,如HTTP協議。
優雅關閉
在生產環境下需要保證TCP服務器關閉前完成必要的清理工作,包括將完成正在進行的數據傳輸,關閉TCP連接等。這種關閉模式稱爲優雅關閉,可以避免資源泄露以及客戶端未收到完整數據造成異常。
TCP 服務器的優雅關閉模式通常爲: 先關閉listener阻止新連接進入,然後遍歷所有連接逐個進行關閉。
本節完整源代碼地址: https://github.com/HDT3213/godis/tree/master/src/server
首先修改一下TCP服務器:
// handler 是應用層服務器的抽象
type Handler interface {
Handle(ctx context.Context, conn net.Conn)
Close()error
}
func ListenAndServe(cfg *Config, handler tcp.Handler) {
listener, err := net.Listen("tcp", cfg.Address)
if err != nil {
logger.Fatal(fmt.Sprintf("listen err: %v", err))
}
// 監聽中斷信號
// atomic.AtomicBool 是作者寫的封裝: https://github.com/HDT3213/godis/blob/master/src/lib/sync/atomic/bool.go
var closing atomic.AtomicBool
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
go func() {
sig := <-sigCh
switch sig {
case syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
// 收到中斷信號後開始關閉流程
logger.Info("shuting down...")
// 設置標誌位爲關閉中, 使用原子操作保證線程可見性
closing.Set(true)
// listener 關閉後 listener.Accept() 會立即返回錯誤
listener.Close()
}
}()
logger.Info(fmt.Sprintf("bind: %s, start listening...", cfg.Address))
// 在出現未知錯誤或panic後保證正常關閉
// 注意defer順序,先關閉 listener 再關閉應用層服務器 handler
defer handler.Close()
defer listener.Close()
ctx, _ := context.WithCancel(context.Background())
for {
conn, err := listener.Accept()
if err != nil {
if closing.Get() {
// 收到關閉信號後進入此流程,此時listener已被監聽系統信號的 goroutine 關閉
// handler 會被上文的 defer 語句關閉直接返回
return
}
logger.Error(fmt.Sprintf("accept err: %v", err))
continue
}
// handle
logger.Info("accept link")
go handler.Handle(ctx, conn)
}
}
接下來修改應用層服務器:
// 客戶端連接的抽象
type Client struct {
// tcp 連接
Conn net.Conn
// 當服務端開始發送數據時進入waiting, 阻止其它goroutine關閉連接
// wait.Wait是作者編寫的帶有最大等待時間的封裝:
// https://github.com/HDT3213/godis/blob/master/src/lib/sync/wait/wait.go
Waiting wait.Wait
}
type EchoHandler struct {
// 保存所有工作狀態client的集合(把map當set用)
// 需使用併發安全的容器
activeConn sync.Map
// 和 tcp server 中作用相同的關閉狀態標識位
closing atomic.AtomicBool
}
func MakeEchoHandler()(*EchoHandler) {
return &EchoHandler{
}
}
// 關閉客戶端連接
func (c *Client)Close()error {
// 等待數據發送完成或超時
c.Waiting.WaitWithTimeout(10 * time.Second)
c.Conn.Close()
return nil
}
func (h *EchoHandler)Handle(ctx context.Context, conn net.Conn) {
if h.closing.Get() {
// closing handler refuse new connection
conn.Close()
}
client := &Client {
Conn: conn,
}
h.activeConn.Store(client, 1)
reader := bufio.NewReader(conn)
for {
msg, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
logger.Info("connection close")
h.activeConn.Delete(conn)
} else {
logger.Warn(err)
}
return
}
// 發送數據前先置爲waiting狀態
client.Waiting.Add(1)
// 模擬關閉時未完成發送的情況
//logger.Info("sleeping")
//time.Sleep(10 * time.Second)
b := []byte(msg)
conn.Write(b)
// 發送完畢, 結束waiting
client.Waiting.Done()
}
}
func (h *EchoHandler)Close()error {
logger.Info("handler shuting down...")
h.closing.Set(true)
// TODO: concurrent wait
h.activeConn.Range(func(key interface{}, val interface{})bool {
client := key.(*Client)
client.Close()
return true
})
return nil
}