0.1、索引
https://waterflow.link/articles/1664591292871
1、tcp的3次握手(建立連接)

- 客戶端的協議堆疊向服務器端發送了 SYN 包,并告訴服務器端當前發送序列號 j,客戶端進入 SYNC_SENT 狀態;
- 服務器端的協議堆疊收到這個包之后,和客戶端進行 ACK 應答,應答的值為 j+1,表示對 SYN 包 j 的確認,同時服務器也發送一個 SYN 包,告訴客戶端當前我的發送序列號為 k,服務器端進入 SYNC_RCVD 狀態;
- 客戶端協議堆疊收到 ACK 之后,使得應用程式從 connect 呼叫回傳,表示客戶端到服務器端的單向連接建立成功,客戶端的狀態為 ESTABLISHED,同時客戶端協議堆疊也會對服務器端的 SYN 包進行應答,應答資料為 k+1;
- 應答包到達服務器端后,服務器端協議堆疊使得 accept 阻塞呼叫回傳,這個時候服務器端到客戶端的單向連接也建立成功,服務器端也進入 ESTABLISHED 狀態,
2、tcp的4次揮手(關閉連接)

- 一方應用程式呼叫 close,我們稱該方為主動關閉方,該端的 TCP 發送一個 FIN 包,表示需要關閉連接,之后主動關閉方進入 FIN_WAIT_1 狀態,
- 接收到這個 FIN 包的對端執行被動關閉,這個 FIN 由 TCP 協議堆疊處理,我們知道,TCP 協議堆疊為 FIN 包插入一個檔案結束符 EOF 到接識訓沖區中,應用程式可以通過 read 呼叫來感知這個 FIN 包,一定要注意,這個 EOF 會被放在已排隊等候的其他已接收的資料之后,這就意味著接收端應用程式需要處理這種例外情況,因為 EOF 表示在該連接上再無額外資料到達,此時,被動關閉方進入 CLOSE_WAIT 狀態,
- 被動關閉方將讀到這個 EOF,于是,應用程式也呼叫 close 關閉它的套接字,這導致它的 TCP 也發送一個 FIN 包,這樣,被動關閉方將進入 LAST_ACK 狀態,
- 主動關閉方接收到對方的 FIN 包,并確認這個 FIN 包,主動關閉方進入 TIME_WAIT 狀態,而接收到 ACK 的被動關閉方則進入 CLOSED 狀態,進過 2MSL 時間之后,主動關閉方也進入 CLOSED 狀態,
3、socket中的連接建立和關閉

我看先看下流程:
- 服務端呼叫socket、bind系結ip埠、listen開啟服務端監聽,
- accept阻塞等待下次呼叫,并回傳一個tcp連接,
- 客戶端呼叫connect連接服務端,
- 此時服務端accept結束阻塞,代表客戶端和服務端成功建立連接,
- 然后就是資料互動讀寫讀寫,
- 當客戶端連接關閉時,服務端的read方法會讀取一個io.EOF的錯誤,代表客戶端關閉連接,服務端收到關閉連接的錯誤后也呼叫close關閉連接,
4、golang中的連接建立
我們先看下服務端:
package main
import (
"fmt"
"net"
)
func main() {
server := ":8330"
tcpAddr, err := net.ResolveTCPAddr("tcp", server)
if err != nil {
fmt.Println("resolve err:", err)
return
}
// 監聽某個埠的tcp網路
listen, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
fmt.Println("listen err:", err)
return
}
defer listen.Close()
for {
// 等待下次請求過來并建立連接
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept err:", err)
continue
}
// 在這個連接上做一些事情
go handler(conn)
}
}
func handler(conn net.Conn) {
}
- 首先我們定義好ip和埠,開啟監聽
- 然后呼叫accept等待下次請求過來,并建立tcp連接
我們運行下上面的代碼:
go run server.go
然后在另一個shell中執行下面的命令:
watch -d 'netstat -nat |grep "8330"'
Every 2.0s: netstat -nat |grep "8330" userdeMacBook-Pro.local: Thu Sep 29 16:38:42 2022
tcp46 0 0 *.8330 *.* LISTEN
可以看到此時8330埠已經開啟監聽
客戶端:
package main
import (
"fmt"
"net"
)
func main() {
serverAddr := ":8330"
tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddr)
if err != nil {
fmt.Println("resolve err:", err)
return
}
// 發起一個tcp的網路撥號
_, err = net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
fmt.Println("dial err:", err)
return
}
closed := make(chan bool)
// 客戶端阻塞不直接關閉
for {
select {
case <-closed:
fmt.Println("服務端關閉")
return
}
}
}
其中核心的方法就是net.DialTCP,第一個引數會回傳一個建立成功的連接,第二個引數會回傳沒建立成功的錯誤資訊,
然后我們命令列執行下:
go run client.go
接著看下watch -d 'netstat -nat |grep "8330"'的回傳,這個命令是實時的,所以不需要重復執行
Every 2.0s: netstat -nat |grep "8330" userdeMacBook-Pro.local: Thu Sep 29 16:45:57 2022
tcp4 0 0 127.0.0.1.8330 127.0.0.1.59146 ESTABLISHED
tcp4 0 0 127.0.0.1.59146 127.0.0.1.8330 ESTABLISHED
tcp46 0 0 *.8330 *.* LISTEN
可以看到客戶端服務端,服務端和客戶端都成功建立了連接(連接是否建立成功不是看是否有條線真連上了,連接狀態是維護在各個端的)
同時我們也可以在wireshark中看到三次握手建立連接的流程:

