GO語言TCP編程範式

一直用go編寫TCP、HTTP、websocket服務器,得空總結一些簡單的範式,供參考。代碼在github上都可以看到。

1、TCP server

之前用c++寫TCP server,一般兩種模式:
- 1個listener線程 + N個processor線程
- 通過REUSEPORT機制,N個listener線程,TCP包的處理可以直接在listener線程中做,也可以起processor線程處理。

用go寫TCP的server,做併發簡單多了,因爲goroutine比線程輕量很多,在一定的併發下,創建以及銷燬一個goroutine的性能損耗可以不用過多的考慮。所以通常的做法是:一個listener goroutine,來了新連接就啓動兩個goroutine,一個負責讀一個負責寫。 至於其他的邏輯處理,可以在讀的goroutine中處理,也可以在其他業務邏輯的goroutine中處理。

我的這個範式中,先定義個處理TCP包的handler

type MessageHandler struct {

    conn net.Conn 
    ExitCmd chan bool
    writeChan chan []byte
}

func (this *MessageHandler)WaitingForRead(){
    ......
}

func (this *MessageHandler)WaitingForWrite(){
    ......
}

tcp server的代碼片段一般如下:

serverAddr, err := net.ResolveTCPAddr("tcp", hostAndPort)
    if err != nil {
        log.Printf("Resolving %s failed: %s\n", hostAndPort, err.Error())
        return err
    }

    listener, err := net.ListenTCP("tcp", serverAddr)
    if err != nil {
        log.Printf("listen %s failed: %s", hostAndPort, err.Error())
        return err
    }
    log.Printf("start tcp server %s\n", hostAndPort)
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Printf("accept error: %s , close this socket and exit\n", err.Error())
            listener.Close()
            return err
        }
        handler := NewMessageHandler(conn)
        //this.clientList = append(this.clientMap, handler)

        go handler.WaitingForRead()
        go handler.WaitingForWrite()
    }
}

每一個新的連接,new一個MessageHandler,然後分別啓動一個讀和寫的goroutine來等待socket的讀寫事件。
一般我們都要定義好服務端跟客戶端之間的協議,封好應用層的包寫到socket中。但是TCP是流式傳輸字節流,通過TCP傳輸數據,存在粘包情況,例如:一端write兩個包,另一端某次read,可能read到0.5個包,也有可能read到1.5個包。所以對於tcp的讀,需要判斷什麼時候讀到了完整的應用層包。一般做法有兩個:
- 每一個應用層的包以特定字符串結尾,比如”/r/n/r/n”。
這樣的話,read的時候需要對讀到的每一個字符串做比較,以判斷是否到了結束符。這個字符串比較也是不小的開銷。
- 將應用層的包設計成head+body樣式,比如head爲固定的2個字節,表示body的長度。read的時候,先取2個字節,解析出body的長度,然後再取該長度的字節流解析出body。
這種做法,read的時候,需要知道約定的消息格式,代碼中socket的讀需要跟應用層的協議耦合在一起。但是避免了頻繁的字符串比較,所以我們一般都選擇這種做法。

比如我們約定消息格式如下:

         8        16       24       32 
|--------|--------|--------|--------|
|      bodyLen    |        magic    |
|--------|--------|--------|--------|
|                seq                |   
|--------|--------|--------|--------|
|                 body              |
|                                   |

那read的代碼如下:

func (this *MessageHandler)WaitingForRead(){
    //......
    var ibuf []byte = make([]byte, 1024)//[註釋1] 1024字節大小的緩衝區

    var needRead int = 1024
    var bodyLen uint16 = 0
    var endPos int = 0
    var startPos int = 0
    var magic uint16 = 0
    var seq uint32 = 0
    for {
        //[註釋2]開始等待讀,每次讀到緩衝區endPos指定的位置
        length, err := this.conn.Read(ibuf[endPos:])
        log.Printf("read data: %d\n", length)
        switch err {
        case nil:
            endPos += length
            //有可能一次讀到了多個應用層的包,所以要循環處理
            for {
                if endPos-startPos < 8 {
                    break
                }
                if bodyLen == 0 {
                    bodyLen = binary.BigEndian.Uint16(ibuf[startPos : startPos+2])
                    magic = binary.BigEndian.Uint16(ibuf[startPos+2 : startPos+4])
                    seq = binary.BigEndian.Uint32(ibuf[startPos+4 : startPos+8])
                }
                needRead = int(bodyLen) - (endPos - startPos - 8)
                log.Printf("startPos:%d, endPos:%d, bodyLen:%d, magic:%d, seq:%d, needRead:%d", startPos, endPos, bodyLen, magic, seq, needRead)
                if needRead > 0 {
                    break
                } else {
                    //[註釋3]讀到完整的消息後,處理
                    res := this.handleMsg(8, bodyLen, ibuf[startPos:], magic, seq)
                    if res == -1 {
                        log.Printf("handle msg error, close the connection\n")
                        goto DISCONNECT
                    }
                    startPos += int(bodyLen) + 8
                    bodyLen = 0
                    needRead = 0
                    if startPos == endPos {
                        startPos = 0
                        endPos = 0
                    }
                }
            }
            //[註釋4]循環處理完後,如果緩衝區中還有剩餘的數據未處理,則挪到緩衝區最左端,避免緩衝區滿無法再讀數據
            if startPos < endPos && startPos > 0 {
                reader := bytes.NewReader(ibuf)
                reader.ReadAt(ibuf, int64(endPos-startPos))
                startPos = 0
                endPos -= startPos
            }
        case syscall.Errno(0xb): // try again
            log.Printf("read need try again\n")
            continue
        default:
            log.Printf("read error %s\n", err.Error())
            goto DISCONNECT
        }

    }
DISCONNECT:
    //......

}

這裏有幾處可能存在問題:
- [註釋1] 的地方,緩衝區應該根據實際設置大一點,粘包情況還是比較頻繁的。
- [註釋2] 的地方,這裏用簡單的阻塞Read。也可以用io.ReadFull,每次讀滿一定的字節,比如先讀8個字節的head,解析出body長度,再讀出該長度的數據處理,這樣不存在邊界問題。但是跟Read相比,可能會多出很多ReadFull的系統調用。因爲如果對端數據比較頻繁,Read可以一次從緩衝區讀出多個應用層包的數據。
- [註釋3] 的地方,要特別注意,slice作爲傳參只是傳的引用,緩衝區的數據可能被覆蓋,所以如果HandleMessage是在其他goroutine中處理,一定要先把數據copy出去,否則極有可能前一個包的數據被覆蓋造成解包錯誤。
- [註釋4] 的地方,涉及到緩衝區移動數據,太頻繁的ReadAt調用可能有性能損耗。可以採用環形緩衝區減少字節移動,或者在大緩衝區情況下,當緩衝區快到右邊邊界時才挪動數據。

以上幾點有興趣的童鞋可以自己改進。具體代碼實例如下:tcpserver

2、TCP client

client端的代碼讀寫部分可以參考server部分的代碼,這裏就不做過多描述。具體代碼實例如下:tcpclient

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章