一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

腳本之家,腳本語言編程技術及教程分享平臺!
分類導航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服務器之家 - 腳本之家 - Golang - Golang TCP粘包拆包問題的解決方法

Golang TCP粘包拆包問題的解決方法

2020-05-26 10:37短路的鮑勃 Golang

這篇文章主要給大家介紹了Golang TCP粘包拆包問題的解決方法,文中通過示例代碼介紹的非常詳細,對大家學習或者使用Golang具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧

什么是粘包問題

最近在使用Golang編寫Socket層,發現有時候接收端會一次讀到多個數據包的問題。于是通過查閱資料,發現這個就是傳說中的TCP粘包問題。下面通過編寫代碼來重現這個問題:

服務端代碼 server/main.go

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func main() {
    l, err := net.Listen("tcp", ":4044")
    if err != nil {
        panic(err)
    }
    fmt.Println("listen to 4044")
    for {
  // 監聽到新的連接,創建新的 goroutine 交給 handleConn函數 處理
        conn, err := l.Accept()
        if err != nil {
            fmt.Println("conn err:", err)
        } else {
            go handleConn(conn)
        }
    }
}
 
func handleConn(conn net.Conn) {
    defer conn.Close()
    defer fmt.Println("關閉")
    fmt.Println("新連接:", conn.RemoteAddr())
 
    result := bytes.NewBuffer(nil)
    var buf [1024]byte
    for {
        n, err := conn.Read(buf[0:])
        result.Write(buf[0:n])
        if err != nil {
            if err == io.EOF {
                continue
            } else {
                fmt.Println("read err:", err)
                break
            }
        } else {
            fmt.Println("recv:", result.String())
        }
        result.Reset()
    }
}

客戶端代碼 client/main.go

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
    data := []byte("[這里才是一個完整的數據包]")
    conn, err := net.DialTimeout("tcp", "localhost:4044", time.Second*30)
    if err != nil {
        fmt.Printf("connect failed, err : %v\n", err.Error())
  return
    }
    for i := 0; i <1000; i++ {
        _, err = conn.Write(data)
        if err != nil {
            fmt.Printf("write failed , err : %v\n", err)
            break
        }
    }
}

運行結果

listen to 4044
新連接: [::1]:53079
recv: [這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據?
recv: ][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包][這里才是一個完整的數據包][這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包]
...省略其它的...

從服務端的控制臺輸出可以看出,存在三種類型的輸出:

  1. 一種是正常的一個數據包輸出。
  2. 一種是多個數據包“粘”在了一起,我們定義這種讀到的包為粘包。
  3. 一種是一個數據包被“拆”開,形成一個破碎的包,我們定義這種包為半包。

為什么會出現半包和粘包?

  • 客戶端一段時間內發送包的速度太多,服務端沒有全部處理完。于是數據就會積壓起來,產生粘包。
  • 定義的讀的buffer不夠大,而數據包太大或者由于粘包產生,服務端不能一次全部讀完,產生半包。

什么時候需要考慮處理半包和粘包?

TCP連接是長連接,即一次連接多次發送數據。
每次發送的數據是結構的,比如 JSON格式的數據 或者 數據包的協議是由我們自己定義的(包頭部包含實際數據長度、協議魔數等)。

解決思路

  1. 定長分隔(每個數據包最大為該長度,不足時使用特殊字符填充) ,但是數據不足時會浪費傳輸資源
  2. 使用特定字符來分割數據包,但是若數據中含有分割字符則會出現Bug
  3. 在數據包中添加長度字段,彌補了以上兩種思路的不足,推薦使用

拆包演示

通過上述分析,我們最好通過第三種思路來解決拆包粘包問題。

Golang的bufio庫中有為我們提供了Scanner,來解決這類分割數據的問題。

type Scanner
Scanner provides a convenient interface for reading data such as a file of newline-delimited lines of text. Successive calls to the Scan method will step through the 'tokens' of a file, skipping the bytes between the tokens. The specification of a token is defined by a split function of type SplitFunc; the default split function breaks the input into lines with line termination stripped. Split functions are defined in this package for scanning a file into lines, bytes, UTF-8-encoded runes, and space-delimited words. The client may instead provide a custom split function.

簡單來講即是:

