Golang網絡編程-套接字(socket)篇
作者:尹正傑
版權聲明:原創作品,謝絕轉載!否則將追究法律責任。
一.網絡概述
1>.什么是協議
從應用的角度出發,協議可理解為“規則”,是數據傳輸和數據的解釋的規則。假設,A、B雙方欲傳輸文件。規定: 第一次,傳輸文件名,接收方接收到文件名,應答OK給傳輸方; 第二次,發送文件的尺寸,接收方接收到該數據再次應答一個OK; 第三次,傳輸文件內容。同樣,接收方接收數據完成后應答OK表示文件內容接收成功。 由此,無論A、B之間傳遞何種文件,都是通過三次數據傳輸來完成。A、B之間形成了一個最簡單的數據傳輸規則。雙方都按此規則發送、接收數據。A、B之間達成的這個相互遵守的規則即為協議。 這種僅在A、B之間被遵守的協議稱之為原始協議。 當此協議被更多的人采用,不斷的增加、改進、維護、完善。最終形成一個穩定的、完整的文件傳輸協議,被廣泛應用於各種文件傳輸過程中。該協議就成為一個標准協議。最早的ftp協議就是由此衍生而來。 典型協議: 應用層: 常見的協議有HTTP協議,FTP協議。 HTTP超文本傳輸協議(Hyper Text Transfer Protocol)是互聯網上應用最為廣泛的一種網絡協議。 FTP文件傳輸協議(File Transfer Protocol) 傳輸層: 常見協議有TCP/UDP協議。 TCP傳輸控制協議(Transmission Control Protocol)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。 UDP用戶數據報協議(User Datagram Protocol)是OSI參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。 網絡層: 常見協議有IP協議、ICMP協議、IGMP協議。 IP協議是因特網互聯協議(Internet Protocol) ICMP協議是Internet控制報文協議(Internet Control Message Protocol)它是TCP/IP協議族的一個子協議,用於在IP主機、路由器之間傳遞控制消息。 IGMP協議是 Internet 組管理協議(Internet Group Management Protocol),是因特網協議家族中的一個組播協議。該協議運行在主機和組播路由器之間。 鏈路層: 常見協議有ARP協議、RARP協議。 ARP協議是正向地址解析協議(Address Resolution Protocol),通過已知的IP,尋找對應主機的MAC地址。 RARP是反向地址轉換協議,通過MAC地址確定IP地址。
2>.什么是socket
Socket,英文含義是【插座、插孔】,一般稱之為套接字,用於描述IP地址和端口。可以實現不同程序間的數據通信。 Socket起源於Unix,而Unix基本哲學之一就是"一切皆文件",都可以用"打開open –> 讀寫write/read –> 關閉close"模式來操作。
Socket就是該模式的一個實現,網絡的Socket數據傳輸是一種特殊的I/O,Socket也是一種文件描述符。
Socket也具有一個類似於打開文件的函數調用:Socket(),該函數返回一個整型的Socket描述符,隨后的連接建立、數據傳輸等操作都是通過該Socket實現的。 在TCP/IP協議中,"IP地址+TCP或UDP端口號"唯一標識網絡通訊中的一個進程。"IP地址+端口號"就對應一個socket。
欲建立連接的兩個進程各自有一個socket來標識,那么這兩個socket組成的socket pair就唯一標識一個連接。因此可以用Socket來描述網絡連接的一對一關系。 常用的Socket類型有兩種: 流式Socket(SOCK_STREAM):。 流式是一種面向連接的Socket,針對於面向連接的TCP服務應用; 數據報式Socket(SOCK_DGRAM): 數據報式Socket是一種無連接的Socket,對應於無連接的UDP服務應用。 溫馨提示: 套接字的內核實現較為復雜,不宜在學習初期深入學習,了解到如下結構足矣。
3>.網絡應用程序設計模式
C/S模式 傳統的網絡應用設計模式,客戶機(client)/服務器(server)模式。需要在通訊兩端各自部署客戶機和服務器來完成數據通信。 優點: 1>.客戶端位於目標主機上可以保證性能,將數據緩存至客戶端本地,從而提高數據傳輸效率。 2>.一般來說客戶端和服務器程序由一個開發團隊創作,所以他們之間所采用的協議相對靈活。可以在標准協議的基礎上根據需求裁剪及定制。例如,騰訊所采用的通信協議,即為ftp協議的修改剪裁版。 因此,傳統的網絡應用程序及較大型的網絡應用程序都首選C/S模式進行開發。如知名的網絡游戲魔獸世界。3D畫面,數據量龐大,使用C/S模式可以提前在本地進行大量數據的緩存處理,從而提高觀感。 缺點: 1>.由於客戶端和服務器都需要有一個開發團隊來完成開發。工作量將成倍提升,開發周期較長。
2>.從用戶角度出發,需要將客戶端安裝至用戶主機上,對用戶主機的安全性構成威脅。這也是很多用戶不願使用C/S模式應用程序的重要原因。 B/S模式 瀏覽器(Browser)/服務器(Server)模式。只需在一端部署服務器,而另外一端使用每台PC都默認配置的瀏覽器即可完成數據的傳輸。 優點: 1>.B/S模式相比C/S模式而言,由於它沒有獨立的客戶端,使用標准瀏覽器作為客戶端,其工作開發量較小。只需開發服務器端即可。
2>.另外由於其采用瀏覽器顯示數據,因此移植性非常好,不受平台限制。如早期的偷菜游戲,在各個平台上都可以完美運行。 缺點: 1>.B/S模式的缺點也較明顯。由於使用第三方瀏覽器,因此網絡應用支持受限。
2>.沒有客戶端放到對方主機上,緩存數據不盡如人意,從而傳輸數據量受到限制。應用的觀感大打折扣。
3>.必須與瀏覽器一樣,采用標准http協議進行通信,協議選擇不靈活。
綜上所述,在開發過程中,模式的選擇由上述各自的特點決定。根據實際需求選擇應用程序設計模式。
4>.博主推薦閱讀
計算機網絡基礎之網絡拓撲介紹: https://www.cnblogs.com/yinzhengjie/p/11846279.html 計算機網絡基礎之OSI參考模型(理論上的標准): https://www.cnblogs.com/yinzhengjie/p/11846473.html 計算機網絡基礎之網絡設備: https://www.cnblogs.com/yinzhengjie/p/11853809.html 計算機網絡基礎之TCP/IP 協議棧(事實上的標准): https://www.cnblogs.com/yinzhengjie/p/11854107.html 計算機網絡基礎之IP地址詳解: https://www.cnblogs.com/yinzhengjie/p/11854562.html
二.TCP的socket編程實戰案例
1>.簡單C/S模型通信

