GO語言網絡編程


socket編程

Socket是BSD UNIX的進程通信機制,通常也稱作”套接字”,用於描述IP地址和端口,是一個通信鏈的句柄。Socket可以理解為TCP/IP網絡的API,它定義了許多函數或例程,程序員可以用它們來開發TCP/IP網絡上的應用程序。電腦上運行的應用程序通常通過”套接字”向網絡發出請求或者應答網絡請求。
Socket是應用層與TCP/IP協議族通信的中間軟件抽象層。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket后面,對用戶來說只需要調用Socket規定的相關函數,讓Socket去組織符合指定的協議數據然后進行通信。

GO語言實現TCP通信

TCP協議

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

TCP服務端

一個TCP服務端可以同時連接很多個客戶端,Go語言中創建多個goroutine實現並發非常方便和高效,所以可以每建立一次鏈接就創建一個goroutine去處理。
TCP服務端程序的處理流程:

  • 監聽端口
  • 接收客戶端請求建立鏈接
  • 創建goroutine處理鏈接
    TCP服務端:
//TCP server端

func process(conn net.Conn)  {
    defer conn.Close()  //關閉連接
    for {
        reader := bufio.NewReader(conn)
        var buf [128]byte
        n,err := reader.Read(buf[:])    //讀取數據
        if err != nil{
            fmt.Println("連接客戶端失敗,錯誤信息:",err)
        }
        recvStr := string(buf[:n])
        fmt.Println("收到客戶端信息:",recvStr)
        conn.Write([]byte(recvStr)) //發送數據
    }
}
func main()  {
    listen,err := net.Listen("tcp","127.0.0.1:8888")
    if err != nil{
        fmt.Println("監聽失敗,錯誤:",err)
        return
    }
    for {
        conn,err := listen.Accept() //建立連接
        if err!= nil{
            fmt.Println("建立連接失敗,錯誤:",err)
            continue
        }
        go process(conn)    //啟動一個goroutine處理連接
    }
}

TCP客戶端

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

  • 建立與服務端的鏈接
  • 進行數據收發
  • 關閉鏈接

TCP客戶端:

//客戶端

func main()  {
    conn ,err := net.Dial("tcp","127.0.0.1:8888")
    if err != nil {
        fmt.Println("連接失敗,錯誤:",err)
        return
    }
    defer conn.Close()
    inputReader := bufio.NewReader(os.Stdout)
    for {
        input, _ := inputReader.ReadString('\n')    //讀取用戶輸入
        inputInfo := strings.Trim(input,"\r\n")
        if strings.ToUpper(inputInfo) == "q"{
            return  //如果輸入q就退出
        }
        _,err = conn.Write([]byte(inputInfo))   //發送數據
        if err != nil{
            return 
        }
        buf := [512]byte{}
        n,err := conn.Read(buf[:])
        if err != nil{
            fmt.Println("接受失敗,錯誤:",err)
            return 
        }
        fmt.Println(string(buf[:n]))
    }
}

先啟動server,后啟動client:

$go run main.go
我是客戶端
我是客戶端
$go run main.go
收到客戶端信息: 我是客戶端

GO語言實現UDP通信

UDp協議

UDP協議(User Datagram Protocol)中文名稱是用戶數據報協議,是OSI(Open System Interconnection,開放式系統互聯)參考模型中一種無連接的傳輸層協議,不需要建立連接就能直接進行數據發送和接收,屬於不可靠的、沒有時序的通信,但是UDP協議的實時性比較好,通常用於視頻直播相關領域。

UDP服務端

//服務端
func main()  {
    listen,err := net.ListenUDP("udp",&net.UDPAddr{
        IP:net.IPv4(0,0,0,0),
        Port:8888,
    })
    if err != nil{
        fmt.Println("監聽失敗,錯誤:",err)
        return
    }
    defer listen.Close()
    for {
        var data [1024]byte
        n,addr,err := listen.ReadFromUDP(data[:])
        if err != nil{
            fmt.Println("接收udp數據失敗,錯誤:",err)
            continue
        }
        fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
        _ ,err = listen.WriteToUDP(data[:n],addr)   //發送數據
        if err != nil{
            fmt.Println("發送數據失敗,錯誤:",err)
            continue
        }
    }
}

UDP客戶端

//客戶端
func main()  {
    socket,err := net.DialUDP("udp",nil,&net.UDPAddr{
        IP:net.IPv4(0,0,0,0),
        Port:8888,
    })
    if err != nil{
        fmt.Println("連接服務器失敗,錯誤:",err)
        return
    }
    defer socket.Close()
    sendData := []byte("hello world!")
    _,err = socket.Write(sendData)
    if err != nil{
        fmt.Println("發送數據失敗,錯誤:",err)
        return
    }
    data := make([]byte,4096)
    n,remoteAddr,err := socket.ReadFromUDP(data)
    if err != nil{
        fmt.Println("接受數據失敗,錯誤:",err)
        return
    }
    fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}

先啟動server,后啟動client:

$go run main.go
recv:hello world! addr:127.0.0.1:8888 count:12
$go run main.go
data:hello world! addr:127.0.0.1:51222 count:12

HTTP客戶端和服務端

HTTP協議

超文本傳輸協議(HTTP,HyperText Transfer Protocol)是互聯網上應用最為廣泛的一種網絡傳輸協議,所有的WWW文件都必須遵守這個標准。設計HTTP最初的目的是為了提供一種發布和接收HTML頁面的方法。

HTTP服務端

net/http包是對net包的進一步封裝,專門用來處理HTTP協議的數據。

