深刻Java網絡編程與NIO(一)

1. 計算機網絡編程基礎

##1.七層模型 七層模型(OSI,Open System Interconnection參考模型),是參考是國際標準化組織制定的一個用於計算機或通訊系統間互聯的標準體系。它是一個七層抽象的模型,不只包括一系列抽象的術語和概念,也包括具體的協議。 經典的描述以下:web

簡述每一層的含義:面試

  1. 物理層(Physical Layer):創建、維護、斷開物理鏈接。
  2. 數據鏈路層 (Link):邏輯鏈接、進行硬件地址尋址、差錯校驗等。
  3. 網絡層 (Network):進行邏輯尋址,實現不一樣網絡之間的路徑選擇。
  4. 傳輸層 (Transport):定義傳輸數據的協議端口號,及流控和差錯校驗。
  5. 會話層(Session Layer):創建、管理、終止會話。
  6. 表示層(Presentation Layer):數據的表示、安全、壓縮。
  7. 應用層 (Application):網絡服務與最終用戶的一個接口

每一層利用下一層提供的服務與對等層通訊,每一層使用本身的協議。瞭解了這些,然並卵。可是,這一模型確實是絕大多數網絡編程的基礎,做爲抽象類存在的,而TCP/IP協議棧只是這一模型的一個具體實現。編程

##2.TCP/IP協議模型 IP數據包結構: IP數據包結構

TCP數據包結構: TCP數據包結構segmentfault

###一個模型例子: 尋址過程:每臺機子都有個物理地址MAC地址和邏輯地址IP地址,物理地址用於底層的硬件的通訊,邏輯地址用於上層的協議間的通訊。尋址過程會先使用ip地址進行路由尋址,在不一樣網絡中進行路由轉發,到了同一個局域網時,再根據物理地址進行廣播尋址,數據在以太網的局域網中都是以廣播方式傳輸的,整個局域網中的全部節點都會收到該幀,只有目標MAC地址與本身的MAC地址相同的幀纔會被接收。數組

創建可靠的鏈接:A向B傳輸一個文件時,若是文件中有部分數據丟失,就可能會形成在B上沒法正常閱讀或使用。 TCP協議就是創建了可靠的鏈接: TCP三次握手肯定了雙方數據包的序號、最大接受數據的大小(window)以及MSS(Maximum Segment Size)安全

會話層用來創建、維護、管理應用程序之間的會話,主要功能是對話控制和同步,編程中所涉及的session是會話層的具體體現。表示層完成數據的解編碼,加解密,壓縮解壓縮等。服務器

#2.Socket編程 在Linux世界,「一切皆文件」,操做系統把網絡讀寫做爲IO操做,就像讀寫文件那樣,對外提供出來的編程接口就是Socket。因此,socket(套接字)是通訊的基石,是支持TCP/IP協議網絡通訊的基本操做單元。socket實質上提供了進程通訊的端點。進程通訊以前,雙方首先必須各自建立一個端點,不然是沒有辦法創建聯繫並相互通訊的。一個完整的socket有一個本地惟一的socket號,這是由操做系統分配的。網絡

在許多操做系統中,Socket描述符和其餘IO描述符是集成在一塊兒的,操做系統把socket描述符實現爲一個指針數組,這些指針指向內部數據結構。進程進行Socket操做時,也有着多種處理方式,如阻塞式IO,非阻塞式IO,多路複用(select/poll/epoll),AIO等等。 多路複用每每在提高性能方面有着重要的做用。 當前主流的Server側Socket實現大都採用了epoll的方式,例如Nginx, 在配置文件能夠顯式地看到 use epoll。session

舉個栗子 Java中Socket服務端的簡單實現:基本思路就是一個大循環不斷監聽客戶端請求,爲了提升處理效率可使用線程池多個線程進行每一個鏈接的數據讀取數據結構

public class BIOServer {
    private ServerSocket serverSocket;
    private ExecutorService executorService = Executors.newCachedThreadPool();

    class Handler implements Runnable {

        Socket socket;
        
        public Handler(Socket socket) {
           this.socket = socket;
        }

        @Override
        public void run() {
            try {
                BufferedReader buf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String readData = buf.readLine();
                while (readData != null) {
                    readData = buf.readLine();
                    System.out.println(readData);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public BIOServer(int port) {
        try {
            serverSocket = new ServerSocket(port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void run() {
        try {
            Socket socket = serverSocket.accept();
            executorService.submit(new Handler(socket));
        } catch (Exception e) {

        }
    }
}

客戶端:創建socket鏈接、發起請求、讀取響應

public class IOClient {

    public void start(String host, int port) {
        try {
            Socket s = new Socket("127.0.0.1",8888);
            InputStream is = s.getInputStream();
            OutputStream os = s.getOutputStream();

            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
            bw.write("測試客戶端和服務器通訊,服務器接收到消息返回到客戶端\n");
            bw.flush();

            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String mess = br.readLine();
            System.out.println("服務器:"+mess);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

#3.IO模型

對於一次IO訪問(以read舉例),數據會先被拷貝到操做系統內核的緩衝區page cache中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。因此說,當一個read操做發生時,它會經歷兩個階段:

  1. 等待數據準備
  2. 將數據從內核拷貝到進程中

IO模型的分類有下:

  • 阻塞 I/O(blocking IO)
  • 非阻塞 I/O(nonblocking IO)
  • I/O 多路複用( IO multiplexing)
  • 異步 I/O(asynchronous IO)

BIO 阻塞 I/O

缺點:一個請求一個線程,浪費線程,且上下文切換開銷大;

上面寫的socket列子就是典型的BIO bio

當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據(對於網絡IO來講,不少時候數據在一開始尚未到達。好比,尚未收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程須要等待,也就是說數據被拷貝到操做系統內核的緩衝區中是須要一個過程的。而在用戶進程這邊,整個進程會被阻塞(固然,是進程本身選擇的阻塞)。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,而後kernel返回結果,用戶進程才解除block的狀態,從新運行起來。

NIO 非阻塞 I/O

bio1 當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error 。從用戶進程角度講 ,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存,而後返回。

nonblocking IO的特色是用戶進程須要不斷的主動詢問kernel數據好了沒有。

I/O 多路複用

IO multiplexing就是咱們說的select,poll,epoll,有些地方也稱這種IO方式爲event driven IO。select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。

機制:一個線程以阻塞的方式監聽客戶端請求;另外一個線程採用NIO的形式select已經接收到數據的channel信道,處理請求;

  • select,poll,epoll模型 - 處理更多的鏈接 bio2 上面所說的多路複用的select,poll,epoll本質上都是同步IO,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是阻塞的,其實是指阻塞在select上面,必須等到讀就緒、寫就緒等網絡事件。異步IO則無需本身負責進行讀寫,異步IO的實現會負責把數據從內核拷貝到用戶空間。

I/O 多路複用的特色是經過一種機制一個進程能同時等待多個文件描述符, 而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select() 函數就能夠返回。因此,若是處理的鏈接數不是很高的話,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/ epoll的優點並非對於單個鏈接能處理得更快,而是在於能處理更多的鏈接。

  • 一個面試問題:select、poll、epoll的區別?

Java中的I/O 多路複用: Reactor模型

(主從Reactor模型)netty就是主從Reactor模型的實現,至關於這個模型在 bio3

對比與傳統的I/O 多路複用,Reactor模型增長了事件分發器,基於事件驅動,可以將相應的讀寫事件分發給不一樣的線程執行,真正實現了非阻塞I/O。

基於Reactor Pattern 處理模式中,定義如下三種角色

  • Reactor將I/O事件分派給對應的Handler
  • Acceptor處理客戶端新鏈接,並分派請求處處理器鏈中
  • Handlers執行非阻塞讀/寫 任務

舉個栗子 回顧咱們上面寫的代碼,是否是每一個線程處理一個鏈接,顯然在高併發狀況下是不適用的,應該採用 IO多路複用 的思想,使得一個線程可以處理多個鏈接,而且不能阻塞讀寫操做,添加一個 選擇器在buffer有數據的時候就開始寫入用戶空間.這裏的多路是指N個鏈接,每個鏈接對應一個channel,或者說多路就是多個channel。複用,是指多個鏈接複用了一個線程或者少許線程

如今咱們來優化下上面的socket IO模型 ####優化後的IO模型: 實現一個最簡單的Reactor模式:註冊全部感興趣的事件處理器,單線程輪詢選擇就緒事件,執行事件處理器。流程就是不斷輪詢能夠進行處理的事件,而後交給不一樣的handler進行處理. 上面提到的主要是四個網絡事件:有鏈接就緒,接收就緒,讀就緒,寫就緒。I/O複用主要是經過 Selector複用器來實現的,能夠結合下面這個圖理解上面的敘述

io mul


public class NIOServer {

    private ServerSocketChannel serverSocket;
    private Selector selector;
    private ReadHandler readHandler;
    private WriteHandler writeHandler;
    private ExecutorService executorService = Executors.newCachedThreadPool();

    abstract class Handler {
        protected SelectionKey key;
    }

    class ReadHandler extends Handler implements Runnable {

        @Override
        public void run() {
            ///...讀操做
        }
    }

    class WriteHandler extends Handler implements Runnable {
        @Override
        public void run() {
            ///...寫操做
        }
    }

    public NIOServer(int port) {
        try {
            selector = Selector.open();
            serverSocket = ServerSocketChannel.open();
            serverSocket.bind(new InetSocketAddress(port));
            serverSocket.register(this.selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void run() {
        while (!Thread.interrupted()) {
            try {
                selector.select();  //阻塞等待事件
                Iterator<SelectionKey> iterator = this.selector.keys().iterator();  // 事件列表 , key -> channel ,每一個KEY對應了一個channel
                while (iterator.hasNext()) {
                    iterator.remove();
                    dispatch(iterator.next());  //分發事件
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

    private void dispatch(SelectionKey key) {
        if (key.isAcceptable()) {
            register(key);  //新鏈接創建,註冊一個新的讀寫處理器
        } else if (key.isReadable()) {
            this.executorService.submit(new ReadHandler(key));  //能夠寫,執行寫事件
        } else if (key.isWritable()) {
            this.executorService.submit(new WriteHandler(key));  //能夠讀。執行讀事件
        }
    }

    private void register(SelectionKey key) {
        ServerSocketChannel channel = (ServerSocketChannel) key.channel();   //經過key找到對應的channel
        try {
            SocketChannel socketChannel = channel.accept();
            channel.configureBlocking(false);
            channel.register(this.selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

優化線程模型

上述模型還能夠繼續優化。由於上述模型只是增多個客戶端鏈接的數量,可是在高併發的狀況下,

參考資料:

老曹眼中的網絡編程基礎 Linux IO模式及 select、poll、epoll詳解

相關文章
相關標籤/搜索