package main import ( "fmt" "net" ) func main() { /** 使用Listen函數創建監聽socket,其函數簽名如下: func Listen(network, address string) (Listener, error) 以下是對函數簽名的參數說明: network: 指定服務端socket的協議,如tcp/udp,注意是小寫字母喲~ address: 指定服務端監聽的IP地址和端口號,如果不指定地址默認監聽當前服務器所有IP地址喲~ */ socket, err := net.Listen("tcp", "127.0.0.1:8888") if err != nil { fmt.Println("開啟監聽失敗,錯誤原因: ", err) return } defer socket.Close() fmt.Println("開啟監聽...") for { /** 等待客戶端連接請求 */ conn, err := socket.Accept() if err != nil { fmt.Println("建立鏈接失敗,錯誤原因: ", err) return } defer conn.Close() fmt.Println("建立鏈接成功,客戶端地址是: ", conn.RemoteAddr()) /** 接收客戶端數據 */ buf := make([]byte, 1024) conn.Read(buf) fmt.Printf("讀取到客戶端的數據為: %s\n", string(buf)) /** 發送數據給客戶端 */ tmp := "Blog地址:[https://www.cnblogs.com/yinzhengjie/]" conn.Write([]byte(tmp)) } }

package main import ( "fmt" "net" ) func main() { /** 使用Dial函數鏈接服務端,其函數簽名如下所示: func Dial(network, address string) (Conn, error) 以下是對函數簽名的各參數說明: network: 指定客戶端socket的協議,如tcp/udp,該協議應該和需要鏈接服務端的協議一致喲~ address: 指定客戶端需要鏈接服務端的socket信息,即指定服務端的IP地址和端口 */ conn, err := net.Dial("tcp", "127.0.0.1:8888") if err != nil { fmt.Println("連接服務端出錯,錯誤原因: ", err) return } defer conn.Close() fmt.Println("與服務端連接建立成功...") /** 給服務端發送數據 */ conn.Write([]byte("服務端,請問博客地址的URL是多少呢?")) /** 獲取服務器的應答 */ var buf = make([]byte, 1024) conn.Read(buf) fmt.Printf("從服務端獲取到的數據為:%s\n", string(buf)) }

