自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go 中如何強(qiáng)制關(guān)閉 TCP 連接

網(wǎng)絡(luò) 通信技術(shù)
本文我們介紹了 TCP 默認(rèn)關(guān)閉與強(qiáng)制關(guān)閉兩種方式(其實(shí)還有種折中的方式:SetLinger(sec > 0)),它們都源于 TCP 的協(xié)議設(shè)計(jì)。

[[425673]]

本文轉(zhuǎn)載自微信公眾號「Golang技術(shù)分享」,作者機(jī)器鈴砍菜刀。轉(zhuǎn)載本文請聯(lián)系Golang技術(shù)分享公眾號。

在《Go 網(wǎng)絡(luò)編程和 TCP 抓包實(shí)操》一文中,我們編寫了 Go 版本的 TCP 服務(wù)器與客戶端代碼,并通過 tcpdump 工具進(jìn)行抓包獲取分析。在該例中,客戶端代碼通過調(diào)用 Conn.Close() 方法發(fā)起了關(guān)閉 TCP 連接的請求,這是一種默認(rèn)的關(guān)閉連接方式。

默認(rèn)關(guān)閉需要四次揮手的確認(rèn)過程,這是一種”商量“的方式,而 TCP 為我們提供了另外一種”強(qiáng)制“的關(guān)閉模式。

如何強(qiáng)制性關(guān)閉?具體在 Go 代碼中應(yīng)當(dāng)怎樣實(shí)現(xiàn)?這就是本文探討的內(nèi)容。

默認(rèn)關(guān)閉

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

以客戶端主動關(guān)閉連接為例,當(dāng)它調(diào)用 Close 函數(shù)后,就會向服務(wù)端發(fā)送 FIN 報(bào)文,如果服務(wù)器的本端 socket 接收緩存區(qū)里已經(jīng)沒有數(shù)據(jù),那服務(wù)端的 read 將會得到一個(gè) EOF 錯(cuò)誤。

發(fā)起關(guān)閉方會經(jīng)歷 FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSE 的狀態(tài)變化,這些狀態(tài)需要得到被關(guān)閉方的反饋而更新。

強(qiáng)制關(guān)閉

默認(rèn)的關(guān)閉方式,不管是客戶端還是服務(wù)端主動發(fā)起關(guān)閉,都要經(jīng)過對方的應(yīng)答,才能最終實(shí)現(xiàn)真正的關(guān)閉連接。那能不能在發(fā)起關(guān)閉時(shí),不關(guān)心對方是否同意,就結(jié)束掉連接呢?

答案是肯定的。TCP 協(xié)議為我們提供了一個(gè) RST 的標(biāo)志位,當(dāng)連接的一方認(rèn)為該連接異常時(shí),可以通過發(fā)送 RST 包并立即關(guān)閉該連接,而不用等待被關(guān)閉方的 ACK 確認(rèn)。

SetLinger() 方法

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

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

函數(shù)的注釋已經(jīng)非常清晰,但是需要讀者有 socket 緩沖區(qū)的概念。

  • socket 緩沖區(qū)

當(dāng)應(yīng)用層代碼通過 socket 進(jìn)行讀與寫的操作時(shí),實(shí)質(zhì)上經(jīng)過了一層 socket 緩沖區(qū),它分為發(fā)送緩沖區(qū)和接受緩沖區(qū)。

緩沖區(qū)信息可通過執(zhí)行 netstat -nt 命令查看

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

其中,Recv-Q 代表的就是接收緩沖區(qū),Send-Q 代表的是發(fā)送緩沖區(qū)。

默認(rèn)關(guān)閉方式中,即 sec < 0 。操作系統(tǒng)會將緩沖區(qū)里未處理完的數(shù)據(jù)都完成處理,再關(guān)閉掉連接。

當(dāng) sec > 0 時(shí),操作系統(tǒng)會以與默認(rèn)關(guān)閉方式運(yùn)行。但是當(dāng)超過定義的時(shí)間 sec 后,如果還沒處理完緩存區(qū)的數(shù)據(jù),在某些操作系統(tǒng)下,緩沖區(qū)中未完成的流量可能就會被丟棄。

而 sec == 0 時(shí),操作系統(tǒng)會直接丟棄掉緩沖區(qū)里的流量數(shù)據(jù),這就是強(qiáng)制性關(guān)閉。

示例代碼與抓包分析

我們通過示例代碼來學(xué)習(xí) SetLinger() 的使用,并以此來分析強(qiáng)制關(guān)閉的區(qū)別。

服務(wù)端代碼

