linux TCP協議(1)---連接管理與狀態機


前言:TCP是傳輸層協議,實現了一種可靠的通信。它從不同角度提供了多種可靠性保障措施來為網絡傳輸提供確定性。連接性就是其中之一,不像UDP的無連接狀態,TCP在數據傳輸之前會進行連接,只有雙方都協調完成后,才會進行數據傳輸;同樣的,在結束時,又會斷開連接,通告傳輸的完成;在數據傳輸過程中,又會對每個傳輸進行確認。更多的可靠性措施在后面的系列中會仔細說明,這一篇,重點從連接這個角度看看TCP協議。

一. TCP狀態機的運轉

二. TCP的連接與斷開

2.1 TCP連接處理

2.1.1 listen()調用

listen()系統調用是服務器側編程的一個必要動作,主要是把創建的主動socket變成被動socket,那么這里主動和被動有什么區別呢?通過代碼一探里面的操作:當調用socket()時,會調用對應的協議無關層的接口

sock->ops->listen(sock, backlog);

這里的ops->listen最終調用的就是inet_listen(),在這個函數中,我們看到

old_state = sk->sk_state;
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
	goto out;

/* Really, if the socket is already in listen state
* we can only allow the backlog to be adjusted.
*/
if (old_state != TCP_LISTEN) {
err = inet_csk_listen_start(sk, backlog);
if (err)
	goto out;
}

sk->sk_max_ack_backlog = backlog;
err = 0;

在開始就是檢查套接字的狀態,如果已經是listen狀態,則只要更改backlog的值;如果不是listen狀態,就啟動設置listen。看這個inet_csk_listen_start(sk, backlog);,里面把套接字狀態設置為listen:

sk->sk_state = TCP_LISTEN;

所以,所說的把主動套接字變成被動套接字主要就是改變TCP的初始狀態,從closed狀態轉為listen狀態。從第一節的狀態機上可以看出不同的起始狀態后續的處理也不同。

2.1.2 TCP發起連接

TCP的連接過程主要是3次握手,如下圖所示:

這是一張截取《TCP/IP 詳解》中的握手圖,其中的序列號注意一下。左端為客戶端,右端為服務端。

一般來說是客戶端主動發起連接,而服務端則接收並建立連接。服務端是在調用connect()時發起SYN分節,那接下來就來看看這個connect系統調用里面都做了哪些事:

err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
				 sock->file->f_flags);

一樣的,這里的ops->connect在INET域就是inet_stream_connect(),在這個函數中,看到在檢查了套接字還沒有連接的前提下,就調用TCP的連接函數:

err = sk->sk_prot->connect(sk, uaddr, addr_len);
if (err < 0)
  goto out;

這里的sk_prot->connect就是tcp_v4_connect(),在這個函數中,查找路由,填充各種信息等,最后調用tcp_connect(),這個函數主要就是構建一個SYN報文發送出去。后面是tcp_transmit_skb(),填充一下消息頭等各種操作,最后發送到隊列中:

err = icsk->icsk_af_ops->queue_xmit(skb, 0);
if (likely(err <= 0))
  return err;

最后會調用icsk_af_ops->queue_xmit()將數據包往IP層發送,那么這個queue_xmit是什么呢?如果我們去搜這個icsk_af_ops注冊的地方,就會發現在TCP操作集的初始化tcp_v4_init_sock()中,icsk_af_ops->queue_xmit()被設置為:

icsk->icsk_af_ops = &ipv4_specific;

const struct inet_connection_sock_af_ops ipv4_specific = {
	.queue_xmit	   = ip_queue_xmit,
	.send_check	   = tcp_v4_send_check,
	.rebuild_header	   = inet_sk_rebuild_header,
	.conn_request	   = tcp_v4_conn_request,
	.syn_recv_sock	   = tcp_v4_syn_recv_sock,
	.remember_stamp	   = tcp_v4_remember_stamp,
	.net_header_len	   = sizeof(struct iphdr),
	.setsockopt	   = ip_setsockopt,
	.getsockopt	   = ip_getsockopt,
	.addr2sockaddr	   = inet_csk_addr2sockaddr,
	.sockaddr_len	   = sizeof(struct sockaddr_in),
	.bind_conflict	   = inet_csk_bind_conflict,
#ifdef CONFIG_COMPAT
	.compat_setsockopt = compat_ip_setsockopt,
	.compat_getsockopt = compat_ip_getsockopt,
#endif
};

可以看出來,最后的queue_xmit就是ip_queue_xmit()。然后就交給IP層處理。

那么這個tcp_v4_init_sock()是什么時候初始化的呢?我們看到這個函數是作為TCP操作集init的回調函數。它是在創建socket的時候,初始化的。系統調用socket會調用inet_init(),在這個函數中:

if (sk->sk_prot->init) {
  err = sk->sk_prot->init(sk);
if (err)
  sk_common_release(sk);

當創建的是流式套接字時,對應於INET族的就是TCP協議,就會調用tcp_v4_init_sock()

這就是客戶端主動發起連接的過程,當然里面還有復雜的多種其他任務的處理,自行根據需要分析。

2.1.3 TCP接收連接

對於tcp接收,首先要確認它的處理函數—tcp_v4_rcv(),這是根據接收的報文頭層層傳遞上來的,具體的過程,在講設備無關層是具體說明。忽略其他的處理,然后就到了tcp_v4_do_rcv(),在這里進行報文的實際處理,主要分為三條線:

  1. 對於已經建立的tcp連接,此時接收的就是數據報文,進入到內層處理:

    if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
      TCP_CHECK_TIMER(sk);
      if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
        rsk = sk;
        goto reset;
      }
      TCP_CHECK_TIMER(sk);
      return 0;
    }
    

    對於內層的tcp_rcv_established()就不進行詳細說明了,對於它的工作,很顯然的是拷貝報文到用戶態應用程序,是在里面的tcp_copy_to_iovec()中做的,就是拷貝報文。

  2. 對於是tcp listen狀態的,則進入建立連接的處理。最終進入到tcp_rcv_state_process()狀態機處理。

  3. 對於不是以上兩種狀態的,則直接進入到狀態機處理。

從以上可以看出,對於接收,最重要的處理過程就是tcp狀態機。理解了狀態機的轉換過程,也就明白了代碼處理的邏輯。

2.2 TCP斷開處理

tcp的斷開過程形象的說是4次揮手的過程,如下圖所示:

這里一共有4次交互過程,關於為什么中間的ack M+1和FIN N不合並成一條呢?在《unix 網絡編程》中作者曾提到過,其中一個原因就是另一方暫時不想斷開,也就是說tcp是雙工的,允許一個方向斷開,而另個方向暫時不斷開的情形,就是所說的半關閉狀態。

其中主動關閉的一方可以在調用close()時發送FIN報文,開始關閉過程。如果調用shutdown()會觸發單向關閉,可以去查看源代碼:在用戶調用close()時,最終根據套接字類型,會找到tcp_close()函數。在其中最后調用了tcp_send_fin()發送FIN報文。然后就是接收報文進入狀態機進行處理。

對於斷開連接有一個問題:TIME_WAIT狀態會保持2MSL時間,其中的解釋如下(取自網絡)

主動發起關閉連接的操作的一方將達到TIME_WAIT狀態,而且這個狀態要保持Maximum Segment Lifetime的兩倍時間。為什么要這樣做而不是直接進入CLOSED狀態?

原因有二:

一、保證TCP協議的全雙工連接能夠可靠關閉

二、保證這次連接的重復數據段從網絡中消失

先說第一點,如果Client直接CLOSED了,那么由於IP協議的不可靠性或者是其它網絡原因,導致Server沒有收到Client最后回復的ACK。那么Server就會在超時之后繼續發送FIN,此時由於Client已經CLOSED了,就找不到與重發的FIN對應的連接,最后Server就會收到RST而不是ACK,Server就會以為是連接錯誤把問題報告給高層。這樣的情況雖然不會造成數據丟失,但是卻導致TCP協議不符合可靠連接的要求。所以,Client不是直接進入CLOSED,而是要保持TIME_WAIT,當再次收到FIN的時候,能夠保證對方收到ACK,最后正確的關閉連接。

再說第二點,如果Client直接CLOSED,然后又再向Server發起一個新連接,我們不能保證這個新連接與剛關閉的連接的端口號是不同的。也就是說有可能新連接和老連接的端口號是相同的。一般來說不會發生什么問題,但是還是有特殊情況出現:假設新連接和已經關閉的老連接端口號是一樣的,如果前一次連接的某些數據仍然滯留在網絡中,這些延遲數據在建立新連接之后才到達Server,由於新連接和老連接的端口號是一樣的,又因為TCP協議判斷不同連接的依據是socket pair,於是,TCP協議就認為那個延遲的數據是屬於新連接的,這樣就和真正的新連接的數據包發生混淆了。所以TCP連接還要在TIME_WAIT狀態等待2倍MSL,這樣可以保證本次連接的所有數據都從網絡中消失。

2.3 異常處理

  1. 超時

    由於tcp是可靠的協議,因此,在一方發送數據后,期望能夠確認數據發送成功或者失敗。在linux中提供了重傳定時器,用於在數據超時的時候進行重傳。比如在tcp_connect()中,發送SYN連接時,設置的超時定時器:

    inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
    				  inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
    

三. TCP連接的特殊狀態

3.1 半打開狀態

半打開狀態是指連接的一方由於異常關閉,而另一端對此並不知情的場景。常見的觸發原因有:主機掉電等。在這個時候,另一端如果發送數據,必然是不會通的,因為此時掉電重啟的主機已經沒有了連接的信息,會回復一個RST報文復位連接。對於檢測半打開狀態,可以使用keepalive保活定時器,默認是120分鍾。也可以自己實現心跳來保持。

3.2 半關閉狀態

由於tcp是全雙工的,因此雙方都可以關閉自己的連接。而有一種特殊的情況就是:主動發起關閉的一方發送FIN后,被動一方對其進行了確認,然而並沒有接着發送自己的FIN,此時,表示被動一方仍然想要傳輸數據,當然,主動的一方雖然關閉了發送通道,但是仍然可以接收被動方的數據。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM