一、說明
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) } }