三次握手和四次揮手說完了,還讓我手動寫個HTTP協議代碼
最近阿粉的同事們在準備面試,其中也有收到offer的幾個不錯的人,畢竟疫情穩定了,而阿粉在電話面試的時候,被問到關于HTTP協議的內容的時候,卻顯得有點麻木了,為什么呢?因為套路太深了,讓阿粉猝不及防呀。
面試官:你了解TCP/IP協議么?說實話在阿粉聽到這個問題的時候,阿粉的第一想法就是,我回答了這個問題,接下來肯定還有一個三次握手和四次揮手等著我,但是還是得回答呀,于是阿粉就開始作答了。
阿粉開始作答:TCP/IP協議雖然會放在一起說,但是他們其實呢是屬于兩個不同的協議。
- IP協議:IP協議實際上是用來查找地址的,而它對應的層級也是網絡層,也可以稱之為網際互聯層,區別不大,叫法不同而已。
- TCP協議:TCP協議是用來規范傳輸規則的,和IP協議是不同的,而它對應的層級是傳輸層,而這樣的話,也就是IP去尋找地址,把所有的傳輸任務都交給TCP,而TCP這時候就相當于一個快遞員的身份出現并且存在。
面試官:那你說說什么是三次握手,什么是四次揮手吧
1. 三次握手
大家看這個圖,圖是來自于百度搜索,而且百度上有各種各樣的圖,當你看到圖的時候第一時間肯定是看不懂的,也就是只能通過這個畫的標志的“線”來進行分析,其實這僅僅只是一個方面。
那么我們就來根據圖來解析一下這個圖中都代表了什么意思,圖中存在著兩個序號和三個不同的標志位其中有大小寫容易混淆的呦。
序號:
- seq:sequence number 的縮寫,直譯的話,序號,對沒錯,它就是序號,你沒有翻譯錯,相信自己,而這個seq表示的則是自己傳遞的序號,TCP在傳輸的時候,其中的每一個字節,都會有一個序號,發送數據的時候,會把第一個數據的第一個序號發送給對方,就是我們所看到的第一步,而接收的這一方面,會按照這個序號來檢查是否是一個連接完整的數據,如果說你數據是完整的,那么好,我們可以繼續下一步,如果你不是完整的,那就重新傳送唄,而這樣的話也能保證數據的完整性不被破壞。
- ack:注意,這是小寫的ack,也就是acknoledgement number的縮寫,而他表示的是確認號,這個要和ACK(確認位)進行區分,接收端這時候用它來給發送端返回成功接收消息的數據信息,而這時候,它的值就是表明,我現在想接收下一個數據包了,而這個值就是下一個數據包的開始的序號,而這個ack所代表的的值的序號前面的數據都已經接收成功了。
- ACK:確認位,確認位來了,只有當ACK=1的時候ack才會起到自己應該起的作用,而在我們第一次發起請求的時候,因為沒有需要我們確認的接收的數據,所以這個時候的ACK就是0,而正常通信的情況下,ACK就1.
- SYN:同步位,而同步位的作用就是用于建立連接時同步序號,而剛連接的時候,說ACK是0,那么ack就不起作用,這時候SYN就來說,你看沒我你們不行了把,要你們有何用,當接收端接收到SYN=1的報文的時候,就會將ack設置為接收到的seq+1的值,這也是大家在看百度上提供的內容的時候看到的,各種seq=k,ACK=k+1,這玩意就是這么來的,這時候ack的值就是根據SYN來直接設置的,這樣你才能正常的進行傳輸,而SYN有時候會被面試官問到為什么在前兩次握手的時候都是1呢?其實這是因為傳輸數據的雙方的ack都是要一個初始值的,不然你還怎么傳輸,還怎么玩。
- FIN:終止位,這個在本圖中,并沒有完全的體現,在四次揮手的時候就能完全的體現出來了。而它則是用來在數據傳輸都完成之后來釋放連接的。
那么關于這個圖,我們怎么給面試官說呢?
(1) 第一次握手(SYN=1, seq=x):
客戶端發送一個 TCP 的 SYN 標志位置1的包,指明客戶端打算連接的服務器的端口,以及初始序號 X,保存在包頭的序列號(Sequence Number)字段里。
發送完畢后,客戶端進入 SYN_SEND 狀態。
(2) 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):
服務器發回確認包(ACK)應答。即 SYN 標志位和 ACK 標志位均為1。服務器端選擇自己 ISN 序列號,放到 Seq 域里,同時將確認序號(Acknowledgement Number)設置為客戶的 ISN 加1,即X+1。發送完畢后,服務器端進入 SYN_RCVD 狀態。
(3) 第三次握手(ACK=1,ACKnum=y+1)
客戶端再次發送確認包(ACK),SYN 標志位為0,ACK 標志位為1,并且把服務器發來 ACK 的序號字段+1,放在確定字段中發送給對方,并且在數據段放寫ISN的+1
發送完畢后,客戶端進入 ESTABLISHED 狀態,當服務器端接收到這個包時,也進入 ESTABLISHED 狀態,TCP 握手結束。
你如果這么說,面試官有可能還會問,你這也太官方了,能不能說說你的理解,那么你可以用一個實際上的例子來給他說一下,
阿粉:雞丁,嘿,我是阿粉,你聽的到我說話么?
雞丁:吵吵啥,聽到了,除了你我還能認識誰。
阿粉:你聽的到你還不趕緊回復,怪不得你沒有女朋友呢。那我們再繼續交流一下吧。
而這三次的對話過程就是通俗的三次握手,期間對話三次,以此來確定兩個方向上的數據傳輸通道是否正常。
2. 四次揮手
那么四次揮手怎么來回答呢?
(1)第一次揮手(FIN=1,seq=x)
假設客戶端想要關閉連接,客戶端發送一個 FIN 標志位置為1的包,表示自己已經沒有數據可以發送了,但是仍然可以接受數據。
發送完畢后,客戶端進入 FIN_WAIT_1 狀態。
(2) 第二次揮手(ACK=1,ACKnum=x+1)
服務器端確認客戶端的 FIN 包,發送一個確認包,表明自己接受到了客戶端關閉連接的請求,但還沒有準備好關閉連接。
發送完畢后,服務器端進入 CLOSE_WAIT 狀態,客戶端接收到這個確認包之后,進入 FIN_WAIT_2 狀態,等待服務器端關閉連接。
(3) 第三次揮手(FIN=1,seq=y)
服務器端準備好關閉連接時,向客戶端發送結束連接請求,FIN 置為1。
發送完畢后,服務器端進入 LAST_ACK 狀態,等待來自客戶端的最后一個ACK。
(4) 第四次揮手(ACK=1,ACKnum=y+1)
客戶端接收到來自服務器端的關閉請求,發送一個確認包,并進入 TIME_WAIT狀態,等待可能出現的要求重傳的 ACK 包。
服務器端接收到這個確認包之后,關閉連接,進入 CLOSED 狀態。
客戶端等待了某個固定時間(兩個最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,沒有收到服務器端的 ACK ,認為服務器端已經正常關閉連接,于是自己也關閉連接,進入 CLOSED 狀態。兩次后會重傳直到超時。如果多了會有大量半鏈接阻塞隊列。
那么怎么去通俗的給面試官說呢?
阿粉:雞丁呀,我要說的都說完了,你還有啥事么?
雞丁:你說的我都明白了,但是別斷,我還有要囑咐你的,給我找女朋友的事情。
雞丁:xxxxx,我說完了。
阿粉,行啦,別BB了,記住了,掛了把。
如果面試官問你的時候,你這么回答的話,既有官方的解釋,還有本身自己的理解,那么這個問題就已經算是差不多了,
而面試官顯然不可能會這么放過你,肯定再給你來個雷,為啥是三次握手,而是四次揮手呢?為啥不是三次呢?
這是因為服務端在LISTEN狀態下,收到建立連接請求的SYN報文后,把ACK和SYN放在一個報文里發送給客戶端。而關閉連接時,當收到對方的FIN報文時,僅僅表示對方不再發送數據了但是還能接收數據,己方是否現在關閉發送數據通道,需要上層應用來決定,因此,己方ACK和FIN一般都會分開發送。所以這時候揮手的時候就是四次,而不再是三次了。
那么我們怎么去手寫一個HTTP協議呢?代碼送上:
- public class Server {
- public static void main(String[] args) throws Exception{
- ServerSocketChannel ssc = ServerSocketChannel.open();
- ssc.socket().bind(new InetSocketAddress(8080));
- ssc.configureBlocking(false);
- Selector selector = Selector.open();
- ssc.register(selector, SelectionKey.OP_ACCEPT);
- while (true){
- if (selector.select(3000)==0){
- continue;
- }
- Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
- while (keyIterator.hasNext()){
- SelectionKey key = keyIterator.next();
- new Thread(new HttpHandler(key)).run();
- keyIterator.remove();
- }
- }
- }
- private static class HttpHandler implements Runnable{
- private int bufferSize = 1024;
- private String localCharset = "UTF-8";
- private SelectionKey key;
- public HttpHandler(SelectionKey key){
- this.key=key;
- }
- public void handleAccept()throws IOException{
- SocketChannel clientChannel = ((ServerSocketChannel)key.channel()).accept();
- clientChannel.configureBlocking(false);
- clientChannel.register(key.selector(),SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
- }
- @Override
- public void run() {
- try {
- if (key.isAcceptable()){
- handleAccept();
- }
- if (key.isReadable()){
- handleRead();
- }
- }catch (IOException e){
- e.printStackTrace();
- }
- }
- public void handleRead() throws IOException{
- SocketChannel sc = (SocketChannel) key.channel();
- ByteBuffer buffer = (ByteBuffer) key.attachment();
- buffer.clear();
- if (sc.read(buffer)==-1){
- sc.close();
- }else {
- buffer.flip();
- String receiveString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
- String[] requestMessage = receiveString.split("\r\n");
- for (String s:requestMessage) {
- System.out.println(s);
- if (s.isEmpty()){
- break;
- }
- String[] firstLine = requestMessage[0].split(" ");
- System.out.println();
- System.out.println("Method:\t"+firstLine[0]);
- System.out.println("url:\t"+firstLine[1]);
- System.out.println("HTTP Version:\t"+firstLine[2]);
- System.out.println();
- StringBuffer sendString = new StringBuffer();
- sendString.append("HTTP/1.1 200 OK\r\n");
- sendString.append("Content-Type:text/html;charset="+localCharset+"\r\n");
- sendString.append("\r\n");
- sendString.append("<html><head><title>顯示報文</title></head><body>");
- sendString.append("接受到的請求報文是:<br/>");
- for (String s1:requestMessage) {
- sendString.append(s1+"<br/>");
- }
- sendString.append("</body></html>");
- buffer = ByteBuffer.wrap(sendString.toString().getBytes(localCharset));
- sc.write(buffer);
- sc.close();
- }
- }
- }
- }
- }
這是一個簡單的實現,只是實現思路,并不是真正的處理請求,而大家也要注意設置Content-Type的類型,不然容易出問題的,畢竟長度是有限制的。