GO-網路通訊
網路通訊1:UDP
UDP協議
1.簡介
UDP(User Datagram Protocol),使用者資料報協議,是OSI(Open System Interconnection,開放式系統互聯) 參考模型中一種無連線的傳輸層協議,提供面向事務的簡單不可靠資訊傳送服務,IETF RFC 768是UDP的正式規範。UDP提供了無連線通訊,且不對傳送資料包進行可靠性保證,適合於一次傳輸少量資料,UDP傳輸的可靠性由應用層負責。常用的UDP埠號有:
UDP報文沒有可靠性保證、順序保證和流量控制欄位等,可靠性較差。但是正因為UDP協議的控制選項較少,在資料傳輸過程中延遲小、資料傳輸效率高
UDP在IP報文中的位置如下圖所示:
2.UDP使用
在選擇使用協議的時候,選擇UDP必須要謹慎。在網路質量令人十分不滿意的環境下,UDP協議資料包丟失會比較嚴重。但是由於UDP的特性:它不屬於連線型協議,因而具有資源消耗小,處理速度快的優點,所以通常音訊、視訊和普通資料在傳送時使用UDP較多,因為它們即使偶爾丟失一兩個資料包,也不會對接收結果產生太大影響。比如我們聊天用的QQ就是使用的UDP協議。
既然UDP是一種不可靠的網路協議,那麼還有什麼使用價值或必要呢?其實不然,在有些情況下UDP協議可能會變得非常有用。因為UDP具有TCP所望塵莫及的速度優勢。雖然TCP協議中植入了各種安全保障功能,但是在實際執行的過程中會佔用大量的系統開銷,無疑使速度受到嚴重的影響。反觀UDP由於排除了資訊可靠傳遞機制,將安全和排序等功能移交給上層應用來完成,極大降低了執行時間,使速度得到了保證。
關於UDP協議的最早規範是RFC768,1980年釋出。儘管時間已經很長,但是UDP協議仍然繼續在主流應用中發揮著作用。包括視訊電話會議系統在內的許多應用都證明了UDP協議的存在價值。因為相對於可靠性來說,這些應用更加註重實際效能,所以為了獲得更好的使用效果(例如,更高的畫面幀重新整理速率)往往可以犧牲一定的可靠性(例如,畫面質量)。這就是UDP和TCP兩種協議的權衡之處。根據不同的環境和特點,兩種傳輸協議都將在今後的網路世界中發揮更加重要的作用。
3.UDP報頭
UDP報頭由4個域組成,其中每個域各佔用2個位元組,如下圖所示:
-
UDP協議使用埠號為不同的應用保留其各自的資料傳輸通道。UDP和TCP協議正是採用這一機制實現對同一時刻內多項應用同時傳送和接收資料的支援。資料傳送一方(可以是客戶端或伺服器端)將UDP資料包通過源埠傳送出去,而資料接收一方則通過目標埠接收資料。有的網路應用只能使用預先為其預留或註冊的靜態埠;而另外一些網路應用則可以使用未被註冊的動態埠。因為UDP報頭使用兩個位元組存放埠號,所以埠號的有效範圍是從0到65535。一般來說,大於49151的埠號都代表動態埠。
-
資料報的長度是指包括報頭和資料部分在內的總位元組數。因為報頭的長度是固定的,所以該域主要被用來計算可變長度的資料部分(又稱為資料負載)。資料報的最大長度根據操作環境的不同而各異。從理論上說,包含報頭在內的資料報的最大長度為65535位元組。不過,一些實際應用往往會限制資料報的大小,有時會降低到8192位元組。(對於一次傳送多少位元組比較好,後面會講到)
-
UDP協議使用報頭中的校驗值來保證資料的安全。校驗值首先在資料傳送方通過特殊的演算法計算得出,在傳遞到接收方之後,還需要再重新計算。如果某個資料報在傳輸過程中被第三方篡改或者由於線路噪音等原因受到損壞,傳送和接收方的校驗計算值將不會相符,由此UDP協議可以檢測是否出錯。這與TCP協議是不同的,後者要求必須具有校驗值。
-
許多鏈路層協議都提供錯誤檢查,包括流行的乙太網協議,也許你想知道為什麼UDP也要提供檢查和校驗。其原因是鏈路層以下的協議在源端和終端之間的某些通道可能不提供錯誤檢測。雖然UDP提供有錯誤檢測,但檢測到錯誤時,UDP不做錯誤校正,只是簡單地把損壞的訊息段扔掉,或者給應用程式提供警告資訊。
UDP特性
-
UDP是一個無連線協議,傳輸資料之前源端和終端不建立連線,當UDP它想傳送時就簡單地去抓取來自應用程式的資料,並儘可能快地把它扔到網路上。在傳送端,UDP傳送資料的速度僅僅是受應用程式生成資料的速度、計算機的能力和傳輸頻寬的限制;在接收端,UDP把每個訊息段放在佇列中,應用程式每次從佇列中讀一個訊息段。
-
由於傳輸資料不建立連線,因此也就不需要維護連線狀態,包括收發狀態等,因此一臺服務機可同時向多個客戶機傳輸相同的訊息。
-
UDP資訊包的標題很短,只有8個位元組,相對於TCP的20個位元組資訊包的額外開銷很小。
-
吞吐量不受擁擠控制演算法的調節,只受應用軟體生成資料的速率、傳輸頻寬、源端和終端主機效能的限制。
-
UDP使用盡最大努力交付,即不保證可靠交付,因此主機不需要維持複雜的連結狀態表(這裡面有許多引數)。
-
UDP是面向報文的。傳送方的UDP對應用程式交下來的報文,在新增首部後就向下交付給IP層。既不拆分,也不合並,而是保留這些報文的邊界,因此,應用程式需要選擇合適的報文大小。
例項
服務端實現
import (
"os"
"fmt"
"net"
)
//處理錯誤資訊
func checkError(err error) {
if err != nil { //指標不為空
fmt.Println("Error", err.Error())
os.Exit(1)
}
}
func receiveUDPMsg(udpConn *net.UDPConn) {
//宣告30位元組的緩衝區
buffer := make([]byte, 30)
//從udpConn讀取客戶端傳送過來的資料,放在緩衝區中(阻塞方法)
//返回值:n=讀到的位元組長度,remoteAddr=客戶端地址,err=錯誤
n, remoteAddr, err := udpConn.ReadFromUDP(buffer) //從udp接收資料
if err != nil {
fmt.Println("Error", err.Error()) //列印錯誤資訊
return
}
fmt.Printf("接收到來自%v的訊息:%s", remoteAddr,string(buffer[0:n]))
//向遠端地址中寫入資料
_, err = udpConn.WriteToUDP([]byte("hao nimei"), remoteAddr)
checkError(err)
}
func main() {
//解析IP和埠得到UDP地址
udp_addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8848")
checkError(err)
//在解析得到的地址上建立UDP監聽
udpConn, err := net.ListenUDP("udp", udp_addr)
defer udpConn.Close() //關閉連結
checkError(err)
//從udpConn中接收UDP訊息
receiveUDPMsg(udpConn) //收訊息
}
客戶端實現
import (
"net"
"fmt"
"os"
)
func main() {
//請求連線伺服器,得到連線物件
conn,err:=net.Dial("udp","127.0.0.1:8848")
defer conn.Close()
if err!=nil{
fmt.Println("網路連接出錯")
os.Exit(1)
}
//向連線中寫入訊息
conn.Write([]byte("hello nimei"))
fmt.Println("傳送訊息","hello nimei")
//讀取代表收取訊息(阻塞)
buffer := make([]byte, 30)
n, err := conn.Read(buffer)
if err!=nil{
fmt.Println("讀取訊息錯誤:err=",err)
os.Exit(1)
}
fmt.Println("收到訊息",string(buffer[:n]))
}
網路通訊2:TCP簡單通訊
首部格式
圖釋:
各個段位說明:
- 源埠和目的埠:各佔 2 位元組.埠是傳輸層與應用層的服務介面.傳輸層的複用和分用功能都要通過端口才能實現
- 序號:佔 4 位元組.TCP 連線中傳送的資料流中的每一個位元組都編上一個序號.序號欄位的值則指的是本報文段所傳送的資料的第一個位元組的序號
- 確認號:佔 4 位元組,是期望收到對方的下一個報文段的資料的第一個位元組的序號
- 資料偏移/首部長度:佔 4 位,它指出 TCP 報文段的資料起始處距離 TCP 報文段的起始處有多遠.“資料偏移”的單位是 32位字(以 4 位元組為計算單位)
- 保留:佔 6 位,保留為今後使用,但目前應置為 0
- 緊急URG:當 URG=1 時,表明緊急指標欄位有效.它告訴系統此報文段中有緊急資料,應儘快傳送(相當於高優先順序的資料)
- 確認ACK:只有當 ACK=1 時確認號欄位才有效.當 ACK=0 時,確認號無效
- PSH(PuSH):接收 TCP 收到 PSH = 1 的報文段,就儘快地交付接收應用程序,而不再等到整個快取都填滿了後再向上交付
- RST (ReSeT):當 RST=1 時,表明 TCP連線中出現嚴重差錯(如由於主機崩潰或其他原因),必須釋放連線,然後再重新建立運輸連線
- 同步 SYN:同步 SYN = 1 表示這是一個連線請求或連線接受報文
- 終止 FIN:用來釋放一個連線.FIN=1 表明此報文段的傳送端的資料已傳送完畢,並要求釋放運輸連線
- 檢驗和:佔 2 位元組.檢驗和欄位檢驗的範圍包括首部和資料這兩部分.在計算檢驗和時,要在 TCP 報文段的前面加上 12 位元組的偽首部
- 緊急指標:佔 16 位,指出在本報文段中緊急資料共有多少個位元組(緊急資料放在本報文段資料的最前面)
- 選項:長度可變.TCP 最初只規定了一種選項,即最大報文段長度 MSS.MSS 告訴對方TCP:“我的快取所能接收的報文段的資料欄位的最大長度是 MSS 個位元組.” [MSS(Maximum Segment Size)是TCP 報文段中的資料欄位的最大長度.資料欄位加上 TCP 首部才等於整個的 TCP 報文段]
- 填充:這是為了使整個首部長度是 4 位元組的整數倍
- 其他選項:
(1).視窗擴大:佔 3 位元組,其中有一個位元組表示移位值 S.新的視窗值等於TCP 首部中的視窗位數增大到(16 + S),相當於把視窗值向左移動 S 位後獲得實際的視窗大小
(2).時間戳:佔10 位元組,其中最主要的欄位時間戳值欄位(4位元組)和時間戳回送回答欄位(4位元組)
(3).選擇確認:接收方收到了和前面的位元組流不連續的兩2位元組.如果這些位元組的序號都在接收視窗之內,那麼接收方就先收下這些資料,但要把這些資訊準確地告訴傳送方,使傳送方不要再重複傳送這些已收到的資料
資料單位
TCP 傳送的資料單位協議是 TCP 報文段(segment)
特點
TCP 是面向連線的傳輸層協議 每一條 TCP 連線只能有兩個端點(endpoint),每一條 TCP 連線只能是點對點的(一對一) TCP 提供可靠交付的服務 TCP 提供全雙工通訊 面向位元組流
例項
服務端實現
import (
"fmt"
"net"
"os"
)
func CheckErrorS(err error){
if err !=nil{
fmt.Println("網路錯誤",err.Error())
os.Exit(1)
}
}
func Processinfo(conn net.Conn){
buf :=make([]byte,1024)//開創緩衝區
defer conn.Close() //關閉連線
//與客戶端源源不斷地IO
for{
n,err:=conn.Read(buf) //讀取資料
if err !=nil{
break
}
if n !=0{
msg := string(buf[:n])
fmt.Println("收到訊息",msg)
conn.Write([]byte("已閱:"+msg))
if msg=="分手吧"{
break
}
}
}
}
func main() {
//建立TCP伺服器
listener,err:=net.Listen("tcp","127.0.0.1:8898")
defer listener.Close() //關閉網路
CheckErrorS(err)
//迴圈接收,來者不拒
for {
//接入一個客戶端
conn,err:= listener.Accept() //新的客戶端連線
CheckErrorS(err)
//為每一個客戶端開一條獨立的協程與其IO
go Processinfo(conn)
}
}
客戶端實現
import (
"fmt"
"net"
"os"
"bufio"
)
func CheckErrorC(err error){
if err !=nil{
fmt.Println("網路錯誤",err.Error())
os.Exit(1)
}
}
func main() {
conn,err :=net.Dial("tcp","127.0.0.1:8898")//建立TCP伺服器
defer conn.Close() //延遲關閉網路連線
CheckErrorC(err)//檢查錯誤
//建立一個黑視窗(標準輸入)的讀取器
reader := bufio.NewReader(os.Stdin)
buffer := make([]byte, 1024)
for{
lineBytes, _, err := reader.ReadLine()
CheckErrorC(err)
conn.Write(lineBytes)
fmt.Println("傳送訊息")
/*接收訊息*/
n, err := conn.Read(buffer)
CheckErrorC(err)
msg := string(buffer[:n])
fmt.Println("收到服務端訊息:",msg)
}
}
網路通訊3:TCP互動通訊
服務端實現
import (
"fmt"
"net"
"os"
"strings"
)
func CheckErrorS(err error) {
if err != nil {
fmt.Println("網路錯誤", err.Error())
os.Exit(1)
}
}
func Processinfo(conn net.Conn) {
buffer := make([]byte, 1024) //開創緩衝區
defer conn.Close() //關閉連線
for {
n, err := conn.Read(buffer) //讀取資料
CheckErrorS(err)
if n != 0 {
//拿到客戶端地址
remoteAddr := conn.RemoteAddr()
msg := string(buffer[:n])
fmt.Println("收到訊息",msg, "來自", remoteAddr)
if strings.Contains(msg,"錢") {
conn.Write([]byte("fuckoff"))
break
}
conn.Write([]byte("已閱:"+msg))
}
}
}
func main() {
//建立TCP伺服器
listener, err := net.Listen("tcp", "127.0.0.1:8898")
CheckErrorS(err)
defer listener.Close() //關閉網路
fmt.Println("伺服器正在等待")
for {
conn, err := listener.Accept() //新的客戶端連線
CheckErrorS(err)
//處理每一個客戶端
go Processinfo(conn)
}
}
客戶端實現
import (
"fmt"
"net"
"bufio"
"os"
)
func CheckErrorC(err error) {
if err != nil {
fmt.Println("網路錯誤", err.Error())
os.Exit(1)
}
}
func MessageSend(conn net.Conn) {
var msg string
reader := bufio.NewReader(os.Stdin) //讀取鍵盤輸入
for {
lineBytes, _, _ := reader.ReadLine() //讀取一行
msg = string(lineBytes) //鍵盤輸入轉化為字串
if msg == "exit" {
conn.Close()
fmt.Println("客戶端關閉")
break
}
_, err := conn.Write([]byte(msg)) //輸入寫入字串
if err != nil {
conn.Close()
fmt.Println("客戶端關閉")
break
}
}
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8898") //建立TCP伺服器
CheckErrorC(err) //檢查錯誤
defer conn.Close()
//傳送訊息中有阻塞讀取標準輸入的程式碼
//為了避免阻塞住訊息的接收,所以把它獨立的協程中
go MessageSend(conn)
buffer := make([]byte, 1024)
for {
n, err := conn.Read(buffer)
CheckErrorC(err)
msg := string(buffer[:n])
fmt.Println("收到伺服器訊息", msg)
if msg=="fuckoff"{
break
}
}
fmt.Println("連線已斷開")
}
網路通訊4:TCP廣播
服務端實現
import (
"fmt"
"net"
"os"
"strings"
)
//儲存客戶端連線, key,ip埠,value 連結物件
var onlineConnsMap = make(map[string]net.Conn)
//訊息佇列,緩衝區
var messageQueue = make(chan string, 1000)
//訊息,處理程式退出
var quitchan = make(chan bool)
func CheckErrorS(err error) {
if err != nil {
fmt.Println("網路錯誤", err.Error())
os.Exit(1)
}
}
func Processinfo(conn net.Conn) {
buf := make([]byte, 1024) //開創緩衝區
defer func() {
//關閉連線
conn.Close()
fmt.Println("連線已關閉:",conn.RemoteAddr())
}()
for {
n, err := conn.Read(buf) //讀取資料
if err != nil {
fmt.Println("讀取客戶端訊息出錯,err=",err)
break
}
if n != 0 {
//訊息處理,
message := string(buf[0:n])
fmt.Printf("受到客戶端%v訊息:%s\n",conn.RemoteAddr(),message)
//訊息佇列儲存訊息
messageQueue <- message
fmt.Println("該條訊息已加入佇列...")
}else{
fmt.Println("讀取客戶端訊息長度為0")
}
}
}
//訊息的協程
func consumeMessage() {
for {
select {
case message := <-messageQueue: //取出訊息
fmt.Println("已從佇列中取出訊息")
strs := strings.Split(message, "#") //字串切割
if len(strs) > 1 {
//127.0.0.1:12345#去死
//擷取地址並裁減頭尾空白
addr := strs[0]
addr = strings.TrimSpace(addr)
//擷取內容
msg := strs[1]
//給所有客戶端群發此訊息
/*for addr, conn := range onlineConnsMap { //迴圈線上列表
_, err := conn.Write([]byte(msg))
fmt.Println("伺服器傳送訊息", msg, "給", addr)
if err != nil {
fmt.Println("線上連結傳送失敗")
}
}*/
//@某人,發訊息
if conn, ok := onlineConnsMap[addr]; ok {
_, err := conn.Write([]byte(msg))
fmt.Println("伺服器傳送訊息", msg)
if err != nil {
fmt.Println("線上連結傳送失敗")
}
}
}
case <-quitchan: //處理退出
os.Exit(0)
}
}
}
func main() {
//建立TCP伺服器
listener, err := net.Listen("tcp", "127.0.0.1:8898")
CheckErrorS(err)
defer listener.Close() //關閉網路
fmt.Println("伺服器正在等待")
go consumeMessage()
for {
conn, err := listener.Accept() //新的客戶端連線
CheckErrorS(err)
//處理每一個客戶端
addr := fmt.Sprint(conn.RemoteAddr()) //取出地址
onlineConnsMap[addr] = conn
fmt.Println("客戶端列表")
fmt.Println("-------------------")
for key := range onlineConnsMap { //迴圈每一個連結
fmt.Println(key)
}
go Processinfo(conn)
}
}
客戶端呢實現
import (
"fmt"
"net"
"bufio"
"os"
)
func CheckErrorC(err error) {
if err != nil {
fmt.Println("網路錯誤", err.Error())
}
}
func MessageSend(conn net.Conn) {
var input string
for {
reader := bufio.NewReader(os.Stdin) //讀取鍵盤輸入
data, _, _ := reader.ReadLine() //讀取一行
input = string(data) //鍵盤輸入轉化為字串
if input == "exit" {
conn.Close()
fmt.Println("客戶端關閉")
break
}
_, err := conn.Write([]byte(input)) //輸入寫入字串
fmt.Println("傳送訊息", input)
if err != nil {
conn.Close()
fmt.Println("客戶端關閉")
break
}
}
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8898") //建立TCP伺服器
defer conn.Close() //延遲關閉網路連線
CheckErrorC(err) //檢查錯誤
go MessageSend(conn) //開啟一個協程,處理髮送
//conn.Write([]byte("hello nimei"))
//協程,負責收取訊息
buf := make([]byte, 1024)
for {
numOfBytes, err := conn.Read(buf)
CheckErrorC(err)
fmt.Println("收到伺服器訊息", string(buf[:numOfBytes]))
}
fmt.Println("客戶端關閉")
}
網路通訊5:執行HTTP的GET/POST請求
匯入依賴包
import (
"fmt"
"net/http"
"io/ioutil"
"strings"
)
提交GET請求並獲得返回
func main521() {
url := "http://www.baidu.com/s?wd=肉"
resp, err := http.Get(url)
if err != nil {
fmt.Println("錯誤")
}
defer resp.Body.Close()
bodyBytes, _ := ioutil.ReadAll(resp.Body) //讀取資訊
fmt.Println(string(bodyBytes)) //讀取網頁原始碼
}
提交POST請求並獲得返回
func main522() {
//url := "http://www.baidu.com"
url := "https://httpbin.org/post?name=張三"
resp, err := http.Post(
url,
"application/x-www-form-urlencoded",
strings.NewReader("id=nimei"))
if err != nil {
fmt.Println("錯誤")
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body) //讀取資訊
fmt.Println(string(body)) //讀取網頁原始碼
}
網路通訊6:搭建HTTP伺服器
1、Web工作方式
我們平時瀏覽網頁的時候,會開啟瀏覽器,輸入網址後按下回車鍵,然後就會顯示出你想要 瀏覽的內容。在這個看似簡單的使用者行為背後,到底隱藏了些什麼呢?
對於普通的上網過程,系統其實是這樣做的:瀏覽器本身是一個客戶端,當你輸入URL的 時候,首先瀏覽器會去請求DNS伺服器,通過DNS獲取相應的域名對應的IP,然後通過 IP地址找到IP對應的伺服器後,要求建立TCP連線,等瀏覽器傳送完HTTP Request (請求)包後,伺服器接收到請求包之後才開始處理請求包,伺服器呼叫自身服務,返回 HTTP Response(響應)包;客戶端收到來自伺服器的響應後開始渲染這個Response包 裡的主體(body),等收到全部的內容隨後斷開與該伺服器之間的TCP連線。
Web伺服器的工作原理可以簡單地歸納為:
- 客戶機通過TCP/IP協議建立到伺服器的TCP連線
- 客戶端向伺服器傳送HTTP協議請求包,請求伺服器裡的資源文件
- 伺服器向客戶機發送HTTP協議應答包,如果請求的資源包含有動態語言的內容,那麼伺服器會呼叫動態語言的解釋引擎負責處理“動態內容”,並將處理得到的資料返回給客戶端
- 客戶機與伺服器斷開。由客戶端解釋HTML文件,在客戶端螢幕上渲染圖形結果
2.Go如何使得Web工作
前面小節介紹瞭如何通過Go搭建一個Web服務,我們可以看到簡單應用一個net/http包 就方便的搭建起來了。那麼Go在底層到底是怎麼做的呢?
web工作方式的幾個概念
- Request:使用者請求的資訊,用來解析使用者的請求資訊,包括post、get、cookie、url等資訊
- Response:伺服器需要反饋給客戶端的資訊
- Conn:使用者的每次請求連結
- Handler:處理請求和生成返回資訊的處理邏輯
分析 http包執行機制
如下圖所示,是Go實現Web服務的工作模式的流程圖
- 建立Listen Socket, 監聽指定的埠, 等待客戶端請求到來。
- Listen Socket接受客戶端的請求, 得到Client Socket, 接下來通過Client Socket與 客戶端通訊。
- 處理客戶端的請求, 首先從Client Socket讀取HTTP請求的協議頭, 如果是POST 方法, 還可能要讀取客戶端提交的資料,然後交給相應的handler處理請求, handler處理完 畢準備好客戶端需要的資料, 通過Client Socket寫給客戶端。
例項
匯入依賴包
import (
"net/http"
"io/ioutil"
_"net/http/pprof"
)
定義路由處理,並監聽在指定埠
func main() {
//定義對/hello路由的響應
http.HandleFunc(
"/hello",
func(responseWriter http.ResponseWriter, request *http.Request) {
//向響應中寫入客戶端地址
responseWriter.Write([]byte(request.RemoteAddr+";"))
//向響應中寫入客戶端方法
responseWriter.Write([]byte(request.Method))
//向響應中寫入內容
responseWriter.Write([]byte("hello 祥哥 hello go"))
})
//定義對/sister路由的響應
http.HandleFunc(
"/sister",
func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("你妹"))
})
//定義對/fuck路由的響應
http.HandleFunc("/fuck", func(writer http.ResponseWriter, request *http.Request) {
//從本地html檔案中讀入HTML頁面的原始位元組
contentBytes, _ := ioutil.ReadFile("/home/sirouyang/Desktop/demos/W3/day5/02HTTP/fuck.html")
//向客戶端寫出響應
writer.Write(contentBytes)
})
//開啟伺服器並監聽在8080埠
http.ListenAndServe("127.0.0.1:8080", nil)
}