<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    Go 中如何強制關閉 TCP 連接

    一顆小胡椒2022-07-20 11:04:49

    默認關閉需要四次揮手的確認過程,這是一種”商量“的方式,而 TCP 為我們提供了另外一種”強制“的關閉模式。

    如何強制性關閉?具體在 Go 代碼中應當怎樣實現?這就是本文探討的內容。

    默認關閉

    相信每個程序員都知道 TCP 斷開連接的四次揮手過程,這是面試八股文中的股中股。我們在 Go 代碼中調用默認的 Conn.Close() 方法,它就是典型的四次揮手。

    以客戶端主動關閉連接為例,當它調用 Close 函數后,就會向服務端發送 FIN 報文,如果服務器的本端 socket 接收緩存區里已經沒有數據,那服務端的 read 將會得到一個 EOF 錯誤。

    發起關閉方會經歷 FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSE 的狀態變化,這些狀態需要得到被關閉方的反饋而更新。

    強制關閉

    默認的關閉方式,不管是客戶端還是服務端主動發起關閉,都要經過對方的應答,才能最終實現真正的關閉連接。那能不能在發起關閉時,不關心對方是否同意,就結束掉連接呢?

    答案是肯定的。TCP 協議為我們提供了一個 RST 的標志位,當連接的一方認為該連接異常時,可以通過發送 RST 包并立即關閉該連接,而不用等待被關閉方的 ACK 確認。

    SetLinger() 方法

    在 Go 中,我們可以通過 net.TCPConn.SetLinger() 方法來實現。

    // SetLinger sets the behavior of Close on a connection which still
    // has data waiting to be sent or to be acknowledged.
    //
    // If sec < 0 (the default), the operating system finishes sending the
    // data in the background.
    //
    // If sec == 0, the operating system discards any unsent or
    // unacknowledged data.
    //
    // If sec > 0, the data is sent in the background as with sec < 0. On
    // some operating systems after sec seconds have elapsed any remaining
    // unsent data may be discarded.
    func (c *TCPConn) SetLinger(sec int) error {}
    

    函數的注釋已經非常清晰,但是需要讀者有 socket 緩沖區的概念。

    • socket 緩沖區

    當應用層代碼通過 socket 進行讀與寫的操作時,實質上經過了一層 socket 緩沖區,它分為發送緩沖區和接受緩沖區。

    緩沖區信息可通過執行 netstat -nt 命令查看

    $ netstat -nt
    Active Internet connections
    Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
    tcp4       0      0  127.0.0.1.57721        127.0.0.1.49448        ESTABLISHED
    

    其中,Recv-Q 代表的就是接收緩沖區,Send-Q 代表的是發送緩沖區。

    默認關閉方式中,即 sec < 0 。操作系統會將緩沖區里未處理完的數據都完成處理,再關閉掉連接。

    sec > 0 時,操作系統會以與默認關閉方式運行。但是當超過定義的時間 sec 后,如果還沒處理完緩存區的數據,在某些操作系統下,緩沖區中未完成的流量可能就會被丟棄。

    sec == 0 時,操作系統會直接丟棄掉緩沖區里的流量數據,這就是強制性關閉。

    示例代碼與抓包分析

    我們通過示例代碼來學習 SetLinger() 的使用,并以此來分析強制關閉的區別。

    服務端代碼

    以服務端為主動關閉連接方示例

    package main
    import (
     "log"
     "net"
     "time"
    )
    func main() {
     // Part 1: create a listener
     l, err := net.Listen("tcp", ":8000")
     if err != nil {
      log.Fatalf("Error listener returned: %s", err)
     }
     defer l.Close()
     for {
      // Part 2: accept new connection
      c, err := l.Accept()
      if err != nil {
       log.Fatalf("Error to accept new connection: %s", err)
      }
      // Part 3: create a goroutine that reads and write back data
      go func() {
       log.Printf("TCP session open")
       defer c.Close()
       for {
        d := make([]byte, 100)
        // Read from TCP buffer
        _, err := c.Read(d)
        if err != nil {
         log.Printf("Error reading TCP session: %s", err)
         break
        }
        log.Printf("reading data from client: %s\n", string(d))
        // write back data to TCP client
        _, err = c.Write(d)
        if err != nil {
         log.Printf("Error writing TCP session: %s", err)
         break
        }
       }
      }()
      // Part 4: create a goroutine that closes TCP session after 10 seconds
      go func() {
       // SetLinger(0) to force close the connection
       err := c.(*net.TCPConn).SetLinger(0)
       if err != nil {
        log.Printf("Error when setting linger: %s", err)
       }
       <-time.After(time.Duration(10) * time.Second)
       defer c.Close()
      }()
     }
    }
    

    服務端代碼根據邏輯分為四個部分

    第一部分:端口監聽。我們通過 net.Listen("tcp", ":8000")開啟在端口 8000 的 TCP 連接監聽。

    第二部分:建立連接。在開啟監聽成功之后,調用 net.Listener.Accept()方法等待 TCP 連接。Accept 方法將以阻塞式地等待新的連接到達,并將該連接作為 net.Conn 接口類型返回。

    第三部分:數據傳輸。當連接建立成功后,我們將啟動一個新的 goroutine 來處理 c 連接上的讀取和寫入。本文服務器的數據處理邏輯是,客戶端寫入該 TCP 連接的所有內容,服務器將原封不動地寫回相同的內容。

    第四部分:強制關閉連接邏輯。啟動一個新的 goroutine,通過 c.(*net.TCPConn).SetLinger(0) 設置強制關閉選項,并于 10 s 后關閉連接。

    客戶端代碼

    以客戶端為被動關閉連接方示例

    package main
    import (
     "log"
     "net"
    )
    func main() {
     // Part 1: open a TCP session to server
     c, err := net.Dial("tcp", "localhost:8000")
     if err != nil {
      log.Fatalf("Error to open TCP connection: %s", err)
     }
     defer c.Close()
     // Part2: write some data to server
     log.Printf("TCP session open")
     b := []byte("Hi, gopher?")
     _, err = c.Write(b)
     if err != nil {
      log.Fatalf("Error writing TCP session: %s", err)
     }
     // Part3: read any responses until get an error
     for {
      d := make([]byte, 100)
      _, err := c.Read(d)
      if err != nil {
       log.Fatalf("Error reading TCP session: %s", err)
      }
      log.Printf("reading data from server: %s\n", string(d))
     }
    }
    

    客戶端代碼根據邏輯分為三個部分

    第一部分:建立連接。我們通過 net.Dial("tcp", "localhost:8000")連接一個 TCP 連接到服務器正在監聽的同一個 localhost:8000 地址。

    第二部分:寫入數據。當連接建立成功后,通過 c.Write() 方法寫入數據 Hi, gopher? 給服務器。

    第三部分:讀取數據。除非發生 error,否則客戶端通過 c.Read() 方法(記住,是阻塞式的)循環讀取 TCP 連接上的內容。

    tcpdump 抓包結果

    tcpdump 是一個非常好用的數據抓包工具,在《Go 網絡編程和 TCP 抓包實操》一文中已經簡單介紹了它的命令選項,這里就不再贅述。

    • 開啟 tcpdump 數據包監聽
    tcpdump -S -nn -vvv -i lo0 port 8000
    
    • 運行服務端代碼
    $ go run main.go
    2021/09/25 20:21:44 TCP session open
    2021/09/25 20:21:44 reading data from client: Hi, gopher?
    2021/09/25 20:21:54 Error reading TCP session: read tcp 127.0.0.1:8000->127.0.0.1:59394: use of closed network connection
    

    服務器和客戶端建立連接之后,從客戶端讀取到數據 Hi, gopher? 。在 10s 后,服務端強制關閉了 TCP 連接,阻塞在 c.Read 的服務端代碼返回了錯誤: use of closed network connection

    • 運行客戶端代碼
    $ go run main.go
    2021/09/25 20:21:44 TCP session open
    2021/09/25 20:21:44 reading data from server: Hi, gopher?
    2021/09/25 20:21:54 Error reading TCP session: read tcp 127.0.0.1:59394->127.0.0.1:8000: read: connection reset by peer
    

    客戶端和服務器建立連接之后,發送數據給服務端,服務端返回相同的數據 Hi, gopher? 回來。在 10s 后,由于服務器強制關閉了 TCP 連接,因此阻塞在 c.Read 的客戶端代碼捕獲到了錯誤:connection reset by peer

    • tcpdump 的抓包結果
    $ tcpdump -S -nn -vvv -i lo0 port 8000
    tcpdump: listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes
    20:21:44.682942 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!)
        127.0.0.1.59394 > 127.0.0.1.8000: Flags [S], cksum 0xfe34 (incorrect -> 0xfa62), seq 3783365585, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 725769370 ecr 0,sackOK,eol], length 0
    20:21:44.683042 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!)
        127.0.0.1.8000 > 127.0.0.1.59394: Flags [S.], cksum 0xfe34 (incorrect -> 0x23d3), seq 1050611715, ack 3783365586, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 725769370 ecr 725769370,sackOK,eol], length 0
    20:21:44.683050 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
        127.0.0.1.59394 > 127.0.0.1.8000: Flags [.], cksum 0xfe28 (incorrect -> 0x84dc), seq 3783365586, ack 1050611716, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 0
    20:21:44.683055 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
        127.0.0.1.8000 > 127.0.0.1.59394: Flags [.], cksum 0xfe28 (incorrect -> 0x84dc), seq 1050611716, ack 3783365586, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 0
    20:21:44.683302 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 63, bad cksum 0 (->3cb7)!)
        127.0.0.1.59394 > 127.0.0.1.8000: Flags [P.], cksum 0xfe33 (incorrect -> 0x93f5), seq 3783365586:3783365597, ack 1050611716, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 11
    20:21:44.683311 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
        127.0.0.1.8000 > 127.0.0.1.59394: Flags [.], cksum 0xfe28 (incorrect -> 0x84d1), seq 1050611716, ack 3783365597, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 0
    20:21:44.683499 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 152, bad cksum 0 (->3c5e)!)
        127.0.0.1.8000 > 127.0.0.1.59394: Flags [P.], cksum 0xfe8c (incorrect -> 0x9391), seq 1050611716:1050611816, ack 3783365597, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 100
    20:21:44.683511 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
        127.0.0.1.59394 > 127.0.0.1.8000: Flags [.], cksum 0xfe28 (incorrect -> 0x846e), seq 3783365597, ack 1050611816, win 6378, options [nop,nop,TS val 725769370 ecr 725769370], length 0
    20:21:54.688350 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40, bad cksum 0 (->3cce)!)
        127.0.0.1.8000 > 127.0.0.1.59394: Flags [R.], cksum 0xfe1c (incorrect -> 0xcd39), seq 1050611816, ack 3783365597, win 6379, length 0
    

    我們重點關注內容 Flags [],其中 [S] 代表 SYN 包,用于建立連接;[P] 代表 PSH 包,表示有數據傳輸;[R]代表 RST 包,用于重置連接;[.] 代表對應的 ACK 包。例如 [S.] 代表 SYN-ACK。

    搞懂了這幾個 Flags 的含義,那我們就可以分析出本次服務端強制關閉的 TCP 通信全過程。

    可以看到,當通過設定 SetLinger(0) 之后,主動關閉方調用 Close() 時,系統內核會直接發送 RST 包給被動關閉方。這個過程并不需要被動關閉方的回復,就已關閉了連接。主動關閉方也就沒有了默認關閉模式下 FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSE 的狀態改變。

    總結

    本文我們介紹了 TCP 默認關閉與強制關閉兩種方式(其實還有種折中的方式:SetLinger(sec > 0)),它們都源于 TCP 的協議設計。

    在大多數的場景中,我們都應該選擇使用默認關閉方式,因為這樣才能確保數據的完整性(不會丟失 socket 緩沖區里的數據)。

    當使用默認方式關閉時,每個連接都會經歷一系列的連接狀態轉變,讓其在操作系統上停留一段時間。尤其是服務器要主動關閉連接時(大多數應用場景,都應該是由客戶端主動發起關閉操作),這會消耗服務器的資源。

    如果短時間內有大量的或者惡意的連接涌入,我們或許需要采用強制關閉方式。因為使用強制關閉,能立即關閉這些連接,釋放資源,保證服務器的可用與性能。

    當然,我們還可以選擇折中的方式,容忍一段時間的緩存區數據處理時間,再進行關閉操作。

    這里給讀者朋友留一個思考題。如果在本文示例中,我們將 SetLinger(0) 改為 SetLinger(1) ,抓包結果又會是如何?

    最后,讀者朋友們在項目中,有使用過強制關閉方式嗎?歡迎留言交流。

    tcptcp四次揮手
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    而UDP是一個面向無連接的傳輸層協議。當意識到丟包了或者網絡環境不佳,TCP 會根據具體情況調整自己的行為,控制自己的發送速度或者重發。由此證明男方擁有愛的能力。SYN 需要對端的確認, 而 ACK 并不需要,因此 SYN 消耗一個序列號而 ACK 不需要。
    人如其名,要對數據的傳輸進行一個詳細的 控制。FIN:通知對方,本端要關閉了,我們稱攜帶FIN標識的為結束報文段,該位為 1 時,表示今后不會再有數據發送,希望斷開連接。
    在發送 TCP 數據段時,由發送端計算校驗和,當到達目的地時又進行一次檢驗和計算。表示緊急數據的末尾在 TCP 數據部分中的位置。如果不能及時收到一個確認,將重發這個報文段。流量控制TCP 連接的每一方都有固定大小的緩沖空間,TCP 的接收端只允許發送端發送接收端緩存區能接納的數據。TCP 使用的流量控制協議是可變大小的滑動窗口協議。擁塞控制當網絡擁塞時,減少數據的發送。
    TCP 和UDP協議的區別以及原理最近重新認知了一下 TCP 和 UDP 的原理以及區別,做一個簡單的總結。這包數據稱之為SYN 包,如果對端同意連接,則回復一包 SYN+ACK 包,客戶端收到之后,發送一包 ACK 包,連接建立,因為這個過程中互相發送了三包數據,所以稱之為三次握手。也是為了保證在不可靠的網絡鏈路中進行可靠的連接斷開確認。
    數據鏈路層在不可靠的物理介質上提供可靠的傳輸。與IP協議配套使用實現其功能的還有地址解析協議ARP、逆地址解析協議RARP、因特網報文協議ICMP、因特網組管理協議IGMP。ARP 是即插即用的,一個ARP表是自動建立的,不需要系統管理員來配置。
    數據鏈路層在不可靠的物理介質上提供可靠的傳輸。以太網協議詳解MAC地址:每一個設備都擁有唯一的MAC地址,共48位,使用十六進制表示。與IP協議配套使用實現其功能的還有地址解析協議ARP、逆地址解析協議RARP、因特網報文協議ICMP、因特網組管理協議IGMP。ARP 是即插即用的,一個ARP表是自動建立的,不需要系統管理員來配置。
    數據鏈路層在不可靠的物理介質上提供可靠的傳輸。以太網協議詳解MAC地址:每一個設備都擁有唯一的MAC地址,共48位,使用十六進制表示。與IP協議配套使用實現其功能的還有地址解析協議ARP、逆地址解析協議RARP、因特網報文協議ICMP、因特網組管理協議IGMP。ARP 是即插即用的,一個ARP表是自動建立的,不需要系統管理員來配置。
    數據鏈路層在不可靠的物理介質上提供可靠的傳輸。以太網協議詳解MAC地址:每一個設備都擁有唯一的MAC地址,共48位,使用十六進制表示。網絡層網絡層的目的是實現兩個端系統之間的數據透明傳送,具體功能包括尋址和路由選擇、連接的建立、保持和終止等。
    數據鏈路層在不可靠的物理介質上提供可靠的傳輸。與IP協議配套使用實現其功能的還有地址解析協議ARP、逆地址解析協議RARP、因特網報文協議ICMP、因特網組管理協議IGMP。ARP 是即插即用的,一個ARP表是自動建立的,不需要系統管理員來配置。
    函數的注釋已經非常清晰,但是需要讀者有 socket 緩沖區的概念。socket 緩沖區當應用層代碼通過 socket 進行讀與寫的操作時,實質上經過了一層 socket 緩沖區,它分為發送緩沖區和接受緩沖區。操作系統會將緩沖區里未處理完的數據都完成處理,再關閉掉連接。當 sec > 0 時,操作系統會以與默認關閉方式運行。服務端代碼根據邏輯分為四個部分第一部分:端口監聽。
    一顆小胡椒
    暫無描述
      亚洲 欧美 自拍 唯美 另类