記一個詭異的TCP揮手亂序問題

本文內容包括但不限於:tcp四次揮手(同時關閉),tcp包的seq/ack號規則,tcp狀態機,內核tcp代碼,tcp發送窗口等知識。

問題是什麼?

內核版本linux 5.10.112

一句話:四次揮手中,由於fin包和ack包亂序,導致等了一次timeout才關閉連接。

過程細節:

  • 同時關閉的場景,server和client幾乎同時向對方發送fin包。
  • client先收到了server的fin包,並回傳ack包。
  • 然而server處發生亂序,先收到了client的ack包,後收到了fin包。
  • 結果表現爲server未能正確處理client的fin包,未能返回正確的ack包。
  • client沒收到(針對fin的)ack包,因此等待超時後重傳fin包,之後纔回歸正常關閉連接的流程。

問題抓包具體分析

圖中上半部分是client,下半部分是server。

重點關注id爲14913,14914,20622,20623這四個包,後面爲了方便分析,對seq和ack號取後四位:

  • 20622(seq=4416,ack=753),client發送的fin包:client主動關閉連接,向server發送fin包;
  • 14913(seq=753,ack=4416),server發送的fin包:server主動關閉連接,向client發送fin包;
  • 20623(seq=4417,ack=754),client響應的ack包:client收到server的fin,響應一個ack包;
  • 14914(seq=754,ack=4416),server發送的ack包;

問題發生在server處(紅框位置),發送14913後:

  • 先收到20623(seq=4417),但此時期望收到的seq爲4416,所以被標記爲[previous segment not captured]
  • 然後收到20622,回傳了一個ack包,id爲14914,問題就出現在這裏:這個數據包的ack=4416,這意味着server還在等待seq=4416的數據包,換言之,fin-20622沒有被server真正接收到。
  • client發現20622沒有被正確接收,因此在等到timeout後,重新發送了fin包(id=20624),此後連接正常關閉。

(這裏再次強調一下ack-20623和fin-20622,後面會經常提到這兩個包)

首先,這個現象在直覺上是很不合理的,tcp應當有恰當的機制保證亂序恢復。這裏20622和20623都已經到達了server,雖然發生了亂序,也不應當影響server把兩者都接收,這是主要的疑問點所在。

經過初步分析,我們推測最可能的原因是20622被server的內核忽略了(原因目前未知)。既然是內核的行爲,就先嚐試在本地環境復現這個問題。然而喜聞樂見的沒有成功。

新問題:嘗試復現未成功

爲了模擬上述亂序的場景,我們使用兩臺ecs,在client上僞造tcp包,與server處的正常socket通信。

server處的抓包結果如下:

注意看No.爲5,6,7,8的包:

  • 5: server向client發送fin(這裏不知爲何有一次重傳,但是不影響後面的效果,沒有深究)
  • 6: client先傳回了seq=1002的ack包
  • 7: client後傳回了seq=1001的fin包
  • 8: server傳回了ack=1002的ack包,ack=1002意味着client的fin包被正常接收了!(如果在問題場景下,此時回傳的ack包,ack應當爲1001)

之後,爲了保持內核版本一致,把相同的程序轉移到本地虛擬機上運行,得到同樣的結果。換言之,復現失敗了。

附:模擬程序代碼

工具:python + scapy

這裏用scapy僞造client,發送亂序的ack和fin,是爲了觀察server回傳的ack包。因爲client並未真的走了tcp協議,所以無論復現成功與否,都不能觀察到超時重傳。

(1)server處正常socket監聽:

import socket

server_ip = "0.0.0.0"
server_port = 12346

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((server_ip, server_port))
server_socket.listen(5)

connection, client_address = server_socket.accept()

connection.close() #發送fin
server_socket.close()

(2)client模擬亂序:

from scapy.all import *
import time
import sys

target_ip = "略"
target_port = 12346
src_port = 1234

#僞造數據包,建立tcp連接
ip = IP(dst=target_ip)
syn = TCP(sport=src_port, dport=target_port, flags="S", seq=1000)
syn_ack = sr1(ip / syn)
if syn_ack and TCP in syn_ack and syn_ack[TCP].flags == "SA":
    print("Received SYN-ACK")
    ack = TCP(sport=src_port, dport=target_port, 
              flags="A", seq=syn_ack.ack, ack=syn_ack.seq+1)
    send(ip / ack)
    print("Sent ACK")