5、golang中的讀和寫
我們現在稍微修改下服務端的代碼:
package main
import (
"fmt"
"io"
"net"
"time"
)
func main() {
server := ":8330"
tcpAddr, err := net.ResolveTCPAddr("tcp", server)
if err != nil {
fmt.Println("resolve err:", err)
return
}
listen, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
fmt.Println("listen err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept err:", err)
continue
}
go handler(conn)
}
}
func handler(conn net.Conn) {
go func() {
for {
// 指定從buffer中讀取資料的最大容量
var buf = make([]byte, 1024)
// 從buffer中讀取資料并保存到buf中,n代表實際回傳的資料大小
n, err := conn.Read(buf)
if err != nil {
// 客戶端關倍訓觸發EOF
if err == io.EOF {
conn.Close()
return
}
fmt.Println("read err:", err)
return
}
fmt.Println("read data ", n, ":", string(buf))
}
}()
curTime := time.Now().String()
// 資料寫到緩沖區
_, err := conn.Write([]byte(curTime))
if err != nil {
fmt.Println("write err:", err)
return
}
fmt.Println("send data:", curTime)
}
首先要明白,作業系統內核會為每個連接的客戶端和服務端分配發送緩沖區和接識訓沖區,
- 當客戶端需要發送資料到服務端,呼叫conn.Write從客戶端緩沖區發送資料到作業系統內核的發送緩沖區,實際所做的事情是把資料從應用程式緩沖區中拷貝到作業系統內核的發送緩沖區中,并不一定是把資料通過套接字寫出去,
- 資料通過tcp發送到服務端的接識訓沖區,然后服務端的程式從接識訓沖區讀取資料,
非阻塞I/O,當應用程式呼叫非阻塞 I/O 完成某個操作時,內核立即回傳,不會把 CPU 時間切換給其他行程,應用程式在回傳后,可以得到足夠的 CPU 時間繼續完成其他事情,
讀操作:如果套接字對應的接識訓沖區沒有資料可讀,在非阻塞情況下 read 呼叫會立即回傳,一般回傳 EWOULDBLOCK 或 EAGAIN 出錯資訊,
寫操作:在非阻塞 I/O 的情況下,如果套接字的發送緩沖區已達到了極限,不能容納更多的位元組,那么作業系統內核會盡最大可能從應用程式拷貝資料到發送緩沖區中,并立即從 write 等函式呼叫中回傳,可想而知,在拷貝動作發生的瞬間,有可能一個字符也沒拷貝,有可能所有請求字符都被拷貝完成,那么這個時候就需要回傳一個數值,告訴應用程式到底有多少資料被成功拷貝到了發送緩沖區中,應用程式需要再次呼叫 write 函式,以輸出未完成拷貝的位元組,
非阻塞 I/O 操作:拷貝→回傳→再拷貝→再回傳,
阻塞 I/O 操作:拷貝→直到所有資料拷貝至發送緩沖區完成→回傳,
golang中底層使用的還是非阻塞的I/O,但是在代碼層面做了一些處理,讓用戶感覺是以阻塞方式呼叫的,
...
for {
n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
if err != nil {
n = 0
// 非阻塞方式呼叫,如果遇到syscall.EAGAIN報錯,代表沒拿到資料,繼續回圈
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
}
err = fd.eofError(n, err)
return n, err
}
...
6、golang中的關閉
在socket中,當客戶端呼叫close()方法時,其實就是發送一個FIN標志位,意思就是我要主動關閉TCP連接了,Close方法會讓對端的所有讀寫操作結束阻塞,并回傳,
在golang中呼叫Close方法,會讓對端的Read讀取到EOF的錯誤,此時就代表我想關閉連接,對端接收到關閉的請求后也可以呼叫Close方法關閉連接,
客戶端:
package main
import (
"fmt"
"io"
"net"
)
func main() {
serverAddr := ":8330"
tcpAddr, err := net.ResolveTCPAddr("tcp", serverAddr)
if err != nil {
fmt.Println("resolve err:", err)
return
}
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
fmt.Println("dial err:", err)
return
}
closed := make(chan bool)
go func() {
for {
var buf = make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
// 讀取到EOF,服務端關閉連接
if err == io.EOF {
conn.Close()
closed <- true
return
}
fmt.Println("read err:", err)
return
}
fmt.Println("read data ", n, ":", string(buf))
}
}()
for {
select {
case <-closed:
fmt.Println("服務端關閉")
return
}
}
}
服務端:
package main
import (
"fmt"
"io"
"net"
"time"
)
func main() {
server := ":8330"
tcpAddr, err := net.ResolveTCPAddr("tcp", server)
if err != nil {
fmt.Println("resolve err:", err)
return
}
listen, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
fmt.Println("listen err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept err:", err)
continue
}
go handler(conn)
}
}
func handler(conn net.Conn) {
go func() {
for {
var buf = make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
// 讀取到EOF,客戶端關閉連接
if err == io.EOF {
conn.Close()
return
}
fmt.Println("read err:", err)
return
}
fmt.Println("read data ", n, ":", string(buf))
}
}()
curTime := time.Now().String()
_, err := conn.Write([]byte(curTime))
if err != nil {
fmt.Println("write err:", err)
return
}
fmt.Println("send data:", curTime)
}
參考:
《極客時間:網路編程實戰》
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/519008.html
標籤:Go
上一篇:golang中經常會犯的一些錯誤
下一篇:golang中的nil接收器
