【go語言學習】網(wǎng)絡編程之TCP

一、go語言實現(xiàn)TCP通信

TCP/IP(Transmission Control Protocol/Internet Protocol) 即傳輸控制協(xié)議/網(wǎng)間協(xié)議,是一種面向連接(連接導向)的、可靠的、基于字節(jié)流的傳輸層(Transport layer)通信協(xié)議,因為是面向連接的協(xié)議,數(shù)據(jù)像水流一樣傳輸,會存在黏包問題。

TCP通信的實現(xiàn):

1、socket 編程是對 tcp 通訊過程的封裝,unix server 端網(wǎng)絡編程過程為 Server->Bind->Listen->Accept,go 中直接使用 Listen + Accept
2、client 與客戶端建立好的請求可以被新建的 goroutine(go func) 處理 named connHandler
3、goroutine 的處理過程其實是輸入流/輸出流的應用場景

下面以一個簡單的需求來實現(xiàn)go語言的TCP通信:

socket編程實現(xiàn)客戶端client服務端server進行通訊,通訊測試場景:
1、client 發(fā)送 hello, server 返回 world
2、client 發(fā)送 你好, server 返回 世界
3、其余client發(fā)送內(nèi)容, server 回顯即可
4、client 發(fā)送 exit,客戶端退出

二、TCP服務端

TCP服務端程序的處理流程:

1、監(jiān)聽端口
2、接收客戶端請求建立鏈接
3、創(chuàng)建goroutine處理鏈接

我們使用Go語言的net包實現(xiàn)的TCP服務端代碼如下:

// tcp\server\main.go
package main

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

func main() {
    // 1.建立服務,監(jiān)聽端口
    listener, err := net.Listen("tcp", "127.0.0.1:3000")
    if err != nil {
        fmt.Println("net listen failed err: ", err)
        return
    }
    // 打印一句提示信息
    fmt.Println("listen at 127.0.0.1:3000")
    for {
        // 2.接收來自client的連接,一直阻塞直到有連接
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("listener accept failed err: ", err)
            return
        }
        // 3.起一個協(xié)程,處理來自客戶端的連接(收發(fā)數(shù)據(jù))
        go sConnHandler(conn)
    }
}

// 服務端處理連接函數(shù)
func sConnHandler(c net.Conn) {
    // 關閉連接
    defer c.Close()
    // 1.判斷conn是否有效
    if c == nil {
        fmt.Println("無效的連接。")
        return
    }
    // 2.存儲接收到的數(shù)據(jù)
    buf := make([]byte, 1024*4)
    // 3.循環(huán)讀取客戶端發(fā)送的數(shù)據(jù)
    for {
        // 3.1 客戶端發(fā)送的數(shù)據(jù)讀入buf
        cn, err := c.Read(buf)
        // 3.2 數(shù)據(jù)讀盡,發(fā)生錯誤 關閉連接
        if err != nil {
            return
        }
        // 3.3 根據(jù)接收的數(shù)據(jù),進行邏輯處理
        // 3.3.1 buf數(shù)據(jù)去除兩端空格
        inStr := strings.TrimSpace(string(buf[:cn]))
        fmt.Printf("來自%v客戶端輸入:%v\n", c.RemoteAddr(), inStr)
        // 3.3.2 switch選擇結(jié)構(gòu)處理
        switch inStr {
        case "hello":
            c.Write([]byte("world"))
        case "你好":
            c.Write([]byte("世界"))
        default:
            c.Write([]byte(inStr))
        }
    }
}

將上面的代碼保存之后編譯成server或server.exe可執(zhí)行文件。

三、TCP客戶端

一個TCP客戶端進行TCP通信的流程如下:

1、建立與服務端的鏈接
2、進行數(shù)據(jù)收發(fā)
3、關閉鏈接

使用Go語言的net包實現(xiàn)的TCP客戶端代碼如下:

// tcp\client\main.go
package main

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

func main() {
    // 1.客戶端發(fā)起連接
    conn, err := net.Dial("tcp", "127.0.0.1:3000")
    if err != nil {
        fmt.Println("net dial failed err: ", err)
        return
    }
    // 打印一句提示信息
    fmt.Println("dial 127.0.0.1:3000 success")
    // 2.處理連接
    cConnHandler(conn)
}