else:
    print("Failed to establish TCP connection")
  
def handle_packet(packet):
    if TCP in packet and packet[TCP].flags & 0x01:
        print("Received FIN packet") #若收到server的fin,先傳ack,再傳fin
        ack = TCP(sport=src_port, dport=target_port, 
                  flags="A", seq=packet.ack+1, ack=packet.seq+1)
        send(ip / ack)
      
        time.sleep(0.1)
        fin = TCP(sport=src_port, dport=target_port, 
                  flags="FA", seq=packet.ack, ack=packet.seq)
        send(ip / fin)
        sys.exit(0)
sniff(prn=handle_packet)

問題出現的位置?

server處出現亂序,結果連接未能正常關閉,而是等到client超時重傳fin包,才關閉連接。

問題帶來什麼影響?

server處連接關閉的時間變長了(額外增加200ms),對時延敏感的場景影響明顯。

本文要解決什麼問題?

  • 該現象是否是內核的合法行爲?(先劇透一下,是合法行爲)
  • 爲什麼本地復現失敗了?

問題排查

經過了大約6個週末的間斷式[看代碼-試驗]循環,終於找到了問題所在!下面將簡要描述問題排查的過程,也包括我們的一些失敗嘗試。

初步分析

回到上面的問題,現在不僅不清楚問題原因,本地復現還完美符合理想情況。簡單來說:

  • 本地復現-亂序不影響揮手;
  • 問題場景-亂序導致超時重傳。

可以確定,問題很大概率出現在server對ack-20623和fin-20622的處理上。

(下面會以ack-20623和fin-20622代指亂序的ack和fin包)

關鍵在於:server發送fin後(進入FIN_WAIT_1狀態),對後面收到的亂序ack-20623和fin-20622是如何處理的。這裏涉及到tcp的狀態轉移,所以,首要問題是確定其中的狀態轉移過程。之後才能根據狀態轉移鎖定對應的代碼片段,做具體分析。

確定狀態轉移

由於問題發生在揮手過程中,很自然想到通過觀察狀態轉移來判斷數據包的接收/處理情況。

我們結合復現過程,利用ss和ebpf,監控tcp的狀態變化。確定了server在收到ack-20623後,由FIN_WAIT_1進入了FIN_WAIT_2狀態,這意味着ack-20623被正確處理了。那麼問題大概率出現在fin-20622的處理上,這也證實了我們最初的猜測。

這裏還有一個奇怪的點:按照正確的揮手流程,server在FIN_WAIT_2收到fin後應當進入TIMEWAIT狀態。我們在ss中觀察到了這個狀態轉移,但是使用ebpf監控時,並沒有捕捉到這個狀態轉移。當時我們並未關注這個問題,後來才知曉原因:ebpf實現中,只記錄tcp_set_state()引發的狀態轉移。而此處雖然進入了TIMEWAIT狀態,卻並未經過tcp_set_state(),因此ebpf中無法看到。關於這裏如何進入TIMEWAIT,請看末尾的“番外”一節。

附:ebpf監控結果

(FIN_WAIT1轉移到FIN_WAIT2時,snd_una有更新,確定ack-20623被正確處理了)

<idle>-0    [000] d.s. 42261.233642: PASSIVE_ESTABLISHED: start monitor tcp state change
<idle>-0    [000] d.s. 42261.233651: port:12346,snd_nxt:154527568,snd_una:154527568
<idle>-0    [000] d.s. 42261.233652: rcv_nxt:1001,recved:0,acked:0

<...>-9451 [007] d... 42261.233808: changing from ESTABLISHED to FIN_WAIT1
<...>-9451 [007] d... 42261.233815: port:12346,snd_nxt:154527568,snd_una:154527568
<...>-9451 [007] d... 42261.233816: rcv_nxt:1001,recved:0,acked:0

<idle>-0    [000] dNs. 42261.464578: changing from FIN_WAIT1 to FIN_WAIT2
<idle>-0    [000] dNs. 42261.464588: port:12346,snd_nxt:154527569,snd_una:154527569
<idle>-0    [000] dNs. 42261.464589: rcv_nxt:1001,recved:0,acked:1

