1. 程式人生 > 其它 >54. 心跳的實現 | 厚土Go學習筆記

54. 心跳的實現 | 厚土Go學習筆記

在多客戶端同時訪問伺服器的工作模式下,首先要保證服務端的執行正常。因此,Server在和Client建立通訊後,確保連線的及時斷開就非常重要。否則,多個客戶端長時間佔用著連線不關閉,是非常可怕的伺服器資源浪費。會使得伺服器可服務的客戶端數量大幅度減少。 因此,針對短連線和長連線,根據業務的需要,配套不同的處理機制。

短連線

一般建立完連線,就立刻傳輸資料。傳輸完資料,連線就關閉。服務端根據需要,設定連線的時長。超過時間長度,就算客戶端超時。立刻關閉連線。

長連線

建立連線後,傳輸資料,然後要保持連線,然後再次傳輸資料。直到連線關閉。


socket 讀寫可以通過 SetDeadline、SetReadDeadline、SetWriteDeadline設定阻塞的時間。

func (*IPConn) SetDeadline  
func (c *IPConn) SetDeadline(t time.Time) error  
  
func (*IPConn) SetReadDeadline  
func (c *IPConn) SetReadDeadline(t time.Time) error  
  
func (*IPConn) SetWriteDeadline  
func (c *IPConn) SetWriteDeadline(t time.Time) error 

如果做短連線,直接在 Server 端的連線上設定SetReadDeadline。當你設定的時限到達,無論客戶端是否還在繼續傳遞資訊,服務端都不會再接收。並且已經關閉連線。

func main() {
    server := ":7373"
    netListen, err := net.Listen("tcp", server)
    if err != nil{
        Log("connect error: ", err)
        os.Exit(1)
    }
    Log("Waiting for Client ...")
    for{
        conn, err := netListen.Accept()
        if err != nil{
            Log(conn.RemoteAddr().String(), "Fatal error: ", err)
            continue
        }

        //設定短連線(10秒)
        conn.SetReadDeadline(time.Now().Add(time.Duration(10)*time.Second))

        Log(conn.RemoteAddr().String(), "connect success!")
        ...
    }
}

這就可以了。在這段程式碼中,每當10秒中的時限一到,連線就終止了。 這個很容易做到。而我們重點要講的是,在長連線中如何做到超時控制。 根據業務需要,客戶端可能需要長時間保持連線。但是服務端不能無限制的保持。這就需要一個機制,如果超過某個時間長度,服務端沒有獲得客戶端的資料,就判定客戶端已經不需要連線了(比如客戶端掛掉了)。 做到這個,需要一個心跳機制。在限定的時間內,客戶端給服務端傳送一個指定的訊息,以便服務端知道客戶端還活著。

func sender(conn *net.TCPConn) {
    for i := 0; i < 10; i++{
        words := strconv.Itoa(i)+" Hello I'm MyHeartbeat Client."
        msg, err := conn.Write([]byte(words))
        if err != nil {
            Log(conn.RemoteAddr().String(), "Fatal error: ", err)
            os.Exit(1)
        }
        Log("服務端接收了", msg)
        time.Sleep(2 * time.Second)
    }
    for i := 0; i < 2 ; i++ {
        time.Sleep(12 * time.Second)
    }
    for i := 0; i < 10; i++{
        words := strconv.Itoa(i)+" Hi I'm MyHeartbeat Client."
        msg, err := conn.Write([]byte(words))
        if err != nil {
            Log(conn.RemoteAddr().String(), "Fatal error: ", err)
            os.Exit(1)
        }
        Log("服務端接收了", msg)
        time.Sleep(2 * time.Second)
    }

}

這段客戶端程式碼,實現了兩個相同的資訊傳送頻率給服務端。兩個頻率中間,我們讓執行休息了12秒。 然後,我們在服務端的對應機制是這樣的。

func HeartBeating(conn net.Conn, bytes chan byte, timeout int) {
    select {
    case fk := <- bytes:
        Log(conn.RemoteAddr().String(), "心跳:第", string(fk), "times")
        conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
        break

        case <- time.After(5 * time.Second):
            Log("conn dead now")
            conn.Close()
    }
}

每次接收到心跳資料就 SetDeadline 延長一個時間段 timeout。如果沒有接到心跳資料,5秒後連線關閉。

服務端完整程式碼示例