Scanner為 讀取數據 提供了方便的 接口。連續調用Scan方法會逐個得到文件的“tokens”,跳過 tokens 之間的字節。token 的規范由 SplitFunc 類型的函數定義。我們可以改為提供自定義拆分功能。

接下來看看 SplitFunc 類型的函數是什么樣子的:

?
1
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

Golang官網文檔上提供的使用例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
    // An artificial input source.
    const input = "1234 5678 1234567901234567890"
    scanner := bufio.NewScanner(strings.NewReader(input))
    // Create a custom split function by wrapping the existing ScanWords function.
    split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        advance, token, err = bufio.ScanWords(data, atEOF)
        if err == nil && token != nil {
            _, err = strconv.ParseInt(string(token), 10, 32)
        }
        return
    }
    // Set the split function for the scanning operation.
    scanner.Split(split)
    // Validate the input
    for scanner.Scan() {
        fmt.Printf("%s\n", scanner.Text())
    }
 
    if err := scanner.Err(); err != nil {
        fmt.Printf("Invalid input: %s", err)
    }
}

于是,我們可以這樣改寫我們的程序:

服務端代碼 server/main.go

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
func main() {
    l, err := net.Listen("tcp", ":4044")
    if err != nil {
        panic(err)
    }
    fmt.Println("listen to 4044")
    for {
        conn, err := l.Accept()
        if err != nil {
            fmt.Println("conn err:", err)
        } else {
            go handleConn2(conn)
        }
    }
}
 
func packetSlitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
  // 檢查 atEOF 參數 和 數據包頭部的四個字節是否 為 0x123456(我們定義的協議的魔數)
    if !atEOF && len(data) > 6 && binary.BigEndian.Uint32(data[:4]) == 0x123456 {
        var l int16
    // 讀出 數據包中 實際數據 的長度(大小為 0 ~ 2^16)
        binary.Read(bytes.NewReader(data[4:6]), binary.BigEndian, &l)
        pl := int(l) + 6
        if pl <= len(data) {
            return pl, data[:pl], nil
        }
    }
    return
}
 
func handleConn2(conn net.Conn) {
    defer conn.Close()
    defer fmt.Println("關閉")
    fmt.Println("新連接:", conn.RemoteAddr())
    result := bytes.NewBuffer(nil)
  var buf [65542]byte // 由于 標識數據包長度 的只有兩個字節 故數據包最大為 2^16+4(魔數)+2(長度標識)
    for {
        n, err := conn.Read(buf[0:])
        result.Write(buf[0:n])
        if err != nil {
            if err == io.EOF {
                continue
            } else {
                fmt.Println("read err:", err)
                break
            }
        } else {
            scanner := bufio.NewScanner(result)
            scanner.Split(packetSlitFunc)
            for scanner.Scan() {
                fmt.Println("recv:", string(scanner.Bytes()[6:]))
            }
        }
        result.Reset()
    }
}

客戶端代碼 client/main.go

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
func main() {
    l, err := net.Listen("tcp", ":4044")
    if err != nil {
        panic(err)
    }
    fmt.Println("listen to 4044")
    for {
        conn, err := l.Accept()
        if err != nil {
            fmt.Println("conn err:", err)
        } else {
            go handleConn2(conn)
        }
    }
}
 
func packetSlitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
  // 檢查 atEOF 參數 和 數據包頭部的四個字節是否 為 0x123456(我們定義的協議的魔數)
    if !atEOF && len(data) > 6 && binary.BigEndian.Uint32(data[:4]) == 0x123456 {
        var l int16
    // 讀出 數據包中 實際數據 的長度(大小為 0 ~ 2^16)
        binary.Read(bytes.NewReader(data[4:6]), binary.BigEndian, &l)
        pl := int(l) + 6
        if pl <= len(data) {
            return pl, data[:pl], nil
        }
    }
    return
}
 
func handleConn2(conn net.Conn) {
    defer conn.Close()
    defer fmt.Println("關閉")
    fmt.Println("新連接:", conn.RemoteAddr())
    result := bytes.NewBuffer(nil)
  var buf [65542]byte // 由于 標識數據包長度 的只有兩個字節 故數據包最大為 2^16+4(魔數)+2(長度標識)
    for {
        n, err := conn.Read(buf[0:])
        result.Write(buf[0:n])
        if err != nil {
            if err == io.EOF {
                continue
            } else {
                fmt.Println("read err:", err)
                break
            }
        } else {
            scanner := bufio.NewScanner(result)
            scanner.Split(packetSlitFunc)
            for scanner.Scan() {
                fmt.Println("recv:", string(scanner.Bytes()[6:]))
            }
        }
        result.Reset()
    }
}

