Golang socket websocket

理論知識可以參考
網(wǎng)絡(luò)信息怎么在網(wǎng)線中傳播的 (轉(zhuǎn)載自知乎)
Android 網(wǎng)絡(luò)(一) 概念 TCP/IP Socket Http Restful
腦殘式網(wǎng)絡(luò)編程入門(一):跟著動(dòng)畫來學(xué)TCP三次握手和四次揮手
腦殘式網(wǎng)絡(luò)編程入門(二):我們在讀寫Socket時(shí)趁桃,究竟在讀寫什么?
TCP 粘包問題淺析及其解決方案,這個(gè)帖子里大家一頓噴粘包這個(gè)叫法
我工作五年的時(shí)候也不知道 “TCP 粘包”疆前,繼續(xù)吐槽

一、API
1.服務(wù)端通過Listen加Accept
package main

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

func main() {
    //通過 ResolveTCPAddr 獲取一個(gè) TCPAddr
    //ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
    
    //net參數(shù)是"tcp4"较剃、"tcp6"纬朝、"tcp"中的任意一個(gè),
    //分別表示 TCP(IPv4-only),TCP(IPv6-only)
    //或者 TCP(IPv4,IPv6 的任意一個(gè))
    
    //addr 表示域名或者IP地址掀潮,
    //例如"www.google.com:80" 或者"127.0.0.1:22".
    service := ":7777"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    
    //ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    
    //func (l *TCPListener) Accept() (c Conn, err os.Error)
    for {
        conn, err := listener.Accept()
            if err != nil {
            continue
        }
        
        daytime := time.Now().String()
        // don't care about return value
        conn.Write([]byte(daytime)) 
        
        // we're finished with this client
        conn.Close() 
    }
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

上面的服務(wù)跑起來之后菇夸,它將會(huì)一直在那里等待,直到有新的客戶端請求到達(dá)仪吧。當(dāng)有新的客戶端請求到達(dá)并同意接受 Accept 該請求的時(shí)候他會(huì)反饋當(dāng)前的時(shí)間信息庄新。值得注意的是,在代碼中 for 循環(huán)里薯鼠,當(dāng)有錯(cuò)誤發(fā)生時(shí)择诈,直接 continue而不是退出,是因?yàn)樵诜?wù)器端跑代碼的時(shí)候出皇,當(dāng)有錯(cuò)誤發(fā)生的情況下最好是由服務(wù)端記錄錯(cuò)誤羞芍,然后當(dāng)前連接的客戶端直接報(bào)錯(cuò)而退出,從而不會(huì)影響到當(dāng)前服務(wù)端運(yùn)行的整個(gè)服務(wù)郊艘。

上面的代碼有個(gè)缺點(diǎn)荷科,執(zhí)行的時(shí)候是單任務(wù)的,不能同時(shí)接收多個(gè)請求纱注,那么該如何改造以使它支持多并發(fā)呢畏浆?

...
for {
    conn, err := listener.Accept()
    if err != nil {
        continue
    }
    go handlerClient(conn)
}
...

func handleClient(conn net.Conn) {
    defer conn.Close()
    daytime := time.Now().String()
    // don't care about return value
    conn.Write([]byte(daytime)) 
    
    // we're finished with this client
}
...
2.客戶端直接調(diào)用 Dial
package main
    import (
        "fmt"
        "io/ioutil"
        "net"
        "os"
    )
    
    func main() {
        if len(os.Args) != 2 {
            fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0])
            os.Exit(1)
        }
    
        service := os.Args[1]
        tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
        checkError(err)
        
        conn, err := net.DialTCP("tcp", nil, tcpAddr)
        checkError(err)
        
        _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
        checkError(err)
        
        result, err := ioutil.ReadAll(conn)
        checkError(err)
        
        fmt.Println(string(result))
        os.Exit(0)
    }
    
    func checkError(err error) {
        if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

首先程序?qū)⒂脩舻妮斎胱鳛閰?shù) service 傳入net.ResolveTCPAddr 獲取一個(gè) tcpAddr,然后把 tcpAddr 傳入 DialTCP 后創(chuàng)建了一個(gè) TCP連接 conn ,通過 conn 來發(fā)送請求信息狞贱,最后通過 ioutil.ReadAll 從 conn 中讀取全部的文本刻获,也就是服務(wù)端響應(yīng)反饋的信息。

