TCP通信過程
下圖是一次TCP通訊的時序圖。TCP連接建立斷開。包含大家熟知的三次握手和四次握手。
在這個例子中,首先客戶端主動發起連接、發送請求,然后服務器端響應請求,然后客戶端主動關閉連接。兩條豎線表示通訊的兩端,從上到下表示時間的先后順序。注意,數據從一端傳到網絡的另一端也需要時間,所以圖中的箭頭都是斜的。
三次握手 建立連接
建立連接(三次握手)的過程:
- 客戶端發送一個帶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協議自動將發送緩沖區中的數據包重發。
總結:
3次握手:
1、主動: 發送 SYN 標志位。
2、被動:接收 SYN、同時回復 ACK 並且發送SYN
3、主動: 發送 ACK 標志位。 ―――――― Accpet() / Dial()
四次揮手
關閉連接(四次握手)的過程:
由於TCP連接是全雙工的,因此每個方向都必須單獨進行關閉。這原則是當一方完成它的數據發送任務后就能發送一個FIN來終止這個方向的連接。收到一個 FIN只意味着這一方向上沒有數據流動,一個TCP連接在收到一個FIN后仍能發送數據。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。
- 客戶端發出段7,FIN位表示關閉連接的請求。
- 服務器發出段8,應答客戶端的關閉連接請求。
- 服務器發出段9,其中也包含FIN位,向客戶端發送關閉連接請求。
- 客戶端發出段10,應答服務器的關閉連接請求。
建立連接的過程是三次握手,而關閉連接通常需要4個段,服務器的應答和關閉連接請求通常不合並在一個段中,因為有連接半關閉的情況,這種情況下客戶端關閉連接之后就不能再發送數據給服務器了,但是服務器還可以發送數據給客戶端,直到服務器也關閉連接為止。
總結:
4次揮手:
1、主動關閉連接:發送 FIN 標志位。
2、被動關閉連接:接收 FIN、同時回復 ACK ―― 半關閉完成。
3、被動關閉連接:發送 FIN 標志位。
4、主動關閉連接:接收 FIN、同時回復 ACK ―― Close()/Close() ―― 4次揮手完成。
TCP狀態轉換
TCP狀態圖很多人都知道,它對排除和定位網絡或系統故障時大有幫助。如果能熟練掌握這張圖,了解圖中的每一個狀態,能大大提高我們對於TCP的理解和認識。下面對這張圖的11種狀態詳細解析一下,以便加強記憶!不過在這之前,一定要熟練掌握TCP建立連接的三次握手過程,以及關閉連接的四次揮手過程。
CLOSED:表示初始狀態。
LISTEN:該狀態表示服務器端的某個SOCKET處於監聽狀態,可以接受連接。
SYN_SENT:這個狀態與SYN_RCVD遙相呼應,當客戶端SOCKET執行CONNECT連接時,它首先發送SYN報文,隨即進入到了SYN_SENT狀態,並等待服務端的發送三次握手中的第2個報文。SYN_SENT狀態表示客戶端已發送SYN報文。
SYN_RCVD: 該狀態表示接收到SYN報文,在正常情況下,這個狀態是服務器端的SOCKET在建立TCP連接時的三次握手會話過程中的一個中間狀態,很短暫。此種狀態時,當收到客戶端的ACK報文后,會進入到ESTABLISHED狀態。
ESTABLISHED:表示連接已經建立。
FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2狀態的真正含義都是表示等待對方的FIN報文。區別是:
FIN_WAIT_1狀態是當socket在ESTABLISHED狀態時,想主動關閉連接,向對方發送了FIN報文,此時該socket進入到FIN_WAIT_1狀態。
FIN_WAIT_2狀態是當對方回應ACK后,該socket進入到FIN_WAIT_2狀態,正常情況下,對方應馬上回應ACK報文,所以FIN_WAIT_1狀態一般較難見到,而FIN_WAIT_2狀態可用netstat看到。
FIN_WAIT_2:主動關閉鏈接的一方,發出FIN收到ACK以后進入該狀態。稱之為半連接或半關閉狀態。該狀態下的socket只能接收數據,不能發。
TIME_WAIT: 表示收到了對方的FIN報文,並發送出了ACK報文,等2MSL后即可回到CLOSED可用狀態。如果FIN_WAIT_1狀態下,收到對方同時帶 FIN標志和ACK標志的報文時,可以直接進入到TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。
CLOSING: 這種狀態較特殊,屬於一種較罕見的狀態。正常情況下,當你發送FIN報文后,按理來說是應該先收到(或同時收到)對方的 ACK報文,再收到對方的FIN報文。但是CLOSING狀態表示你發送FIN報文后,並沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。什么情況下會出現此種情況呢?如果雙方幾乎在同時close一個SOCKET的話,那么就出現了雙方同時發送FIN報文的情況,也即會出現CLOSING狀態,表示雙方都正在關閉SOCKET連接。
CLOSE_WAIT: 此種狀態表示在等待關閉。當對方關閉一個SOCKET后發送FIN報文給自己,系統會回應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態。接下來呢,察看是否還有數據發送給對方,如果沒有可以 close這個SOCKET,發送FIN報文給對方,即關閉連接。所以在CLOSE_WAIT狀態下,需要關閉連接。
LAST_ACK: 該狀態是被動關閉一方在發送FIN報文后,最后等待對方的ACK報文。當收到ACK報文后,即可以進入到CLOSED可用狀態。
2MSL (Maximum Segment Lifetime) 和與之對應的TIME_WAIT狀態,可以讓4次握手關閉流程更加可靠。4次握手的最后一個ACK是是由主動關閉方發送出去的,若這個ACK丟失,被動關閉方會再次發一個FIN過來。若主動關閉方能夠保持一個2MSL的TIME_WAIT狀態,則有更大的機會讓丟失的ACK被再次發送出去。注意,TIME_WAIT狀態一定出現在主動關閉這一方。
總結:
TCP狀態轉換:
1. 主動端:
CLOSE --> SYN --> SYN_SEND狀態 --> ESTABLISHED狀態(數據通信期間處於的狀態) ---> FIN --> FIN_WAIT_1狀態。
---> 接收 ACK ---> FIN_WAIT_2狀態 (半關閉―― 只出現在主動端) ---> 接收FIN、回ACK ――> TIME_WAIT (等2MSL)
---> 確保最后一個ACK能被對端收到。(只出現在主動端)
2. 被動端:
CLOSE --> LISTEN ---> ESTABLISHED狀態(數據通信期間處於的狀態) ---> 接收 FIN、回復ACK -->
CLOSE_WAIT(對應 對端處於 半關閉) --> 發送FIN --> LAST_ACK ---> 接收ACK ---> CLOSE
查看狀態命令:
windows:netstat -an | findstr 8001(端口號)
Linux: netstat -an | grep 8001
UDP通信
UDP服務器
由於UDP是“無連接”的,所以,服務器端不需要額外創建監聽套接字,只需要指定好IP和port,然后監聽該地址,等待客戶端與之建立連接,即可通信。
創建監聽地址: func ResolveUDPAddr(network, address string) (*UDPAddr, error) 創建用戶通信的socket: func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error) 接收udp數據: func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error) 寫出數據到udp: func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
服務端完整代碼實現如下:
UDP簡單服務器:
1. 獲取 服務器的 UDP地址結構體 srvAddr := ResolveUDPAddr(“udp”,“IP+port”)
2. 創建 用於數據通信套接字。 conn := ListenUDP("udp", srvAddr )
3. 讀取客戶端發送數據。 n, cltAddr, err := conn.ReadFromUDP(buf)
4. 回寫數據給客戶端。 conn.WriteToUDP("數據內容", cltAddr )

