網路基礎

ytyif發表於2021-01-05

前言

對於開發人員來說,網路部分一直是一個不痛不癢的基礎內容。但是作為開發人員,對一些常見的通訊協議還是需要學習和了解的,只是不用像網路專業人士那樣學的很深入,作為分散式基礎的一部分,這裡也對網路知識做出一個簡單的總結。

網路分層模型

這個其實是大學計算機網路基礎課(奈何這門課概念巨多,長期沒接觸,大部分都遺忘了)。目前的網路分層模型主要是兩個,OSI七層參考模型和TCP/IP四層模型,但是OSI分層模型並不實用,現在提到的網路分層模型一般都是指的TCP/IP網路模型

這篇部落格中的一些圖片總結的不錯,可以參考:網路分層模型

需要知道TCP/IP網路模型的具體具體指的是:應用層、網路層、傳輸層、資料鏈路層和物理層。

網路協議

針對網路協議,我們需要重點了解IP協議和TCP/UDP協議。

協議:其實簡單點理解就是規定了報文的交換方式和包含的含義,沒什麼特別的

IP協議

IP協議詳細解釋,可以參考下面的部落格,其中針對IP資料包也有介紹,這裡不做詳細討論:IP協議及資料包詳解

IP協議其實是網路層的協議,是為TCP和UDP協議服務的,IP協議寫明瞭目的地址,就像我們寄包裹一樣需要寫明目的地址。但是IP協議只是一個"盡力而為"的協議,在網路傳輸過程中可能會出現報文丟失、報文順序打亂和報文重複傳送的問題。對於報文的準確送達與否,交給了上一層來完成。TCP、UDP這兩層協議就是建立在IP層提供的基礎服務之上。根據應用程式需求的不同,可以選擇可靠的傳輸(TCP)或者不可靠的傳輸(UDP)協議

TCP/UDP協議

TCP協議與UDP協議最大的不同就在於,TCP協議能夠檢測和恢復IP層提供的主機到主機的通訊中可能發生的報文丟失、報文重複及其他錯誤。TCP提供了一個可信賴的位元組流通道。

UPD協議只是簡單的擴充套件了IP協議的“盡力而為”的功能,因此使用UDP報文,在必要的時候需要考慮報文的丟失和順序混亂的問題。

TCP同時是一種面向連線的協議,這是其保證資料不丟失的基本所在,針對TCP建立和斷開連線的過程,就是我們常常談到的三次握手和四次揮手(本科學的巨好,現在忘得一乾二淨,扎心啊),下面會重點針對TCP如何建立和斷開連線進行詳細探討。

三次握手和四次揮手

這個也是一個基礎知識,網上一大堆博文在講這個,但是大多都只是在介紹概念,解釋的不是十分通俗,在參考了很多部落格之後,發現這篇文章還算介紹的比較通俗:三次握手和四次揮手通俗解釋(本文中的一些圖,也是盜用的這篇部落格)

三次握手

其實上面的一個圖就通俗的解釋了三次握手,以及握手為什麼是三次,總的來說,任何一種通訊都沒法保證絕對可靠,只是三次握手是建立可靠連線的基本保證。

這個結合上一個圖理解三次握手,就不難了。

針對三次握手中還有一個SYN攻擊,這個攻擊會造成大量的資料包阻塞,這個問題可以參考這篇博文SYN攻擊(這個只是瞭解瞭解)

四次揮手

在理解了三次握手的基礎上,再理解四次握手,就比較容易了,先上圖

說明:其中的ACK是在收到的報文seq的基礎上+1,

這裡只需要解釋為什麼是四次揮手即可,四次揮手其實就是表示TCP斷開連線的時候,客戶端和服務端均可主動發起揮手動作,為什麼是四次,其根本原因就在於三次握手的時候,Client端(客戶端)可以直接傳送SYN+ACK報文,但是在斷開連線的過程中,服務端(這裡依舊以客戶端主動發起斷開連線為例)收到FIN報文的時候並不會立即關閉通訊管道,因為可能還有訊息沒有處理完,所以只能暫時回覆客戶端一個ACK報文,表示客戶端傳送的斷開連線的報文收到了,等我的資料傳送完成之後我才能傳送FIN報文,於是就有了上面的那張圖。

傳輸過程的流量控制和確認機制

針對傳輸過程中的流量控制和確認,其實核心內容就是關於滑動視窗的,但是這一部分也是理解後面的BIO和NIO的基礎

滑動視窗

這個其實也不是什麼新的概念,在老謝的計算機網路教材中依舊存在,只是早就還給老師了。

滑動視窗協議的主要目的就是解決擁塞問題,避免出現接收方來不及接受的情況。滑動視窗是一種流量控制技術,早期的網路通訊中,通訊雙方不會考慮網路的擁塞情況,同時傳送資料,導致中間節點阻塞或者掉包,會出現誰也傳送不了資料的情況,因此就有了滑動視窗協議來解決這個問題。

滑動視窗的大小在TCP三次握手建立連線的時候就已經確定了。同時另外一點,也是最重要的一點,滑動視窗滑動的依據,就是隻有收到接受方確認的訊息回覆,滑動視窗才會向前移動一個窗格。下圖是滑動視窗的動畫示例圖

滑動視窗示例

Socket