二瞎嬉、實(shí)現(xiàn)一個(gè)可以接受不同命令的服務(wù)端

參考使用 Go 進(jìn)行 Socket 編程
我們實(shí)現(xiàn)一個(gè)服務(wù)端, 它可以接受下面這些命令:

  • ping 探活的命令, 服務(wù)端會(huì)返回 “pong”
  • echo 服務(wù)端會(huì)返回收到的字符串
  • quit 服務(wù)端收到這個(gè)命令后就會(huì)關(guān)閉連接

具體的服務(wù)端代碼如下所示:

package main

import (
    "fmt"
    "net"
    "strings"
)

func connHandler(c net.Conn) {
    if c == nil {
        return
    }

    buf := make([]byte, 4096)

    for {
        cnt, err := c.Read(buf)
        if err != nil || cnt == 0 {
            c.Close()
            break
        }

        inStr := strings.TrimSpace(string(buf[0:cnt]))

        inputs := strings.Split(inStr, " ")

        switch inputs[0] {
        case "ping":
            c.Write([]byte("pong\n"))
        case "echo":
            echoStr := strings.Join(inputs[1:], " ") + "\n"
            c.Write([]byte(echoStr))
        case "quit":
            c.Close()
            break
        default:
            fmt.Printf("Unsupported command: %s\n", inputs[0])
        }
    }

    fmt.Printf("Connection from %v closed. \n", c.RemoteAddr())
}

func main() {
    server, err := net.Listen("tcp", ":1208")
    if err != nil {
        fmt.Printf("Fail to start server, %s\n", err)
    }

    fmt.Println("Server Started ...")

    for {
        conn, err := server.Accept()
        if err != nil {
            fmt.Printf("Fail to connect, %s\n", err)
            break
        }

        go connHandler(conn)
    }
}

客戶端的實(shí)現(xiàn)

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
    "strings"
)

func connHandler(c net.Conn) {
    defer c.Close()

    reader := bufio.NewReader(os.Stdin)
    buf := make([]byte, 1024)

    for {
        input, _ := reader.ReadString('\n')
        input = strings.TrimSpace(input)

        if input == "quit" {
            return
        }

        c.Write([]byte(input))

        cnt, err := c.Read(buf)
        if err != nil {
            fmt.Printf("Fail to read data, %s\n", err)
            continue
        }

        fmt.Print(string(buf[0:cnt]))
    }
}

func main() {
    conn, err := net.Dial("tcp", "localhost:1208")
    if err != nil {
        fmt.Printf("Fail to connect, %s\n", err)
        return
    }

    connHandler(conn)
}
三蝎毡、解決golang開發(fā)socket服務(wù)時(shí)粘包半包bug

基礎(chǔ)知識可以參考tcp是流的一些思考--拆包和粘包
tcp中有一個(gè)negal算法厚柳,用途是這樣的:通信兩端有很多小的數(shù)據(jù)包要發(fā)送,雖然傳送的數(shù)據(jù)很少顶掉,但是流程一點(diǎn)沒少草娜,也需要tcp的各種確認(rèn),校驗(yàn)痒筒。這樣小的數(shù)據(jù)包如果很多宰闰,會(huì)造成網(wǎng)絡(luò)資源很大的浪費(fèi),negal算法做了這樣一件事簿透,當(dāng)來了一個(gè)很小的數(shù)據(jù)包移袍,我不急于發(fā)送這個(gè)包,而是等來了更多的包老充,將這些小包組合成大包之后一并發(fā)送葡盗,不就提高了網(wǎng)絡(luò)傳輸?shù)男实穆铩_@個(gè)想法收到了很好的效果啡浊,但是我們想一下觅够,如果是分屬于兩個(gè)不同頁面的包,被合并在了一起巷嚣,那客戶那邊如何區(qū)分它們呢喘先?
這就是粘包問題。從粘包問題我們更可以看出為什么tcp被稱為流協(xié)議廷粒,因?yàn)樗透饕粯泳秸菦]有邊界的,沒有消息的邊界保護(hù)機(jī)制坝茎,所以tcp只有流的概念涤姊,沒有包的概念。