/**
* MyHeartbeatServer
* @Author:  Jian Junbo
* @Email:   [email protected]
* @Create:  2017/9/16 14:02
* Copyright (c) 2017 Jian Junbo All rights reserved.
*
* Description:  
*/
package main

import (
    "net"
    "fmt"
    "os"
    "time"
)

func main() {
    server := ":7373"
    netListen, err := net.Listen("tcp", server)
    if err != nil{
        Log("connect error: ", err)
        os.Exit(1)
    }
    Log("Waiting for Client ...")
    for{
        conn, err := netListen.Accept()
        if err != nil{
            Log(conn.RemoteAddr().String(), "Fatal error: ", err)
            continue
        }

        //設定短連線(10秒)
        conn.SetReadDeadline(time.Now().Add(time.Duration(10)*time.Second))

        Log(conn.RemoteAddr().String(), "connect success!")
        go handleConnection(conn)

    }
}
func handleConnection(conn net.Conn) {
    buffer := make([]byte, 1024)
    for {
        n, err := conn.Read(buffer)
        if err != nil {
            Log(conn.RemoteAddr().String(), " Fatal error: ", err)
            return
        }

        Data := buffer[:n]
        message := make(chan byte)
        //心跳計時
        go HeartBeating(conn, message, 4)
        //檢測每次是否有資料傳入
        go GravelChannel(Data, message)

        Log(time.Now().Format("2006-01-02 15:04:05.0000000"), conn.RemoteAddr().String(), string(buffer[:n]))
    }

    defer conn.Close()
}
func GravelChannel(bytes []byte, mess chan byte) {
    for _, v := range bytes{
        mess <- v
    }
    close(mess)
}
func HeartBeating(conn net.Conn, bytes chan byte, timeout int) {
    select {
    case fk := <- bytes:
        Log(conn.RemoteAddr().String(), "心跳:第", string(fk), "times")
        conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
        break

        case <- time.After(5 * time.Second):
            Log("conn dead now")
            conn.Close()
    }
}
func Log(v ...interface{}) {
    fmt.Println(v...)
    return
}

客戶端完整程式碼示例

/**
* MyHeartbeatClient
* @Author:  Jian Junbo
* @Email:   [email protected]
* @Create:  2017/9/16 14:21
* Copyright (c) 2017 Jian Junbo All rights reserved.
*
* Description:  
*/
package main

import (
    "net"
    "fmt"
    "os"
    "strconv"
    "time"
)

func main() {
    server := "127.0.0.1:7373"

    tcpAddr, err := net.ResolveTCPAddr("tcp4",server)
    if err != nil{
        Log(os.Stderr,"Fatal error:",err.Error())
        os.Exit(1)
    }
    conn, err := net.DialTCP("tcp",nil,tcpAddr)
    if err != nil{
        Log("Fatal error:",err.Error())
        os.Exit(1)
    }
    Log(conn.RemoteAddr().String(), "connection succcess!")

    sender(conn)
    Log("send over")
}
func sender(conn *net.TCPConn) {
    for i := 0; i < 10; i++{
        words := strconv.Itoa(i)+" Hello I'm MyHeartbeat Client."
        msg, err := conn.Write([]byte(words))
        if err != nil {
            Log(conn.RemoteAddr().String(), "Fatal error: ", err)
            os.Exit(1)
        }
        Log("服務端接收了", msg)
        time.Sleep(2 * time.Second)
    }
    for i := 0; i < 2 ; i++ {
        time.Sleep(12 * time.Second)
    }
    for i := 0; i < 10; i++{
        words := strconv.Itoa(i)+" Hi I'm MyHeartbeat Client."
        msg, err := conn.Write([]byte(words))
        if err != nil {
            Log(conn.RemoteAddr().String(), "Fatal error: ", err)
            os.Exit(1)
        }
        Log("服務端接收了", msg)
        time.Sleep(2 * time.Second)
    }

}
func Log(v ...interface{}) {
    fmt.Println(v...)
    return
}

服務端執行效果是這樣的

服務端只接收了第一次迴圈資料

服務端根據5秒原則,在客戶端第一個迴圈間歇的12秒的時間內,關閉了連線。

客戶端執行效果是這樣的

客戶端完全不知道服務端已經關閉了連線,還是繼續傳送了第二次迴圈。

這樣做服務端確實達到了不佔用過多資源的目的。但是對客戶端來說不夠友好。友好的方式,是每次訊息往來都執行三次握手的模式。

當然,具體是否採用,需要根據業務場景分析確定。