這個其實已經不再陌生,Java中對TCP和UDP通訊做了相應的封裝,其中Socket就是用來互相通訊的物件而已,其使用起來更像是一個流物件,如果還不能對Socket有更好的理解,可以參看這篇部落格Java Socket理解

這裡先來一個簡單的Socket例項,目的是為了引出後面的BIO和NIO

TCP簡單例項

客戶端例項程式碼

package com.learn;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * Created by liman on 2018/8/11.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 */
public class ClientSocketDemo {

    public static void main(String[] args) {
        Socket socket = null;

        try {
            socket = new Socket("127.0.0.1",8080);
            PrintWriter out = new PrintWriter(socket.getOutputStream(),true);
            out.write("hello ,this is client");
            //清空緩衝區中的資料
            out.flush();
            System.out.println("資料已經傳送");
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(socket!=null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

Socket服務端程式碼

package com.learn;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Created by liman on 2018/8/11.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 */
public class ServerSocketDemo {

    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        BufferedReader bufferReader = null;

        try {
            serverSocket = new ServerSocket(8080);

            System.out.println("服務端啟動......");

            //服務端會在這裡阻塞,獲取客戶端的資料
            Socket socket = serverSocket.accept();

            //讀取客戶端的資料
            bufferReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            //輸出客戶端資料
            System.out.println("收到的客戶端資料為:"+bufferReader.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(bufferReader!=null){
                try {
                    bufferReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if(serverSocket!=null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

執行結果:

UDP簡單例項

直接上例項吧

服務端程式碼

package com.learn.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * Created by liman on 2018/8/11.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 */
public class UDPServer {

    public static void main(String[] args) {
        //建立服務並且接受一個資料包
        try {
            DatagramSocket datagramSocket = new DatagramSocket(8089);
            byte[] receiveData = new byte[1024];
            DatagramPacket receivePacket = new DatagramPacket(receiveData,receiveData.length);

            //socket將接受的資料放到receivePacket中
            datagramSocket.receive(receivePacket);

            System.out.println("服務端收到的資料為:");

            System.out.println(new String(receiveData,0,receivePacket.getLength()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

客戶端程式碼

package com.learn.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * Created by liman on 2018/8/11.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 */
public class UDPClient {

    public static void main(String[] args) {
        try {

            DatagramSocket datagramSocket = new DatagramSocket();

            InetAddress address = InetAddress.getByName("localhost");
            byte[] sendData = "hello this is client udp".getBytes();

            DatagramPacket sendPacket = new DatagramPacket(sendData,sendData.length,address,8089);


            datagramSocket.send(sendPacket);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

執行結果:

上面的兩個例項可以說非常熟悉了,其實並不是什麼新的東西,就相當於Socket的helloworld,可以說只要談到Socket上述的兩個例子是必須要拿出來走一遍的。

下面附上一個服務端和客戶端能雙向通訊的例項,為了方便我們更好的理解雙方的資料互動

Client端程式碼:

package com.learn.tcp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * Created by liman on 2018/8/11.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 * <p>
 * 可以多次傳送資料
 */
public class ClientMultiDemo {

    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1", 8080);

            BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));

            PrintWriter os = new PrintWriter(socket.getOutputStream());

            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            String line = sin.readLine();
            while (!"bye".equals(line)) {
                os.println(line);
                os.flush();
                System.out.println("客戶端準備傳送的資訊:" + line);
                System.out.println("接收到的服務端資訊:" + is.readLine());
                line = sin.readLine();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Server端程式碼

package com.learn.tcp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Created by liman on 2018/8/11.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 *
 * 可以處理多個客戶端請求的ServerSocket
 */
public class ServerSocketMutilDemo {

    public static void main(String[] args) {
        ServerSocket server = null;

        try {
            server = new ServerSocket(8080);
            Socket socket = server.accept();

            //從客戶端中讀取資訊
            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            //用於向客戶端寫資料的輸出流
            PrintWriter os = new PrintWriter(socket.getOutputStream());

            //這個只是讀取控制檯輸入
            BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));


            String line = sin.readLine();
            while(!"bye".equals(line)){
                os.println(line);
                os.flush();
                System.out.println("服務端即將傳送的資料:"+line);
                System.out.println("收到的客戶端資料為:"+is.readLine());
                line = sin.readLine();
            }


            //這裡為了簡單,直接在這裡關閉流物件
            os.close();
            is.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

執行例項:

BIO簡單理解

上述程式碼的執行過程,可以用下圖簡單來表示

其中每次伺服器處理請求的時候,客戶端就只能阻塞,等待伺服器處理相應的請求。如果我們想在上面做點優化,首先想到的就是利用多執行緒或者執行緒池。如下圖所示:

其實這個就是BIO的一個模型了,tomcat之前的版本就是基於BIO的,在7.7以後變成了NIO。

其實最大的問題就是在與基於緩衝區的資料傳送和接受機制,如果緩衝區沒有填滿資料,緩衝區是不可讀的,因此在資料填滿緩衝區之前,客戶端和服務端都是阻塞的,這就非常影響效率了

於是就有了NIO,每一個客戶端在服務端註冊一個channel,然後 服務端直接輪詢這些channel,這個具體後面學習到Netty的時候,再進行補充。

這篇部落格暫時更新到這兒,後續學習netty的時候進行補充。(To Be Continued......未完待續)

相關文章