以服務(wù)端為主動關(guān)閉連接方示例

  1. package main 
  2.  
  3. import ( 
  4.  "log" 
  5.  "net" 
  6.  "time" 
  7.  
  8. func main() { 
  9.  // Part 1: create a listener 
  10.  l, err := net.Listen("tcp"":8000"
  11.  if err != nil { 
  12.   log.Fatalf("Error listener returned: %s", err) 
  13.  } 
  14.  defer l.Close() 
  15.  
  16.  for { 
  17.   // Part 2: accept new connection 
  18.   c, err := l.Accept() 
  19.   if err != nil { 
  20.    log.Fatalf("Error to accept new connection: %s", err) 
  21.   } 
  22.  
  23.   // Part 3: create a goroutine that reads and write back data 
  24.   go func() { 
  25.    log.Printf("TCP session open"
  26.    defer c.Close() 
  27.  
  28.    for { 
  29.     d := make([]byte, 100) 
  30.  
  31.     // Read from TCP buffer 
  32.     _, err := c.Read(d) 
  33.     if err != nil { 
  34.      log.Printf("Error reading TCP session: %s", err) 
  35.      break 
  36.     } 
  37.     log.Printf("reading data from client: %s\n", string(d)) 
  38.  
  39.     // write back data to TCP client 
  40.     _, err = c.Write(d) 
  41.     if err != nil { 
  42.      log.Printf("Error writing TCP session: %s", err) 
  43.      break 
  44.     } 
  45.    } 
  46.   }() 
  47.  
  48.   // Part 4: create a goroutine that closes TCP session after 10 seconds 
  49.   go func() { 
  50.    // SetLinger(0) to force close the connection 
  51.    err := c.(*net.TCPConn).SetLinger(0) 
  52.    if err != nil { 
  53.     log.Printf("Error when setting linger: %s", err) 
  54.    } 
  55.  
  56.    <-time.After(time.Duration(10) * time.Second
  57.    defer c.Close() 
  58.   }() 
  59.  } 

服務(wù)端代碼根據(jù)邏輯分為四個(gè)部分

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

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

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

第四部分:強(qiáng)制關(guān)閉連接邏輯。啟動一個(gè)新的 goroutine,通過 c.(*net.TCPConn).SetLinger(0) 設(shè)置強(qiáng)制關(guān)閉選項(xiàng),并于 10 s 后關(guān)閉連接。

客戶端代碼

以客戶端為被動關(guān)閉連接方示例

  1. package main 
  2.  
  3. import ( 
  4.  "log" 
  5.  "net" 
  6.  
  7. func main() { 
  8.  // Part 1: open a TCP session to server 
  9.  c, err := net.Dial("tcp""localhost:8000"
  10.  if err != nil { 
  11.   log.Fatalf("Error to open TCP connection: %s", err) 
  12.  } 
  13.  defer c.Close() 
  14.  
  15.  // Part2: write some data to server 
  16.  log.Printf("TCP session open"
  17.  b := []byte("Hi, gopher?"
  18.  _, err = c.Write(b) 
  19.  if err != nil { 
  20.   log.Fatalf("Error writing TCP session: %s", err) 
  21.  } 
  22.  
  23.  // Part3: read any responses until get an error 
  24.  for { 
  25.   d := make([]byte, 100) 
  26.   _, err := c.Read(d) 
  27.   if err != nil { 
  28.    log.Fatalf("Error reading TCP session: %s", err) 
  29.   } 
  30.   log.Printf("reading data from server: %s\n", string(d)) 
  31.  } 

客戶端代碼根據(jù)邏輯分為三個(gè)部分

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

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

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

tcpdump 抓包結(jié)果

tcpdump 是一個(gè)非常好用的數(shù)據(jù)抓包工具,在《Go 網(wǎng)絡(luò)編程和 TCP 抓包實(shí)操》一文中已經(jīng)簡單介紹了它的命令選項(xiàng),這里就不再贅述。

  • 開啟 tcpdump 數(shù)據(jù)包監(jiān)聽
  1. tcpdump -S -nn -vvv -i lo0 port 8000 
  • 運(yùn)行服務(wù)端代碼
  1. $ go run main.go 
  2. 2021/09/25 20:21:44 TCP session open 
  3. 2021/09/25 20:21:44 reading data from client: Hi, gopher? 
  4. 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 

服務(wù)器和客戶端建立連接之后,從客戶端讀取到數(shù)據(jù) Hi, gopher? 。在 10s 后,服務(wù)端強(qiáng)制關(guān)閉了 TCP 連接,阻塞在 c.Read 的服務(wù)端代碼返回了錯(cuò)誤: use of closed network connection。

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

客戶端和服務(wù)器建立連接之后,發(fā)送數(shù)據(jù)給服務(wù)端,服務(wù)端返回相同的數(shù)據(jù) Hi, gopher? 回來。在 10s 后,由于服務(wù)器強(qiáng)制關(guān)閉了 TCP 連接,因此阻塞在 c.Read 的客戶端代碼捕獲到了錯(cuò)誤:connection reset by peer。

  • tcpdump 的抓包結(jié)果
  1. $ tcpdump -S -nn -vvv -i lo0 port 8000 
  2. tcpdump: listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes 
  3. 20:21:44.682942 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!) 
  4.     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 
  5. 20:21:44.683042 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!) 
  6.     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 
  7. 20:21:44.683050 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!) 
  8.     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 
  9. 20:21:44.683055 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!) 
  10.     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 
  11. 20:21:44.683302 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 63, bad cksum 0 (->3cb7)!) 
  12.     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 
  13. 20:21:44.683311 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!) 
  14.     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 
  15. 20:21:44.683499 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 152, bad cksum 0 (->3c5e)!) 
  16.     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 
  17. 20:21:44.683511 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!) 
  18.     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 
  19. 20:21:54.688350 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40, bad cksum 0 (->3cce)!) 
  20.     127.0.0.1.8000 > 127.0.0.1.59394: Flags [R.], cksum 0xfe1c (incorrect -> 0xcd39), seq 1050611816, ack 3783365597, win 6379, length 0 

我們重點(diǎn)關(guān)注內(nèi)容 Flags [],其中 [S] 代表 SYN 包,用于建立連接;[P] 代表 PSH 包,表示有數(shù)據(jù)傳輸;[R]代表 RST 包,用于重置連接;[.] 代表對應(yīng)的 ACK 包。例如 [S.] 代表 SYN-ACK。

搞懂了這幾個(gè) Flags 的含義,那我們就可以分析出本次服務(wù)端強(qiáng)制關(guān)閉的 TCP 通信全過程。

我們和《Go 網(wǎng)絡(luò)編程和 TCP 抓包實(shí)操》一文中,客戶端正常關(guān)閉的通信過程進(jìn)行比較

可以看到,當(dāng)通過設(shè)定 SetLinger(0) 之后,主動關(guān)閉方調(diào)用 Close() 時(shí),系統(tǒng)內(nèi)核會直接發(fā)送 RST 包給被動關(guān)閉方。這個(gè)過程并不需要被動關(guān)閉方的回復(fù),就已關(guān)閉了連接。主動關(guān)閉方也就沒有了默認(rèn)關(guān)閉模式下 FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSE 的狀態(tài)改變。

總結(jié)

本文我們介紹了 TCP 默認(rèn)關(guān)閉與強(qiáng)制關(guān)閉兩種方式(其實(shí)還有種折中的方式:SetLinger(sec > 0)),它們都源于 TCP 的協(xié)議設(shè)計(jì)。

在大多數(shù)的場景中,我們都應(yīng)該選擇使用默認(rèn)關(guān)閉方式,因?yàn)檫@樣才能確保數(shù)據(jù)的完整性(不會丟失 socket 緩沖區(qū)里的數(shù)據(jù))。