package main import ( "fmt" "net" "strconv" ) func main() { /** 使用Listen函數創建監聽socket,其函數簽名如下: func Listen(network, address string) (Listener, error) 以下是對函數簽名的參數說明: network: 指定服務端socket的協議,如tcp/udp,注意是小寫字母喲~ address: 指定服務端監聽的IP地址和端口號,如果不指定地址默認監聽當前服務器所有IP地址喲~ */ socket, err := net.Listen("tcp", "127.0.0.1:8888") if err != nil { fmt.Println("開啟監聽失敗,錯誤原因: ", err) return } defer socket.Close() fmt.Println("開啟監聽...") for { /** 等待客戶端連接請求 */ conn, err := socket.Accept() if err != nil { fmt.Println("建立鏈接失敗,錯誤原因: ", err) return } defer conn.Close() fmt.Println("建立鏈接成功,客戶端地址是: ", conn.RemoteAddr()) /** 分兩次接收客戶端數據: 第一次最終接收數據的長度; 第二次根據第一次接受的長度,創建容量大小; */ tmp := make([]byte, 2) conn.Read(tmp) dataLength, err := strconv.Atoi(string(tmp)) //把字節切片轉換成整型 if err != nil { fmt.Println("獲取數據長度失敗: ", err) return } fmt.Println("獲取到的數據長度是: ", dataLength) conn.Write([]byte("已獲取到數據長度")) /** 開始讀取數據 */ buf := make([]byte, dataLength) conn.Read(buf) fmt.Printf("讀取到客戶端的數據為: %s\n", string(buf)) /** 發送數據給客戶端 */ data := "Blog地址:[https://www.cnblogs.com/yinzhengjie/]" conn.Write([]byte(data)) } }

package main import ( "fmt" "net" "strconv" ) func main() { /** 使用Dial函數鏈接服務端,其函數簽名如下所示: func Dial(network, address string) (Conn, error) 以下是對函數簽名的各參數說明: network: 指定客戶端socket的協議,如tcp/udp,該協議應該和需要鏈接服務端的協議一致喲~ address: 指定客戶端需要鏈接服務端的socket信息,即指定服務端的IP地址和端口 */ conn, err := net.Dial("tcp", "127.0.0.1:8888") if err != nil { fmt.Println("連接服務端出錯,錯誤原因: ", err) return } defer conn.Close() fmt.Println("與服務端連接建立成功...") /** 定義需要發送的數據,第一次給服務端發送要發的長度 */ data := []byte("服務端,請問博客地址的URL是多少呢?") lenData := len(data) /** 給服務端發送數據的長度 */ conn.Write([]byte(strconv.Itoa(lenData))) /** 獲取服務器的應答 */ var buf = make([]byte, 1024) conn.Read(buf) fmt.Printf("從服務端獲取到的數據為:%s\n", string(buf)) /** 第二次給服務器發送數據 */ conn.Write(data) conn.Read(buf) fmt.Printf("獲取到的數據為:%s\n", string(buf)) }
2>.並發C/S模型通信

package main import ( "fmt" "net" "strings" ) func HandleConn(conn net.Conn) { //函數調用完畢,自動關閉conn defer conn.Close() //獲取客戶端的網絡地址信息 addr := conn.RemoteAddr().String() fmt.Println(addr, " conncet sucessful") buf := make([]byte, 2048) for { //讀取用戶數據 n, err := conn.Read(buf) if err != nil { fmt.Println("err = ", err) return } fmt.Printf("[%s]: %s\n", addr, string(buf[:n])) fmt.Println("len = ", len(string(buf[:n]))) //if "exit" == string(buf[:n-1]) { // nc測試,發送時,只有 \n if "exit" == string(buf[:n-2]) { // 自己寫的客戶端測試, 發送時,多了2個字符, "\r\n" fmt.Println(addr, " exit") return } //把數據轉換為大寫,再給用戶發送 conn.Write([]byte(strings.ToUpper(string(buf[:n])))) } } func main() { /** 使用Listen函數創建監聽socket,其函數簽名如下: func Listen(network, address string) (Listener, error) 以下是對函數簽名的參數說明: network: 指定服務端socket的協議,如tcp/udp,注意是小寫字母喲~ address: 指定服務端監聽的IP地址和端口號,如果不指定地址默認監聽當前服務器所有IP地址喲~ */ socket, err := net.Listen("tcp", "127.0.0.1:8888") if err != nil { fmt.Println("開啟監聽失敗,錯誤原因: ", err) return } defer socket.Close() fmt.Println("開啟監聽...") //接收多個用戶 for { /** 等待客戶端連接請求 */ conn, err := socket.Accept() if err != nil { fmt.Println("建立鏈接失敗,錯誤原因: ", err) return } //處理用戶請求, 新建一個go程 go HandleConn(conn) } }

package main import ( "fmt" "net" "strconv" ) func main() { /** 使用Dial函數鏈接服務端,其函數簽名如下所示: func Dial(network, address string) (Conn, error) 以下是對函數簽名的各參數說明: network: 指定客戶端socket的協議,如tcp/udp,該協議應該和需要鏈接服務端的協議一致喲~ address: 指定客戶端需要鏈接服務端的socket信息,即指定服務端的IP地址和端口 */ conn, err := net.Dial("tcp", "127.0.0.1:8888") if err != nil { fmt.Println("連接服務端出錯,錯誤原因: ", err) return } defer conn.Close() fmt.Println("與服務端連接建立成功...") /** 定義需要發送的數據,第一次給服務端發送要發的長度 */ data := []byte("服務端,請問博客地址的URL是多少呢?") lenData := len(data) /** 給服務端發送數據的長度 */ conn.Write([]byte(strconv.Itoa(lenData))) /** 獲取服務器的應答 */ var buf = make([]byte, 1024) conn.Read(buf) fmt.Printf("從服務端獲取到的數據為:%s\n", string(buf)) /** 第二次給服務器發送數據 */ conn.Write(data) conn.Read(buf) fmt.Printf("獲取到的數據為:%s\n", string(buf)) }
三.UDP的socket編程實戰案例
1>.UDP與TCP的差異概述
TCP和UDP的主要區別如下:
1>.TCP是面向連接,UDP是面向無連接
TCP在建立/端口連接時分別要進行三次握手/四次斷開,所以我們說TCP是可靠的連接,而說UDP是不可靠的連接;
2>.TCP是流式傳輸,可能會出現"粘包"問題,UDP是數據報傳輸,UDP可能會出現"丟包"問題
"粘包"問題可以通過發送數據包的長度解決
"丟包"問題可以通過每一個數據報添加標識位
3>.TCP要求系統資源較多,UDP要求系統資源較少
TCP需要創建連接再進行通信,所以效率要比UDP慢
4>.TCP程序結構較復雜,UDP程序結構較簡單
5>.TCP可以保證數據的准確性,而UDP則不保證數據的准確性
應用場景:
TCP的應用場景:
比如文件傳輸,重要數據傳輸等。
UDP的應用常見:
比如打電話,直播等.
2>.簡單C/S模型通信

package main import ( "fmt" "net" ) func main() { /** 創建監聽的地址,並且指定udp協議 */ udp_addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:9999") if err != nil { fmt.Println("獲取監聽地址失敗,錯誤原因: ", err) return } /** 創建數據通信socket */ conn, err := net.ListenUDP("udp", udp_addr) if err != nil { fmt.Println("開啟UDP監聽失敗,錯誤原因: ", err) return } defer conn.Close() fmt.Println("開啟監聽...") buf := make([]byte, 1024) /** 通過ReadFromUDP可以讀取數據,可以返回如下三個參數: dataLength: 數據的長度 raddr: 遠程的客戶端地址 err: 錯誤信息 */ dataLength, raddr, err := conn.ReadFromUDP(buf) if err != nil { fmt.Println("獲取客戶端傳遞數據失敗,錯誤原因: ", err) return } fmt.Println("獲取到客戶端的數據為: ", string(buf[:dataLength])) /** 寫回數據 */ conn.WriteToUDP([]byte("服務端已經收到數據啦~"), raddr) }

package main import ( "fmt" "net" ) func main() { /** 使用Dial函數鏈接服務端,其函數簽名如下所示: func Dial(network, address string) (Conn, error) 以下是對函數簽名的各參數說明: network: 指定客戶端socket的協議,如tcp/udp,該協議應該和需要鏈接服務端的協議一致喲~ address: 指定客戶端需要鏈接服務端的socket信息,即指定服務端的IP地址和端口 */ conn, err := net.Dial("udp", "127.0.0.1:9999") if err != nil { fmt.Println("連接服務端出錯,錯誤原因: ", err) return } defer conn.Close() fmt.Println("與服務端連接建立成功...") /** 給服務端發送數據 */ conn.Write([]byte("Hi,My name is Jason Yin.")) /** 讀取服務端返回的數據 */ tmp := make([]byte, 1024) n, _ := conn.Read(tmp) fmt.Println("獲取到服務器返回的數據為: ", string(tmp[:n])) }