Go的網路編程詳解
一 互聯網協議介紹
1.1互聯網分層模型
互聯網的邏輯實現被分為好幾層。每一層都有自己的功能,就像建築物一樣,每一層都靠下一層支援。用戶接觸到的只是最上面的那一層,根本不會感覺到下面的幾層。要理解互聯網就需要自下而上理解每一層的實現的功能。
如上圖所示,互聯網按照不同的模型劃分會有不用的分層,但是不論按照什麼模型去劃分,越往上的層越靠近用戶,越往下的層越靠近硬體。在軟體開發中我們使用最多的是上圖中將互聯網劃分為五個分層的模型。
接下來我們一層一層的自底向上介紹一下每一層。
物理層
我們的電腦要與外界互聯網通訊,需要先把電腦連接網路,我們可以用雙絞線、光纖、無線電波等方式。這就叫做」實物理層」,它就是把電腦連接起來的物理手段。它主要規定了網路的一些電氣特性,作用是負責傳送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去組織符合指定的協議數據然後進行通訊。
- Socket又稱「套接字」,應用程式通常通過「套接字」向網路發出請求或者應答網路請求
- 常用的Socket類型有兩種:流式Socket和數據報式Socket,流式是一種面向連接的Socket,針對於面向連接的TCP服務應用,數據報式Socket是一種無連接的Socket,針對於無連接的UDP服務應用
- TCP:比較靠譜,面向連接,比較慢
- UDP:不是太靠譜,比較快
舉個例子:TCP就像貨到付款的快遞,送到家還必須見到你人才算一整套流程。UDP就像某快遞快遞櫃一扔就走管你收到收不到,一般直播用UDP。
三 TCP編程
Go語言實現TCP通訊
TCP協議
TCP/IP(Transmission Control Protocol/Internet Protocol) 即傳輸控制協議/網間協議,是一種面向連接(連接導向)的、可靠的、基於位元組流的傳輸層(Transport layer)通訊協議,因為是面向連接的協議,數據像水流一樣傳輸,會存在黏包問題。
TCP服務端
一個TCP服務端可以同時連接很多個客戶端,例如世界各地的用戶使用自己電腦上的瀏覽器訪問淘寶網。因為Go語言中創建多個goroutine實現並發非常方便和高效,所以我們可以每建立一次鏈接就創建一個goroutine去處理。
TCP服務端程式的處理流程:
1.監聽埠
2.接收客戶端請求建立鏈接
3.創建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通訊的流程如下:
1.建立與服務端的鏈接
2.進行數據收發
3.關閉鏈接
使用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通訊。
四 UDP編程
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編程
web工作流程
Web伺服器的工作原理可以簡單地歸納為:
- 客戶機通過TCP/IP協議建立到伺服器的TCP連接
- 客戶端向伺服器發送HTTP協議請求包,請求伺服器里的資源文檔
- 伺服器向客戶機發送HTTP協議應答包,如果請求的資源包含有動態語言的內容,那麼伺服器會調用動態語言的解釋引擎負責處理「動態內容」,並將處理得到的數據返回給客戶端
- 客戶機與伺服器斷開。由客戶端解釋HTML文檔,在客戶端螢幕上渲染圖形結果
HTTP協議
超文本傳輸協議(HTTP,HyperText Transfer Protocol)是互聯網上應用最為廣泛的一種網路協議,它詳細規定了瀏覽器和萬維網伺服器之間互相通訊的規則,通過網際網路傳送萬維網文檔的數據傳送協議
HTTP協議通常承載於TCP協議之上
HTTP服務端
package main
import (
"fmt"
"net/http"
)
func main() {
////127.0.0.1:8000/go
// 單獨寫回調函數
http.HandleFunc("/go", myHandler)
//http.HandleFunc("/ungo",myHandler2 )
// addr:監聽的地址
// handler:回調函數
http.ListenAndServe("127.0.0.1:8000", nil)
}
// handler函數
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.RemoteAddr, "連接成功")
// 請求方式:GET POST DELETE PUT UPDATE
fmt.Println("method:", r.Method)
// /go
fmt.Println("url:", r.URL.Path)
fmt.Println("header:", r.Header)
fmt.Println("body:", r.Body)
// 回復
w.Write([]byte("www.5lmh.com"))
}
HTTP服務端
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
//resp, _ := http.Get("//www.baidu.com")
//fmt.Println(resp)
resp, _ := http.Get("//127.0.0.1:8000/go")
defer resp.Body.Close()
// 200 OK
fmt.Println(resp.Status)
fmt.Println(resp.Header)
buf := make([]byte, 1024)
for {
// 接收服務端資訊
n, err := resp.Body.Read(buf)
if err != nil && err != io.EOF {
fmt.Println(err)
return
} else {
fmt.Println("讀取完畢")
res := string(buf[:n])
fmt.Println(res)
break
}
}
}
六 WebSocket編程
webSocket是什麼
- WebSocket是一種在單個TCP連接上進行全雙工通訊的協議
- WebSocket使得客戶端和伺服器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據
- 在WebSocket API中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸
- 需要安裝第三方包:
cmd中:go get -u -v github.com/gorilla/websocket
我們啟動一個http伺服器,指定根路徑路由到一個html頁面,該頁面用來模擬websocket通訊的客戶端,頁面會提供一個按鈕觸發一段執行websocket通訊的js。服務端接收到websocket請求,然後將請求的內容完整地響應給瀏覽器。
接收websocket請求的服務端:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil) // error ignored for sake of simplicity
for {
// Read message from browser
msgType, msg, err := conn.ReadMessage()
if err != nil {
return
}
// Print the message to the console
fmt.Printf("%s sent: %s\n", conn.RemoteAddr(), string(msg))
// Write message back to browser
if err = conn.WriteMessage(msgType, msg); err != nil {
return
}
}
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "H:\\go\\main\\websockets.html")
})
http.ListenAndServe(":8080", nil)
}
發送websocket請求的客戶端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSockets</title>
</head>
<body>
<input id="input" type="text" />
<button onclick="send()">Send</button>
<pre id="output"></pre>
<script>
var input = document.getElementById("input");
var output = document.getElementById("output");
var socket = new WebSocket("ws://localhost:8080/echo");
socket.onopen = function () {
output.innerHTML += "Status: Connected\n";
};
socket.onmessage = function (e) {
output.innerHTML += "Server: " + e.data + "\n";
};
function send() {
socket.send(input.value);
input.value = "";
}
</script>
</body>
</html>