// http server
func sayHi(w http.ResponseWriter,r *http.Request)  {
    fmt.Fprintln(w,"你好,ares!")
}
func main()  {
    http.HandleFunc("/",sayHi)
    err := http.ListenAndServe(":8888",nil)
    if err != nil{
        fmt.Println("Http 服務建立失敗,err:",err)
        return
    }
}

HTTP客戶端

func main() {
    resp, err := http.Get("https://www.baidu.com/")
    if err != nil {
        fmt.Println("get failed, err:", err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Printf("%T\n",body)
    fmt.Println(string(body))
}

執行之后就能在終端輸出www.baidu.com網站首頁的內容了。

TCP粘包

粘包服務端

//粘包
func process(conn net.Conn)  {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    var buf [1024]byte
    for {
        n,err := reader.Read(buf[:])
        if err == io.EOF{
            break
        }
        if err != nil{
            fmt.Println("讀取客戶端失敗,err",err)
            break
        }
        recvStr := string(buf[:n])
        fmt.Println("收到client發來的數據:",recvStr)
    }
}
func main()  {
    listen,err := net.Listen("tcp","127.0.0.1:8888")
    if err != nil{
        fmt.Println("監聽失敗,err",err)
        return
    }
    defer listen.Close()
    for {
        conn,err := listen.Accept()
        if err != nil{
            fmt.Println("接受失敗,err",err)
            continue
        }
        go process(conn)
    }
}

粘包客戶端

func main()  {
    conn,err := net.Dial("tcp","127.0.0.1:8888")
    if err != nil{
        fmt.Println("連接失敗,err",err)
        return
    }
    defer conn.Close()
    for i:=0;i<20;i++{
        msg := "Ares is a bird!"
        conn.Write([]byte(msg))
    }
}

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

$go run main.go
收到client發來的數據: Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!
收到client發來的數據: Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!

客戶端分10次發送的數據,在服務端並沒有成功的輸出10次,而是多條數據“粘”到了一起。

TCP為什么會出現粘包

在socket網絡程序中,TCP和UDP分別是面向連接和非面向連接的。因此TCP的socket編程,收發兩端(客戶端和服務器端)都要有成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小、數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。

對於UDP,不會使用塊的合並優化算法,這樣,實際上目前認為,是由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。所以UDP不會出現粘包問題。

粘包產生原因

1發送端需要等緩沖區滿才發送出去,造成粘包
2接收方不及時接收緩沖區的包,造成多個包接收
具體點:
(1)發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一包數據。若連續幾次發送的數據都很少,通常TCP會根據優化算法把這些數據合成一包后一次發送出去,這樣接收方就收到了粘包數據。

(2)接收方引起的粘包是由於接收方用戶進程不及時接收數據,從而導致粘包現象。這是因為接收方先把收到的數據放在系統接收緩沖區,用戶進程從該緩沖區取數據,若下一包數據到達時前一包數據尚未被用戶進程取走,則下一包數據放到系統接收緩沖區時就接到前一包數據之后,而用戶進程根據預先設定的緩沖區大小從系統接收緩沖區取數據,這樣就一次取到了多包數據。
參考:TCP通信粘包問題分析和解決

解決辦法

出現”粘包”的關鍵在於接收方不確定將要傳輸的數據包的大小,因此我們可以對數據包進行封包和拆包的操作。
自定義一個協議,比如數據包的前4個字節為包頭,里面存儲的是發送的數據的長度。

// Encode 將消息編碼
func Encode(message string)([]byte ,error)  {
    // 讀取消息的長度,轉換成int32類型(占4個字節)
    var length = int32(len(message))
    var pkg = new(bytes.Buffer)
    //寫入消息頭
    err := binary.Write(pkg,binary.LittleEndian,length)
    if err != nil{
        return nil,err
    }
    //寫入消息實體
    err = binary.Write(pkg,binary.LittleEndian,[]byte(message))
    if err != nil{
        return nil,err
    }
    return pkg.Bytes(),nil
}

// Decode 消息解碼
func Decode(reader *bufio.Reader)(string,error)  {
    //讀取消息長度
    lengthByte,_ := reader.Peek(4) //讀取前4個字節數據
    lengthBuff := bytes.NewBuffer(lengthByte)
    var length int32
    err := binary.Read(lengthBuff,binary.LittleEndian,&length)
    if err != nil{
        return "",err
    }
    // Buffered返回緩沖中現有的可讀取的字節數。
    if int32(reader.Buffered()) < length+4{
        return "",err
    }
    //讀取真正的消息數據
    pack := make([]byte,int(4+length))
    _,err = reader.Read(pack)
    if err != nil{
        return "",err
    }
    return string(pack[4:]),nil
}

server端:

func process(conn net.Conn)  {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    for {
        msg,err := proto.Decode(reader)
        if err == io.EOF{
            return
        }
        if err != nil{
            fmt.Println("decode 失敗,err",err)
            return
        }
        fmt.Println("收到client數據:",msg)
    }
}
func main()  {
    listen,err := net.Listen("tcp","127.0.0.1:8888")
    if err != nil{
        fmt.Println("監聽失敗,err",err)
        return
    }
    defer listen.Close()
    for {
        conn,err := listen.Accept()
        if err != nil{
            fmt.Println("接受失敗,err",err)
            continue
        }
        go process(conn)
    }
}

client端:

func main()  {
    conn,err := net.Dial("tcp","127.0.0.1:8888")
    if err != nil{
        fmt.Println("dial失敗,err",err)
        return
    }
    defer conn.Close()
    for i:=0;i<20;i++{
        msg := "Hello Ares!"
        data,err := proto.Encode(msg)
        if err != nil{
            fmt.Println("encode失敗,err",err)
            return
        }
        conn.Write(data)
    }
}

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

go run main.go
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!
收到client數據: Hello Ares!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM