概述
網絡協議
從應用的角度出發,協議可理解為“規則”,是數據傳輸和數據的解釋的規則。假設,A、B雙方欲傳輸文件。規定:
- 第一次,傳輸文件名,接收方接收到文件名,應答OK給傳輸方;
- 第二次,發送文件的尺寸,接收方接收到該數據再次應答一個OK;
- 第三次,傳輸文件內容。同樣,接收方接收數據完成后應答OK表示文件內容接收成功。
由此,無論A、B之間傳遞何種文件,都是通過三次數據傳輸來完成。A、B之間形成了一個最簡單的數據傳輸規則。雙方都按此規則發送、接收數據。A、B之間達成的這個相互遵守的規則即為協議。
這種僅在A、B之間被遵守的協議稱之為原始協議。
當此協議被更多的人采用,不斷的增加、改進、維護、完善。最終形成一個穩定的、完整的文件傳輸協議,被廣泛應用於各種文件傳輸過程中。該協議就成為一個標准協議。最早的ftp協議就是由此衍生而來。
典型協議
應用層
: 常見的協議有HTTP協議,FTP協議。
傳輸層
: 常見協議有TCP/UDP協議。
網絡層
: 常見協議有IP協議、ICMP協議、IGMP協議。
網絡接口層
: 常見協議有ARP協議、RARP協議。
各個協議用途簡述:
IP協議是因特網互聯協議(Internet Protocol)
TCP傳輸控制協議(Transmission Control Protocol)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。
UDP用戶數據報協議(User Datagram Protocol)是OSI參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。
ICMP協議是Internet控制報文協議(Internet Control Message Protocol)它是TCP/IP協議族的一個子協議,用於在IP主機、路由器之間傳遞控制消息。
IGMP協議是 Internet 組管理協議(Internet Group Management Protocol),是因特網協議家族中的一個組播協議。該協議運行在主機和組播路由器之間。
ARP協議是正向地址解析協議(Address Resolution Protocol),通過已知的IP,尋找對應主機的MAC地址。
RARP是反向地址轉換協議,通過MAC地址確定IP地址。
分層模型
網絡分層架構
為了減少協議設計的復雜性,大多數網絡模型均采用分層的方式來組織。每一層都有自己的功能,就像建築物一樣,每一層都靠下一層支持。每一層利用下一層提供的服務來為上一層提供服務,本層服務的實現細節對上層屏蔽。
造車比喻(協議分層)
減少復雜度,解耦
越下面的層,越靠近硬件;越上面的層,越靠近用戶。至於每一層叫什么名字,對應編程而言不重要,但面試的時候,面試官可能會問每一層的名字。
業內普遍的分層方式有兩種。OSI七層模型 和TCP/IP四層模型。可以通過背誦兩個口訣來快速記憶:
OSI七層模型: 應、表、會、傳、網、數、物
TCP/IP四層模型: 應、傳、網、鏈
物理層
: 主要定義物理設備標准,如網線的接口類型、光纖的接口類型、各種傳輸介質的傳輸速率等。它的主要作用是傳輸比特流(就是由1、0轉化為電流強弱來進行傳輸,到達目的地后再轉化為1、0,也就是我們常說的數模轉換與模數轉換)。這一層的數據叫做比特。
數據鏈路層
: 定義了如何讓格式化數據以幀為單位進行傳輸,以及如何讓控制對物理介質的訪問。這一層通常還提供錯誤檢測和糾正,以確保數據的可靠傳輸。如:串口通信中使用到的115200、8、N、1
網絡層
: 在位於不同地理位置的網絡中的兩個主機系統之間提供連接和路徑選擇。Internet的發展使得從世界各站點訪問信息的用戶數大大增加,而網絡層正是管理這種連接的層。
傳輸層
: 定義了一些傳輸數據的協議和端口號(WWW端口80等),如:TCP(傳輸控制協議,傳輸效率低,可靠性強,用於傳輸可靠性要求高,數據量大的數據),UDP(用戶數據報協議,與TCP特性恰恰相反,用於傳輸可靠性要求不高,數據量小的數據,如QQ聊天數據就是通過這種方式傳輸的)。 主要是將從下層接收的數據進行分段和傳輸,到達目的地址后再進行重組。常常把這一層數據叫做段。
會話層
: 通過傳輸層(端口號:傳輸端口與接收端口)建立數據傳輸的通路。主要在你的系統之間發起會話或者接受會話請求(設備之間需要互相認識可以是IP也可以是MAC或者是主機名)。
應用層
: 是最靠近用戶的OSI層。這一層為用戶的應用程序(例如電子郵件、文件傳輸和終端仿真)提供網絡服務。
層與協議
每一層都是為了完成一種功能,為了實現這些功能,就需要大家都遵守共同的規則。大家都遵守這規則,就叫做“協議”(protocol)。
網絡的每一層,都定義了很多協議。這些協議的總稱,叫“TCP/IP協議”。TCP/IP協議是一個大家族,不僅僅只有TCP和IP協議,它還包括其它的協議,如下圖:
協議功能
鏈路層
以太網規定,連入網絡的所有設備,都必須具有“網卡”接口。數據包必須是從一塊網卡,傳送到另一塊網卡。通過網卡能夠使不同的計算機之間連接,從而完成數據通信等功能。網卡的地址——MAC 地址,就是數據包的物理發送地址和物理接收地址。
網卡對應到協議里面就是與鏈路層ARP協議相關的
每個網卡有自己唯一的Mac地址
ARP可以幫助借助IP獲取Mac地址
RARP可以借助Mac地址獲取IP。
網絡層
網絡層的作用是引進一套新的地址,使得我們能夠區分不同的計算機是否屬於同一個子網絡。這套地址就叫做“網絡地址”,這是我們平時所說的IP地址。這個IP地址好比我們的手機號碼,通過手機號碼可以得到用戶所在的歸屬地。
網絡地址幫助我們確定計算機所在的子網絡,MAC 地址則將數據包送到該子網絡中的目標網卡。網絡層協議包含的主要信息是源IP和目的IP。
於是,“網絡層”出現以后,每台計算機有了兩種地址,一種是 MAC 地址,另一種是網絡地址
。兩種地址之間沒有任何聯系,MAC 地址是綁定在網卡上的,網絡地址則是管理員分配的,它們只是隨機組合在一起。
網絡地址幫助我們確定計算機所在的子網絡,MAC 地址則將數據包送到該子網絡中的目標網卡。因此,從邏輯上可以推斷,必定是先處理網絡地址,然后再處理 MAC 地址。
IP地址本質:2進制數。----點分十進制IP地址(string)
傳輸層
當我們一邊聊QQ,一邊聊微信,當一個數據包從互聯網上發來的時候,我們怎么知道,它是來自QQ的內容,還是來自微信的內容?
也就是說,我們還需要一個參數,表示這個數據包到底供哪個程序(進程)使用。這個參數就叫做“端口”(port),它其實是每一個使用網卡的程序的編號。每個數據包都發到主機的特定端口,所以不同的程序就能取到自己所需要的數據。
端口就是在傳輸層指定的。
port -- 在一台主機上唯一標識一個進程
端口特點:
- 對於同一個端口,在不同系統中對應着不同的進程
- 對於同一個系統,一個端口只能被一個進程擁有
常用協議:TCP、UDP
應用層
應用程序收到“傳輸層”的數據,接下來就要進行解讀。由於互聯網是開放架構,數據來源五花八門,必須事先規定好格式,否則根本無法解讀。“應用層”的作用,就是規定應用程序的數據格式。
FTP、HTTP、或自定義協議
對數據進行封裝、解封裝
通信過程
數據通信過程
封裝: 應用層 ----------------- 傳輸層 ---------------- 網絡層 ----------- 鏈路層
解封裝: 鏈路層 ------------- 網路層 ------------- 傳輸層 ------------ 應用層
Socket編程
什么是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(SOCK_DGRAM)。流式是一種面向連接的Socket,針對於面向連接的TCP服務應用;數據報式Socket是一種無連接的Socket,對應於無連接的UDP服務應用。
Socket是典型的雙向全雙工
網絡應用程序設計模式
C/S模式
傳統的網絡應用設計模式,客戶機(client)/服務器(server)模式。需要在通訊兩端各自部署客戶機和服務器來完成數據通信。
B/S模式
瀏覽器(Browser)/服務器(Server)模式。只需在一端部署服務器,而另外一端使用每台PC都默認配置的瀏覽器即可完成數據的傳輸。
優缺點
對於C/S模式來說,其優點明顯。客戶端位於目標主機上可以保證性能,將數據緩存至客戶端本地,從而提高數據傳輸效率。且,一般來說客戶端和服務器程序由一個開發團隊創作,所以他們之間所采用的協議相對靈活。可以在標准協議的基礎上根據需求裁剪及定制。例如,騰訊所采用的通信協議,即為ftp協議的修改剪裁版。
因此,傳統的網絡應用程序及較大型的網絡應用程序都首選C/S模式進行開發。如,知名的網絡游戲魔獸世界。3D畫面,數據量龐大,使用C/S模式可以提前在本地進行大量數據的緩存處理,從而提高觀感。
C/S模式的缺點也較突出。由於客戶端和服務器都需要有一個開發團隊來完成開發。工作量將成倍提升,開發周期較長。另外,從用戶角度出發,需要將客戶端安插至用戶主機上,對用戶主機的安全性構成威脅。這也是很多用戶不願使用C/S模式應用程序的重要原因。
B/S模式相比C/S模式而言,由於它沒有獨立的客戶端,使用標准瀏覽器作為客戶端,其工作開發量較小。只需開發服務器端即可。另外由於其采用瀏覽器顯示數據,因此移植性非常好,不受平台限制。如早期的偷菜游戲,在各個平台上都可以完美運行。
B/S模式的缺點也較明顯。由於使用第三方瀏覽器,因此網絡應用支持受限。另外,沒有客戶端放到對方主機上,緩存數據不盡如人意,從而傳輸數據量受到限制。應用的觀感大打折扣。第三,必須與瀏覽器一樣,采用標准http協議進行通信,協議選擇不靈活。
因此在開發過程中,模式的選擇由上述各自的特點決定。根據實際需求選擇應用程序設計模式。
TCP的C/S架構
服務器首先啟動一個net.Listen(),這個net.Listen()從名字上看好像是啟動一個監聽,實際上這是由於套接字socket最早期設計的原因,在Go語言設計的時候還是沿用了Unix當初設計的思想,直接把函數名拿過來了,這個函數初學的同學都會有一個誤解,認為它是監聽,實際上它不是,這個listen()函數不是真正的監聽客戶端,要監聽的話監聽什么?要監聽客戶端和我的連接,但是這個Listen不是監聽客戶端,而是我設置服務器監聽的資源(IP、端口),Accept()才是真正監聽的,那言外之意,監聽嘛,我等着你對我進行訪問吧,那就是說,你沒訪問我之前是不是應該一直處於等待狀態,一會兒我們寫程序看一下,是在Listen()的時候等着還是在Accept的時候等着,所以Accept是表示接受的意思,當它Accpet調用起來以后,它就等着客戶端和我建立連接,比方說,圖示上已經說了,它會阻塞等待用戶建立連接,那言外之意,我沒有用戶建立連接之前它就一直阻塞在那里等待着,實際上監聽是在Accept的時候才發起的,當然Accept不是無源之水,它必要Listen設置好了連接方式(tcp還是udp)、IP地址以及端口以后才能阻塞去監聽,當有一個客戶端和服務器發起請求之后,我調Accept()函數完成了,那就說明我服務器和客戶端之間的連接建立好了,接來下干什么呢?進行數據傳輸,我建立好連接的目的就是為了進行數據傳遞,我們這里假定那一般實際上也是這樣,客戶端主動找服務器建立連接,連接建立好了,客戶端先發送數據給服務器,服務器被動的接受客戶端發來的請求,被動接受客戶端請求數據,接受到了請求以后,服務器進行相應的分析處理,處理完以后把你要請求的數據回寫回去,服務端Read()是讀取客戶端發送過來的請求,Write()是我把你的請求處理完之后再給你寫回去,當這些都做完了,說明我們跟客戶端的一次通信就完成了,那這個時候我們就可以關閉連接。當然如果你還想后續繼續通信的話,這個close()關閉就要延遲。
客戶端這個流程很簡單,因為服務器先要站出來在那兒等着客戶端和我建立連接,所以說,服務器就得先啟動,客戶端相當於是我得等你服務器啟動起來以后你都准備好了,我在給你發送訪問請求,客戶端發送訪問請求,也是調用一個函數,叫做net.Dail()函數,這個Dail()函數會對阻塞的Accept()發送一個請求,如果服務器准備好,Accept()返回的時候,Dail也返回,咱們就說客戶端和服務器建立好了連接,客戶端先發送數據,所以客戶端先是一個寫操作,發送完數據,服務器那邊讀到客戶端請求進行處理,處理完之后寫回來,客戶端再Read()讀取服務器寫回來的數據,讀完以后客戶端也可以做簡單處理,比方說我讀到了以后,打印顯示,完成了寫,完成了讀,一次跟網絡端的通信也就完成了,客戶端可以關閉連接,大致的流程就是這樣。
前面說過,socket通信,既然要通信,至少得是一對,如上圖所示,Accpet()和Dail()成功后都會返回一個socket。
其實Listen()的時候也會創建一個socket,但是這個socket不是用於通信的,只是創建用於通信的socket,綁定IP地址和端口設置監聽的
簡單的C/S模型通信
Server端
Listen函數:
network: 選用的協議:TCP、UDP 如: "tcp"或"udp"
address:IP地址+端口號 如: "127.0.0.1:8000"或":8000"
Listener接口:
Conn接口:
參看 https://studygolang.com/pkgdoc 中文幫助文檔中的demo:
TCP服務器端代碼:
package main
import (
"fmt"
"net"
)
func main() {
//指定服務器 通信協議,IP地址,port. 創建一個用戶監聽的socket
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("服務器設置監聽失敗,原因是:%v\n", err)
return
}
defer listener.Close()
fmt.Println("服務器等待客戶端建立連接...")
//阻塞監聽客戶端連接請求,成功建立連接,返回用於通信的socket
conn, err := listener.Accept()
if err != nil {
fmt.Printf("服務器監聽失敗,原因是:%v\n", err)
}
defer conn.Close()
fmt.Println("服務器與客戶端成功建立連接!!!")
//讀取客戶端發送的數據
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
fmt.Printf("Conn Read()錯誤,原因是:%v\n", err)
}
//處理數據 -- 打印
fmt.Println("服務器讀到數據:", string(buf[:n]))
}
運行代碼
利用nc模式客戶端請求
再次查看運行的終端,可以發現已經成功建立了連接
我們用nc連接后,還可以發送數據
再次查看運行的終端
如圖,在整個通信過程中,服務器端有兩個socket參與進來,但用於通信的只有 conn 這個socket。它是由 listener創建的。隸屬於服務器端。
Client端
Dial函數:
network: 選用的協議:TCP、UDP 如: "tcp"或"udp"
address:IP地址+端口號 如: "127.0.0.1:8000"或":8000"
Conn接口:
客戶端代碼:
package main
import (
"fmt"
"net"
)
func main() {
//指定服務器IP+port創建 通信套接字
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("net.Dial err:%v\n", err)
return
}
defer conn.Close()
//主動寫數據給服務器
_, err = conn.Write([]byte("Are you ready?"))
if err != nil {
fmt.Printf("conn.Write err:%v\n", err)
return
}
buf := make([]byte, 1024)
//接受服務器回發的數據
n, err := conn.Read(buf)
if err != nil {
fmt.Printf("conn.Read err:%v\n", err)
return
}
fmt.Printf("服務器回發的數據為:%v\n", string(buf[:n]))
}
由於我們想要服務器端回寫內容,所以需要修改一下之前的服務端代碼
更新服務器端代碼:
package main
import (
"fmt"
"net"
)
func main() {
//指定服務器 通信協議,IP地址,port. 創建一個用戶監聽的socket
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("服務器設置監聽失敗,原因是:%v\n", err)
return
}
defer listener.Close()
fmt.Println("服務器等待客戶端建立連接...")
//阻塞監聽客戶端連接請求,成功建立連接,返回用於通信的socket
conn, err := listener.Accept()
if err != nil {
fmt.Printf("服務器監聽失敗,原因是:%v\n", err)
}
defer conn.Close()
fmt.Println("服務器與客戶端成功建立連接!!!")
//讀取客戶端發送的數據
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
fmt.Printf("Conn Read()錯誤,原因是:%v\n", err)
}
//處理數據 -- 回寫給客戶端
if string(buf[:n]) == "Are you ready?" {
conn.Write([]byte("I am ready!!!"))
} else {
conn.Write([]byte("I don`t know what you say!!!"))
}
}
並發的C/S模型通信
並發Server
現在已經完成了客戶端與服務端的通信,但是服務端只能接收一個用戶發送過來的數據,怎樣接收多個客戶端發送過來的數據,實現一個高效的並發服務器呢?
Accept()函數的作用是等待客戶端的鏈接,如果客戶端沒有鏈接,該方法會阻塞。如果有客戶端鏈接,那么該方法返回一個Socket負責與客戶端進行通信。所以,每來一個客戶端,該方法就應該返回一個Socket與其通信,因此,可以使用一個死循環,將Accept()調用過程包裹起來。
需要注意的是,實現並發處理多個客戶端數據的服務器,就需要針對每一個客戶端連接,單獨產生一個Socket,並創建一個單獨的goroutine與之完成通信。
func main() {
//創建監聽套接字
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("net.Listen() err:%v\n", err)
return
}
defer listener.Close()
//監聽客戶端連接請求
for {
fmt.Println("服務器等待客戶端連接...")
conn, err := listener.Accept()
if err != nil {
fmt.Printf("listener.Accept() err:%v\n", err)
return
}
//具體完成服務器和客戶端的數據通信
go HandleConnect(conn)
}
}
將客戶端的數據處理工作封裝到HandleConn方法中,需將Accept()返回的Socket傳遞給該方法,變量conn的類型為:net.Conn。可以使用conn.RemoteAddr()來獲取成功與服務器建立連接的客戶端IP地址和端口號:
客戶端可能持續不斷的發送數據,因此接收數據的過程可以放在for循環中,服務端也持續不斷的向客戶端返回處理后的數據。
func HandleConnect(conn net.Conn) {
defer conn.Close()
//獲取連接的客戶端 Addr
addr := conn.RemoteAddr()
fmt.Println(addr, "客戶端成功連接!")
//循環讀取客戶端發送數據
buf := make([]byte, 1024)
for {
//注意,read讀取時,會將命令行里的換行符也給讀取了,在*Unix上換行符是\n,在Windows上時\r\n
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("客戶端退出了!!!")
break
} else {
fmt.Printf("conn.Read() err:%v\n", err)
return
}
}
fmt.Printf("服務器讀到數據:%v", string(buf[:n]))
//小寫轉大寫,回發給客戶端
conn.Write(bytes.ToUpper(buf[:n]))
}
}
並發Client
客戶端不僅需要持續的向服務端發送數據,同時也要接收從服務端返回的數據。因此可將發送和接收放到不同的協程中。
主協程循環接收服務器回發的數據(該數據應已轉換為大寫),並打印至屏幕;子協程循環從鍵盤讀取用戶輸入數據,寫給服務器。讀取鍵盤輸入可使用 os.Stdin.Read(str)。定義切片str,將讀到的數據保存至str中。
這樣,客戶端也實現了多任務。
客戶端代碼:
package main
import (
"fmt"
"io"
"net"
"os"
)
func main() {
//發起連接請求
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("net.Dial() err:%v\n", err)
return
}
defer conn.Close()
//獲取用戶鍵盤輸入(os.Stdin),並將輸入數據發送給服務器
go func() {
str := make([]byte, 1024)
for {
n, err := os.Stdin.Read(str)
if err != nil {
fmt.Printf("os.Stdin.Read() err:%v\n", err)
continue
}
//寫給服務器,讀多少,寫多少
conn.Write(str[:n])
}
}()
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("服務端退出了!!!")
return
} else {
fmt.Printf("conn.Read() err:%v\n", err)
continue
}
}
fmt.Printf("客戶端讀到服務器回發數據:%s",buf[:n])
}
}
TCP通信
下圖是一次TCP通訊的時序圖。TCP連接建立斷開。包含大家熟知的三次握手和四次揮手。
在這個例子中,首先客戶端主動發起連接、發送請求,然后服務器端響應請求,然后客戶端主動關閉連接。兩條豎線表示通訊的兩端,從上到下表示時間的先后順序。注意,數據從一端傳到網絡的另一端也需要時間,所以圖中的箭頭都是斜的。
三次握手
所謂三次握手(Three-Way Handshake)即建立TCP連接,就是指建立一個TCP連接時,需要客戶端和服務端總共發送3個包以確認連接的建立。好比兩個人在打電話:
Client: "喂,你能聽得到嗎?"
Server: "我聽得到,你聽得到我嗎?"
Client: "我能聽到你,今天balabala..."
建立連接(三次握手)的過程:
- 客戶端發送一個帶SYN標志的TCP報文到服務器。這是上圖中三次握手過程中的段1。客戶端發出SYN位表示連接請求。序號是1000,這個序號在網絡通訊中用作臨時的地址,每發一個數據字節,這個序號要加1,這樣在接收端可以根據序號排出數據包的正確順序,也可以發現丟包的情況。
另外,規定SYN位和FIN位也要占一個序號,這次雖然沒發數據,但是由於發了SYN位,因此下次再發送應該用序號1001。
mss表示最大段尺寸,如果一個段太大,封裝成幀后超過了鏈路層的最大長度,就必須在IP層分片,為了避免這種情況,客戶端聲明自己的最大段尺寸,建議服務器端發來的段不要超過這個長度。 - 服務器端回應客戶端,是三次握手中的第2個報文段,同時帶ACK標志和SYN標志。表示對剛才客戶端SYN的回應;同時又發送SYN給客戶端,詢問客戶端是否准備好進行數據通訊。
服務器發出段2,也帶有SYN位,同時置ACK位表示確認,確認序號是1001,表示“我接收到序號1000及其以前所有的段,請你下次發送序號為1001的段”,也就是應答了客戶端的連接請求,同時也給客戶端發出一個連接請求,同時聲明最大尺寸為1024。 - 客戶必須再次回應服務器端一個ACK報文,這是報文段3。
客戶端發出段3,對服務器的連接請求進行應答,確認序號是8001。在這個過程中,客戶端和服務器分別給對方發了連接請求,也應答了對方的連接請求,其中服務器的請求和應答在一個段中發出。
因此一共有三個段用於建立連接,稱為“三方握手”。在建立連接的同時,雙方協商了一些信息,例如,雙方發送序號的初始值、最大段尺寸等。
數據傳輸的過程:
- 客戶端發出段4,包含從序號1001開始的20個字節數據。
- 服務器發出段5,確認序號為1021,對序號為1001-1020的數據表示確認收到,同時請求發送序號1021開始的數據,服務器在應答的同時也向客戶端發送從序號8001開始的10個字節數據。
- 客戶端發出段6,對服務器發來的序號為8001-8010的數據表示確認收到,請求發送序號8011開始的數據。
在數據傳輸過程中,ACK和確認序號是非常重要的,應用程序交給TCP協議發送的數據會暫存在TCP層的發送緩沖區中,發出數據包給對方之后,只有收到對方應答的ACK段才知道該數據包確實發到了對方,可以從發送緩沖區中釋放掉了,如果因為網絡故障丟失了數據包或者丟失了對方發回的ACK段,經過等待超時后TCP協議自動將發送緩沖區中的數據包重發。
四次揮手
所謂四次揮手(Four-Way-Wavehand)即終止TCP連接,就是指斷開一個TCP連接時,需要客戶端和服務端總共發送4個包以確認連接的斷開。在socket編程中,這一過程由客戶端或服務器任一方執行close來觸發。好比兩個人打完電話要掛斷:
Client: "我要說的事情都說完了,我沒事了。掛啦?"
Server: "等下,我還有一個事兒。Balabala…"
Server: "好了,我沒事兒了。掛了啊。"
Client: "ok!拜拜"
關閉連接(四次握手)的過程:
由於TCP連接是全雙工的,因此每個方向都必須單獨進行關閉。這原則是當一方完成它的數據發送任務后就能發送一個FIN來終止這個方向的連接。收到一個 FIN只意味着這一方向上沒有數據流動,一個TCP連接在收到一個FIN后仍能發送數據。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。
- 客戶端發出段7,FIN位表示關閉連接的請求。
- 服務器發出段8,應答客戶端的關閉連接請求。
- 服務器發出段9,其中也包含FIN位,向客戶端發送關閉連接請求。
- 客戶端發出段10,應答服務器的關閉連接請求。
建立連接的過程是三次握手,而關閉連接通常需要4個段,服務器的應答和關閉連接請求通常不合並在一個段中,因為有連接半關閉的情況,這種情況下客戶端關閉連接之后就不能再發送數據給服務器了,但是服務器還可以發送數據給客戶端,直到服務器也關閉連接為止。
下圖是TCP狀態轉換圖
UDP通信
在之前的案例中,我們一直使用的是TCP協議來編寫Socket的客戶端與服務端。其實也可以使用UDP協議來編寫Socket的客戶端與服務端。
UDP服務器
由於UDP是“無連接”的,所以,服務器端不需要額外創建監聽套接字,只需要指定好IP和port,然后監聽該地址,等待客戶端與之建立連接,即可通信。
-
創建監聽地址
func ResolveUDPAddr(network, address string) (*UDPAddr, error)
ResolveUDPAddr將addr作為UDP地址解析並返回。參數addr格式為"host:port"或"[ipv6-host%zone]:port",解析得到網絡名和端口名;net必須是"udp"、"udp4"或"udp6"。
IPv6地址字面值/名稱必須用方括號包起來,如"[::1]:80"、"[ipv6-host]:http"或"[ipv6-host%zone]:80"。 -
創建用於通信的socket
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
ListenUDP創建一個接收目的地是本地地址laddr的UDP數據包的網絡連接。net必須是"udp"、"udp4"、"udp6";如果laddr端口為0,函數將選擇一個當前可用的端口,可以用Listener的Addr方法獲得該端口。返回的*UDPConn的ReadFrom和WriteTo方法可以用來發送和接收UDP數據包(每個包都可獲得來源地址或設置目標地址)。
-
接受UDP數據
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)
ReadFromUDP從c讀取一個UDP數據包,將有效負載拷貝到b,返回拷貝字節數和數據包來源地址。
ReadFromUDP方法會在超過一個固定的時間點之后超時,並返回一個錯誤。
-
寫出數據到UDP
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
WriteToUDP通過c向地址addr發送一個數據包,b為包的有效負載,返回寫入的字節。
WriteToUDP方法會在超過一個固定的時間點之后超時,並返回一個錯誤。在面向數據包的連接上,寫入超時是十分罕見的。
服務器端代碼:
package main
import (
"fmt"
"net"
)
func main() {
//0.本應從步驟1開始,但是在寫步驟1的時候發現,步驟1還需要*UDPAddr類型的參數,所以需要先創建一個*DUPAddr
//組織一個udp地址結構,指定服務器的IP+port
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("net.ResolveUDPAddr()函數執行出錯,錯誤為:%v\n", err)
return
}
fmt.Printf("UDP服務器地址結構創建完成!!!\n")
//1.創建用戶通信的socket
//由於ListenUDP需要一個*UDPAddr類型的參數,所以我們還需要先創建一個監聽地址
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
fmt.Printf("net.ListenUDP()函數執行出錯,錯誤為:%v\n", err)
return
}
defer udpConn.Close()
fmt.Printf("UDP服務器通信socket創建完成!!!\n")
//2.讀取客戶端發送的數據(阻塞發生在ReadFromUDP()方法中)
buf := make([]byte, 4096)
//ReadFromUDP()方法返回三個值,分別是讀取到的字節數,客戶端的地址,error
n, clientUDPAddr, err := udpConn.ReadFromUDP(buf)
if err != nil {
fmt.Printf("*UDPAddr.ReadFromUDP()方法執行出錯,錯誤為:%v\n", err)
return
}
//3.模擬處理數據
fmt.Printf("服務器讀到%v的數據:%s",clientUDPAddr, buf[:n])
//4.回寫數據給客戶端
_, err = udpConn.WriteToUDP([]byte("I am OK!"), clientUDPAddr)
if err != nil {
fmt.Printf("*UDPAddr.WriteToUDP()方法執行出錯,錯誤為:%v\n", err)
return
}
}
運行代碼
通過nc請求測試
服務端讀取請求數據,並回寫"I am OK!"
UDP客戶端
udp客戶端的編寫與TCP客戶端的編寫,基本上是一樣的,只是將協議換成udp.代碼如下:
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("udp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("net.Dial()函數執行出錯,錯誤為:%v\n", err)
return
}
defer conn.Close()
conn.Write([]byte("hello, I`m a client in UDP!"))
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
fmt.Printf("Conn.Read()方法執行出錯,錯誤為:%v\n", err)
return
}
fmt.Printf("服務器發來數據:%s\n", buf[:n])
}
並發
其實對於UDP而言,服務器不需要並發,只要循環處理客戶端數據即可。客戶端也等同於TCP通信並發的客戶端。
服務器:
package main
import (
"fmt"
"net"
)
func main() {
//0.本應從步驟1開始,但是在寫步驟1的時候發現,步驟1還需要*UDPAddr類型的參數,所以需要先創建一個*DUPAddr
//組織一個udp地址結構,指定服務器的IP+port
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("net.ResolveUDPAddr()函數執行出錯,錯誤為:%v\n", err)
return
}
fmt.Printf("UDP服務器地址結構創建完成!!!\n")
//1.創建用戶通信的socket
//由於ListenUDP需要一個*UDPAddr類型的參數,所以我們還需要先創建一個監聽地址
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
fmt.Printf("net.ListenUDP()函數執行出錯,錯誤為:%v\n", err)
return
}
defer udpConn.Close()
fmt.Printf("UDP服務器通信socket創建完成!!!\n")
for {
//2.讀取客戶端發送的數據(阻塞發生在ReadFromUDP()方法中)
buf := make([]byte, 4096)
//ReadFromUDP()方法返回三個值,分別是讀取到的字節數,客戶端的地址,error
n, clientUDPAddr, err := udpConn.ReadFromUDP(buf)
if err != nil {
fmt.Printf("*UDPAddr.ReadFromUDP()方法執行出錯,錯誤為:%v\n", err)
continue
}
//3.模擬處理數據
fmt.Printf("服務器讀到%v的數據:%s\n",clientUDPAddr, buf[:n])
//4.回寫數據給客戶端
_, err = udpConn.WriteToUDP([]byte("I am OK!"), clientUDPAddr)
if err != nil {
fmt.Printf("*UDPAddr.WriteToUDP()方法執行出錯,錯誤為:%v\n", err)
continue
}
}
}
客戶端:
package main
import (
"fmt"
"net"
"os"
)
func main() {
conn, err := net.Dial("udp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("net.Dial()函數執行出錯,錯誤為:%v\n", err)
return
}
defer conn.Close()
go func() {
buf := make([]byte, 4096)
for {
//從鍵盤讀取內容,放入buf
n, err := os.Stdin.Read(buf)
if err != nil {
fmt.Printf("os.Stdin.Read()執行出錯,錯誤為:%v\n", err)
return
}
//給服務器發送
conn.Write(buf[:n])
}
}()
for {
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
fmt.Printf("Conn.Read()方法執行出錯,錯誤為:%v\n", err)
return
}
fmt.Printf("服務器發來數據:%s\n", buf[:n])
}
}
UDP與TCP的差異
TCP | UDP |
---|---|
面向連接 | 面向無連接 |
要求系統資源較多 | 要求系統資源較少 |
TCP程序結構復雜 | UDP程序結構比較簡單 |
使用流式 | 使用數據包式 |
保證數據准確性 | 不保證數據准確性 |
保證數據順序 | 不保證數據順序 |
通訊速度較慢 | 通訊速度較快 |
使用場景
TCP: 對數據傳輸安全性、穩定性要求高的場合。網絡文件傳輸。下載、上傳。
UDP: 對數據實時傳輸要求較高的場合。視頻直播、在線電話會議。游戲
相關文章: