1.TCP建立連接過程簡介:
TCP是面向連接的協議。面向連接的傳輸層協議在源點和終點之間建立了一條虛路徑。同屬於一個報文的所有報文段都沿着這條虛路徑發送。為整個報文使用一條虛路徑能夠更容易地實施確認過程以及對損傷或丟失報文的重傳。
在TCP中,面向連接的傳輸需要經過三個階段:連接建立、數據傳輸和連接終止。
三次握手建立連接
在TCP中使用的連接建立過程稱為三向握手(three way handshaking)。在我們的例子中,一個稱為客戶的應用程序希望使用TCP作為傳輸層協議來和另一個稱為服務器的應用程序建立連接。
這個過程從服務器開始。服務器程序告訴它的TCP自己已准備好接受連接。這個請求稱為被動打開請求。雖然服務器的TCP已准備好接受來自世界上任何一個機器的連接,但是它自己並不能完成這個連接。
客戶程序發出的請求稱為主動打開。打算與某個開放的服務器進行連接的客戶告訴它的TCP,自己需要連接到某個特定的服務器上。TCP現在可以開始進行如下圖所示的三向握手過程。
每個報文段的首部字段值都是完整的,並且可能還有一些可選字段也有相應的數值,不過,為了方便我們理解每個階段,只畫出了其中很少幾個字段。圖中顯示了序號、確認號、控制標志(只有那些置1的)以及有關的窗口大小。這個階段的三個步驟如下所示。.
1.客戶發送第一個報文段(SYN報文段),在這個報文段中只有SYN標志置為1。這個報文段的作用是同步序號。在我們的例子中,客戶選擇了一個隨機數作為第一個序號,並把這個序號發送給服務器。這個序號稱為初始序號(ISN)。 請注意,這個報文段中不包括確認號,也沒有定義窗口大小。只有當一個報文段中包含了確認時,定義窗口大小才有意義。這個報文段還可以包含一些選項。請注意,SYN報文段是一個控制報文段,它不攜帶任何數據。但是,它消耗了一個序號。當數據傳送開始時,序號就應當加1。我們可以說,SYN報文段不包含真正的數據,但是我們可以想象它包含了一個虛字節。
SYN報文段不攜帶任何數據,但是它要消耗一個序號。
2.服務器發送第二個報文段,即SYN + ACK報文段,其中的兩個標志(SYN和ACK)置為1。這個報文段有兩個目的。首先,它是另一個方向上通信的SYN報文段。服務器使用這個報文段來同步它的初始序號,以便從服務器向客戶發送字節。其次,服務器還通過ACK標志來確認已收到來自客戶端的SYN報文段,同時給出期望從客戶端收到的下一個序號。因為這個報文段包含了確認,所以它還需要定義接收窗口大小,即rwnd (由客戶端使用)。
SYN+ACK報文段不攜帶數據,但要消耗一個序號。
3.客戶發送第三個報文段。這僅僅是一一個ACK報文段。它使用ACK標志和確認號:字段來確認收到了第二個報文段。請注意,這個報文段的序號和SYN報文段使用的序號一樣,也就是說,這個ACK報文段不消耗任何序號。客戶還必須定義服務器的窗口大小。在某些實現中,連接階段的第三個報文段可以攜帶客戶的第一一個數據塊。在這種情況下,第三個報文段必須有一個新的序號來表示數據中的第一個字節的編號。通常,第三個報文段不攜帶數據,因而不消耗序號。
ACK報文段如果不攜帶數據就不消耗序號。
2.connect及bind、listen、accept背后的三次握手
TCP建立連接的過程,也就是三次握手的過程已經介紹過了,下面就應該介紹,隱藏在socketAPI后面的關於TCP連接時序的問題了。
根據socket編程的流程圖,我們直觀的感覺到,TCP連接的建立應該與connect函數直接相關,而諸如bind、listen、accept,都應該是建立連接之前做的准備或是建立連接之后的數據的傳送。
查閱相關資料可知:
1. bind 函數主要是服務器端使用,把一個本地協議地址賦予套接字;socket 函數並沒有為套接字綁定本地地址和端口號,對於服務器端則必須顯性綁定地址和端口號。
2. listen() 函數的主要作用就是將套接字( sockfd )變成被動的連接監聽套接字(被動等待客戶端的連接),至於參數 backlog 的作用是設置內核中連接隊列的長度(這個長度有什么用,后面做詳細的解釋),TCP 三次握手也不是由這個函數完成,listen()的作用僅僅告訴內核一些信息。
3. accept()函數功能是,從處於 established 狀態的連接隊列頭部取出一個已經完成的連接,如果這個隊列沒有已經完成的連接,accept()函數就會阻塞,直到取出隊列中已完成的用戶連接為止。與三次握手也沒有直接的聯系,
下面給出這幾個函數的關系圖:
顯然這幾個函數中只有connect函數與TCP三次握手直接相關,所以下面直接分析connect函數:
鑒於上次對系統調用的分析,connect函數功能的實現本質上是進行了相關系統調用,所以應該查詢__sys_connect()函數,下面給出這個函數的源碼:
int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen) { struct socket *sock; struct sockaddr_storage address; int err, fput_needed; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (!sock) goto out; err = move_addr_to_kernel(uservaddr, addrlen, &address); if (err < 0) goto out_put; err = security_socket_connect(sock, (struct sockaddr *)&address, addrlen); if (err) goto out_put; err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags); out_put: fput_light(sock->file, fput_needed); out: return err; }
閱讀源碼,並查看相關資料可得,__sys_connect()函數的工作過程大致如下:
1.查找文件句柄對應的socket
2.從用戶態復制地址參數到內核中
3.安全審計
4.調用傳輸層的connet方法inet_stream_connect或inet_dgram_connect
其中真的牽涉到TCP建立連接的是函數inet_stream_connect()
為了驗證猜想的真實性,在函數inet_stream_connect()處打上斷點,並進行跟蹤可得:
給出斷點列表(這里的列表已經囊括了后面提及的一些API函數)
給出追蹤結果:
通過截圖可看出,果然可以追蹤到此函數,通過打斷點的提示信息,找到此函數的文件所在地,翻出源碼分析如下:
這里張貼出其代碼和注釋:
/* connect系統調用的套接口層實現 */ int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags) { struct sock *sk = sock->sk; int err; long timeo; lock_sock(sk);/* 獲取套接口的鎖 */ if (uaddr->sa_family == AF_UNSPEC) {/* 未指定地址類型,錯誤 */ err = sk->sk_prot->disconnect(sk, flags); sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED; goto out; } switch (sock->state) { default: err = -EINVAL; goto out; case SS_CONNECTED:/* 已經與對方端口連接*/ err = -EISCONN; goto out; case SS_CONNECTING:/*正在連接過程中*/ err = -EALREADY; /* Fall out of switch with err, set for this state */ break; case SS_UNCONNECTED:/* 只有此狀態才能調用connect */ err = -EISCONN; if (sk->sk_state != TCP_CLOSE)/* 如果不是TCP_CLOSE狀態,說明已經連接了 */ goto out; /* 調用傳輸層接口tcp_v4_connect建立與服務器連接,並發送SYN段 */ err = sk->sk_prot->connect(sk, uaddr, addr_len); if (err < 0) goto out; /* 發送SYN段后,設置狀態為SS_CONNECTING */ sock->state = SS_CONNECTING; err = -EINPROGRESS;/* 如果是以非阻塞方式進行連接,則默認的返回值為 EINPROGRESS,表示正在連接 */ break; } /* 獲取連接超時時間,如果指定非阻塞方式,則不等待直接返回 */ timeo = sock_sndtimeo(sk, flags & O_NONBLOCK); if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {/* 發送完SYN 后,連接狀態一般為這兩種狀態,但是如果連接建立非常快,則可能越過這兩種狀態 */ if (!timeo || !inet_wait_for_connect(sk, timeo))/* 等待連接完成或超時 */ goto out; err = sock_intr_errno(timeo); if (signal_pending(current)) goto out; } if (sk->sk_state == TCP_CLOSE)/* 運行到這里說明連接建立失敗 */ goto sock_error; sock->state = SS_CONNECTED;/* 連接建立成功,設置為已經連接狀態 */ err = 0; out: release_sock(sk); return err; sock_error: err = sock_error(sk) ? : -ECONNABORTED; sock->state = SS_UNCONNECTED; if (sk->sk_prot->disconnect(sk, flags)) sock->state = SS_DISCONNECTING; goto out; }
可以分析出
inet_stream_connect()函數主要功能如下:
(1)調用tcp_v4_connect函數建立與服務器聯系並發送SYN段;
(2)獲取連接超時時間timeo,如果timeo不為0,則會調用inet_wait_for_connect一直等待到連接成功或超時;
這個時候就很明顯了,由tcp_v4_connect函數建立與服務器聯系並發送SYN段,所以三次握手環節肯定體現在該函數中,查閱資料可得,該函數包含的內容很多,針對從此次的實驗,主要是tcp_connect(struct sock *sk)函數起到了發送SYN段並通過三次握手建立連接的作用。
和上面的環節一樣,為了驗證猜想,還是為tcp_connect函數打上斷點,並追蹤它,查看程序運行時是否調用過它:
根據提示信息查看源碼,分析可得:
/* 構造並發送SYN段 */ int tcp_connect(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *buff; tcp_connect_init(sk);/* 初始化傳輸控制塊中與連接相關的成員 */ /* 為SYN段分配報文並進行初始化 */ buff = alloc_skb(MAX_TCP_HEADER + 15, sk->sk_allocation); if (unlikely(buff == NULL)) return -ENOBUFS; /* Reserve space for headers. */ skb_reserve(buff, MAX_TCP_HEADER); TCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN; TCP_ECN_send_syn(sk, tp, buff); TCP_SKB_CB(buff)->sacked = 0; skb_shinfo(buff)->tso_segs = 1; skb_shinfo(buff)->tso_size = 0; buff->csum = 0; TCP_SKB_CB(buff)->seq = tp->write_seq++; TCP_SKB_CB(buff)->end_seq = tp->write_seq; tp->snd_nxt = tp->write_seq; tp->pushed_seq = tp->write_seq; tcp_ca_init(tp); /* Send it off. */ TCP_SKB_CB(buff)->when = tcp_time_stamp; tp->retrans_stamp = TCP_SKB_CB(buff)->when; /* 將報文添加到發送隊列上 */ __skb_queue_tail(&sk->sk_write_queue, buff); sk_charge_skb(sk, buff); tp->packets_out += tcp_skb_pcount(buff); /* 發送SYN段 */ tcp_transmit_skb(sk, skb_clone(buff, GFP_KERNEL)); TCP_INC_STATS(TCP_MIB_ACTIVEOPENS); /* Timer for repeating the SYN until an answer. */ /* 啟動重傳定時器 */ tcp_reset_xmit_timer(sk, TCP_TIME_RETRANS, tp->rto); return 0; }
tcp_connect()中又調用了tcp_transmit_skb函數:
/* 發送一個TCP報文 */ static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb) { if (skb != NULL) { struct inet_sock *inet = inet_sk(sk); struct tcp_sock *tp = tcp_sk(sk); struct tcp_skb_cb *tcb = TCP_SKB_CB(skb); int tcp_header_size = tp->tcp_header_len; struct tcphdr *th; int sysctl_flags; int err; BUG_ON(!tcp_skb_pcount(skb)); #define SYSCTL_FLAG_TSTAMPS 0x1 #define SYSCTL_FLAG_WSCALE 0x2 #define SYSCTL_FLAG_SACK 0x4 sysctl_flags = 0;/* 標識TCP選項 */ /* 根據TCP選項調整TCP首部長度 */ if (tcb->flags & TCPCB_FLAG_SYN) {/* 如果當前段是SYN段,需要特殊處理一下 */ /* SYN段必須通告MSS,因此報頭加上MSS通告選項的長度 */ tcp_header_size = sizeof(struct tcphdr) + TCPOLEN_MSS; if(sysctl_tcp_timestamps) {/* 啟用了時間戳 */ /* 報頭加上時間戳標志 */ tcp_header_size += TCPOLEN_TSTAMP_ALIGNED; sysctl_flags |= SYSCTL_FLAG_TSTAMPS; } if(sysctl_tcp_window_scaling) {/* 處理窗口擴大因子選項 */ tcp_header_size += TCPOLEN_WSCALE_ALIGNED; sysctl_flags |= SYSCTL_FLAG_WSCALE; } if(sysctl_tcp_sack) {/* 處理SACK選項 */ sysctl_flags |= SYSCTL_FLAG_SACK; if(!(sysctl_flags & SYSCTL_FLAG_TSTAMPS)) tcp_header_size += TCPOLEN_SACKPERM_ALIGNED; } } else if (tp->rx_opt.eff_sacks) {/* 非SYN段,但是有SACK塊 */ /* 根據SACK塊數調整TCP首部長度 */ tcp_header_size += (TCPOLEN_SACK_BASE_ALIGNED + (tp->rx_opt.eff_sacks * TCPOLEN_SACK_PERBLOCK)); } if (tcp_is_vegas(tp) && tcp_packets_in_flight(tp) == 0) tcp_vegas_enable(tp); /* 在報文首部中加入TCP首部 */ th = (struct tcphdr *) skb_push(skb, tcp_header_size); /* 更新TCP首部指針 */ skb->h.th = th; /* 設置報文的傳輸控制塊 */ skb_set_owner_w(skb, sk); /* Build TCP header and checksum it. */ /* 填充TCP首部中的數據 */ th->source = inet->sport; th->dest = inet->dport; th->seq = htonl(tcb->seq); th->ack_seq = htonl(tp->rcv_nxt); *(((__u16 *)th) + 6) = htons(((tcp_header_size >> 2) << 12) | tcb->flags); /* 設置TCP首部的接收窗口 */ if (tcb->flags & TCPCB_FLAG_SYN) { th->window = htons(tp->rcv_wnd);/* 對SYN段來說,接收窗口初始值為rcv_wnd */ } else { /* 對其他段來說,調用tcp_select_window計算當前接收窗口的大小 */ th->window = htons(tcp_select_window(sk)); } /* 初始化校驗碼和帶外數據指針 */ th->check = 0; th->urg_ptr = 0; if (tp->urg_mode &&/* 發送時設置了緊急方式 */ between(tp->snd_up, tcb->seq+1, tcb->seq+0xFFFF)) {/* 緊急指針在報文序號開始的65535范圍內 */ /* 設置緊急指針和帶外數據標志位 */ th->urg_ptr = htons(tp->snd_up-tcb->seq); th->urg = 1; } /* 開始構建TCP首部選項 */ if (tcb->flags & TCPCB_FLAG_SYN) { /* 調用tcp_syn_build_options構建SYN段的首部 */ tcp_syn_build_options((__u32 *)(th + 1), tcp_advertise_mss(sk), (sysctl_flags & SYSCTL_FLAG_TSTAMPS), (sysctl_flags & SYSCTL_FLAG_SACK), (sysctl_flags & SYSCTL_FLAG_WSCALE), tp->rx_opt.rcv_wscale, tcb->when, tp->rx_opt.ts_recent); } else { /* 構建普通段的首部 */ tcp_build_and_update_options((__u32 *)(th + 1), tp, tcb->when); TCP_ECN_send(sk, tp, skb, tcp_header_size); } /* 計算傳輸層的校驗和 */ tp->af_specific->send_check(sk, th, skb->len, skb); /* 如果發送的段有ACK標志,則通知延時確認模塊,遞減快速發送ACK 段的數量,同時停止延時確認定時器 */ if (tcb->flags & TCPCB_FLAG_ACK) tcp_event_ack_sent(sk); if (skb->len != tcp_header_size)/* 發送的段有負載,則檢測擁塞窗口閑置是否超時 */ tcp_event_data_sent(tp, skb, sk); TCP_INC_STATS(TCP_MIB_OUTSEGS); /* 調用IP層的發送函數發送報文 */ err = tp->af_specific->queue_xmit(skb, 0); if (err <= 0) return err; /* 如果發送失敗,則類似於接收到顯式擁塞通知的處理 */ tcp_enter_cwr(tp); return err == NET_XMIT_CN ? 0 : err; } return -ENOBUFS; #undef SYSCTL_FLAG_TSTAMPS #undef SYSCTL_FLAG_WSCALE #undef SYSCTL_FLAG_SACK }
至此TCP三次握手涉及到的代碼已全部介紹完。