很幸運,在華為的上機考試中一道題,也沒有做,然后就去參加面試,鬼知道時怎么回事,方正比其他人幸運多了。
但可悲的是二面沒過,天哪,我知道是什么原因,我先簡單談一談我的面試經歷。
第一面的時候,起始很隨意,就考察一些基本知識,沒有什么難度,然后講一講自己的項目經歷,這些都挺簡單的,過了一面,然后做了下性格測試,因為以前的性格測試不合格。
哦,真是的,這詞做完只用了二十分鍾,比第一次做的時候快多了,然后性格測試也過了,旁邊一哥們被性格測試刷下去了。。。。
到了總和面的時候,確實沒有准備好,因為前一天,忙着把一個設備驅動的源碼分析了下,很少去復習一些基本知識,明知道三次握手,四次揮手肯定會考查,沒有准備好,那我先談一談三次握手和四次揮手吧,挺簡單的是,如果當時課上多想想,那面試的時候就不會那么尷尬了。
三次握手:
TCP 的連接建立:
若A 是運行TCP客戶程序的務器,而B 為運行服務器程序的服務器。兩者最初的狀態都是CLOSEDE,狀態。
TCP 的連接是由服務器開始的。B運行服務器程序的進程首先創建傳輸控制塊,說白了,應該就是一大堆sock的結構體集合。首先我們關注這些狀態的集合,注意
這是用來表征連接狀態的,要與sock的狀態區分開來。如果分析過內核源碼會發現 連接狀態為 socket->sock->sk_state.
而描述 socket 的狀態集合如下:該集合用來描述 socket->state
這也就是我們經常所說的socket 編程,這樣我們就很容易理解。socket對於用戶層來講,可以直觀的看到socket 狀態的變化,而sock 是運行在內核中,對上層用戶透明。
好了到這兒,我們繼續TCP 連接的建立。
首先客戶端和服務端都處於CLOSED 狀態。首先服務器端創建socket ,我不太習慣使用套接字這個東西,簡單的講,我寧願將他描述為一個用於通信的描述符。
首先我們需要明白,TCP 和UDP 等協議的基本架構就是C/S 結構,簡單的客戶服務器模式,所以所謂的套接字編程,就是實現簡單的客戶服務器程序模型。
這也就是為什么要先運行服務器端,那服務器怎么知道,是哪個客戶向自己發起請求呢,好吧就是綁定端口,基於C/S 模式的通信程序都是這樣做的。服務器先綁定一個端口,然后
不斷監聽這個端口,來發現是否有客戶端發起請求,然后進入LISTEN狀態,這個端口是邏輯上的端口。若發現有客戶進行請求,則立即處理該請求。
運行客戶程序的進程也會創建傳輸控制塊,我更願意把它稱為sock ,哦,天哪,我感覺人們起的名字真奇妙。好吧,客戶端會創建 socket ,然后發出主動連接請求,該請求調用函數為
connect () 一堆參數,在寫socket 的時候你肯定見過的。這時候,服務器端得有個接受函數 accept 函數,若接收到客戶端的請求,服務器端的accep 函數會創建一個socket 與之通信,接下來,雙方就開始通信了,客戶服務器模式就是這樣的,挺簡單的吧,我們簡單說明一下這個過程中發生了什么,三次握手到底是怎么來的,為什么是三次,不是兩次一次呢,四次揮手,為什么不是三次,兩次,而是四次呢,我盡量講的簡單明白,並結合內核源碼加深理解吧。
首先,我先簡單的講一講tcp報文的格式吧 簡單的說就是tcp 頭部的構成,很簡單,看代碼。
struct tcphdr { __be16 source;// 16位的源端口 __be16 dest;// 16位的目的端口 __be32 seq;// 表示此次發送的數據在整個報文段中的起始字節數,序列號,在建立通信時,雙方使用一個隨機的序列號 __be32 ack_seq;// 期望下一次收到的第一個數據字節的序號,我們經常說的ack #if defined(__LITTLE_ENDIAN_BITFIELD) __u16 res1:4,// 保留位 兩個字節 doff:4,//tcp 頭部的長度,指明在tcp 頭部包含多少個32位的字。即數據部分在本地報文段開始的偏移量,因為首部的長度是可變的,所以數據偏移字段的設立是必須的 fin:1,//用於釋放一個連接,fin=1 表示欲發送的數據已發送完畢,並要求釋放傳輸連接。 syn:1,// 同步序號,用來發起一個連接。當syn=1 ,而ack=0,時表示這個報文是一個連接請求報文,若對方同意連接,則會在應答報文中使得syn=1,ack=1,可見 syn=1,表示該報文是一個連接請求報文還是一個連接接受報文 rst:1,//rst=1 ,表示tcp連接中出現了重大問題,必須釋放傳輸連接,而后再重建。該位可以用來拒絕一個非法的報文段或拒絕一個連接請求 psh:1,//psh=1 表示請求接收端tcp 將此報文段立即送往應用層,而不是將它緩存起來直到整個緩沖區被填滿后再向上交付。 ack:1,//當ack=1 時,確認好才有意義,tcp規定所有鏈接建立后,在連接后所有傳送的報文都必須吧ack 置為1 urg:1,// 緊急指針,表示本報文的數據的緊急程度,urg=1 表示該報文應該具有高優先級,應盡快被發送。若接收端收到 urg=1 的報文段,他將利用緊急指針的值從報文段提取緊急數據,不再按序交給應用層需。 ece:1, cwr:1; #elif defined(__BIG_ENDIAN_BITFIELD) __u16 doff:4, res1:4, cwr:1, ece:1, urg:1, ack:1, psh:1, rst:1, syn:1, fin:1; #else #error "Adjust your <asm/byteorder.h> defines" #endif __be16 window;// 窗口,用來控制流的大小。窗口值的的大小為 0-65535 ,通常由接收端確定,指的是發送報文段的一方的接收窗口大小。窗口值為0 ,表示接收端狀態不佳。 __sum16 check;// 校驗和,該校驗和是整個報文段,包括首部和數據。 __be16 urg_ptr;// 緊急指針,urg=1 時才有意義,他指出了緊急數據在報文中的位置,使得接收端能知道緊急數據的字節數。 };
連接建立主要分為以下三步:
1 客戶進程向服務器發出連接請求的報文,調用函數 tcp_connect () 發起主動連接,會創建一個tcp 報文,其中SYN=1,同時選擇一個 sn 即序列號,表明在即將傳輸的數據的第一個
字節序列號為i。TCP 標准規定,對 SYN =1 的報文段要賦一個序列號,即使這個報文沒有數據,此時客戶端進入 SYN_SENT 狀態。更准確的說是進入 TCP_SYN_SNET 狀態。
我們看一看內核源碼:
/* Build a SYN and send it off. */ int tcp_connect(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *buff; int err; tcp_connect_init(sk); if (unlikely(tp->repair)) { tcp_finish_connect(sk, NULL); return 0; } buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation); if (unlikely(!buff)) return -ENOBUFS; tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); tp->retrans_stamp = tcp_time_stamp; tcp_connect_queue_skb(sk, buff); tcp_ecn_send_syn(sk, buff); /* Send off SYN; include data in Fast Open. */ err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) : tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);// 構造tcp 報文並發送 if (err == -ECONNREFUSED) return err; /* We change tp->snd_nxt after the tcp_transmit_skb() call * in order to make this packet get counted in tcpOutSegs. */ tp->snd_nxt = tp->write_seq; tp->pushed_seq = tp->write_seq; TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS); /* Timer for repeating the SYN until an answer. */ inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX); return 0; }
2 服務器接收到連接請求后,如果同意連接,則回答此報文。確認報文首部中的SYN=1,ACK=1,序列號為 seq=j,ack_seq=i+1.此時服務器端進入 SYN_RECV 狀態。