解決tcp粘包的方法:
客戶端會(huì)定義一個(gè)標(biāo)示嗤放,比如數(shù)據(jù)的前4位是數(shù)據(jù)的長度思喊,后面才是數(shù)據(jù)。那么客戶端只需發(fā)送 ( 數(shù)據(jù)長度+數(shù)據(jù) ) 的格式數(shù)據(jù)就可以了次酌,接收方根據(jù)包頭信息里的數(shù)據(jù)長度讀取buffer.
客戶端:

//客戶端發(fā)送封包
package main

import (
    "fmt"
    "math/rand"
    "net"
    "os"
    "strconv"
    "strings"
    "time"
)

func main() {

    server := "127.0.0.1:5000"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", server)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }

    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }

    defer conn.Close()

    for i := 0; i < 50; i++ {
        //msg := strconv.Itoa(i)
        msg := RandString(i)
        msgLen := fmt.Sprintf("%03s", strconv.Itoa(len(msg)))
        //fmt.Println(msg, msgLen)
        words := "aaaa" + msgLen + msg
        //words := append([]byte("aaaa"), []byte(msgLen), []byte(msg))
        fmt.Println(len(words), words)
        conn.Write([]byte(words))
    }
}

/**
*生成隨機(jī)字符
**/
func RandString(length int) string {
    rand.Seed(time.Now().UnixNano())
    rs := make([]string, length)
    for start := 0; start < length; start++ {
        t := rand.Intn(3)
        if t == 0 {
            rs = append(rs, strconv.Itoa(rand.Intn(10)))
        } else if t == 1 {
            rs = append(rs, string(rand.Intn(26)+65))
        } else {
            rs = append(rs, string(rand.Intn(26)+97))
        }
    }
    return strings.Join(rs, "")
}

服務(wù)端實(shí)例代碼:

package main

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

func main() {
    netListen, err := net.Listen("tcp", ":5000")
    CheckError(err)

    defer netListen.Close()

    for {
        conn, err := netListen.Accept()
        if err != nil {
            continue
        }

        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    allbuf := make([]byte, 0)
    buffer := make([]byte, 1024)
    for {
        readLen, err := conn.Read(buffer)
        //fmt.Println("readLen: ", readLen, len(allbuf))
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("read error")
            return
        }

        if len(allbuf) != 0 {
            allbuf = append(allbuf, buffer...)
        } else {
            allbuf = buffer[:]
        }
        var readP int = 0
        for {
            //fmt.Println("allbuf content:", string(allbuf))

            //buffer長度小于7
            if readLen-readP < 7 {
                allbuf = buffer[readP:]
                break
            }

            msgLen, _ := strconv.Atoi(string(allbuf[readP+4 : readP+7]))
            logLen := 7 + msgLen
            //fmt.Println(readP, readP+logLen)
            //buffer剩余長度>將處理的數(shù)據(jù)長度
            if len(allbuf[readP:]) >= logLen {
                //fmt.Println(string(allbuf[4:7]))
                fmt.Println(string(allbuf[readP : readP+logLen]))
                readP += logLen
                //fmt.Println(readP, readLen)
                if readP == readLen {
                    allbuf = nil
                    break
                }
            } else {
                allbuf = buffer[readP:]
                break
            }
        }
    }
}

func CheckError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}
四恨课、io包的ReadFull

對于第三部分的解決golang開發(fā)socket服務(wù)時(shí)粘包半包bug,有作者認(rèn)為太復(fù)雜了和措,參見golang tcp拆包的正確姿勢庄呈,他提出可以用ReadFull來簡化蜕煌。

關(guān)于io包基礎(chǔ)知識派阱,參考Golang io reader writer
關(guān)于ReadFull,可以參考達(dá)達(dá)的博客系列:
Go語言小貼士1 - io包
Go語言小貼士2 - 協(xié)議解析
Go語言小貼士3 - bufio包

原文不再轉(zhuǎn)述斜纪,現(xiàn)在引用一下重點(diǎn):

io.Reader的定義如下:

type Reader interface {
        Read(p []byte) (n int, err error)
}

其中文檔的說明非常重要贫母,文檔中詳細(xì)描述了Read方法的各種返回可能性文兑。