當(dāng)使用默認(rèn)方式關(guān)閉時(shí),每個(gè)連接都會經(jīng)歷一系列的連接狀態(tài)轉(zhuǎn)變,讓其在操作系統(tǒng)上停留一段時(shí)間。尤其是服務(wù)器要主動關(guān)閉連接時(shí)(大多數(shù)應(yīng)用場景,都應(yīng)該是由客戶端主動發(fā)起關(guān)閉操作),這會消耗服務(wù)器的資源。

如果短時(shí)間內(nèi)有大量的或者惡意的連接涌入,我們或許需要采用強(qiáng)制關(guān)閉方式。因?yàn)槭褂脧?qiáng)制關(guān)閉,能立即關(guān)閉這些連接,釋放資源,保證服務(wù)器的可用與性能。

當(dāng)然,我們還可以選擇折中的方式,容忍一段時(shí)間的緩存區(qū)數(shù)據(jù)處理時(shí)間,再進(jìn)行關(guān)閉操作。

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

最后,讀者朋友們在項(xiàng)目中,有使用過強(qiáng)制關(guān)閉方式嗎?

 

責(zé)任編輯:武曉燕 來源: Golang技術(shù)分享
相關(guān)推薦

2021-10-14 20:33:16

TCP連接關(guān)閉

2009-10-12 19:45:58

Windows 7連接數(shù)限制

2010-09-13 15:55:17

SQL Server數(shù)

2019-09-02 10:39:15

TCPWindows連接

2010-06-13 15:02:05

TCP IP協(xié)議

2022-01-14 13:53:03

TCP進(jìn)程窗口

2023-12-01 14:57:22

TCP連接

2009-10-10 08:50:30

Windows 7系統(tǒng)黑屏使用辦法

2019-12-26 09:28:34

TCPPython通信

2021-07-09 12:37:31

GoPython編程語言

2019-02-14 09:30:40

Ubuntu應(yīng)用程序UI

2015-07-20 11:20:03

Windows 10更新

2018-05-14 09:48:45

Linux內(nèi)核模塊Kgotobed

2024-03-19 14:15:48

Go程序os.Exit()

2015-04-23 18:46:38

TCPTCP協(xié)議

2018-04-23 10:10:25

Windows 10關(guān)閉通知

2022-04-08 12:08:28

Hydra市場非法黑市網(wǎng)絡(luò)犯罪

2018-11-08 10:40:25

Windows 10Windows藍(lán)屏死機(jī)

2023-10-29 17:15:57

2015-10-09 13:15:03

TCP網(wǎng)絡(luò)協(xié)議
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號