54. 心跳的實現(xiàn)

在多客戶端同時訪問服務(wù)器的工作模式下,首先要保證服務(wù)端的運(yùn)行正常。因此,Server在和Client建立通訊后,確保連接的及時斷開就非常重要。否則,多個客戶端長時間占用著連接不關(guān)閉,是非??膳碌姆?wù)器資源浪費(fèi)。會使得服務(wù)器可服務(wù)的客戶端數(shù)量大幅度減少。
因此,針對短連接和長連接,根據(jù)業(yè)務(wù)的需要,配套不同的處理機(jī)制。

短連接

一般建立完連接,就立刻傳輸數(shù)據(jù)。傳輸完數(shù)據(jù),連接就關(guān)閉。服務(wù)端根據(jù)需要,設(shè)定連接的時長。超過時間長度,就算客戶端超時。立刻關(guān)閉連接。

長連接

建立連接后,傳輸數(shù)據(jù),然后要保持連接,然后再次傳輸數(shù)據(jù)。直到連接關(guān)閉。


socket 讀寫可以通過 SetDeadline、SetReadDeadline、SetWriteDeadline設(shè)置阻塞的時間。

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 端的連接上設(shè)置SetReadDeadline。當(dāng)你設(shè)置的時限到達(dá),無論客戶端是否還在繼續(xù)傳遞信息,服務(wù)端都不會再接收。并且已經(jīng)關(guān)閉連接。

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
        }

        //設(shè)置短連接(10秒)
        conn.SetReadDeadline(time.Now().Add(time.Duration(10)*time.Second))

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

這就可以了。在這段代碼中,每當(dāng)10秒中的時限一到,連接就終止了。
這個很容易做到。而我們重點要講的是,在長連接中如何做到超時控制。
根據(jù)業(yè)務(wù)需要,客戶端可能需要長時間保持連接。但是服務(wù)端不能無限制的保持。這就需要一個機(jī)制,如果超過某個時間長度,服務(wù)端沒有獲得客戶端的數(shù)據(jù),就判定客戶端已經(jīng)不需要連接了(比如客戶端掛掉了)。
做到這個,需要一個心跳機(jī)制。在限定的時間內(nèi),客戶端給服務(wù)端發(fā)送一個指定的消息,以便服務(wù)端知道客戶端還活著。

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("服務(wù)端接收了", 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("服務(wù)端接收了", msg)
        time.Sleep(2 * time.Second)
    }

}

這段客戶端代碼,實現(xiàn)了兩個相同的信息發(fā)送頻率給服務(wù)端。兩個頻率中間,我們讓運(yùn)行休息了12秒。
然后,我們在服務(wù)端的對應(yīng)機(jī)制是這樣的。

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()
    }
}

每次接收到心跳數(shù)據(jù)就 SetDeadline 延長一個時間段 timeout。如果沒有接到心跳數(shù)據(jù),5秒后連接關(guān)閉。
服務(wù)端完整代碼示例

/**
* MyHeartbeatServer
* @Author:  Jian Junbo
* @Email:   junbojian@qq.com
* @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
        }

        //設(shè)置短連接(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)
        //檢測每次是否有數(shù)據(jù)傳入
        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:   junbojian@qq.com
* @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("服務(wù)端接收了", 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("服務(wù)端接收了", msg)
        time.Sleep(2 * time.Second)
    }

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

服務(wù)端運(yùn)行效果是這樣的


服務(wù)端只接收了第一次循環(huán)數(shù)據(jù)

服務(wù)端根據(jù)5秒原則,在客戶端第一個循環(huán)間歇的12秒的時間內(nèi),關(guān)閉了連接。

客戶端運(yùn)行效果是這樣的


客戶端完全不知道服務(wù)端已經(jīng)關(guān)閉了連接,還是繼續(xù)發(fā)送了第二次循環(huán)。

這樣做服務(wù)端確實達(dá)到了不占用過多資源的目的。但是對客戶端來說不夠友好。友好的方式,是每次消息往來都執(zhí)行三次握手的模式。
當(dāng)然,具體是否采用,需要根據(jù)業(yè)務(wù)場景分析確定。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 最全的iOS面試題及答案 iOS面試小貼士 ———————————————回答好下面的足夠了-----------...
    zweic閱讀 2,802評論 0 73
  • __block和__weak修飾符的區(qū)別其實是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,593評論 0 6
  • 實時消息協(xié)議---流的分塊 版權(quán)聲明: 版權(quán)(c)2009 Adobe系統(tǒng)有限公司。全權(quán)所有。 摘要: 本備忘錄描...
    一個人zy閱讀 2,063評論 0 9
  • 馬上,后天就要高考了。 無數(shù)回憶涌上心頭,舍友們不斷的討論著,那年所見所感所聞所想。
    段友閱讀 181評論 0 0
  • 就快過年啦,難免聚會,閨蜜、前男友等,然而皮膚暗黃、斑點、膚色不均、皮膚黯淡無光讓自己的形象大打折扣,愛美的美女是...
    蓉蓉_6e75閱讀 366評論 0 0

友情鏈接更多精彩內(nèi)容