Go語言基礎之網絡編程
現在我們幾乎每天都在使用互聯網,我們前面已經學習了如何編寫Go語言程序,但是如何才能讓我們的程序通過網絡互相通信呢?本章我們就一起來學習下Go語言中的網絡編程。 關於網絡編程其實是一個很龐大的領域,本文只是簡單的演示了如何使用net包進行TCP和UDP通信。如需了解更詳細的網絡編程請自行檢索和閱讀專業資料。
互聯網協議介紹
互聯網的核心是一系列協議,總稱為”互聯網協議”(Internet Protocol Suite),正是這一些協議規定了電腦如何連接和組網。我們理解了這些協議,就理解了互聯網的原理。由於這些協議太過龐大和復雜,沒有辦法在這里一概而全,只能介紹一下我們日常開發中接觸較多的幾個協議。
互聯網分層模型
互聯網的邏輯實現被分為好幾層。每一層都有自己的功能,就像建築物一樣,每一層都靠下一層支持。用戶接觸到的只是最上面的那一層,根本不會感覺到下面的幾層。要理解互聯網就需要自下而上理解每一層的實現的功能。
如上圖所示,互聯網按照不同的模型划分會有不用的分層,但是不論按照什么模型去划分,越往上的層越靠近用戶,越往下的層越靠近硬件。在軟件開發中我們使用最多的是上圖中將互聯網划分為五個分層的模型。
接下來我們一層一層的自底向上介紹一下每一層。
物理層
我們的電腦要與外界互聯網通信,需要先把電腦連接網絡,我們可以用雙絞線、光纖、無線電波等方式。這就叫做”實物理層”,它就是把電腦連接起來的物理手段。它主要規定了網絡的一些電氣特性,作用是負責傳送0和1的電信號。
數據鏈路層
單純的0和1沒有任何意義,所以我們使用者會為其賦予一些特定的含義,規定解讀電信號的方式:例如:多少個電信號算一組?每個信號位有何意義?這就是”數據鏈接層”的功能,它在”物理層”的上方,確定了物理層傳輸的0和1的分組方式及代表的意義。早期的時候,每家公司都有自己的電信號分組方式。逐漸地,一種叫做”以太網”(Ethernet)的協議,占據了主導地位。
以太網規定,一組電信號構成一個數據包,叫做”幀”(Frame)。每一幀分成兩個部分:標頭(Head)和數據(Data)。其中”標頭”包含數據包的一些說明項,比如發送者、接受者、數據類型等等;”數據”則是數據包的具體內容。”標頭”的長度,固定為18字節。”數據”的長度,最短為46字節,最長為1500字節。因此,整個”幀”最短為64字節,最長為1518字節。如果數據很長,就必須分割成多個幀進行發送。
那么,發送者和接受者是如何標識呢?以太網規定,連入網絡的所有設備都必須具有”網卡”接口。數據包必須是從一塊網卡,傳送到另一塊網卡。網卡的地址,就是數據包的發送地址和接收地址,這叫做MAC地址。每塊網卡出廠的時候,都有一個全世界獨一無二的MAC地址,長度是48個二進制位,通常用12個十六進制數表示。前6個十六進制數是廠商編號,后6個是該廠商的網卡流水號。有了MAC地址,就可以定位網卡和數據包的路徑了。
我們會通過ARP協議來獲取接受方的MAC地址,有了MAC地址之后,如何把數據准確的發送給接收方呢?其實這里以太網采用了一種很”原始”的方式,它不是把數據包准確送到接收方,而是向本網絡內所有計算機都發送,讓每台計算機讀取這個包的”標頭”,找到接收方的MAC地址,然后與自身的MAC地址相比較,如果兩者相同,就接受這個包,做進一步處理,否則就丟棄這個包。這種發送方式就叫做”廣播”(broadcasting)。
網絡層
按照以太網協議的規則我們可以依靠MAC地址來向外發送數據。理論上依靠MAC地址,你電腦的網卡就可以找到身在世界另一個角落的某台電腦的網卡了,但是這種做法有一個重大缺陷就是以太網采用廣播方式發送數據包,所有成員人手一”包”,不僅效率低,而且發送的數據只能局限在發送者所在的子網絡。也就是說如果兩台計算機不在同一個子網絡,廣播是傳不過去的。這種設計是合理且必要的,因為如果互聯網上每一台計算機都會收到互聯網上收發的所有數據包,那是不現實的。
因此,必須找到一種方法區分哪些MAC地址屬於同一個子網絡,哪些不是。如果是同一個子網絡,就采用廣播方式發送,否則就采用”路由”方式發送。這就導致了”網絡層”的誕生。它的作用是引進一套新的地址,使得我們能夠區分不同的計算機是否屬於同一個子網絡。這套地址就叫做”網絡地址”,簡稱”網址”。
“網絡層”出現以后,每台計算機有了兩種地址,一種是MAC地址,另一種是網絡地址。兩種地址之間沒有任何聯系,MAC地址是綁定在網卡上的,網絡地址則是網絡管理員分配的。網絡地址幫助我們確定計算機所在的子網絡,MAC地址則將數據包送到該子網絡中的目標網卡。因此,從邏輯上可以推斷,必定是先處理網絡地址,然后再處理MAC地址。
規定網絡地址的協議,叫做IP協議。它所定義的地址,就被稱為IP地址。目前,廣泛采用的是IP協議第四版,簡稱IPv4。IPv4這個版本規定,網絡地址由32個二進制位組成,我們通常習慣用分成四段的十進制數表示IP地址,從0.0.0.0一直到255.255.255.255。
根據IP協議發送的數據,就叫做IP數據包。IP數據包也分為”標頭”和”數據”兩個部分:”標頭”部分主要包括版本、長度、IP地址等信息,”數據”部分則是IP數據包的具體內容。IP數據包的”標頭”部分的長度為20到60字節,整個數據包的總長度最大為65535字節。
傳輸層
有了MAC地址和IP地址,我們已經可以在互聯網上任意兩台主機上建立通信。但問題是同一台主機上會有許多程序都需要用網絡收發數據,比如QQ和瀏覽器這兩個程序都需要連接互聯網並收發數據,我們如何區分某個數據包到底是歸哪個程序的呢?也就是說,我們還需要一個參數,表示這個數據包到底供哪個程序(進程)使用。這個參數就叫做”端口”(port),它其實是每一個使用網卡的程序的編號。每個數據包都發到主機的特定端口,所以不同的程序就能取到自己所需要的數據。
“端口”是0到65535之間的一個整數,正好16個二進制位。0到1023的端口被系統占用,用戶只能選用大於1023的端口。有了IP和端口我們就能實現唯一確定互聯網上一個程序,進而實現網絡間的程序通信。
我們必須在數據包中加入端口信息,這就需要新的協議。最簡單的實現叫做UDP協議,它的格式幾乎就是在數據前面,加上端口號。UDP數據包,也是由”標頭”和”數據”兩部分組成:”標頭”部分主要定義了發出端口和接收端口,”數據”部分就是具體的內容。UDP數據包非常簡單,”標頭”部分一共只有8個字節,總長度不超過65,535字節,正好放進一個IP數據包。
UDP協議的優點是比較簡單,容易實現,但是缺點是可靠性較差,一旦數據包發出,無法知道對方是否收到。為了解決這個問題,提高網絡可靠性,TCP協議就誕生了。TCP協議能夠確保數據不會遺失。它的缺點是過程復雜、實現困難、消耗較多的資源。TCP數據包沒有長度限制,理論上可以無限長,但是為了保證網絡的效率,通常TCP數據包的長度不會超過IP數據包的長度,以確保單個TCP數據包不必再分割。
應用層
應用程序收到”傳輸層”的數據,接下來就要對數據進行解包。由於互聯網是開放架構,數據來源五花八門,必須事先規定好通信的數據格式,否則接收方根本無法獲得真正發送的數據內容。”應用層”的作用就是規定應用程序使用的數據格式,例如我們TCP協議之上常見的Email、HTTP、FTP等協議,這些協議就組成了互聯網協議的應用層。
如下圖所示,發送方的HTTP數據經過互聯網的傳輸過程中會依次添加各層協議的標頭信息,接收方收到數據包之后再依次根據協議解包得到數據。
socket編程
Socket是BSD UNIX的進程通信機制,通常也稱作”套接字”,用於描述IP地址和端口,是一個通信鏈的句柄。Socket可以理解為TCP/IP網絡的API,它定義了許多函數或例程,程序員可以用它們來開發TCP/IP網絡上的應用程序。電腦上運行的應用程序通常通過”套接字”向網絡發出請求或者應答網絡請求。
socket圖解
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處理鏈接。
我們使用Go語言的net包實現的TCP服務端代碼如下:
// tcp/server/main.go // 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("read from client failed, err:", err) break } recvStr := string(buf[:n]) fmt.Println("收到client端發來的數據:", recvStr) conn.Write([]byte(recvStr)) // 發送數據 } } func main() { listen, err := net.Listen("tcp", "127.0.0.1:20000") if err != nil { fmt.Println("listen failed, err:", err) return } for { conn, err := listen.Accept() // 建立連接 if err != nil { fmt.Println("accept failed, err:", err) continue } go process(conn) // 啟動一個goroutine處理連接 } }
將上面的代碼保存之后編譯成server
或server.exe
可執行文件。
TCP客戶端
一個TCP客戶端進行TCP通信的流程如下:
- 建立與服務端的鏈接
- 進行數據收發
- 關閉鏈接
使用Go語言的net包實現的TCP客戶端代碼如下:
// tcp/client/main.go // 客戶端 func main() { conn, err := net.Dial("tcp", "127.0.0.1:20000") if err != nil { fmt.Println("err :", err) return } defer conn.Close() // 關閉連接 inputReader := bufio.NewReader(os.Stdin) for { input, _ := inputReader.ReadString('\n') // 讀取用戶輸入 inputInfo := strings.Trim(input, "\r\n") if strings.ToUpper(inputInfo) == "Q" { // 如果輸入q就退出 return } _, err = conn.Write([]byte(inputInfo)) // 發送數據 if err != nil { return } buf := [512]byte{} n, err := conn.Read(buf[:]) if err != nil { fmt.Println("recv failed, err:", err) return } fmt.Println(string(buf[:n])) } }
將上面的代碼編譯成client
或client.exe
可執行文件,先啟動server端再啟動client端,在client端輸入任意內容回車之后就能夠在server端看到client端發送的數據,從而實現TCP通信
Go語言實現UDP通信
UDP協議
UDP協議(User Datagram Protocol)中文名稱是用戶數據報協議,是OSI(Open System Interconnection,開放式系統互聯)參考模型中一種無連接的傳輸層協議,不需要建立連接就能直接進行數據發送和接收,屬於不可靠的、沒有時序的通信,但是UDP協議的實時性比較好,通常用於視頻直播相關領域。
UDP服務端
使用Go語言的net
包實現的UDP服務端代碼如下:
// UDP/server/main.go // UDP server端 func main() { 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 } defer listen.Close() for { var data [1024]byte n, addr, err := listen.ReadFromUDP(data[:]) // 接收數據 if err != nil { fmt.Println("read udp failed, err:", 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("write to udp failed, err:", err) continue } } }
UDP客戶端
使用Go語言的net
包實現的UDP客戶端代碼如下:
// UDP 客戶端 func main() { 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 } defer socket.Close() sendData := []byte("Hello server") _, err = socket.Write(sendData) // 發送數據 if err != nil { fmt.Println("發送數據失敗,err:", err) return } data := make([]byte, 4096) 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) }
HTTP客戶端和服務端
HTTP協議
超文本傳輸協議(HTTP,HyperText Transfer Protocol)是互聯網上應用最為廣泛的一種網絡傳輸協議,所有的WWW文件都必須遵守這個標准。設計HTTP最初的目的是為了提供一種發布和接收HTML頁面的方法。
HTTP 客戶端
使用Go語言編寫一個簡單的發送HTTP請求的Client端,代碼如下:
// http client demo func main() { conn, err := net.Dial("tcp", "www.baidu.com:80") if err != nil { fmt.Println("dial failed, err:", err) return } defer conn.Close() fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n") // 發送請求 var buf [8192]byte // 接收響應 for { n, err := conn.Read(buf[:]) if err == io.EOF { return } if err != nil { fmt.Println("get response failed, err:", err) break } fmt.Print(string(buf[:n])) } }
將上面的代碼保存之后編譯成可執行文件,執行之后就能在終端打印liwenzhou.com
網站首頁的內容了,我們的瀏覽器其實就是一個發送和接收HTTP協議數據的客戶端,我們平時通過瀏覽器訪問網頁其實就是從網站的服務器接收HTTP數據,然后瀏覽器會按照HTML、CSS等規則將網頁渲染展示出來。
我們還可以直接使用Go語言封裝好的net/http
包,它提供了HTTP客戶端和服務端的實現。 有了net/http
包我們請求liwenzhou.com
這個網站的頁面就會比較簡單了,示例代碼如下:
// http_client/main.go package main import ( "fmt" "io/ioutil" "net/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.Println(body) }
將上面的代碼保存之后編譯成可執行文件,執行之后就能在終端輸出liwenzhou.com
網站首頁的內容了。
HTTP 服務端
使用Go語言中的net/http
包來編寫一個簡單的接收HTTP請求的Server端示例,net/http
包是對net包的進一步封裝,專門用來處理HTTP協議的數據。具體的代碼如下:
// http server func sayHello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello World!") } func main() { http.HandleFunc("/", sayHello) err := http.ListenAndServe(":9090", nil) if err != nil { fmt.Printf("http server failed, err:%v\n", err) return } }