文檔描述中有一個(gè)要點(diǎn),就是n可能小于等于len(p)腺劣,也就是說Go在讀IO的時(shí)候绿贞,是不會(huì)保證一次讀取預(yù)期的所有數(shù)據(jù)的。如果我們要確保一次讀取我們所需的所有數(shù)據(jù)橘原,就需要在一個(gè)循環(huán)里調(diào)用Read籍铁,累加每次返回的n并小心設(shè)置下次Read時(shí)p的偏移量,直到n的累加值達(dá)到我們的預(yù)期趾断。

因?yàn)樯鲜鲂枨髮?shí)在太常見了拒名,所以Go在io包中提供了一個(gè)ReadFull函數(shù)來做到一次讀取要求的所有數(shù)據(jù),通過閱讀ReadFull函數(shù)的代碼芋酌,也可以反過來幫助大家理解io.Reader是怎么運(yùn)作的增显。

//io.go源碼
func ReadFull(r Reader, buf []byte) (n int, err error) {
    return ReadAtLeast(r, buf, len(buf))
}

func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {
    if len(buf) < min {
        return 0, ErrShortBuffer
    }
    for n < min && err == nil {
        var nn int
        nn, err = r.Read(buf[n:])
        n += nn
    }
    if n >= min {
        err = nil
    } else if n > 0 && err == EOF {
        err = ErrUnexpectedEOF
    }
    return
}

在很多應(yīng)用場景中,消息包的長度是不固定的脐帝,就像上面的字符串字段一樣同云。我們一樣可以用開頭固定的幾個(gè)字節(jié)來存放消息長度,在解析通訊協(xié)議的時(shí)候就可以從字節(jié)流中截出一個(gè)個(gè)的消息包了堵腹,這樣的操作通常叫做協(xié)議分包或者粘包處理炸站。
貼個(gè)從Socket讀取消息包的偽代碼(沒編譯):

func ReadPacket(conn net.Conn) ([]byte, error) {
        var head [2]byte

        if _, err := io.ReadFull(conn, head[:]); err != nil {
                return err
        }

        size := binary.BigEndian.Uint16(head)
        packet := make([]byte, size)

        if _, err := io.ReadFull(conn, packet); err != nil {
                return err
        }

        return packet
}

上面的代碼就用到了前一個(gè)小貼士中說到的io.ReadFull來確保一次讀取完整數(shù)據(jù)。

要注意秸滴,這段代碼不是線程安全的武契,如果有兩個(gè)線程同時(shí)對一個(gè)net.Conn進(jìn)行ReadPacket操作,很可能會(huì)發(fā)生嚴(yán)重錯(cuò)誤荡含,具體邏輯請自行分析咒唆。

從上面結(jié)構(gòu)體序列化和反序列化的代碼中,大家不難看出释液,實(shí)現(xiàn)一個(gè)二進(jìn)制協(xié)議是挺繁瑣和容易出BUG的全释,只要稍微有一個(gè)數(shù)值計(jì)算錯(cuò)就解析出錯(cuò)了。所以在工程實(shí)踐中误债,不推薦大家手寫二進(jìn)制協(xié)議的解析代碼浸船,項(xiàng)目中通常會(huì)用自動(dòng)化的工具來輔助生成代碼。

Leaf 游戲服務(wù)器框架簡介的tcp_msg.go中寝蹈,Read方法也使用了ReadFull這種方式來處理李命。

五、WebSocket

參考封裝golang websocket
websocket是個(gè)二進(jìn)制協(xié)議箫老,需要先通過Http協(xié)議進(jìn)行握手封字,從而協(xié)商完成從Http協(xié)議向websocket協(xié)議的轉(zhuǎn)換。一旦握手結(jié)束,當(dāng)前的TCP連接后續(xù)將采用二進(jìn)制websocket協(xié)議進(jìn)行雙向雙工交互阔籽,自此與Http協(xié)議無關(guān)流妻。

可以通過這篇知乎了解一下websocket協(xié)議的基本原理:《WebSocket 是什么原理?為什么可以實(shí)現(xiàn)持久連接笆制?》绅这。

1.粘包

我們開發(fā)過TCP服務(wù)的都知道,需要通過協(xié)議decode從TCP字節(jié)流中解析出一個(gè)一個(gè)請求在辆,那么websocket又怎么樣呢证薇?