運行結果

listen to 4044
新連接: [::1]:55738
recv: [這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包]
recv: [這里才是一個完整的數據包]
...省略其它的...

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對服務器之家的支持。

原文鏈接:https://juejin.im/post/5d220f7b6fb9a07ec7553da4

延伸 · 閱讀

精彩推薦
  • Golanggolang的httpserver優雅重啟方法詳解

    golang的httpserver優雅重啟方法詳解

    這篇文章主要給大家介紹了關于golang的httpserver優雅重啟的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,...

    helight2992020-05-14
  • Golanggolang json.Marshal 特殊html字符被轉義的解決方法

    golang json.Marshal 特殊html字符被轉義的解決方法

    今天小編就為大家分享一篇golang json.Marshal 特殊html字符被轉義的解決方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧 ...

    李浩的life12792020-05-27
  • Golanggo語言制作端口掃描器

    go語言制作端口掃描器

    本文給大家分享的是使用go語言編寫的TCP端口掃描器,可以選擇IP范圍,掃描的端口,以及多線程,有需要的小伙伴可以參考下。 ...

    腳本之家3642020-04-25
  • GolangGolang通脈之數據類型詳情

    Golang通脈之數據類型詳情

    這篇文章主要介紹了Golang通脈之數據類型,在編程語言中標識符就是定義的具有某種意義的詞,比如變量名、常量名、函數名等等,Go語言中標識符允許由...

    4272021-11-24
  • Golanggolang 通過ssh代理連接mysql的操作

    golang 通過ssh代理連接mysql的操作

    這篇文章主要介紹了golang 通過ssh代理連接mysql的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧...

    a165861639710342021-03-08
  • GolangGolang中Bit數組的實現方式

    Golang中Bit數組的實現方式

    這篇文章主要介紹了Golang中Bit數組的實現方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧...

    天易獨尊11682021-06-09
  • Golanggolang如何使用struct的tag屬性的詳細介紹

    golang如何使用struct的tag屬性的詳細介紹

    這篇文章主要介紹了golang如何使用struct的tag屬性的詳細介紹,從例子說起,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看...

    Go語言中文網11352020-05-21
  • Golanggo日志系統logrus顯示文件和行號的操作

    go日志系統logrus顯示文件和行號的操作

    這篇文章主要介紹了go日志系統logrus顯示文件和行號的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧...

    SmallQinYan12302021-02-02
主站蜘蛛池模板: 日韩高清一区二区三区不卡 | 亚洲精品无码不卡在线观看 | 日韩欧美在线视频一区二区 | 欧美一区a| 亚洲国产在线视频精品 | 女人又色又爽又黄 | 美女曰逼视频 | 激情三级做爰在线观看激情 | 国产一区二区不卡 | 欧美一区二区三区四区在线观看 | 3d欧美人禽交 | 久久99热在线观看7 久久99精品涩AV毛片观看 | 亚洲国产无线码在线观看 | 天天综合色天天综合网 | 99免费在线视频 | 欧美一区二区视频 | 日本欧美大码a在线视频播放 | 国产精品久久久久久久久 | 瘦老汉gay | 成人涩涩屋福利视频 | 继攵催眠女乱h调教 | 亚洲色大成网站www久久九九 | 久久sese | 蜜桃传媒在线 | 天堂成人在线视频 | 4hc44四虎www在线影院男同 | 99热最新 | 国产福利视频一区二区微拍视频 | 特黄特色大片免费影院 | 俄罗斯美女破苞 | 久久久久琪琪精品色 | 女海盗斯蒂内塔的复仇2免费观看 | 日本大学生xxxxx69泡妞 | 好大好硬好深好爽想要小雪 | aⅴ天堂小视频 | 久热人人综合人人九九精品视频 | 亚洲高清视频在线 | 91精品国产高清久久久久久io | 大胆私拍模特国模377 | 福利国模私拍视频在线观看 | 香蕉在线精品亚洲第一区 |