內核源碼分析

事已至此,不得不看一看內核源碼了。結合上面的分析,問題大概率發生在 tcp_rcv_state_process() 函數中,抽取出其中關於TCP_FIN_WAIT2的片段,然而很遺憾,在這個片段中沒有發現疑點:

(tcp_rcv_state_process是接收數據包時處理狀態轉移的函數,位於net/ipv4/tcp_input.c)

case TCP_FIN_WAIT1:
  case TCP_FIN_WAIT2:
    /* RFC 793 says to queue data in these states,
     * RFC 1122 says we MUST send a reset.
     * BSD 4.4 also does reset.
     */
    if (sk->sk_shutdown & RCV_SHUTDOWN) {
      if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
          after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) { //經過分析,不符合該條件
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
        tcp_reset(sk);
        return 1;
      }
    }
    fallthrough;
  case TCP_ESTABLISHED:
    tcp_data_queue(sk, skb); //如果進入了這個函數,亂序會被糾正,fin的處理也在該函數中
    queued = 1;
    break;

如果運行到了這裏,基本可以確定fin會被正常處理,所以我們將這個位置作爲我們檢查的終點。也就是說,亂序的fin-20622應當是沒有成功到達此處的。我們從這個位置開始,向前查找,找到了一個非常可疑的位置,同樣是在tcp_rcv_state_process中。

//檢查ack值是否合法
  acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH |
              FLAG_UPDATE_TS_RECENT |
              FLAG_NO_CHALLENGE_ACK) > 0;

  if (!acceptable) { //如果不合法
    if (sk->sk_state == TCP_SYN_RECV) //揮手過程中不會進入這個分支
      return 1;  /* send one RST */
    tcp_send_challenge_ack(sk, skb); //回傳一個ack然後丟棄
    goto discard;
  }

假如這裏對fin-20622的ack檢查沒有通過,那麼也會發送一個ack(即包14914, 這段代碼中爲challenge ack),然後丟棄掉(沒有進入處理fin的流程)。這和問題場景是非常符合的。繼續分析tcp_ack()函數,也找到了可能會判定非法的點:

/*這一段是判斷收到的ack值與本地發送窗口的關係,
  這裏snd_una意爲send un-acknowledge,即發送了,但未被ack的位置
*/
    if (before(ack, prior_snd_una)) { //如果收到的ack值,已經被前面的包ack了
    /* RFC 5961 5.2 [Blind Data Injection Attack].[Mitigation] */
···
    goto old_ack;
  }
···
old_ack:
  /* If data was SACKed, tag it and see if we should send more data.
   * If data was DSACKed, see if we can undo a cwnd reduction.
   */
···

  return 0;

總結一下:fin-20622有一種可能的處理路徑,符合問題場景的表現。從server的視角:

  • 首先收到ack-20623,更新了snd_una的值爲該包的ack值,即754。
  • 然後收到fin-20622,在檢查ack值的階段,由於該包的ack=753,小於此時的snd_nxt,因此被判定爲old_ack,非法。之後acceptable返回值爲0。
  • 由於 ack 值被判定爲非法,內核傳回一個challenge ack包, 然後直接丟掉fin-20622。
  • 因此,最終fin-20622被tcp_rcv_state_process丟棄,沒有進入fin包處理的流程。

這樣,相當於server並沒有收到fin信號,與問題場景吻合。

找到了這一條可疑路徑,接下來就要想辦法驗證了。

由於精確到了具體的代碼片段,並且實際代碼相當複雜,僅通過代碼分析很難確定真實的運行路徑。

於是我們放出大招,直接修改內核,驗證上述位點的tcp狀態信息,主要是狀態轉移和發送窗口。

修改內核配合測試

具體過程不再贅述,我們有了新的發現:(提示:使用的依然是“表現正常”的復現腳本)

  1. 收到ack-20623時,snd_una確實被更新了,這符合上面的假設,爲fin包丟棄提供了條件。
  2. 亂序的fin包根本沒有進入tcp_rcv_state_process()函數,而是被外層的tcp_v4_rcv()函數按照TIMEWAIT流程直接處理,最終關閉連接。