websocket以message為單位進(jìn)行通訊,本身就是一個(gè)在TCP層上的一個(gè)分包協(xié)議匆篓,其實(shí)并不需要我們再進(jìn)行粘包處理棕叫。但是因?yàn)閱蝹€(gè)message可能很大很大(比如一個(gè)視頻文件),那么websocket顯然不適合把一個(gè)視頻作為一個(gè)message傳輸(中途斷了前功盡棄)奕删,所以websocket協(xié)議其實(shí)是支持1個(gè)message分多個(gè)frame幀傳輸?shù)摹?/p>

我們的瀏覽器提供的編程API都是message粒度的俺泣,把frame拆幀的細(xì)節(jié)對開發(fā)者隱蔽了,而服務(wù)端websocket框架一般也做了同樣的隱藏完残,會(huì)自動(dòng)幫我們收集所有的frame后拼成messasge再回調(diào)伏钠,所以結(jié)論就是:

websocket以message為單位通訊,不需要開發(fā)者自己處理粘包問題谨设。

更多參考Websocket需要像TCP Socket那樣進(jìn)行邏輯數(shù)據(jù)包的分包與合包嗎?

2.golang實(shí)現(xiàn)

golang官方標(biāo)準(zhǔn)庫里有一個(gè)websocket的包熟掂,但是它提供的就是frame粒度的API,壓根不能用扎拣。

不過官方其實(shí)已經(jīng)認(rèn)可了一個(gè)準(zhǔn)標(biāo)準(zhǔn)庫實(shí)現(xiàn)赴肚,它實(shí)現(xiàn)了message粒度的API,讓開發(fā)者不需要關(guān)心websocket協(xié)議細(xì)節(jié)二蓝,開發(fā)起來非常方便誉券,其文檔地址:https://godoc.org/github.com/gorilla/websocket

開發(fā)websocket服務(wù)時(shí)刊愚,首先要基于http庫對外暴露接口踊跟,然后由websocket庫接管TCP連接進(jìn)行協(xié)議升級,然后進(jìn)行websocket協(xié)議的數(shù)據(jù)交換鸥诽,所以開發(fā)時(shí)總是要用到http庫和websocket庫商玫。

上述websocket文檔中對開發(fā)websocket服務(wù)有明確的注意事項(xiàng)要求,主要是指:

  • 讀和寫API不是并發(fā)安全的牡借,需要啟動(dòng)單個(gè)goroutine串行處理拳昌。
  • 關(guān)閉API是線程安全的,一旦調(diào)用則阻塞的讀和寫API會(huì)出錯(cuò)返回钠龙,從而終止處理炬藤。
六扁远、心跳實(shí)現(xiàn)

Golang 心跳的實(shí)現(xiàn)
在多客戶端同時(shí)訪問服務(wù)器的工作模式下,首先要保證服務(wù)器的運(yùn)行正常刻像。因此,Server和Client建立通訊后并闲,確保連接的及時(shí)斷開就非常重要细睡。否則,多個(gè)客戶端長時(shí)間占用著連接不關(guān)閉帝火,是非沉镝悖可怕的服務(wù)器資源浪費(fèi)。會(huì)使得服務(wù)器可服務(wù)的客戶端數(shù)量大幅度減少蠢壹。因此,針對短鏈接和長連接九巡,根據(jù)業(yè)務(wù)的需求图贸,配套不同的處理機(jī)制。

  • 短連接:一般建立完連接冕广,就立刻傳輸數(shù)據(jù)疏日。傳輸完數(shù)據(jù),連接就關(guān)閉撒汉。服務(wù)端根據(jù)需要沟优,設(shè)定連接的時(shí)長。超過時(shí)間長度睬辐,就算客戶端超時(shí)挠阁。立刻關(guān)閉連接。
  • 長連接:建立連接后溯饵,傳輸數(shù)據(jù)侵俗,然后要保持連接,然后再次傳輸數(shù)據(jù)丰刊。直到連接關(guān)閉坡慌。