package main import ( "fmt" "net" ) func main() { //創建監聽的地址,並且指定udp協議 udp_addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8002") if err != nil { fmt.Println("ResolveUDPAddr err:", err) return } conn, err := net.ListenUDP("udp", udp_addr) //創建數據通信socket if err != nil { fmt.Println("ListenUDP err:", err) return } defer conn.Close() buf := make([]byte, 1024) n, raddr, err := conn.ReadFromUDP(buf) //接收客戶端發送過來的數據,填充到切片buf中。 if err != nil { return } fmt.Println("客戶端發送:", string(buf[:n])) _, err = conn.WriteToUDP([]byte("nice to see u in udp"), raddr) // 向客戶端發送數據 if err != nil { fmt.Println("WriteToUDP err:", err) return } }
UDP客戶端
udp客戶端的編寫與TCP客戶端的編寫,基本上是一樣的,只是將協議換成udp。注意只能使用小寫。
UDP客戶端:
與TCP通信客戶端實現手法一致。
net.Dial("udp", server 的IP+port)
代碼如下:

package main import ( "net" "fmt" ) func main() { conn, err := net.Dial("udp", "127.0.0.1:8002") if err != nil { fmt.Println("net.Dial err:", err) return } defer conn.Close() conn.Write([]byte("Hello! I'm client in UDP!")) buf := make([]byte, 1024) n, err1 := conn.Read(buf) if err1 != nil { return } fmt.Println("服務器發來:", string(buf[:n])) }
並發
其實對於UDP而言,服務器不需要並發,只要循環處理客戶端數據即可。客戶端也等同於TCP通信並發的客戶端。
UDP並發服務器: ―――― UDP 默認支持並發。
1. 獲取 服務器的 UDP地址結構體 srvAddr := ResolveUDPAddr(“udp”,“IP+port”)
2. 創建 用於數據通信套接字。 conn := ListenUDP("udp", srvAddr )
3. for 循環 讀取客戶端發送的數據 for {
n, cltAddr, err := conn.ReadFromUDP(buf)
}
4. 創建 go 程 完成 寫操作,提高程序的並行效率。
go func() {
conn.WriteToUDP("數據內容", cltAddr )
}()
5.由於UDP沒有建立連接過程。所以 TCP 通信狀態 對於 UDP 無效。
服務器:

package main import ( "net" "fmt" ) func main() { // 創建 服務器 UDP 地址結構。指定 IP + port laddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8003") if err != nil { fmt.Println("ResolveUDPAddr err:", err) return } // 監聽 客戶端連接 conn, err := net.ListenUDP("udp", laddr) if err != nil { fmt.Println("net.ListenUDP err:", err) return } defer conn.Close() for { buf := make([]byte, 1024) n, raddr, err := conn.ReadFromUDP(buf) if err != nil { fmt.Println("conn.ReadFromUDP err:", err) return } fmt.Printf("接收到客戶端[%s]:%s", raddr, string(buf[:n])) conn.WriteToUDP([]byte("I-AM-SERVER"), raddr) // 簡單回寫數據給客戶端 } }
客戶端:
UDP並發客戶端:
並發讀取 鍵盤 和 conn。 編碼實現參考 TCP 並發客戶端實現。
修改內容: net.Dial("udp", server 的IP+port)

package main import ( "net" "os" "fmt" ) func main() { conn, err := net.Dial("udp", "127.0.0.1:8003") if err != nil { fmt.Println("net.Dial err:", err) return } defer conn.Close() go func() { str := make([]byte, 1024) for { n, err := os.Stdin.Read(str) //從鍵盤讀取內容, 放在str if err != nil { fmt.Println("os.Stdin. err1 = ", err) return } conn.Write(str[:n]) // 給服務器發送 } }() buf := make([]byte, 1024) for { n, err := conn.Read(buf) if err != nil { fmt.Println("conn.Read err:", err) return } fmt.Println("服務器寫來:", string(buf[:n])) } }
UDP與TCP的差異
TCP |
UDP |
面向連接 |
面向無連接 |
要求系統資源較多 |
要求系統資源較少 |
TCP程序結構較復雜 |
UDP程序結構較簡單 |
使用流式 |
使用數據包式 |
保證數據准確性 |
不保證數據准確性 |
保證數據順序 |
不保證數據順序 |
通訊速度較慢 |
通訊速度較快 |
文件傳輸
網絡文件傳輸:思路
發送端:(client)
1. 建立連接請求 net.Dial() ――> conn defer conn.Close()
2. 通過命令行參數,提取 文件名(帶路徑) os.Args
3. 獲取文件屬性 ,提取 文件名(不帶路徑)os.Stat()
4. 發送文件名 給 接收端 conn.Write
5. 接收對端回發的數據,確認是否是“ok”
6. 發送文件內容 給 接收端。封裝 sendFile(文件名, conn) 函數
1) 只讀方式打開 待發送文件
2) 創建 buf 讀文件,存入buf中
3) 借助 conn 寫 buf中的 數據到 接收端 ―― 讀多少、寫多少。
4) 判斷文件讀取、發送完畢。結束 conn 。斷開連接。
接收端:(sever)
1. 創建監聽套接字 listener := net.Listen()
2. 阻塞等待客戶端連接請求。 conn = listener.Accept()
3. 讀取發送端發送的文件名(不含路徑)-- 保存
4. 回復“ok”給發送端。
5. 接收文件內容,保存成一個新文件。封裝 RecvFile (文件名, conn) 函數
1) os.Create() 按文件名創建文件。 -- f
2) 從 conn 中讀取文件內容。
3) 使用 f 寫到本地新建文件中。 ―― 讀多少、寫多少
4) 判斷文件讀取完畢。結束 conn 。斷開連接。
首先獲取文件名。借助os包中的stat()函數來獲取文件屬性信息。在函數返回的文件屬性中包含文件名和文件大小。Stat參數name傳入的是文件訪問的絕對路徑。FileInfo中的Name()函數可以將文件名單獨提取出來。
func Stat(name string) (FileInfo, error)
type FileInfo interface {
Name() string
Size() int64
Mode() FileMode
ModTime() time.Time
IsDir() bool
Sys() interface{}
}
獲取文件屬性示例:

package main import ( "os" "fmt" ) func main() { list := os.Args // 獲取命令行參數,存入list中 if len(list) != 2 { // 確保用戶輸入了一個命令行參數 fmt.Println("格式為:xxx.go 文件名") return } fileName := list[1] // 從命令行保存文件名(含路徑) fileInfo, err := os.Stat(fileName) //根據文件名獲取文件屬性信息 fileInfo if err != nil { fmt.Println("os.Stat err:", err) return } fmt.Println("文件name為:", fileInfo.Name()) // 得到文件名(不含路徑) fmt.Println("文件size為:", fileInfo.Size()) // 得到文件大小。單位字節 }
客戶端實現:

package main import ( "fmt" "os" "net" "io" ) func SendFile(path string, conn net.Conn) { // 以只讀方式打開文件 f, err := os.Open(path) if err != nil { fmt.Println("os.Open err:", err) return } defer f.Close() // 發送結束關閉文件。 // 循環讀取文件,原封不動的寫給服務器 buf := make([]byte, 4096) for { n, err := f.Read(buf) // 讀取文件內容到切片緩沖中 if err != nil { if err == io.EOF { fmt.Println("文件發送完畢") } else { fmt.Println("f.Read err:", err) } return } conn.Write(buf[:n]) // 原封不動寫給服務器 } } func main() { // 提示輸入文件名 fmt.Println("請輸入需要傳輸的文件:") var path string fmt.Scan(&path) // 獲取文件名 fileInfo.Name() fileInfo, err := os.Stat(path) if err != nil { fmt.Println("os.Stat err:", err) return } // 主動連接服務器 conn, err := net.Dial("tcp", "127.0.0.1:8005") if err != nil { fmt.Println("net.Dial err:", err) return } defer conn.Close() // 給接收端,先發送文件名 _, err = conn.Write([]byte(fileInfo.Name())) if err != nil { fmt.Println("conn.Write err:", err) return } // 讀取接收端回發確認數據 —— ok buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { fmt.Println("conn.Read err:", err) return } // 判斷如果是ok,則發送文件內容 if "ok" == string(buf[:n]) { SendFile(path, conn) // 封裝函數讀文件,發送給服務器,需要path、conn } }

package main import ( "net" "fmt" "os" "io" ) func filesend(filepath string,conn net.Conn){ buf:=make([]byte,4096) f1,err:=os.OpenFile(filepath,os.O_RDONLY,0666) if err!=nil{ fmt.Println("打開文件錯誤",err) return } defer f1.Close() for { n, err := f1.Read(buf) if err != nil { if err ==io.EOF{ fmt.Println("讀取完畢") break }else{ fmt.Println("read err", err) return } } _, err = conn.Write(buf[:n]) if err != nil { if err==io.EOF{ fmt.Println("文件發送完畢") break } fmt.Println("發送err", err) return } } } func main() { list:=os.Args filepath:=list[1] fileinfo,err:=os.Stat(filepath) if err!=nil{ fmt.Println("stat err",err) return } str:=fileinfo.Name() //fmt.Println(str) buf:=make([]byte,4096) conn,err:=net.Dial("tcp","127.0.0.1:8000") if err!=nil{ fmt.Println("conn err",err) return } defer conn.Close() n,err:=conn.Write([]byte(str)) if err!=nil{ fmt.Println("write err",err) return } fmt.Printf("發送的文件名%q",string(buf[:n])) //buf2:=make([]byte,4096) n,err=conn.Read(buf) if err!=nil{ fmt.Println("服務器發來錯誤",err) return } if string(buf[:n])=="ok"{ fmt.Println("服務器接收成功") filesend(filepath,conn) } }
服務端實現:

package main import ( "net" "fmt" "os" "io" ) func RecvFile(fileName string, conn net.Conn) { // 創建新文件 f, err := os.Create(fileName) if err != nil { fmt.Println("Create err:", err) return } defer f.Close() // 接收客戶端發送文件內容,原封不動寫入文件 buf := make([]byte, 4096) for { n, err := conn.Read(buf) if err != nil { if err == io.EOF { fmt.Println("文件接收完畢") } else { fmt.Println("Read err:", err) } return } f.Write(buf[:n]) // 寫入文件,讀多少寫多少 } } func main() { // 創建監聽 listener, err := net.Listen("tcp", "127.0.0.1:8005") if err != nil { fmt.Println("Listen err:", err) return } defer listener.Close() // 阻塞等待客戶端連接 conn, err := listener.Accept() if err != nil { fmt.Println("Accept err:", err) return } defer conn.Close() // 讀取客戶端發送的文件名 buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { fmt.Println("Read err:", err) return } fileName := string(buf[:n]) // 保存文件名 // 回復 0k 給發送端 conn.Write([]byte("ok")) // 接收文件內容 RecvFile(fileName, conn) // 封裝函數接收文件內容, 傳fileName 和 conn }

package main import ( "net" "fmt" "os" "io" ) func main() { listener, err := net.Listen("tcp", "127.0.0.1:8000") if err != nil { fmt.Println("listener err", err) return } defer listener.Close() conn, err := listener.Accept() if err != nil { fmt.Println("conn err", err) return } defer conn.Close() buf := make([]byte, 4096) n, err := conn.Read(buf) if err != nil { fmt.Println("read err", ) return } pathname := string(buf[:n]) fmt.Println(pathname) _, err = conn.Write([]byte("ok")) if err != nil { fmt.Println("write err", err) return } recvfile(pathname,conn) } func recvfile(pathname string,conn net.Conn){ str:="D:/1/"+pathname fmt.Println(str) f1,err:=os.Create(str) if err!=nil{ fmt.Println("create err",err) return } defer f1.Close() buf:=make([]byte,4096) for { n,err:=conn.Read(buf) if err!=nil{ if err==io.EOF{ fmt.Println("文件接收完畢") break } fmt.Println("conn read err",err) break } f1.Write(buf[:n]) } }
小知識
獲取命令行參數:
os.Args 提取命令行參數,保存成 []string
使用格式: go run xxx.go arg1 arg2 arg3 arg4 ...
獲取命令行參數:
arg[0]: xxx.go ――> xxx.exe 的絕對路徑
arg[1]: arg1
arg[2]: arg2
arg[3]: arg3
....
獲取文件屬性:
os.Stat(文件訪問絕對路徑) ――> fileInfo interface { Name() Size() }
提取文件 不帶路徑的“文件名”