顯然,第二點很可能是導致復現失敗的關鍵。

  • 更加證明了我們先前的假設,如果fin能進入tcp_rcv_state_process()函數,應該就能復現出問題。但可能因爲線上場景與復現場景存在某些配置差異,導致代碼路徑分歧。
  • 另外這個發現也顛覆了我們的認知,按照tcp的揮手流程,在收到fin-20622前,server發送fin後收到了ack,那麼應當處於FIN_WAIT_2狀態,工具監控結果也是如此,爲何這裏是TIMEWAIT呢。

帶着這些問題,我們回到代碼中,繼續分析。在ack檢查和fin處理之間,找到一處最可疑的位置:

case TCP_FIN_WAIT1: {
    int tmo;
···

    if (tp->snd_una != tp->write_seq) //一種異常情況,還有數據待發送
      break; //可疑

    tcp_set_state(sk, TCP_FIN_WAIT2); //轉移至FIN_WAIT2,並且關閉發送方向
    sk->sk_shutdown |= SEND_SHUTDOWN;

    sk_dst_confirm(sk);

    if (!sock_flag(sk, SOCK_DEAD)) { //延遲關閉
      /* Wake up lingering close() */
      sk->sk_state_change(sk);
      break; //可疑
    }
···
        //可能會進入timewait相關的邏輯
    tmo = tcp_fin_time(sk); //計算fin超時
    if (tmo > sock_net(sk)->ipv4.sysctl_tcp_tw_timeout) {
            //如果超時時間很大,則啓動keepalive timer探活
      inet_csk_reset_keepalive_timer(sk,
                   tmo - sock_net(sk)->ipv4.sysctl_tcp_tw_timeout);
    } else if (th->fin || sock_owned_by_user(sk)) {
      /* Bad case. We could lose such FIN otherwise.
       * It is not a big problem, but it looks confusing
       * and not so rare event. We still can lose it now,
       * if it spins in bh_lock_sock(), but it is really
       * marginal case.
       */
      inet_csk_reset_keepalive_timer(sk, tmo);
    } else {  //否則直接進入timewait;經過測試,復現失敗時ack包進入了這個分支
      tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
      goto discard;
    }
    break;
  }

這個片段對應ack-20623的處理過程,確實發現了和TIMEWAIT的關聯,所以我們懷疑到前面的兩個break上。如果提前觸發了break,是不是就不會導致TIMEWAIT,進而能夠復現成功?

話不多說直接動手,通過修改代碼,發現兩個break任意觸發一個,都能夠復現出問題場景,導致連接無法正常關閉!

對比兩個break的條件,SOCK_DEAD成爲最大嫌疑者。

關於SOCK_DEAD

從字面意思推測,這個flag應當和tcp的關閉過程有關,在內核代碼中查找,發現兩處相關的函數:

/*
 *  Shutdown the sending side of a connection. Much like close except
 *  that we don't receive shut down or sock_set_flag(sk, SOCK_DEAD).
 */

void tcp_shutdown(struct sock *sk, int how)
{
  /*  We need to grab some memory, and put together a FIN,
   *  and then put it into the queue to be sent.
   *    Tim MacKenzie([email protected]) 4 Dec '92.
   */
  if (!(how & SEND_SHUTDOWN))
    return;

  /* If we've already sent a FIN, or it's a closed state, skip this. */
  if ((1 << sk->sk_state) &
      (TCPF_ESTABLISHED | TCPF_SYN_SENT |
       TCPF_SYN_RECV | TCPF_CLOSE_WAIT)) {
    /* Clear out any half completed packets.  FIN if needed. */
    if (tcp_close_state(sk))
      tcp_send_fin(sk);
  }
}
EXPORT_SYMBOL(tcp_shutdown);

從註釋可以看出,這個函數具有close的一部分功能,但是不會sock_set_flag(sk, SOCK_DEAD)。那麼再看一看tcp_close():

void tcp_close(struct sock *sk, long timeout)
{
  struct sk_buff *skb;
  int data_was_unread = 0;
  int state;

···
  if (unlikely(tcp_sk(sk)->repair)) {
    sk->sk_prot->disconnect(sk, 0);
  } else if (data_was_unread) {
    /* Unread data was tossed, zap the connection. */
    NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
    tcp_set_state(sk, TCP_CLOSE);
    tcp_send_active_reset(sk, sk->sk_allocation);
  } else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
    /* Check zero linger _after_ checking for unread data. */
    sk->sk_prot->disconnect(sk, 0);
    NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
  } else if (tcp_close_state(sk)) {
    /* We FIN if the application ate all the data before
     * zapping the connection.
     */
    tcp_send_fin(sk); //發送fin包
  }

  sk_stream_wait_close(sk, timeout);

