go語言開發基礎45 - 之go語言socket編程(tcp、udp)


一、說明

1.1、什么是socket

Socket起源於Unix,而Unix基本哲學之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關閉close”模式來操作。Socket就是該模式的一個實現,網絡的Socket數據傳輸是一種特殊的I/O,Socket也是一種文件描述符。Socket也具有一個類似於打開文件的函數調用:Socket(),該函數返回一個整型的Socket描述符,隨后的連接建立、數據傳輸等操作都是通過該Socket實現的。

常用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數據報式Socket(SOCK_DGRAM)。流式是一種面向連接的Socket,針對於面向連接的TCP服務應用;數據報式Socket是一種無連接的Socket,對應於無連接的UDP服務應用。

 

1.2、socket種類

Socket有兩種:TCP Socket和UDP Socket,TCP和UDP是協議,而要確定一個進程的需要三元組,需要IP地址、協議和端口,具體看下面示例部分。

 

二、實戰

2.1、tcp socket實戰

(1) tcp socket 服務端實現

package main

import (
    "bufio"
    "fmt"
    "net"
)

func process(conn net.Conn) {
    // 5. 定義defer關閉連接
    defer conn.Close() 
    // 6.無限循環
    for {
        // 7.創建一個帶緩存的讀讀取對象,讀取tcp連接里的內容
        reader := bufio.NewReader(conn)
        // 8.定義一個128字節的數組
        var buf [128]byte
        // 9.調用從當前tcp連接里讀取內容的對象的Read方法將從tcp里讀取的數據存入buf[:]切片里,有兩個返回值,一個是讀取數據總數,另一個是報錯
        n, err := reader.Read(buf[:])
        if err != nil {    // 處理報錯
            fmt.Println("read data error:", err)
            break
        }
        // 10.打印切片里的內容(下標從0到讀取的總數)
        fmt.Printf(string(buf[0:n]))
        // 11.回復客戶端消息(發送的消息一定要是byte類型的切片)     
        conn.Write([]byte("Welcome to Golang")) 
    }

}


func main() {
    // 1.調用net包下的Listen()方法啟動tcp的端口監聽
    listen, err := net.Listen("tcp", "0.0.0.0:8088") 
    if err != nil {                                  // 判斷是否有錯誤
        fmt.Println("listen failed error", err)
        return
    }

    // 2.無限循環,處理請求
    for { 
        // 3.等待鏈接(沒有客戶端連接時在這里阻塞,有客戶端連接時處理連接)
        conn, err := listen.Accept()
        if err != nil {              // 判斷錯誤(打印錯誤,退出當前循環進入下次循環)
            fmt.Println("accept failed err", err)
            continue
        }
        // 4.啟動一個攜程處理連接
        go process(conn) // 5.啟用一個goroutine處理當前的連接
    }

}

(2) tcp socket 客戶端實現

package main

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


func main() {
    // 1.調用net.Dial()方法以tcp協議的方式連接localhost的8088端口
    conn, err := net.Dial("tcp","localhost:8088")
    if err != nil {     // 判斷錯誤
        fmt.Println("Error dialing",err.Error())
        return
    }
    // 2.定義defer,在結束時關閉連接
    defer conn.Close()     

    // 3.從標准輸入里創建一個讀的對象
    inputReader := bufio.NewReader(os.Stdin)     
    // 4.無限循環
    for {
        // 5.調用讀的對象的ReadString方法判斷,如果有'\n'(也就是換行符)就認為輸入結束了,返回兩個值,一個是字符串,另一個是報錯
        input, _ := inputReader.ReadString('\n')
        // 6.利用strings.Trim方法給要發送的數據去掉空格
        trimmedInput := strings.Trim(input,"\r\n")
        // 7.如果輸入內容是q就退出了
        if trimmedInput == "Q" {      
            return
        }
        // 8.發送數據,發送的數據必須要是byte類型的切片,返回值是發送的總數據和錯誤
        _, err = conn.Write([]byte(trimmedInput))      
        if err != nil {     // 錯誤處理
            return
        }
        // 9.讀取服務端發來的消息
        var buf [1024]byte
        n, err := conn.Read(buf[:])    // 獲取到一個讀取的總數量和錯誤
        if err != nil {
            fmt.Println("read server data err", err)
            return
        }
        // 10.打印服務端返回的消息
        fmt.Println(string(buf[:n]))


    }
}

 

