54. 心跳的实现

在多客户端同时访问服务器的工作模式下,首先要保证服务端的运行正常。因此,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:   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
        }

        //设置短连接(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:   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("服务端接收了", 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秒的时间内,关闭了连接。

客户端运行效果是这样的


客户端完全不知道服务端已经关闭了连接,还是继续发送了第二次循环。

这样做服务端确实达到了不占用过多资源的目的。但是对客户端来说不够友好。友好的方式,是每次消息往来都执行三次握手的模式。
当然,具体是否采用,需要根据业务场景分析确定。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,873评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,483评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,525评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,595评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,018评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,958评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,118评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,873评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,643评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,813评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,293评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,615评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,306评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,170评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,968评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,107评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,894评论 2 278

推荐阅读更多精彩内容

  • 最全的iOS面试题及答案 iOS面试小贴士 ———————————————回答好下面的足够了-----------...
    zweic阅读 2,574评论 0 73
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,209评论 0 6
  • 实时消息协议---流的分块 版权声明: 版权(c)2009 Adobe系统有限公司。全权所有。 摘要: 本备忘录描...
    一个人zy阅读 1,806评论 0 9
  • 马上,后天就要高考了。 无数回忆涌上心头,舍友们不断的讨论着,那年所见所感所闻所想。
    段友阅读 156评论 0 0
  • 就快过年啦,难免聚会,闺蜜、前男友等,然而皮肤暗黄、斑点、肤色不均、皮肤黯淡无光让自己的形象大打折扣,爱美的美女是...
    蓉蓉_6e75阅读 287评论 0 0