adjudge_to_death:
  state = sk->sk_state;
  sock_hold(sk);
  sock_orphan(sk); //這裏會設置SOCK_DEAD flag
···
}
EXPORT_SYMBOL(tcp_close);

這裏,tcp_shutdown和tcp_close都是tcp協議的標準接口,可以用於關閉連接:

struct proto tcp_prot = {
  .name      = "TCP",
  .owner      = THIS_MODULE,
  .close      = tcp_close, //close在這
  .pre_connect    = tcp_v4_pre_connect,
  .connect    = tcp_v4_connect,
  .disconnect    = tcp_disconnect,
  .accept      = inet_csk_accept,
  .ioctl      = tcp_ioctl,
  .init      = tcp_v4_init_sock,
  .destroy    = tcp_v4_destroy_sock,
  .shutdown    = tcp_shutdown, //shutdown在這
  .setsockopt    = tcp_setsockopt,
  .getsockopt    = tcp_getsockopt,
  .keepalive    = tcp_set_keepalive,
  .recvmsg    = tcp_recvmsg,
  .sendmsg    = tcp_sendmsg,
···
};
EXPORT_SYMBOL(tcp_prot);

綜上,shutdown和close的一個重要差異在於shutdown不會設置SOCK_DEAD

我們將復現腳本的close()換成shutdown()再測試,終於成功復現了fin被丟棄的結果!

(並且通過打印日誌,確定丟棄原因就是之前提到的old_ack,終於驗證了我們的假設。)

下面只需要迴歸線上場景,確認是否真的調用了shutdown()關閉連接。經過線上同學的確認,此處server確實是用了shutdown()關閉連接(通過nginx的lingering_close)。

至此,終於真相大白!

總結

最後,回答最初的兩個問題作爲總結:

  • 該現象是否是內核的合法行爲?
    • 是合法行爲,是內核檢查ack的邏輯導致的;
    • 內核會根據收到的ack值,更新發送窗口參數snd_una,並由snd_una判斷ack包是否需要處理;
    • 由於fin-20622的ack值小於ack-20623,且ack-20623先到達,更新了snd_una。後到達的fin在ack檢查過程中,對比snd_una時被認爲是已經ack過的包,不需要再處理,結果被直接丟棄,並回傳一個challenge_ack。導致了問題場景。
  • 爲什麼本地復現失敗了?
    • 關閉tcp連接時,使用了close()接口,而線上環境使用的是shutdown()
    • shutdown不會設置SOCK_DEAD,而close則相反,導致復現時的代碼路徑與問題場景出現分歧。

番外:close()下的tcp狀態轉移

其實還遺留了一個問題:

爲什麼用close()關閉連接時,沒有觀察到fin包的狀態轉移FIN_WAIT_2 -> TIMEWAIT(沒有進入tcp_rcv_state_process)?

這要從FIN_WAIT_1收到ack後講起,上面的代碼分析中提到,如果沒有觸發兩個可疑的break,處理ack時將會進入:

 case TCP_FIN_WAIT1: {
    int tmo;
···
        else {
      tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
      goto discard;
    }
    break;
  }

tcp_time_wait()主要邏輯如下:

/*
 * Move a socket to time-wait or dead fin-wait-2 state.
 */