// 客戶端處理連接函數(shù)
func cConnHandler(c net.Conn) {
    // 關閉連接
    defer c.Close()
    // 1.接收控制臺輸入
    reader := bufio.NewReader(os.Stdin)
    // 2.緩存conn中的數(shù)據(jù)
    buf := make([]byte, 1024*4)
    //3.循環(huán)讀寫
    for {
        // 3.1 客戶端輸入
    label:
        input, _ := reader.ReadString('\n')
        // 去除兩端空格
        input = strings.TrimSpace(input)
        // 處理無效輸入
        if input == "" {
            fmt.Println("信息無效,請重新輸入")
            goto label
        }
        // 3.2 輸入exit就斷開連接
        if strings.ToLower(input) == "exit" {
            fmt.Println("客戶端斷開連接")
            return
        }
        // 3.3 發(fā)送數(shù)據(jù):客戶端數(shù)據(jù)寫入conn并傳輸
        _, err := c.Write([]byte(input))
        if err != nil {
            fmt.Println("c write failed err: ", err)
            return
        }
        // 3.4 接收數(shù)據(jù):接收服務端返回的數(shù)據(jù)存入buf
        n, err := c.Read(buf)
        if err != nil {
            fmt.Println("c read failed err: ", err)
            return
        }
        // 3.5 顯示服務端回傳的數(shù)據(jù)
        fmt.Println("服務端返回:", string(buf[:n]))
    }
}

將上面的代碼編譯成client或client.exe可執(zhí)行文件,先啟動server端再啟動client端,在client端輸入任意內(nèi)容回車之后就能夠在server端看到client端發(fā)送的數(shù)據(jù),并能夠在client端顯示server端返回的數(shù)據(jù),從而實現(xiàn)TCP通信。

四、TCP粘包問題

1、粘包的定義:
  • 粘包是指網(wǎng)絡通信中,發(fā)送方發(fā)送的多個數(shù)據(jù)包在接收方的緩沖區(qū)粘在一起,多個數(shù)據(jù)包首尾相連的現(xiàn)象。
  • 例如,基于tcp的socket實現(xiàn)的客戶端向服務端上傳文件時,內(nèi)容往往是按照一段一段的字節(jié)流發(fā)送的,如果不做任何處理,從接收方來看,根本不知道該文件的字節(jié)流從何處開始,在何處結(jié)束。
  • 因此,所謂粘包問題主要是因為接收方不知道消息的邊界,不知道一次提取多少個字節(jié)的數(shù)據(jù)造成的。
2、產(chǎn)生原因

粘包可發(fā)生在發(fā)送端也可發(fā)生在接收端:

  • 由Nagle算法造成的發(fā)送端的粘包:Nagle算法是一種改善網(wǎng)絡傳輸效率的算法。簡單來說就是當我們提交一段數(shù)據(jù)給TCP發(fā)送時,TCP并不立刻發(fā)送此段數(shù)據(jù),而是等待一小段時間看看在等待期間是否還有要發(fā)送的數(shù)據(jù),若有則會一次把這兩段數(shù)據(jù)發(fā)送出去。
  • 接收端接收不及時造成的接收端粘包:TCP會把接收到的數(shù)據(jù)存在自己的緩沖區(qū)中,然后通知應用層取數(shù)據(jù)。當應用層由于某些原因不能及時的把TCP的數(shù)據(jù)取出來,就會造成TCP緩沖區(qū)中存放了幾段數(shù)據(jù)。

UDP協(xié)議不會出現(xiàn)粘包:因為UDP是無連接,面向消息,提供高效服務的。無連接意味著當有數(shù)據(jù)要發(fā)送時,UDP會立即發(fā)送,數(shù)據(jù)包不會積壓;面向消息意味著數(shù)據(jù)包一般很小,因此接收端處理也不會很耗時,一般不會由于接收端來不及處理消息而造成粘包。最重要的是,UDP不使用合并優(yōu)化算法,每個消息都有單獨的包頭,即使出現(xiàn)很短時間內(nèi)收到多個數(shù)據(jù)包的情況,接收方也能根據(jù)包頭信息區(qū)分數(shù)據(jù)的邊界。因此,UDP不會出現(xiàn)粘包,只可能會出現(xiàn)丟包。

粘包示例代碼:
服務端代碼:

// tcp\server\main.go

package main

import (
    "fmt"
    "io"
    "net"
)