2.2、udp socket實戰

(1)udp socket 服務端實現

package main

import (
    "net"
    "fmt"
)

func main() {
    // 1.使用net.ListenUDP()啟動一個UDP的監聽
    listen, err := net.ListenUDP("udp", &net.UDPAddr{
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 30000,
    })
    if err != nil {       // 判斷錯誤
        fmt.Println("listen failed, err:", err)
        return
    }
    // 2.使用defer關閉連接
    defer listen.Close()
    // 3.無限循環
    for {
        // 4.定義存儲從連接里讀取消息的變量
        var data [1024]byte
        // 5.從udp連接里讀取消息到data變量里,返回值有:接收的數據總數量,誰發過來的數據,錯誤
        n, addr, err := listen.ReadFromUDP(data[:])
        if err != nil {         // 錯誤處理
            fmt.Println("read udp failed, err:", err)
            continue
        }
        // 6.打印接收到的消息、發消息的地址和消息長度
        fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
        // 7.給客戶端返回消息,誰發過來的消息就返回給誰,接收到的消息時什么就返回什么消息
        _, err = listen.WriteToUDP(data[:n], addr)
        if err != nil {          // 錯誤處理
            fmt.Println("write to udp failed, err:", err)
            continue
        }
    }
}

(2)udp socket 客戶端實現

package main

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

func main() {
    // 1.調用net.DialUDP()方法以udp協議連接到0.0.0.0:30000地址
    socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 30000,
    })
    if err != nil {      // 錯誤處理
        fmt.Println("連接服務端失敗,err:", err)
        return
    }
    // 2.定義defer關閉連接
    defer socket.Close()
    // 3.無限循環
    for {
        // 4.實例化從終端輸入對象
        input := bufio.NewReader(os.Stdin)
        // 5.沒有輸入內容時會等待,輸入的內容遇到'\n'就認為輸入結束,進入下一次循環。
        s, _ := input.ReadString('\n')   
        // 6.發送數據,發送的數據必須轉為byte類型的切片
        _, err = socket.Write([]byte(s))
        if err != nil {       // 判斷錯誤
            fmt.Println("發送數據失敗,err:", err)
            return
        }
        // 7.定義存儲服務端返回消息的變量
        data := make([]byte, 4096)
        // 8.接收服務端返回的消息存儲到變量data里,返回值有:接收數據的總大小,誰發來的,錯誤
        n, remoteAddr, err := socket.ReadFromUDP(data)
        if err != nil {       // 判斷錯誤
            fmt.Println("接收數據失敗,err:", err)
            return
        }
        // 打印接收到的數據
        fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)    
    }
    
}

 

2.3、tcp客戶端請求百度實現

package main

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

func main() {
    conn, err := net.Dial("tcp","www.baidu.com:80")
    if err != nil {
        fmt.Println("Error dialing",err.Error())
        return
    }
    defer conn.Close()
    msg := "GET / HTTP/1.1\r\n"
    msg += "Host: www.baidu.com\r\n"
    msg += "Connection: close\r\n"
    msg += "\r\n\r\n"

    _, err = io.WriteString(conn, msg)
    if err != nil {
        fmt.Println("write string failed",err)
        return
    }
    buf := make([]byte,4096)
    for {
        count, err := conn.Read(buf)
        if err != nil {
            break
        }
        fmt.Println(string(buf[0:count]))
    }
}

 

三、tcp secket粘包問題解決

說明:

  一般所謂的TCP粘包是在一次接收數據不能完全地體現一個完整的消息數據。TCP通訊為何存在粘包呢?主要原因是TCP是以流的方式來處理數據,再加上網絡上MTU的往往小於在應用處理的消息數據,所以就會引發一次接收的數據無法滿足消息的需要,導致粘包的存在。處理粘包的唯一方法就是制定應用層的數據通訊協議,通過協議來規范現有接收的數據是否滿足消息數據的需要。在應用中處理粘包的基礎方法主要有兩種分別是以4節字描述消息大小或以結束符,實際上也有兩者相結合的如HTTP,redis的通訊協議等。

 

示例:

1、服務端實現

package main

import (
    "bufio"
    "fmt"
    "encoding/binary"
    "io"
    "net"
    "bytes"
)