void tcp_time_wait(struct sock *sk, int state, int timeo)
{
  const struct inet_connection_sock *icsk = inet_csk(sk);
  const struct tcp_sock *tp = tcp_sk(sk);
  struct inet_timewait_sock *tw;
  struct inet_timewait_death_row *tcp_death_row = &sock_net(sk)->ipv4.tcp_death_row;

    //創建tw,其中將tcp狀態置爲TCP_TIME_WAIT
  tw = inet_twsk_alloc(sk, tcp_death_row, state);

  if (tw) { //創建成功,則會進行初始化
    struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
    const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1); //計算超時時間
    struct inet_sock *inet = inet_sk(sk);

    tw->tw_transparent  = inet->transparent;
    tw->tw_mark    = sk->sk_mark;
    tw->tw_priority    = sk->sk_priority;
    tw->tw_rcv_wscale  = tp->rx_opt.rcv_wscale;
    tcptw->tw_rcv_nxt  = tp->rcv_nxt;
    tcptw->tw_snd_nxt  = tp->snd_nxt;
    tcptw->tw_rcv_wnd  = tcp_receive_window(tp);
    tcptw->tw_ts_recent  = tp->rx_opt.ts_recent;
    tcptw->tw_ts_recent_stamp = tp->rx_opt.ts_recent_stamp;
    tcptw->tw_ts_offset  = tp->tsoffset;
    tcptw->tw_last_oow_ack_time = 0;
    tcptw->tw_tx_delay  = tp->tcp_tx_delay;

    /* Get the TIME_WAIT timeout firing. */
        //確定超時時間
    if (timeo < rto)
      timeo = rto;

    if (state == TCP_TIME_WAIT)
      timeo = sock_net(sk)->ipv4.sysctl_tcp_tw_timeout;

    /* tw_timer is pinned, so we need to make sure BH are disabled
     * in following section, otherwise timer handler could run before
     * we complete the initialization.
     */
        //更新維護timewait sock的結構
    local_bh_disable();
    inet_twsk_schedule(tw, timeo);
    /* Linkage updates.
     * Note that access to tw after this point is illegal.
     */
    inet_twsk_hashdance(tw, sk, &tcp_hashinfo); //加入全局哈希表(tcp_hashinfo)
    local_bh_enable();
  } else {
    /* Sorry, if we're out of memory, just CLOSE this
     * socket up.  We've got bigger problems than
     * non-graceful socket closings.
     */
    NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPTIMEWAITOVERFLOW);
  }

  tcp_update_metrics(sk); //更新tcp統計指標,不影響本次行爲
  tcp_done(sk); //銷燬掉sk
}
EXPORT_SYMBOL(tcp_time_wait);

可見,在這個過程中,原本的sk被銷燬了,並且創建了對應的inet_timewait_sock,進入計時。換言之,close的server收到ack時,雖然會進入FIN_WAIT_2,但是之後立即切換到了TIMEWAIT狀態,且沒有經過標準的tcp_set_state()函數,致使ebpf沒有監控到。

之後再收到fin包時,則根本不會進入tcp_rcv_state_process(),而是由外層tcp_v4_rcv()進行timewait流程處理。具體來講,tcp_v4_rcv()將根據收到的skb查詢對應的內核sk,這裏會查到上面創建的timewait_sock,其狀態爲TIMEWAIT,所以直接進入timewait的處理,核心代碼如下:

int tcp_v4_rcv(struct sk_buff *skb)
{
  struct net *net = dev_net(skb->dev);
  struct sk_buff *skb_to_free;
  int sdif = inet_sdif(skb);
  int dif = inet_iif(skb);
  const struct iphdr *iph;
  const struct tcphdr *th;
  bool refcounted;
  struct sock *sk;
  int ret;
···
    th = (const struct tcphdr *)skb->data;
···
lookup:
  sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
             th->dest, sdif, &refcounted); //從全局哈希表tcp_hashinfo中查詢sk
···
process:
  if (sk->sk_state == TCP_TIME_WAIT)
    goto do_time_wait;
···
do_time_wait: //正常的timewait處理流程
···
  goto discard_it;
}

綜上,server調用close()關閉連接,收到ack後會轉入FIN_WAIT_2,然後立刻轉移爲TIMEWAIT,不需要等待client的fin包。

一種簡單的定性理解:調用close()的socket意味着完全關閉接收和發送,這樣進入FIN_WAIT_2等待對方的fin意義不大(等待對方fin的一個主要目的是確定對方發送完畢),所以在確認己方發送的fin被對面收到之後(收到了client針對fin的ack),就可以進入TIMEWAIT狀態了。

作者:
阿里雲虛擬交換機團隊:負責阿里雲網絡虛擬化相關的開發與維護。
阿里雲內核網絡團隊:負責阿里雲服務器操作系統中,內核網絡協議棧的開發與維護。

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章