func main() {
    // 1.建立服務,監(jiān)聽端口
    listener, err := net.Listen("tcp", "127.0.0.1:3000")
    if err != nil {
        fmt.Println("net listen failed, err:", err)
        return
    }
    // 打印一句提示信息
    fmt.Println("服務器建立成功,監(jiān)聽端口:127.0.0.1:3000")
    for {
        // 2.監(jiān)聽來自客戶端的連接
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("listener accept failed, err:", err)
            return
        }
        // 打印一句提示信息
        fmt.Printf("監(jiān)聽到來自%v的連接\n", conn.RemoteAddr())
        // 3.起一個協(xié)程,處理該連接,收發(fā)數(shù)據(jù)
        go sConnHandler(conn)
    }
}

// sConnHandler 服務端處理連接
func sConnHandler(c net.Conn) {
    if c == nil {
        fmt.Println("無效的連接")
        return
    }
    for {
        data := make([]byte, 1024)
        n, err := c.Read(data[:])
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("c read failed, err:", err)
            break
        }
        fmt.Println("來自客戶端的消息:", string(data[:n]))
    }
}

客戶端代碼

// tcp\client\
package main

import (
    "fmt"
    "net"
)

func main() {
    // 1.與服務端建立連接
    conn, err := net.Dial("tcp", "127.0.0.1:3000")
    if err != nil {
        fmt.Println("net dial failed, err:", err)
        return
    }
    // 打印一句提示信息
    fmt.Println("與服務器連接成功,端口號為:127.0.0.1:3000")
    // 2.處理連接
    cConnHandler(conn)
}

func cConnHandler(c net.Conn) {
    defer c.Close()
    for i := 0; i < 20; i++ {
        msg := "人生苦短,let`s go"
        _, err := c.Write([]byte(msg))
        if err != nil {
            fmt.Println("c write failed, err:", err)
            return
        }
    }
}

先啟動服務端再啟動客戶端,可以看到服務端輸出結(jié)果如下:

服務器建立成功,監(jiān)聽端口:127.0.0.1:3000
監(jiān)聽到來自127.0.0.1:1547的連接
來自客戶端的消息: 人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短 
,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
3、粘包的解決

出現(xiàn)”粘包”的關鍵在于接收方不確定將要傳輸?shù)臄?shù)據(jù)包的大小,因此我們可以對數(shù)據(jù)包進行封包和拆包的操作。

封包:封包就是給一段數(shù)據(jù)加上包頭,這樣一來數(shù)據(jù)包就分為包頭和包體兩部分內(nèi)容了(過濾非法包時封包會加入”包尾”內(nèi)容)。包頭部分的長度是固定的,并且它存儲了包體的長度,根據(jù)包頭長度固定以及包頭中含有包體長度的變量就能正確的拆分出一個完整的數(shù)據(jù)包。

我們可以自己定義一個協(xié)議,比如數(shù)據(jù)包的前4個字節(jié)為包頭,里面存儲的是發(fā)送的數(shù)據(jù)的長度。

// tcp\proto\proto.go

package proto

import (
    "bufio"
    "bytes"
    "encoding/binary"
    "errors"
    "fmt"
)

// 這是一個自定義協(xié)議包,里面提供了兩個工具函數(shù):Encode和Decode
// 這兩個函數(shù)用于對收發(fā)數(shù)據(jù)進行編解碼處理
// 每次發(fā)送的數(shù)據(jù)包前4個字節(jié)用來記錄數(shù)據(jù)的長度,后面才是記錄數(shù)據(jù)
// 這樣一方面可以準確獲知應該讀取的長度,并且可以防止粘包現(xiàn)象的發(fā)生

// Encode 編碼
func Encode(msg string) ([]byte, error) {
    // 1.獲取消息長度,轉(zhuǎn)換成int32類型(占4個字節(jié))
    msgLength := int32(len(msg))
    // 2.創(chuàng)建一個數(shù)據(jù)包,用于存放數(shù)據(jù)
    dataBuf := bytes.NewBuffer([]byte{})
    // 3.將消息長度寫入消息頭
    err := binary.Write(dataBuf, binary.BigEndian, msgLength)
    if err != nil {
        fmt.Println("binary write failed, err: ", err)
    }
    // 4.將消息內(nèi)容寫入dataBuf
    err = binary.Write(dataBuf, binary.BigEndian, []byte(msg))
    if err != nil {
        fmt.Println("binary write failed, err: ", err)
        return nil, err
    }
    return dataBuf.Bytes(), nil
}

