GO語言網絡編程

socket編程

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

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頁面的方法。socket

HTTP服務端

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

// 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!
相關文章
相關標籤/搜索