socket讀寫可以通過 SetDeadline、SetReadDeadline藻三、SetWriteDeadline設(shè)置阻塞的時(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è)置的時(shí)限到達(dá)熄求,無論客戶端是否還在繼續(xù)傳遞消息,服務(wù)端都不會(huì)再接收逗概。并且已經(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秒中的時(shí)限一道卿城,連接就終止了枚钓。

根據(jù)業(yè)務(wù)需要,客戶端可能需要長時(shí)間保持連接瑟押。但是服務(wù)端不能無限制的保持搀捷。這就需要一個(gè)機(jī)制,如果超過某個(gè)時(shí)間長度多望,服務(wù)端沒有獲得客戶端的數(shù)據(jù)嫩舟,就判定客戶端已經(jīng)不需要連接了(比如客戶端掛掉了)。做到這個(gè)怀偷,需要一個(gè)心跳機(jī)制家厌。在限定的時(shí)間內(nèi),客戶端給服務(wù)端發(fā)送一個(gè)指定的消息椎工,以便服務(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)
    }

}

這段客戶端代碼,實(shí)現(xiàn)了兩個(gè)相同的信息發(fā)送頻率給服務(wù)端维蒙。兩個(gè)頻率中間镰绎,我們讓運(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 延長一個(gè)時(shí)間段 timeout。如果沒有接到心跳數(shù)據(jù)八千,5秒后連接關(guān)閉吗讶。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市恋捆,隨后出現(xiàn)的幾起案子照皆,更是在濱河造成了極大的恐慌,老刑警劉巖沸停,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件膜毁,死亡現(xiàn)場離奇詭異,居然都是意外死亡愤钾,警方通過查閱死者的電腦和手機(jī)瘟滨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來能颁,“玉大人杂瘸,你說我怎么就攤上這事』锞眨” “怎么了败玉?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵敌土,是天一觀的道長。 經(jīng)常有香客問我运翼,道長返干,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任血淌,我火速辦了婚禮矩欠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘六剥。我一直安慰自己,他們只是感情好峰伙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布疗疟。 她就那樣靜靜地躺著,像睡著了一般瞳氓。 火紅的嫁衣襯著肌膚如雪策彤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天匣摘,我揣著相機(jī)與錄音店诗,去河邊找鬼。 笑死音榜,一個(gè)胖子當(dāng)著我的面吹牛庞瘸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赠叼,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼擦囊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了嘴办?” 一聲冷哼從身側(cè)響起瞬场,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎涧郊,沒想到半個(gè)月后贯被,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡妆艘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年彤灶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片批旺。...
    茶點(diǎn)故事閱讀 39,926評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡枢希,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出朱沃,到底是詐尸還是另有隱情苞轿,我是刑警寧澤茅诱,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站搬卒,受9級特大地震影響瑟俭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜契邀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一摆寄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坯门,春花似錦微饥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至现恼,卻和暖如春肃续,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叉袍。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工始锚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人喳逛。 一個(gè)月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓瞧捌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親润文。 傳聞我的和親對象是個(gè)殘疾皇子察郁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 網(wǎng)絡(luò)編程 一.楔子 你現(xiàn)在已經(jīng)學(xué)會(huì)了寫python代碼皮钠,假如你寫了兩個(gè)python文件a.py和b.py,分別去運(yùn)...
    go以恒閱讀 2,016評論 0 6
  • 說明 本文 翻譯自 realpython 網(wǎng)站上的文章教程 Socket Programming in Pytho...
    keelii閱讀 2,121評論 0 16
  • 目錄一赠法、socket是什么麦轰,socket和HTTP的區(qū)別二、如何建立一個(gè)socket連接三砖织、使用CocoaAsyn...
    意一ineyee閱讀 1,647評論 0 13
  • 計(jì)算機(jī)網(wǎng)絡(luò)概述 網(wǎng)絡(luò)編程的實(shí)質(zhì)就是兩個(gè)(或多個(gè))設(shè)備(例如計(jì)算機(jī))之間的數(shù)據(jù)傳輸款侵。 按照計(jì)算機(jī)網(wǎng)絡(luò)的定義,通過一定...
    蛋炒飯_By閱讀 1,224評論 0 10
  • ES6(ECMAScript2015)的出現(xiàn)侧纯,無疑給前端開發(fā)人員帶來了新的驚喜新锈,它包含了一些很棒的新特性,可以更加...
    c蓋世閱讀 158評論 0 1