func Decode(reader *bufio.Reader) (string, error) {
    // 9.讀取接收到消息的前四個字節獲取本次接收的消息長度,Peek讀取消息時不移動讀取位置
    lengthByte, _ := reader.Peek(4)

    // 10.bytes.NewBuffer:從一個切片構造一個buffer(緩沖區)
    lengthBuff := bytes.NewBuffer(lengthByte)
    // 11.定義一個int32類型的變量存儲要讀取消息的長度
    var length int32
    // 12.讀取lengthBuff變量里的數據存到length變量里,此時length的值是要從連接里讀取數據的長度
    err := binary.Read(lengthBuff, binary.LittleEndian, &length)
    if err != nil {      // 錯誤處理
        return "", err
    }
    // 13.如果緩沖里的數據沒有要讀取的數據長就返回錯誤信息
    if int32(reader.Buffered()) < length+4 {
        return "", err
    }

    // 14.定義一個要讀取數據長度+4的byte類型切片變量
    pack := make([]byte, int(4+length))
    // 15.將從連接里讀取的消息存入pack變量里(即每次從連接里讀取 “int(4+length)”長度的數據)
    _, err = reader.Read(pack)
    if err != nil {      // 錯誤處理
        return "", err
    }
    // 16.返回從連接里讀取的數據(前四個是讀取消息的長度(不返回),只返回下標4之后的內容)
    return string(pack[4:]), nil
}

func process(conn net.Conn) {
    // 5.定義defer關閉連接
    defer conn.Close()
    // 6.實例化一個從當前連接里讀取消息的對象
    reader := bufio.NewReader(conn)
    // 7.無限循環
    for {
        // 8.將讀取消息的對象給Decode函數解碼,返回值是從連接里讀取到的消息和錯誤提示
        msg, err := Decode(reader)
        // 17.判斷錯誤,讀取到結尾錯誤和其他錯誤    
        if err == io.EOF {
            return
        }
        if err != nil {
            fmt.Println("decode msg failed, err:", err)
            return
        }
        // 18.最終將客戶端發來的消息打印出來
        fmt.Println("收到client發來的數據:", msg)
    }
}

func main() {
    // 1.啟動 tcp協議的監聽
    listen, err := net.Listen("tcp", "127.0.0.1:30000")
    if err != nil {         // 錯誤判斷
        fmt.Println("listen failed, err:", err)
        return
    }
    defer listen.Close()       // 定義defer關閉連接
    // 2.無限循環
    for {
        // 3.等待客戶端連接(無連接時會阻塞,有連接時自動處理連接)
        conn, err := listen.Accept()
        if err != nil {      // 錯誤處理
            fmt.Println("accept failed, err:", err)
            continue
        }
        // 4.啟動一個攜程,將連接傳給攜程處理
        go process(conn)
    }
}

 

2、客戶端實現

package main

import (
    "fmt"
    "net"
)

func Encode(message string) ([]byte, error) {
    // 6.讀取消息的長度,轉換成int32類型(占4個字節)
    var length = int32(len(message))
    // 7.定義緩沖區變量
    var pkg = new(bytes.Buffer)
    // 8.往緩沖區變量里寫入消息的長度(binary.LittleEndian:按照小端寫入的方式寫入(百度搜大小端讀寫了解相關內容))
    err := binary.Write(pkg, binary.LittleEndian, length)
    if err != nil {        // 錯誤處理
        return nil, err
    }
    // 9.往緩沖區變量里寫入消息實體
    err = binary.Write(pkg, binary.LittleEndian, []byte(message))
    if err != nil {       // 錯誤處理
        return nil, err
    }
    // 10.返回緩沖區里的所有字節和錯誤信息
    return pkg.Bytes(), nil
}

func main() {
    // 1.建立撥號連接連接服務端
    conn, err := net.Dial("tcp", "127.0.0.1:30000")
    if err != nil {      // 錯誤處理
        fmt.Println("dial failed, err", err)
        return
    }
    // 2.定義defer關閉連接
    defer conn.Close()
    // 3.循環20次
    for i := 0; i < 20; i++ {
        // 4.定義要發送消息內容
        msg := `Hello, Hello. How are you?`
        // 5.將要發送的消息傳給Encode函數進行編碼
        data, err := Encode(msg)
        if err != nil {      // 錯誤處理
            fmt.Println("encode msg failed, err:", err)
            return
        }
        // 11.往連接里寫入編碼后的數據
        conn.Write(data)
    }
}

 


免責聲明!

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



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