// Decode 解碼
func Decode(reader *bufio.Reader) (string, error) {
    // 1.讀取前4個字節(jié)的數(shù)據(jù),表示數(shù)據(jù)包長度的信息
    lengthData := make([]byte, 4)
    _, err := reader.Read(lengthData)
    if err != nil {
        fmt.Println("reader read failed, err:", err)
        return "", err
    }
    // 2.將前4個字節(jié)數(shù)據(jù)讀入字節(jié)緩沖區(qū)
    lengthBuf := bytes.NewBuffer(lengthData)
    // 3.讀取數(shù)據(jù)包長度
    var msgLength int32
    err = binary.Read(lengthBuf, binary.BigEndian, &msgLength)
    if err != nil {
        fmt.Println("binary read failed, err:", err)
        return "", nil
    }
    // 4.判斷數(shù)據(jù)包的長度是否合法
    if int32(reader.Buffered()) < msgLength {
        return "", errors.New("數(shù)據(jù)長度不合法")
    }
    // 5.讀取消息內(nèi)容
    msgData := make([]byte, int(msgLength))
    _, err = reader.Read(msgData)
    if err != nil {
        return "", nil
    }
    msgBuffer := bytes.NewBuffer(msgData)
    var msg string
    err = binary.Read(msgBuffer, binary.BigEndian, msgData)
    if err != nil {
        fmt.Println("binary read failed, err:", err)
        return "", err
    }
    msg = string(msgData)
    return msg, nil
}

接下來在服務端和客戶端分別使用上面定義的proto包的Decode和Encode函數(shù)處理數(shù)據(jù)。

服務端代碼如下:

// tcp\server\main.go

package main

import (
    "bufio"
    "fmt"
    "go_project/tcp/proto"
    "net"
)

func main() {
    // 1.建立服務,監(jiān)聽端口
    listener, err := net.Listen("tcp", "127.0.0.1:3000")
    if err != nil {
        fmt.Println("net listen failed, err:", err)
        return
    }
    // 打印一句提示信息
    fmt.Println("服務器建立成功,監(jiān)聽端口:127.0.0.1:3000")
    for {
        // 2.監(jiān)聽來自客戶端的連接
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("listener accept failed, err:", err)
            return
        }
        // 打印一句提示信息
        fmt.Printf("監(jiān)聽到來自%v的連接\n", conn.RemoteAddr())
        // 3.起一個協(xié)程,處理該連接,收發(fā)數(shù)據(jù)
        go sConnHandler(conn)
    }
}

// sConnHandler 服務端處理連接
func sConnHandler(c net.Conn) {
    if c == nil {
        fmt.Println("無效的連接")
        return
    }
    reader := bufio.NewReader(c)
    for {
        msg, err := proto.Decode(reader)
        if err != nil {
            fmt.Println("proto decode failed, err:", err)
            return
        }
        fmt.Println("來自客戶端的消息:", msg)
    }
}

客戶端代碼如下:

// tcp\client\main.go

package main

import (
    "fmt"
    "go_project/tcp/proto"
    "net"
)

func main() {
    // 1.與服務端建立連接
    conn, err := net.Dial("tcp", "127.0.0.1:3000")
    if err != nil {
        fmt.Println("net dial failed, err:", err)
        return
    }
    // 打印一句提示信息
    fmt.Println("與服務器連接成功,端口號為:127.0.0.1:3000")
    // 2.處理連接
    cConnHandler(conn)
}

func cConnHandler(c net.Conn) {
    defer c.Close()
    for i := 0; i < 20; i++ {
        msg := "人生苦短,let`s go"
        data, err := proto.Encode(msg)
        if err != nil {
            fmt.Println("proto encode failed, err:", err)
            return
        }
        _, err = c.Write(data)
        if err != nil {
            fmt.Println("c write failed, err:", err)
            return
        }
    }
}

先啟動服務端再啟動客戶端,可以看到服務端輸出結(jié)果如下:

服務器建立成功,監(jiān)聽端口:127.0.0.1:3000
監(jiān)聽到來自127.0.0.1:1367的連接
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
來自客戶端的消息: 人生苦短,let`s go
reader read failed, err: EOF
proto decode failed, err: EOF

參考文章
https://www.liwenzhou.com/posts/Go/15_socket/

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

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