1. 程式人生 > 實用技巧 >Java之網路程式設計和NIO

Java之網路程式設計和NIO

第一章 網路程式設計入門

知識點--軟體結構

目標

  • 瞭解軟體結構

路徑

  • C/S結構
  • B/S結構

講解

  • C/S結構 :全稱為Client/Server結構,是指客戶端和伺服器結構。常見程式有QQ、迅雷等軟體。
  • 特點: 客戶端和伺服器是分開的,需要下載客戶端,對網路要求相對低, 開發和維護成本高,相對穩定

B/S結構 :全稱為Browser/Server結構,是指瀏覽器和伺服器結構。常見瀏覽器有谷歌、火狐等。

特點:沒有客戶端,只有伺服器,不需要下載客戶端,直接通過瀏覽器訪問, 對網路要求相對高, 開發和維護成本低,伺服器壓力很大,相對不穩定

兩種架構各有優勢,但是無論哪種架構,都離不開網路的支援。網路程式設計

,就是在一定的協議下,實現兩臺計算機的通訊的程式。

小結

  • 網路程式設計,就是在一定的協議下,實現兩臺計算機的通訊的程式。

知識點--網路程式設計三要素

目標

  • 理解網路程式設計三要素

路徑

  • 協議
  • IP地址
  • 埠號

講解

協議

網路通訊協議:通訊協議是計算機必須遵守的規則,只有遵守這些規則,計算機之間才能進行通訊。這就好比在道路中行駛的汽車一定要遵守交通規則一樣,協議中對資料的傳輸格式、傳輸速率、傳輸步驟等做了統一規定,通訊雙方必須同時遵守,最終完成資料交換。

java.net 包中提供了兩種常見的網路協議的支援:

  • TCP:傳輸控制協議 (Transmission Control Protocol)。TCP協議是面向連線
    的通訊協議,即傳輸資料之前,在傳送端和接收端建立邏輯連線,然後再傳輸資料,它提供了兩臺計算機之間可靠無差錯的資料傳輸。
  • TCP協議特點: 面向連線,傳輸資料安全,傳輸速度低
  • 例如: 村長髮現張三家的牛丟了
  • TCP協議: 村長一定要找到張三,面對面的告訴他他家的牛丟了 打電話: 電話一定要接通,並且是張三接的
    • 連線三次握手:TCP協議中,在傳送資料的準備階段,客戶端與伺服器之間的三次互動,以保證連線的可靠。
      • 第一次握手,客戶端向伺服器端發出連線請求,等待伺服器確認。
      • 第二次握手,伺服器端向客戶端回送一個響應,通知客戶端收到了連線請求。
      • 第三次握手,客戶端再次向伺服器端傳送確認資訊,確認連線。整個互動過程如下圖所示。

​ 完成三次握手,連線建立後,客戶端和伺服器就可以開始進行資料傳輸了。由於這種面向連線的特性,TCP協議可以保證傳輸資料的安全,所以應用十分廣泛,例如下載檔案、瀏覽網頁等

  • UDP:使用者資料報協議(User Datagram Protocol)。UDP協議是一個面向無連線的協議。傳輸資料時,不需要建立連線,不管對方端服務是否啟動,直接將資料、資料來源和目的地都封裝在資料包中,直接傳送。每個資料包的大小限制在64k以內。它是不可靠協議,因為無連線,所以傳輸速度快,但是容易丟失資料。日常應用中,例如視訊會議、QQ聊天等。
  • UDP特點: 面向無連線,傳輸資料不安全,傳輸速度快
  • 例如: 村長髮現張三家的牛丟了
  • UDP協議: 村長在村裡的廣播站廣播一下張三家的牛丟了,資訊丟失,資訊釋出速度快

IP地址

  • IP地址:指網際網路協議地址(Internet Protocol Address),俗稱IP。IP地址用來給一個網路中的計算機裝置做唯一的編號。假如我們把“個人電腦”比作“一臺電話”的話,那麼“IP地址”就相當於“電話號碼”。

**IP地址分類 **

  • IPv4:是一個32位的二進位制數,通常被分為4個位元組,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之間的十進位制整數,那麼最多可以表示42億個。

  • IPv6:由於網際網路的蓬勃發展,IP地址的需求量愈來愈大,但是網路地址資源有限,使得IP的分配越發緊張。有資料顯示,全球IPv4地址在2011年2月分配完畢。

    為了擴大地址空間,擬通過IPv6重新定義地址空間,採用128位地址長度,每16個位元組一組,分成8組十六進位制數,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,號稱可以為全世界的每一粒沙子編上一個網址,這樣就解決了網路地址資源數量不夠的問題。

常用命令

  • 檢視本機IP地址,在控制檯輸入:
ipconfig
  • 檢查網路是否連通,在控制檯輸入:
ping 空格 IP地址
ping 220.181.57.216
ping www.baidu.com

特殊的IP地址

  • 本機IP地址:127.0.0.1localhost

埠號

網路的通訊,本質上是兩個程序(應用程式)的通訊。每臺計算機都有很多的程序,那麼在網路通訊時,如何區分這些程序呢?

如果說IP地址可以唯一標識網路中的裝置,那麼埠號就可以唯一標識裝置中的程序(應用程式)了。

  • 埠號:用兩個位元組表示的整數,它的取值範圍是065535**。其中,01023之間的埠號用於一些知名的網路服務和應用,普通的應用程式需要使用1024以上的埠號。如果埠號被另外一個服務或應用所佔用,會導致當前程式啟動失敗。**

利用協議+IP地址+埠號 三元組合,就可以標識網路中的程序了,那麼程序間的通訊就可以利用這個標識與其它程序進行互動。

小結

  • 協議: 計算機在網路中通訊需要遵守的規則,常見的有TCP,UDP協議
    • TCP: 面向連線,傳輸資料安全,傳輸速度慢
    • UDP: 面向無連線,傳輸資料不安全,傳輸速度快
  • IP地址: 用來標示網路中的計算機裝置
    • 分類: IPV4 IPV6
    • 本地ip地址: 127.0.0.1 localhost
  • 埠號: 用來標示計算機裝置中的應用程式
    • 埠號: 0--65535
    • 自己寫的程式指定的埠號要是1024以上
    • 如果埠號被另外一個服務或應用所佔用,會導致當前程式啟動失敗。

知識點--InetAddress類

目標

  • 能夠通過InetAddress類獲取ip地址

路徑

  • InetAddress類的概述
  • InetAddress類的方法

講解

InetAddress類的概述

  • 一個該類的物件就代表一個IP地址物件。

InetAddress類的方法

  • static InetAddress getLocalHost() 獲得本地主機IP地址物件
  • static InetAddress getByName(String host) 根據IP地址字串或主機名獲得對應的IP地址物件
  • String getHostName();獲得主機名
  • String getHostAddress();獲得IP地址字串
public class InetAddressDemo01 {
    public static void main(String[] args) throws Exception {
              // 獲取本地ip地址物件
        InetAddress ip1 = InetAddress.getLocalHost();
        System.out.println("ip1: "+ip1);// DESKTOP-U8Q5F96/192.168.0.100

        // 獲取百度ip地址物件
        InetAddress ip2 = InetAddress.getByName("www.baidu.com");
        System.out.println("ip2:"+ip2);// www.baidu.com/182.61.200.7

        // 獲得本地的主機名
        String hostName = ip1.getHostName();
        System.out.println("hostName:"+hostName);// DESKTOP-U8Q5F96

        // 獲得本地的ip地址
        String hostAddress = ip1.getHostAddress();
        System.out.println("hostAddress:"+hostAddress);//192.168.0.100
    }
}

第二章 TCP通訊程式

知識點--TCP協議概述

目標

  • 我們先來了解一下TCP協議使用時需要用到的流程和方法.

路徑

  • TCP概述
  • TCP協議相關的類

講解

TCP概述

  • TCP協議是面向連線的通訊協議,即在傳輸資料前先在傳送端和接收器端建立邏輯連線,然後再傳輸資料。它提供了兩臺計算機之間可靠無差錯的資料傳輸。TCP通訊過程如下圖所示:

TCP協議相關的類

  • java.net.Socket : 一個該類的物件就代表一個客戶端程式。
    • Socket(String host, int port) 根據ip地址字串和埠號建立客戶端Socket物件
      * 注意事項:只要執行該方法,就會立即連線指定的伺服器程式,如果連線不成功,則會丟擲異常。
      如果連線成功,則表示三次握手通過。
    • OutputStream getOutputStream(); 獲得位元組輸出流物件
    • InputStream getInputStream();獲得位元組輸入流物件
    • void close();關閉Socket, 會自動關閉相關的流
  • java.net.ServerSocket : 一個該類的物件就代表一個伺服器端程式。
    • ServerSocket(int port); 根據指定的埠號開啟伺服器。
    • Socket accept(); 等待客戶端連線並獲得與客戶端關聯的Socket物件 如果沒有客戶端連線,該方法會一直阻塞
    • void close();關閉ServerSocket,一般不關閉

小結

  • TCP如何建立連線: 在客戶端建立Socket物件,指定伺服器ip地址和埠號,就會自動連線,如果連線成功,就表示三次握手成功,程式繼續執行,如果連線失敗,就會報異常
  • TCP如何傳輸資料: 使用Socket物件獲得輸出流寫出資料,使用Socket物件獲得輸入流讀取資料

實操--TCP通訊案例1

需求

  • 客戶端向伺服器傳送字串資料

路徑

  • 客戶端實現步驟
    • 建立客戶端Socket物件並指定伺服器地址和埠號
    • 呼叫Socket物件的getOutputStream方法獲得位元組輸出流物件
    • 使用位元組輸出流物件的write方法往伺服器端輸出資料
    • 關閉Socket物件斷開連線。
  • 伺服器實現步驟
    • 建立ServerSocket物件並指定埠號(相當於開啟了一個伺服器)
    • 呼叫ServerSocket物件的accept方法等待客端戶連線並獲得對應Socket物件
    • 呼叫Socket物件的getInputStream方法獲得位元組輸入流物件
    • 呼叫位元組輸入流物件的read方法讀取客戶端傳送的資料

實現

  • 客戶端程式碼實現

    public class Client {
        public static void main(String[] args) throws Exception{
            // 建立Socket物件,指定伺服器ip和埠號
            Socket socket = new Socket("127.0.0.1",6666);
            // 通過socket物件獲得輸出流
            OutputStream os = socket.getOutputStream();
            // 寫出資料
            Scanner sc = new Scanner(System.in);
            String str = sc.nextLine();
            os.write(str.getBytes());
            // 關閉流,釋放資源
            socket.close();
        }
    }
    
    
  • 服務端程式碼實現

    // 伺服器
    public class Server {
        public static void main(String[] args) throws Exception{
            // 建立ServerSocket物件,並指定埠號
            ServerSocket ss = new ServerSocket(6666);
            // 呼叫accept()方法等待客戶端連線,連線成功返回Socket物件
            Socket socket = ss.accept();
            // 通過Socket物件獲得輸入流
            InputStream is = socket.getInputStream();
            // 定義一個byte陣列,用來儲存讀取到的位元組資料
            byte[] bys = new byte[1024];
            int len = is.read(bys);
            // 列印資料
            System.out.println(new String(bys,0,len));
            // 關閉資源
            socket.close();
            ss.close();// 伺服器一般不關閉
        }
    }
    

實操--TCP通訊案例2

需求

  • 客戶端向伺服器傳送字串資料,伺服器回寫字串資料給客戶端(模擬聊天)

路徑

  • 客戶端實現步驟
    • 建立客戶端Socket物件並指定伺服器地址和埠號
    • 呼叫Socket物件的getOutputStream方法獲得位元組輸出流物件
    • 使用位元組輸出流物件的write方法往伺服器端輸出資料
    • 呼叫Socket物件的getInputStream方法獲得位元組輸入流物件
    • 呼叫位元組輸入流物件的read方法讀取伺服器端返回的資料
    • 關閉Socket物件斷開連線。
  • 伺服器實現步驟
    • 建立ServerSocket物件並指定埠號(相當於開啟了一個伺服器)
    • 呼叫ServerSocket物件的accept方法等待客端戶連線並獲得對應Socket物件
    • 呼叫Socket物件的getInputStream方法獲得位元組輸入流物件
    • 呼叫位元組輸入流物件的read方法讀取客戶端傳送的資料
    • 呼叫Socket物件的getOutputStream方法獲得位元組輸出流物件
    • 呼叫位元組輸出流物件的write方法往客戶端輸出資料
    • 關閉Socket和ServerSocket物件

實現

  • TCP客戶端程式碼
/*
TCP客戶端程式碼實現步驟
        * 建立客戶端Socket物件並指定伺服器地址和埠號
        * 呼叫Socket物件的getOutputStream方法獲得位元組輸出流物件
        * 呼叫位元組輸出流物件的write方法往伺服器端輸出資料
        * 呼叫Socket物件的getInputStream方法獲得位元組輸入流物件
        * 呼叫位元組輸入流物件的read方法讀取伺服器端返回的資料
        * 關閉Socket物件斷開連線。
*/
public class Client {
    public static void main(String[] args) throws Exception{
        // 建立Socket物件,指定伺服器ip和埠號
        Socket socket = new Socket("127.0.0.1",6666);
        while (true) {
            // 通過socket物件獲得輸出流
            OutputStream os = socket.getOutputStream();
            // 寫出資料
            Scanner sc = new Scanner(System.in);
            System.out.println("請輸入向伺服器傳送的資料:");
            String str = sc.nextLine();
            os.write(str.getBytes());

            // 通過Socket物件獲得輸入流
            InputStream is = socket.getInputStream();
            // 定義一個byte陣列,用來儲存讀取到的位元組資料
            byte[] bys = new byte[1024];
            int len = is.read(bys);
            // 列印資料
            System.out.println(new String(bys,0,len));
        }

        // 關閉流,釋放資源
        //socket.close();
    }
}
  • 服務端程式碼實現
/**
    TCP伺服器端程式碼實現步驟
        * 建立ServerSocket物件並指定埠號(相當於開啟了一個伺服器)
        * 呼叫ServerSocket物件的accept方法等待客端戶連線並獲得對應Socket物件
        * 呼叫Socket物件的getInputStream方法獲得位元組輸入流物件
        * 呼叫位元組輸入流物件的read方法讀取客戶端傳送的資料
        * 呼叫Socket物件的getOutputStream方法獲得位元組輸出流物件
        * 呼叫位元組輸出流物件的write方法往客戶端輸出資料
        * 關閉Socket和ServerSocket物件
 */
public class Server {
    public static void main(String[] args) throws Exception{
        // 建立ServerSocket物件,並指定埠號
        ServerSocket ss = new ServerSocket(6666);
        // 呼叫accept()方法等待客戶端連線,連線成功返回Socket物件
        Socket socket = ss.accept();

        while (true) {
            // 通過Socket物件獲得輸入流
            InputStream is = socket.getInputStream();
            // 定義一個byte陣列,用來儲存讀取到的位元組資料
            byte[] bys = new byte[1024];
            int len = is.read(bys);
            // 列印資料
            System.out.println(new String(bys,0,len));

            // 通過socket物件獲得輸出流
            OutputStream os = socket.getOutputStream();
            // 寫出資料
            Scanner sc = new Scanner(System.in);
            System.out.println("請輸入向客戶端傳送的資料:");
            String str = sc.nextLine();
            os.write(str.getBytes());
        }

        // 關閉資源
        //socket.close();
        //ss.close();// 伺服器一般不關閉
    }
}

第三章 綜合案例

實操--檔案上傳案例

需求

  • 使用TCP協議, 通過客戶端向伺服器上傳一個檔案

分析

  1. 【客戶端】輸入流,從硬碟讀取檔案資料到程式中。

  2. 【客戶端】輸出流,寫出檔案資料到服務端。

  3. 【服務端】輸入流,讀取檔案資料到服務端程式。

  4. 【服務端】輸出流,寫出檔案資料到伺服器硬碟中。

  5. 【服務端】獲取輸出流,回寫資料。

  6. 【客戶端】獲取輸入流,解析回寫資料。

實現

拷貝檔案

public class Client {
    public static void main(String[] args) throws Exception{
        // 客戶端:
        // 1.建立Socket物件,指定伺服器的ip地址和埠號
        Socket socket = new Socket("127.0.0.1",7777);
        // 2.建立位元組輸入流物件,關聯資料來源檔案路徑
        FileInputStream fis = new FileInputStream("day14\\aaa\\4.jpg");
        // 3.通過Socket物件,獲取輸出流物件
        OutputStream os = socket.getOutputStream();
        // 4.定義一個位元組陣列,用來儲存讀取到的位元組資料
        byte[] bys = new byte[8192];
        // 4.定義一個int型別的變數,用來儲存讀取到的位元組個數
        int len;
        // 5.迴圈讀取資料
        while ((len = fis.read(bys)) != -1) {
            // 6.在迴圈中,寫出資料
            os.write(bys,0,len);
        }
        // 7.關閉流,釋放資源
        fis.close();
        socket.close();
    }
}
public class Server {
    public static void main(String[] args) throws Exception{
        // 伺服器:
        // 1.建立ServerSocket物件,指定伺服器埠號
        ServerSocket ss = new ServerSocket(7777);
        // 2.呼叫accept()方法,接收客戶端的請求,建立連線,返回Socket物件
        Socket socket = ss.accept();
        // 3.通過Socekt物件獲取位元組輸入流
        InputStream is = socket.getInputStream();
        // 4.建立位元組輸出流物件,關聯目的地檔案路徑
        FileOutputStream fos = new FileOutputStream("day14\\bbb\\44.jpg");
        // 5.定義一個位元組陣列,用來儲存讀取到的位元組資料
        byte[] bys = new byte[8192];
        // 5.定義一個int型別的變數,用來儲存讀取到的位元組個數
        int len;
        // 6.迴圈讀取資料
        while ((len = is.read(bys)) != -1) {
            // 7.在迴圈中,寫出資料
            fos.write(bys,0,len);
        }
        // 8.關閉流,釋放資源
        fos.close();
        socket.close();

    }
}

檔案上傳成功後伺服器回寫字串資料

// 客戶端
public class Client {
    public static void main(String[] args) throws Exception{
        // 客戶端:
        // 1.建立Socket物件,指定伺服器的ip地址和埠號
        Socket socket = new Socket("127.0.0.1",7777);
        // 2.建立位元組輸入流物件,關聯資料來源檔案路徑
        FileInputStream fis = new FileInputStream("day14\\aaa\\5.jpg");
        // 3.通過Socket物件,獲取輸出流物件
        OutputStream os = socket.getOutputStream();
        // 4.定義一個位元組陣列,用來儲存讀取到的位元組資料
        byte[] bys = new byte[8192];
        // 4.定義一個int型別的變數,用來儲存讀取到的位元組個數
        int len;
        // 5.迴圈讀取資料
        while ((len = fis.read(bys)) != -1) {
            // 6.在迴圈中,寫出資料
            os.write(bys,0,len);
        }

        // 告訴伺服器,不再寫資料了
        socket.shutdownOutput();

        System.out.println("==========開始接收伺服器回寫的資料========");

        //7.通過Socket物件獲取位元組輸入流
        InputStream is = socket.getInputStream();
        //8.讀取伺服器回寫的字串資料
        int len2 = is.read(bys);// 卡
        System.out.println(new String(bys,0,len2));

        // 9.關閉流,釋放資源
        fis.close();
        socket.close();
    }
}

// 伺服器
public class Server {
    public static void main(String[] args) throws Exception{
        // 伺服器:
        // 1.建立ServerSocket物件,指定伺服器埠號
        ServerSocket ss = new ServerSocket(7777);
        // 2.呼叫accept()方法,接收客戶端的請求,建立連線,返回Socket物件
        Socket socket = ss.accept();
        // 3.通過Socekt物件獲取位元組輸入流
        InputStream is = socket.getInputStream();
        // 4.建立位元組輸出流物件,關聯目的地檔案路徑
        FileOutputStream fos = new FileOutputStream("day14\\bbb\\55.jpg");
        // 5.定義一個位元組陣列,用來儲存讀取到的位元組資料
        byte[] bys = new byte[8192];
        // 5.定義一個int型別的變數,用來儲存讀取到的位元組個數
        int len;
        // 6.迴圈讀取資料
        while ((len = is.read(bys)) != -1) {// 卡
            // 7.在迴圈中,寫出資料
            fos.write(bys,0,len);
        }

        // 問題: 伺服器一直在等待讀取客戶端寫過來的資料,無法回寫資料給客戶端???
        // 原因: 伺服器不知道客戶端不會再寫資料了
        // 解決: 客戶端要告訴伺服器不會再寫資料了


        System.out.println("==========開始回寫資料給客戶端========");
        //8.通過Socket物件獲取輸出流
        OutputStream os = socket.getOutputStream();
        //9.寫出字串資料給客戶端("檔案上傳成功!")
        os.write("檔案上傳成功!".getBytes());
        // 10.關閉流,釋放資源
        fos.close();
        socket.close();

    }

    
}


優化檔案上傳案例

1.檔名固定----->優化   自動生成唯一的檔名
2.伺服器只能接受一次 ----> 優化  死迴圈去接收請求,建立連線
3.例如:如果張三先和伺服器建立連線,上傳了一個2GB位元組大小的檔案
       李四後和伺服器建立連線,上傳了一個2MB位元組大小的檔案

       李四就必須等張三上傳完畢,才能上傳檔案

   優化---->多執行緒優化
   張三上傳檔案,開闢一條執行緒
   李四上傳檔案,開闢一條執行緒
// 伺服器
public class Server {
    public static void main(String[] args) throws Exception{
        // 伺服器:
        // 1.建立ServerSocket物件,指定伺服器埠號
        ServerSocket ss = new ServerSocket(7777);

        while (true){
            // 2.呼叫accept()方法,接收客戶端的請求,建立連線,返回Socket物件
            Socket socket = ss.accept();

            // 開啟執行緒,執行檔案上傳的程式碼
            new Thread(new Runnable() {
                @Override
                public void run() {
                    FileOutputStream fos = null;
                    try{
                        // 3.通過Socekt物件獲取位元組輸入流
                        InputStream is = socket.getInputStream();
                        // 4.建立位元組輸出流物件,關聯目的地檔案路徑
                         fos = new FileOutputStream("day14\\bbb\\"+System.currentTimeMillis()+".jpg");
                        // 5.定義一個位元組陣列,用來儲存讀取到的位元組資料
                        byte[] bys = new byte[8192];
                        // 5.定義一個int型別的變數,用來儲存讀取到的位元組個數
                        int len;
                        // 6.迴圈讀取資料
                        while ((len = is.read(bys)) != -1) {// 卡
                            // 7.在迴圈中,寫出資料
                            fos.write(bys,0,len);
                        }

                        // 問題: 伺服器一直在等待讀取客戶端寫過來的資料,無法回寫資料給客戶端???
                        // 原因: 伺服器不知道客戶端不會再寫資料了
                        // 解決: 客戶端要告訴伺服器不會再寫資料了


                        System.out.println("==========開始回寫資料給客戶端========");
                        //8.通過Socket物件獲取輸出流
                        OutputStream os = socket.getOutputStream();
                        //9.寫出字串資料給客戶端("檔案上傳成功!")
                        os.write("檔案上傳成功!".getBytes());

                    }catch (Exception e){

                    }finally {
                        // 10.關閉流,釋放資源
                        try {
                            if (fos != null) {
                                fos.close();
                            }
                        } catch (IOException e) {

                        }finally {
                            try {
                                socket.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }

                    }
                }
            }).start();
        }
    } 
}

// 客戶端
public class Client {
    public static void main(String[] args) throws Exception{
        // 客戶端:
        // 1.建立Socket物件,指定伺服器的ip地址和埠號
        Socket socket = new Socket("127.0.0.1",7777);
        // 2.建立位元組輸入流物件,關聯資料來源檔案路徑
        FileInputStream fis = new FileInputStream("day14\\aaa\\5.jpg");
        // 3.通過Socket物件,獲取輸出流物件
        OutputStream os = socket.getOutputStream();
        // 4.定義一個位元組陣列,用來儲存讀取到的位元組資料
        byte[] bys = new byte[8192];
        // 4.定義一個int型別的變數,用來儲存讀取到的位元組個數
        int len;
        // 5.迴圈讀取資料
        while ((len = fis.read(bys)) != -1) {
            // 6.在迴圈中,寫出資料
            os.write(bys,0,len);
        }

        // 告訴伺服器,不再寫資料了
        socket.shutdownOutput();

        System.out.println("==========開始接收伺服器回寫的資料========");

        //7.通過Socket物件獲取位元組輸入流
        InputStream is = socket.getInputStream();
        //8.讀取伺服器回寫的字串資料
        int len2 = is.read(bys);// 卡
        System.out.println(new String(bys,0,len2));

        // 9.關閉流,釋放資源
        fis.close();
        socket.close();
    }
}

實操--模擬B\S伺服器 擴充套件

需求

  • 模擬網站伺服器,使用瀏覽器訪問自己編寫的服務端程式,檢視網頁效果。

分析

  1. 準備頁面資料,web資料夾。
  2. 我們模擬伺服器端,ServerSocket類監聽埠,使用瀏覽器訪問,檢視網頁效果

實現

瀏覽器工作原理是遇到圖片會開啟一個執行緒進行單獨的訪問,因此在伺服器端加入執行緒技術。

public class Demo {
    public static void main(String[] args) throws Exception {
        // 通過讀取瀏覽器端的請求資訊,獲取瀏覽器需要訪問的頁面的路徑
        // 1.建立ServerSocket物件,指定埠號為9999
        ServerSocket ss = new ServerSocket(9999);

        while (true) {
            // 2.呼叫accept()方法,接收請求,建立連線,返回Socket物件
            Socket socket = ss.accept();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 3.通過返回的Socket物件獲取位元組輸入流,關聯連線通道
                        InputStream is = socket.getInputStream();

                        // 4.把位元組輸入流轉換為字元輸入流
                        InputStreamReader isr = new InputStreamReader(is);

                        // 5.建立字元緩衝輸入流
                        BufferedReader br = new BufferedReader(isr);

                        // 6.使用字元緩衝輸入流讀取第一行資料
                        String line = br.readLine();

                        // 7.使用空格對讀取到的第一行資料進行分割
                        String[] arr = line.split(" ");

                        // 8.獲取分割後陣列中索引為1的元素,對其進行擷取
                        String path = arr[1].substring(1);
                        System.out.println("瀏覽器需要訪問的頁面路徑是:" + path);

                        // 伺服器把瀏覽器需要訪問的頁面響應給瀏覽器
                        // 9.建立一個位元組輸入流,關聯資料來源檔案路徑
                        FileInputStream fis = new FileInputStream(path);

                        // 10.通過Socket物件獲得輸出流,關聯連線通道
                        OutputStream os = socket.getOutputStream();

                        // 11.定義一個變數,用來儲存讀取到的位元組資料
                        byte[] bys = new byte[8192];
                        int len;

                        // 響應頁面的時候需要同時把以下響應過去給瀏覽器
                        os.write("HTTP/1.1 200 OK\r\n".getBytes());
                        os.write("Content-Type:text/html\r\n".getBytes());
                        os.write("\r\n".getBytes());


                        // 12.迴圈讀取
                        while ((len = fis.read(bys)) != -1) {
                            // 13.在迴圈中,寫出資料給瀏覽器
                            os.write(bys, 0, len);
                        }

                        // 關閉Socket物件,釋放資源
                        fis.close();
                        socket.close();
                    } catch (IOException e) {

                    }
                }
            }).start();
        }
    }

    /**
     * 1.讀取到瀏覽器端的請求資訊
     *
     * @return
     * @throws IOException
     */
    private static Socket method01() throws IOException {
        //  1.讀取到瀏覽器端的請求資訊
        // 1.1 建立ServerSocket物件,指定埠號為9999
        ServerSocket ss = new ServerSocket(9999);
        // 1.2 呼叫accept()方法,接收請求,建立連線,返回Socket物件
        Socket socket = ss.accept();
        // 1.3 通過返回的Socket物件獲取輸入流,關聯連線通道
        InputStream is = socket.getInputStream();
        // 1.4 使用輸入流去讀取資料
        byte[] bys = new byte[8192];
        int len = is.read(bys);
        // 1.5 列印讀取到的資料
        System.out.println(new String(bys, 0, len));
        /*
            GET /day12/web/index.html HTTP/1.1
            Host: localhost:9999
            Connection: keep-alive
            Cache-Control: max-age=0
            Upgrade-Insecure-Requests: 1
            User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
            Sec-Fetch-User: ?1
            Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,**;q=0.8,application/signed-exchange;v=b3
                    Sec-Fetch-Site: none
                    Sec-Fetch-Mode: navigate
                    Accept-Encoding: gzip, deflate, br
                    Accept-Language: zh-CN,zh;q=0.9
                    Cookie: Idea-7071e3d8=cc177568-5581-4562-aeac-26fcb6ca7e56
         */
        return socket;
    }
}

圖解:

第四章 NIO

知識點--NIO概述

目的

  • 瞭解NIO的概述

路徑

  • 同步和非同步
  • 阻塞和非阻塞

講解

在我們學習Java的NIO流之前,我們都要了解幾個關鍵詞

  • 同步與非同步(synchronous/asynchronous):同步是一種可靠的有序執行機制,當我們進行同步操作時,後續的任務是等待當前呼叫返回,才會進行下一步;而非同步則相反,其他任務不需要等待當前呼叫返回,通常依靠事件、回撥等機制來實現任務間次序關係
    • 同步: 呼叫方法之後,必須要得到一個返回值 例如: 買火車票,一定要買到票,才能繼續下一步
    • 非同步: 呼叫方法之後,不需要有返回值,但是會有回撥函式,回撥函式指的是滿足條件之後會自動執行的方法 例如: 買火車票, 不一定要買到票,我可以交代售票員,當有票的話,你就幫我出張票
  • 阻塞與非阻塞:在進行阻塞操作時,當前執行緒會處於阻塞狀態,無法從事其他任務,只有當條件就緒才能繼續,比如ServerSocket新連線建立完畢,或者資料讀取、寫入操作完成;而非阻塞則是不管IO操作是否結束,直接返回,相應操作在後臺繼續處理
    • 阻塞:如果沒有達到方法的目的,就會一直停在那裡(等待) , 例如: ServerSocket的accept()方法
    • 非阻塞: 不管方法有沒有達到目的,都直接往下執行(不等待)
  • IO: 同步阻塞
  • NIO: 同步非阻塞
  • AIO: 非同步非阻塞

在Java1.4之前的I/O系統中,提供的都是面向流的I/O系統,系統一次一個位元組地處理資料,一個輸入流產生一個位元組的資料,一個輸出流消費一個位元組的資料,面向流的I/O速度非常慢,而在Java 1.4中推出了NIO,這是一個面向塊的I/O系統,系統以塊的方式處理資料,每一個操作在一步中產生或者消費一個數據,按塊處理要比按位元組處理資料快的多。

在 Java 7 中,NIO 有了進一步的改進,也就是 NIO 2\AIO,引入了非同步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。非同步 IO 操作基於事件和回撥機制,可以簡單理解為,應用操作直接返回,而不會阻塞在那裡,當後臺處理完成,作業系統會通知相應執行緒進行後續工作。

NIO之所以是同步,是因為它的accept/read/write方法的核心I/O操作都會阻塞當前執行緒

首先,我們要先了解一下NIO的三個主要組成部分:Buffer(緩衝區)、Channel(通道)、Selector(選擇器)

小結

  • Buffer(緩衝區)、Channel(通道)、Selector(選擇器)是NIO的三個部分
  • NIO是在訪問個數特別大的時候才使用的 , 比如流行的軟體或者流行的遊戲中會有高併發和大量連線.

第三章 Buffer類(緩衝區)

知識點--Buffer的概述和分類

目標

  • 瞭解Buffer概述

路徑

  • Buffer的概述
  • Buffer的分類

講解

概述:Buffer是一個物件,它是對某種基本型別的陣列進行了封裝。

作用: 在NIO中,就是通過 Buffer 來讀寫資料的。所有的資料都是用Buffer來處理的,它是NIO讀寫資料的中轉池, 通常使用位元組陣列。

Buffer主要有如下幾種:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

知識點--建立ByteBuffer

目標

  • 掌握建立ByteBuffer物件

路徑

  • 建立ByteBuffer物件的三種方式

講解

  • ByteBuffer類內部封裝了一個byte[]陣列,並可以通過一些方法對這個陣列進行操作。

  • 建立ByteBuffer物件

    • 方式一:在堆中建立緩衝區:allocate(int capacity)
    • 方式二: 在系統記憶體建立緩衝區:allocatDirect(int capacity)
    • 方式三:通過陣列建立緩衝區:wrap(byte[] arr)
  • 案例演示:

    • 方式一:在堆中建立緩衝區:allocate(int capacity)

      public static void main(String[] args) {
          	//建立堆緩衝區
              ByteBuffer byteBuffer = ByteBuffer.allocate(10);
      }
      

  • 方式二: 在系統記憶體建立緩衝區:allocatDirect(int capacity)

    public static void main(String[] args) {
        	//建立直接緩衝區
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
    }
    
    • 在堆中建立緩衝區稱為:間接緩衝區

    • 在系統記憶體建立緩衝區稱為:直接緩衝區

    • 間接緩衝區的建立和銷燬效率要高於直接緩衝區

    • 間接緩衝區的工作效率要低於直接緩衝區

  • 方式三:通過陣列建立緩衝區:wrap(byte[] arr)

       public static void main(String[] args) {
              byte[] byteArray = new byte[10];
              ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray);
       }
    
    • 此種方式建立的緩衝區為:間接緩衝區

知識點--新增資料-put

目標

  • 掌握新增資料-put方法的使用

路徑

  • 新增資料-put方法的使用

講解

  • public ByteBuffer put(byte b):向當前可用位置新增資料。

  • public ByteBuffer put(byte[] byteArray):向當前可用位置新增一個byte[]陣列

  • public ByteBuffer put(byte[] byteArray,int offset,int len):新增一個byte[]陣列的一部分

    public static void main(String[] args) {
            yteBuffer b1 = ByteBuffer.allocate(10);
            // 新增資料
            b1.put((byte)11);
            b1.put((byte)12);
            b1.put((byte)13);
    
            // ByteBuffer轉換為普通位元組陣列
            byte[] bytes = b1.array();
            System.out.println(Arrays.toString(bytes));
            // 列印結果: [11, 12, 13, 0, 0, 0, 0, 0, 0, 0]
    }
    
    public class Test_新增資料 {
        public static void main(String[] args) {
            ByteBuffer b1 = ByteBuffer.allocate(10);
            // 新增資料
            b1.put((byte)11);
            b1.put((byte)12);
            b1.put((byte)13);
    
            // ByteBuffer轉換為普通位元組陣列
            byte[] bytes = b1.array();
            System.out.println(Arrays.toString(bytes));
            //列印結果: [11, 12, 13, 0, 0, 0, 0, 0, 0, 0]
    
            System.out.println("=======================");
            byte[] b2 = {14,15,16};
            // 新增資料
            b1.put(b2);
            // ByteBuffer轉換為普通位元組陣列
            byte[] b = b1.array();
            System.out.println(Arrays.toString(b));
            //列印結果: [11, 12, 13, 14, 15, 16, 0, 0, 0, 0]
        }
    }
    
    
    public class Test_新增資料 {
        public static void main(String[] args) {
            ByteBuffer b1 = ByteBuffer.allocate(10);
            // 新增資料
            b1.put((byte)11);
            b1.put((byte)12);
            b1.put((byte)13);
    
            // ByteBuffer轉換為普通位元組陣列
            byte[] bytes = b1.array();
            System.out.println(Arrays.toString(bytes));
            // 列印結果: [11, 12, 13, 0, 0, 0, 0, 0, 0, 0]
    
            System.out.println("=======================");
            byte[] b2 = {14,15,16};
            // 新增資料
            b1.put(b2,0,1);
            // ByteBuffer轉換為普通位元組陣列
            byte[] b = b1.array();
            System.out.println(Arrays.toString(b));
            // 列印結果: [11, 12, 13, 14, 0, 0, 0, 0, 0, 0]
        }
    }
    

知識點--容量-capacity

目標

  • 掌握容量-capacity方法的使用

路徑

  • 容量-capacity方法的使用

講解

  • Buffer的容量(capacity)是指:Buffer所能夠包含的元素的最大數量。定義了Buffer後,容量是不可變的。

  • 示例程式碼:

    public static void main(String[] args) {
        ByteBuffer b1 = ByteBuffer.allocate(10);
        System.out.println("容量:" + b1.capacity());//10。之後不可改變
    
        byte[] byteArray = {97, 98, 99, 100};
        ByteBuffer b2 = ByteBuffer.wrap(byteArray);
        System.out.println("容量:" + b2.capacity());//4。之後不可改變
    }
    
  • 結果:

    容量:10
    容量:4
    

知識點--限制-limit

目標

  • 掌握限制-limit方法的使用

路徑

  • 限制-limit方法的使用

講解

  • 限制limit是指:第一個不應該讀取或寫入元素的index索引。緩衝區的限制(limit)不能為負,並且不能大於容量。

  • 有兩個相關方法:

    • public int limit():獲取此緩衝區的限制。
    • public Buffer limit(int newLimit):設定此緩衝區的限制。
  • 示例程式碼:

    public class Test_新增資料 {
        public static void main(String[] args) {
            ByteBuffer b1 = ByteBuffer.allocate(10);
            // 新增資料
            b1.put((byte)10);
    
            // 獲取限制
            int limit1 = b1.limit();
            System.out.println("limit1:"+limit1);// 10
    
            // 設定限制
            b1.limit(3);
    
            // 新增元素
            b1.put((byte)20);
          b1.put((byte)30);
            // b1.put((byte)40);// 報異常,因為限制位置索引為3,所以再存14就會報異常:BufferOverflowException
    
        }
    }
    

    圖示:

知識點--位置-position

目標

  • 掌握位置-position方法的使用

路徑

  • 位置-position方法的使用

講解

  • 位置position是指:當前可寫入的索引。位置不能小於0,並且不能大於"限制"。

  • 有兩個相關方法:

    • public int position():獲取當前可寫入位置索引。
    • public Buffer position(int p):更改當前可寫入位置索引。
  • 示例程式碼:

    public class Test_新增資料 {
        public static void main(String[] args) {
            ByteBuffer b1 = ByteBuffer.allocate(10);
            // 新增資料
            b1.put((byte)11);
    
            // 獲取當前位置索引
            int position = b1.position();
            System.out.println("position:"+position);// 1
    
            // 設定當前位置索引
            b1.position(5);
    
            b1.put((byte)22);
            b1.put((byte)33);
            System.out.println("position:"+b1.position());// 7
            System.out.println(Arrays.toString(b1.array()));
            // 列印結果:[11, 0, 0, 0, 0, 22, 33, 0, 0, 0]
        }
    }
    

知識點--標記-mark

目標

  • 掌握標記-mark方法的使用

路徑

  • 標記-mark方法的使用

講解

  • 標記mark是指:當呼叫緩衝區的reset()方法時,會將緩衝區的position位置重置為該索引。

  • 相關方法:

    • public Buffer mark():設定此緩衝區的標記為當前的position位置。
    • public Buffer reset() : 將此緩衝區的位置重置為以前標記的位置。
  • 示例程式碼:

    public static void main(String[] args) {
            ByteBuffer b1 = ByteBuffer.allocate(10);
            // 新增資料
            b1.put((byte)11);
    
            // 獲取當前位置索引
            int position = b1.position();
            System.out.println("position:"+position);// 1
    
            // 標記當前位置索引
            b1.mark();
    
            // 新增元素
            b1.put((byte)22);
            b1.put((byte)33);
    
            // 獲取當前位置索引
            System.out.println("position:"+b1.position());// 3
            System.out.println(Arrays.toString(b1.array()));
            // 列印結果:[11, 22, 33, 0, 0, 0, 0, 0, 0, 0]
    
            // 重置當前位置索引
            b1.reset();
            // 獲取當前位置索引
            System.out.println("position:"+b1.position());// 1
    
            // 新增元素
            b1.put((byte)44);
            System.out.println(Arrays.toString(b1.array()));
            // 列印結果:[11, 44, 33, 0, 0, 0, 0, 0, 0, 0]
    
        }
    

知識點--其它方法

目標

  • 瞭解其它方法方法的使用

路徑

  • 其它方法的使用

講解

- public int remaining():獲取position與limit之間的元素數。
- public boolean isReadOnly():獲取當前緩衝區是否只讀。
- public boolean isDirect():獲取當前緩衝區是否為直接緩衝區。
- public Buffer rewind():重繞此緩衝區。
  - 將position位置設定為:0
  - 限制limit不變。
  - 丟棄標記。
- public Buffer clear():還原緩衝區的狀態。
  - 將position設定為:0
  - 將限制limit設定為容量capacity;
  - 丟棄標記mark。
- public Buffer flip():縮小limit的範圍。
  - 將limit設定為當前position位置;
  - 將當前position位置設定為0;
  - 丟棄標記。
public static void main(String[] args) {
        //建立物件
        ByteBuffer buffer = ByteBuffer.allocate(10);

        //新增元素
        buffer.put((byte)11);
        buffer.put((byte)22);
        buffer.put((byte)33);

        //限制
        buffer.limit(6);

        //容量是10 位置是3 限制是6
        System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
        

        //還原
        buffer.clear();

        //容量是10 位置是0 限制是10
        System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
  }

public static void main(String[] args) {
        //建立物件
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //新增元素
        buffer.put((byte)11);
        buffer.put((byte)22);
        buffer.put((byte)33);
        //限制
        buffer.limit(6);

        //容量是10 位置是3 限制是6
        System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());

        //切換
        buffer.flip();

        //容量是10 位置是0 限制是3
        System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
    }

第四章 Channel(通道)

知識點--Channel概述

目標

  • 理解Channel 的概述和分類

路徑

  • Channel 的概述
  • Channel 的分類

講解

Channel 的概述

Channel(通道):Channel是一個物件,可以通過它讀取和寫入資料, 可以把它看做是IO中的流,不同的是:Channel是雙向的, Channel物件既可以呼叫讀取的方法, 也可以呼叫寫出的方法 。

輸入流: 讀

輸出流: 寫

Channel: 讀,寫

Channel 的分類

在Java NIO中的Channel主要有如下幾種型別:

  • FileChannel:從檔案讀取資料的 輸入流和輸出流
  • DatagramChannel:讀寫UDP網路協議資料 DatagramPackge
  • SocketChannel:讀寫TCP網路協議資料 Socket
  • ServerSocketChannel:可以監聽TCP連線 ServerSocket

知識點--FileChannel類的基本使用

目標

  • 使用FileChannel類完成檔案的複製

路徑

  • 獲取FileChannel類的物件
  • 使用FileChannel類完成檔案的複製

講解

獲取FileChannel類的物件

  • java.nio.channels.FileChannel (抽象類):用於讀、寫檔案的通道。

  • FileChannel是抽象類,我們可以通過FileInputStream和FileOutputStream的getChannel()方法方便的獲取一個它的子類物件。

    FileInputStream fi=new FileInputStream(new File(src));
    FileOutputStream fo=new FileOutputStream(new File(dst));
    //獲得傳輸通道channel
    FileChannel inChannel=fi.getChannel();
    FileChannel outChannel=fo.getChannel();
    

使用FileChannel類完成檔案的複製

  • 我們將通過CopyFile這個示例讓大家體會NIO的操作過程。CopyFile執行三個基本的操作:建立一個Buffer,然後從原始檔讀取資料到緩衝區,然後再將緩衝區寫入目標檔案。
 public static void main(String[] args) throws Exception{
        FileInputStream fis = new FileInputStream("day19\\aaa\\a.txt");
        FileOutputStream fos = new FileOutputStream("day19\\aaa\\aCopy1.txt");
        // 獲得FileChannel管道物件
        FileChannel c1 = fis.getChannel();
        FileChannel c2 = fos.getChannel();

        // 建立ByteBuffer陣列
        ByteBuffer b = ByteBuffer.allocate(1000);

        // 迴圈讀取資料
        while ((c1.read(b)) != -1){// 讀取的位元組會填充postion到limit位置之間
            // 重置 postion為0,limit為postion的位置
            b.flip();
            // 寫出資料
            c2.write(b);// 會把postion到limit之間的資料寫出
            // 還原
            b.clear();// positon為:0  limit為: capacity 用於下次讀取
        }

        // 釋放資源
        c2.close();
        c1.close();
        fos.close();
        fis.close();

        /*byte[] bys = new byte[8192];
        int len;
        while ((len = fis.read(bys)) != -1){
            fos.write(bys,0,len);
        }
        fos.close();
        fis.close();*/
    }

知識點--FileChannel結合MappedByteBuffer實現高效讀寫

目標

  • MappedByteBuffer類的使用

路徑

  • MappedByteBuffer類的概述
  • FileChannel結合MapppedByteBuffer複製2G以下檔案
  • FileChannel結合MapppedByteBuffer複製2G以上檔案

講解

MappedByteBuffer類的概述

  • 上例直接使用FileChannel結合ByteBuffer實現的管道讀寫,但並不能提高檔案的讀寫效率。

  • ByteBuffer有個抽象子類:MappedByteBuffer,它可以將檔案直接對映至記憶體,把硬碟中的讀寫變成記憶體中的讀寫, 所以可以提高大檔案的讀寫效率。

  • 可以呼叫FileChannel的map()方法獲取一個MappedByteBuffer,map()方法的原型:

    MappedByteBuffer map(MapMode mode, long position, long size);

    說明:將節點中從position開始的size個位元組對映到返回的MappedByteBuffer中。

複製2GB以下的檔案

  • 複製d:\b.rar檔案,此檔案大概600多兆,複製完畢用時不到2秒。此例不能複製大於2G的檔案,因為map的第三個引數被限制在Integer.MAX_VALUE(位元組) = 2G。
public static void main(String[] args) throws Exception{
        //java.io.RandomAccessFile類,可以設定讀、寫模式的IO流類。
        //"r"表示:只讀--輸入流,只讀就可以。
        RandomAccessFile r1 = new RandomAccessFile("day19\\aaa\\a.txt","r");
        //"rw"表示:讀、寫--輸出流,需要讀、寫。
        RandomAccessFile r2 = new RandomAccessFile("day19\\aaa\\aCopy2.txt","rw");

        // 獲得FileChannel管道物件
        FileChannel c1 = r1.getChannel();
        FileChannel c2 = r2.getChannel();

        // 獲取檔案的大小
        long size = c1.size();

        // 直接把硬碟中的檔案對映到記憶體中
        MappedByteBuffer b1 = c1.map(FileChannel.MapMode.READ_ONLY, 0, size);
        MappedByteBuffer b2 = c2.map(FileChannel.MapMode.READ_WRITE, 0, size);

        // 迴圈讀取資料
        for (long i = 0; i < size; i++) {
            // 讀取位元組
            byte b = b1.get();
            // 儲存到第二個陣列中
            b2.put(b);
        }
        // 釋放資源
        c2.close();
        c1.close();
        r2.close();
        r1.close();
    }
  • 程式碼說明:

  • map()方法的第一個引數mode:對映的三種模式,在這三種模式下得到的將是三種不同的MappedByteBuffer:三種模式都是Channel的內部類MapMode中定義的靜態常量,這裡以FileChannel舉例:
    1). FileChannel.MapMode.READ_ONLY:得到的映象只能讀不能寫(只能使用get之類的讀取Buffer中的內容);

    2). FileChannel.MapMode.READ_WRITE:得到的映象可讀可寫(既然可寫了必然可讀),對其寫會直接更改到儲存節點;

    3). FileChannel.MapMode.PRIVATE:得到一個私有的映象,其實就是一個(position, size)區域的副本罷了,也是可讀可寫,只不過寫不會影響到儲存節點,就是一個普通的ByteBuffer了!!

  • 為什麼使用RandomAccessFile?

    1). 使用InputStream獲得的Channel可以對映,使用map時只能指定為READ_ONLY模式,不能指定為READ_WRITE和PRIVATE,否則會丟擲執行時異常!

    2). 使用OutputStream得到的Channel不可以對映!並且OutputStream的Channel也只能write不能read!

    3). 只有RandomAccessFile獲取的Channel才能開啟任意的這三種模式!

複製2GB以上的檔案

  • 下例使用迴圈,將檔案分塊,可以高效的複製大於2G的檔案
    public static void main(String[] args) throws Exception{
        //java.io.RandomAccessFile類,可以設定讀、寫模式的IO流類。
        //"r"表示:只讀--輸入流,只讀就可以。
        RandomAccessFile r1 = new RandomAccessFile("H:\\課堂資料.zip","r");
        //"rw"表示:讀、寫--輸出流,需要讀、寫。
        RandomAccessFile r2 = new RandomAccessFile("H:\\課堂資料2.zip","rw");

        // 獲得FileChannel管道物件
        FileChannel c1 = r1.getChannel();
        FileChannel c2 = r2.getChannel();

        // 獲取檔案的大小
        long size = c1.size();

        // 每次期望複製500M
        int everySize = 1024*1024*500;

        // 總共需要複製多少次
        long count = size % everySize == 0 ? size/everySize : size/everySize+1;

        // 開始複製
        for (long i = 0; i < count; i++) {
            // 每次開始複製的位置
            long start = everySize*i;

            // 每次複製的實際大小
            long trueSize = size - start > everySize ? everySize : size - start;

            // 直接把硬碟中的檔案對映到記憶體中
            MappedByteBuffer b1 = c1.map(FileChannel.MapMode.READ_ONLY, start, trueSize);
            MappedByteBuffer b2 = c2.map(FileChannel.MapMode.READ_WRITE, start, trueSize);

            // 迴圈讀取資料
            for (long j = 0; j < trueSize; j++) {
                // 讀取位元組
                byte b = b1.get();
                // 儲存到第二個陣列中
                b2.put(b);
            }
        }

        // 釋放資源
        c2.close();
        c1.close();
        r2.close();
        r1.close();
        
    }

知識點--ServerSocketChannel和SocketChannel建立連線

目標

  • ServerSocketChannel和SocketChannel建立連線

路徑

  • SocketChannel建立連線
  • ServerSocketChanne建立連線

講解

SocketChannel建立連線

  • 客戶端:SocketChannel類用於連線的客戶端,它相當於:Socket。

    1). 先呼叫SocketChannel的open()方法開啟通道:

    SocketChannel socket = SocketChannel.open()
    

    2). 呼叫SocketChannel的例項方法connect(SocketAddress add)連線伺服器:

     socket.connect(new InetSocketAddress("localhost", 8888));
    

    示例:客戶端連線伺服器:

    public class Client {
        public static void main(String[] args) throws Exception {
            SocketChannel socket = SocketChannel.open();
            socket.connect(new InetSocketAddress("localhost", 8888));
    		  System.out.println("後續程式碼......");
            
        }
    }
    

ServerSocketChanne建立連線

  • 伺服器端:ServerSocketChannel類用於連線的伺服器端,它相當於:ServerSocket。

  • 呼叫ServerSocketChannel的靜態方法open()就可以獲得ServerSocketChannel物件, 但並沒有指定埠號, 必須通過其套接字的bind方法將其繫結到特定地址,才能接受連線。

    ServerSocketChannel serverChannel = ServerSocketChannel.open()
    
  • 呼叫ServerSocketChannel的例項方法bind(SocketAddress add):繫結本機監聽埠,準備接受連線。

    ​ 注:java.net.SocketAddress(抽象類):代表一個Socket地址。

    ​ 我們可以使用它的子類:java.net.InetSocketAddress(類)

    ​ 構造方法:InetSocketAddress(int port):指定本機監聽埠。

    serverChannel.bind(new InetSocketAddress(8888));
    
  • 呼叫ServerSocketChannel的例項方法accept():等待連線。

    SocketChannel accept = serverChannel.accept();
    System.out.println("後續程式碼...");
    

    示例:伺服器端等待連線(預設-阻塞模式)

    public class Server {
        public static void main(String[] args) throws Exception{
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.bind(new InetSocketAddress(8888));
            System.out.println("【伺服器】等待客戶端連線...");
            SocketChannel accept = serverChannel.accept();
            System.out.println("後續程式碼......");
        }
    }
    

    執行後結果:

    【伺服器】等待客戶端連線...
    
  • 我們可以通過ServerSocketChannel的configureBlocking(boolean b)方法設定accept()是否阻塞

    public class Server {
        public static void main(String[] args) throws Exception {
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.bind(new InetSocketAddress(8888));
            System.out.println("【伺服器】等待客戶端連線...");
            //設定非阻塞連線
            ssc.configureBlocking(false);// 寫成false叫非阻塞, 寫成true叫阻塞
            
             while(true) {
                  //獲取客戶端連線
                  SocketChannel sc = ssc.accept();
    
                  if(sc != null){
                      //不等於null說明連線上了客戶端
                      System.out.println("連線上了。。");
                      //讀取資料操作
                      break;
                  }else{
                      //沒連線上客戶端
                      System.out.println("打會兒遊戲~");
                      Thread.sleep(2000);
                  }
              }
        }
    }
    

    執行後結果:

    【伺服器】等待客戶端連線...
    打會兒遊戲~
    有客戶端來了就輸出: 連線上了。。
    

知識點--NIO網路程式設計收發資訊

目標

  • 使用ServerSocketChannel代替之前的ServerSocket,來完成網路程式設計的收發資料。

路徑

  • 書寫伺服器程式碼
  • 書寫客戶端程式碼

講解

書寫伺服器程式碼

public class Server {
    public static void main(String[] args)  throws IOException{
		//建立物件
        //ServerSocket ss = new ServerSocket(8888);

        //建立
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //伺服器繫結埠
        ssc.bind(new InetSocketAddress(8888));

        //連線上客戶端
        SocketChannel sc = ssc.accept();
       
        //伺服器端接受資料
        //建立陣列
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //接收資料
        int len = sc.read(buffer);
        //列印結構
        System.out.println(new String(buffer.array(),0,len));

        //關閉資源
        sc.close();
    }
}

書寫客戶端程式碼

public class Client {
    public static void main(String[] args) {
        //建立物件
        //Socket s = new Socket("127.0.0.1",8888);

        //建立物件
        SocketChannel sc = SocketChannel.open();
        //連線伺服器
        sc.connect(new InetSocketAddress("127.0.0.1",8888));

        //客戶端發資料
        //建立陣列
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //陣列中新增資料
        buffer.put("你好啊~".getBytes());
        //切換
        buffer.flip();
        //發出資料
        sc.write(buffer);

        //關流
        sc.close();
    }
}

第五章 Selector(選擇器)

知識點--多路複用的概念

目標

  • 瞭解多路複用的概念

路徑

  • 伺服器端的非多路複用效果
  • 伺服器端的多路複用效果

講解

選擇器Selector是NIO中的重要技術之一。它與SelectableChannel聯合使用實現了非阻塞的多路複用。使用它可以節省CPU資源,提高程式的執行效率。

"多路"是指:伺服器端同時監聽多個“埠”的情況。每個埠都要監聽多個客戶端的連線。

  • 伺服器端的非多路複用效

如果不使用“多路複用”,伺服器端需要開很多執行緒處理每個埠的請求。如果在高併發環境下,造成系統性能下降。

  • 伺服器端的多路複用效果

使用了多路複用,只需要一個執行緒就可以處理多個通道,降低記憶體佔用率,減少CPU切換時間,在高併發、高頻段業務環境下有非常重要的優勢

小結

  • 多路複用的意思就是一個Selector可以監聽多個伺服器埠。

知識點--選擇器Selector的獲取和註冊

目標

  • 理解Selector的作用以及基本使用

路徑

  • Selector選擇器的概述和作用
  • Selector選擇器的獲取
  • 註冊Channel到Selector

講解

Selector選擇器的概述和作用

概述: Selector被稱為:選擇器,也被稱為:多路複用器,可以把多個Channel註冊到一個Selector選擇器上, 那麼就可以實現利用一個執行緒來處理這多個Channel上發生的事件,並且能夠根據事件情況決定Channel讀寫。這樣,通過一個執行緒管理多個Channel,就可以處理大量網路連線了, 減少系統負擔, 提高效率。因為執行緒之間的切換對作業系統來說代價是很高的,並且每個執行緒也會佔用一定的系統資源。所以,對系統來說使用的執行緒越少越好。

作用: 一個Selector可以監聽多個Channel發生的事件, 減少系統負擔 , 提高程式執行效率 .

Selector選擇器的獲取

Selector selector = Selector.open();

註冊Channel到Selector

通過呼叫 channel.register(Selector sel, int ops)方法來實現註冊:

channel.configureBlocking(false);// 設定非阻塞
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);

register()方法的第二個引數:是一個int值,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽四種不同型別的事件,而且可以使用SelectionKey的四個常量表示:

  1. 連線就緒--常量:SelectionKey.OP_CONNECT

  2. 接收就緒--常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在註冊時只能使用此項)

  3. 讀就緒--常量:SelectionKey.OP_READ

  4. 寫就緒--常量:SelectionKey.OP_WRITE

    注意:對於ServerSocketChannel在註冊時,只能使用OP_ACCEPT,否則丟擲異常。

  • 案例演示; 監聽一個通道

    public class Test1 {
        public static void main(String[] args) throws Exception{
            /*
                - Selector選擇器的概述和作用
                    概述: Selector被稱為:選擇器,也被稱為:多路複用器,可以把多個Channel註冊到一個Selector選擇器上,
                          那麼就可以實現利用一個執行緒來處理這多個Channel上發生的事件,並且能夠根據事件情況決定Channel讀寫。
                    作用: 一個Selector可以監聽多個Channel發生的事件, 減少系統負擔 , 提高程式執行效率 .
    
                - Selector選擇器的獲取
                      通過Selector.open()來獲取Selector選擇器物件
                - 註冊Channel到Selector
                    通過Channel的register(Selector sel, int ops)方法把Channel註冊到指定的選擇器上
                    引數1: 表示選擇器
                    引數2: 選擇器要監聽Channel的什麼事件
                   注意:
                     1.對於ServerSocketChannel在註冊時,只能使用OP_ACCEPT,否則丟擲異常。
                     2.ServerSocketChannel要設定成非阻塞
             */
            // 獲取ServerSocketChannel伺服器通道物件
            ServerSocketChannel ssc1 = ServerSocketChannel.open();
            // 繫結埠號
            ssc1.bind(new InetSocketAddress(7777));
    
            // 設定非阻塞
            ssc1.configureBlocking(false);
    
            // 獲取Selector選擇器物件
            Selector selector = Selector.open();
    
            // 把伺服器通道的accept()交給選擇器來處理
            // 註冊Channel到Selector選擇器上
            ssc1.register(selector, SelectionKey.OP_ACCEPT);
        }
    }
    
  • 示例:伺服器建立3個通道,同時監聽3個埠,並將3個通道註冊到一個選擇器中

public class Test2 {
    public static void main(String[] args) throws Exception{
        /*
            把多個Channel註冊到一個選擇器上
         */
        // 獲取ServerSocketChannel伺服器通道物件
        ServerSocketChannel ssc1 = ServerSocketChannel.open();
        // 繫結埠號
        ssc1.bind(new InetSocketAddress(7777));

        // 獲取ServerSocketChannel伺服器通道物件
        ServerSocketChannel ssc2 = ServerSocketChannel.open();
        // 繫結埠號
        ssc2.bind(new InetSocketAddress(8888));


        // 獲取ServerSocketChannel伺服器通道物件
        ServerSocketChannel ssc3 = ServerSocketChannel.open();
        // 繫結埠號
        ssc3.bind(new InetSocketAddress(9999));


        // 設定非阻塞
        ssc1.configureBlocking(false);
        ssc2.configureBlocking(false);
        ssc3.configureBlocking(false);

        // 獲取Selector選擇器物件
        Selector selector = Selector.open();

        // 把伺服器通道的accept()交給選擇器來處理
        // 註冊Channel到Selector選擇器上
        ssc1.register(selector, SelectionKey.OP_ACCEPT);
        ssc2.register(selector,SelectionKey.OP_ACCEPT);
        ssc3.register(selector,SelectionKey.OP_ACCEPT);
    }
}

接下來,就可以通過選擇器selector操作三個通道了。

知識點--Selector的常用方法

目標

  • Selector的常用方法

路徑

  • Selector的select()方法
  • Selector的selectedKeys()方法
  • Selector的keys()方法

講解

Selector的select()方法:

  • 作用: 伺服器等待客戶端連線的方法

  • 阻塞問題:

    • 在連線到第一個客戶端之前,會一直阻塞
    • 當連線到客戶端後,如果客戶端沒有被處理,該方法會計入不阻塞狀態
    • 當連線到客戶端後,如果客戶端有被處理,該方法又會進入阻塞狀態
    public class Server1 {
        public static void main(String[] args) throws Exception {
            /*
                - Selector的select()方法
                    作用:伺服器等待客戶端連線的方法
                    阻塞:
                        1.在沒有客戶端連線之前該方法會一直阻塞
                        2.當連線到客戶端後沒有被處理,該方法就會進入不阻塞狀態
                        3.當連線到客戶端後有被處理,該方法就會進入阻塞狀態
             */
            // 獲取ServerSocketChannel伺服器通道物件
            ServerSocketChannel ssc1 = ServerSocketChannel.open();
            // 繫結埠號
            ssc1.bind(new InetSocketAddress(7777));
    
            // 獲取ServerSocketChannel伺服器通道物件
            ServerSocketChannel ssc2 = ServerSocketChannel.open();
            // 繫結埠號
            ssc2.bind(new InetSocketAddress(8888));
    
    
            // 獲取ServerSocketChannel伺服器通道物件
            ServerSocketChannel ssc3 = ServerSocketChannel.open();
            // 繫結埠號
            ssc3.bind(new InetSocketAddress(9999));
    
    
            // 設定非阻塞
            ssc1.configureBlocking(false);
            ssc2.configureBlocking(false);
            ssc3.configureBlocking(false);
    
            // 獲取Selector選擇器物件
            Selector selector = Selector.open();
    
            // 把伺服器通道的accept()交給選擇器來處理
            // 註冊Channel到Selector選擇器上
            ssc1.register(selector, SelectionKey.OP_ACCEPT);
            ssc2.register(selector, SelectionKey.OP_ACCEPT);
            ssc3.register(selector, SelectionKey.OP_ACCEPT);
    
            // 死迴圈一直接受客戶端的連線請求
            while (true) {
                System.out.println(1);
                // 伺服器等待客戶端的連線
                selector.select();// 阻塞
                System.out.println(2);
    
                // 處理客戶端請求的程式碼--->暫時看不懂,先放著
                Set<SelectionKey> keySet = selector.selectedKeys();// 儲存所有被連線的伺服器Channel物件
                for (SelectionKey key : keySet) {
                    ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
                    SocketChannel sc = ssc.accept();
                    System.out.println("...開始處理,接受資料,程式碼省略...");
                    //...
                }
            }
    
        }
    }
    
    

Selector的selectedKeys()方法

  • 獲取已連線的所有通道集合

    public class Server2 {
        public static void main(String[] args) throws Exception {
            /*
                - Selector的selectedKeys()方法
                    作用:  獲取所有被連線的伺服器Channel物件的Set集合
                           該Set集合中的元素型別是SelectionKey,該SelectionKey類其實就是對Channel的一個封裝
                    如何獲取被連線的伺服器Channel物件:
                        遍歷所有被連線的伺服器Channel物件的Set集合
                        獲取該集合中的SelectionKey物件
                        根據SelectionKey物件呼叫channel方法,獲得伺服器Channel物件
                - Selector的keys()方法
             */
            // 獲取ServerSocketChannel伺服器通道物件
            ServerSocketChannel ssc1 = ServerSocketChannel.open();
            // 繫結埠號
            ssc1.bind(new InetSocketAddress(7777));
    
            // 獲取ServerSocketChannel伺服器通道物件
            ServerSocketChannel ssc2 = ServerSocketChannel.open();
            // 繫結埠號
            ssc2.bind(new InetSocketAddress(8888));
    
    
            // 獲取ServerSocketChannel伺服器通道物件
            ServerSocketChannel ssc3 = ServerSocketChannel.open();
            // 繫結埠號
            ssc3.bind(new InetSocketAddress(9999));
    
    
            // 設定非阻塞
            ssc1.configureBlocking(false);
            ssc2.configureBlocking(false);
            ssc3.configureBlocking(false);
    
            // 獲取Selector選擇器物件
            Selector selector = Selector.open();
    
            // 把伺服器通道的accept()交給選擇器來處理
            // 註冊Channel到Selector選擇器上
            ssc1.register(selector, SelectionKey.OP_ACCEPT);
            ssc2.register(selector, SelectionKey.OP_ACCEPT);
            ssc3.register(selector, SelectionKey.OP_ACCEPT);
    
            // 獲取所有被連線的伺服器Channel物件的Set集合
            // 該Set集合中的元素型別是SelectionKey,該SelectionKey類其實就是對Channel的一個封裝
            Set<SelectionKey> keySet = selector.selectedKeys();
            System.out.println("被連線的伺服器物件有多少個:"+keySet.size());// 0
    
            // 死迴圈一直接受客戶端的連線請求
            while (true) {
                System.out.println(1);
                // 伺服器等待客戶端的連線
                selector.select();// 阻塞
                System.out.println(2);
                System.out.println("被連線的伺服器物件個數:"+keySet.size());// 有多少個客戶端連線伺服器成功,就列印幾
    
                // 處理客戶端請求的程式碼--->暫時看不懂,先放著
                // 獲取所有被連線的伺服器Channel物件的集合
                /*Set<SelectionKey> keySet = selector.selectedKeys();
                // 遍歷所有被連線的伺服器Channel物件,拿到每一個SelectionKey
                for (SelectionKey key : keySet) {
                    // 根據SelectionKey獲取伺服器Channel物件
                    ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
                    // 獲得客戶端Channel物件
                    SocketChannel sc = ssc.accept();
                    // 處理
                    System.out.println("...開始處理,接受資料,程式碼省略...");
                    //...
                }*/
            }
    
        }
    }
    
    

Selector的keys()方法

  • 獲取已註冊的所有通道集合

    public class Server3 {
        public static void main(String[] args) throws Exception {
            /*
                - Selector的keys()方法
                    獲取所有被註冊的伺服器Channel物件的Set集合
                    該Set集合中的元素型別是SelectionKey,該SelectionKey類其實就是對Channel的一個封裝
             */
            // 獲取ServerSocketChannel伺服器通道物件
            ServerSocketChannel ssc1 = ServerSocketChannel.open();
            // 繫結埠號
            ssc1.bind(new InetSocketAddress(7777));
    
            // 獲取ServerSocketChannel伺服器通道物件
            ServerSocketChannel ssc2 = ServerSocketChannel.open();
            // 繫結埠號
            ssc2.bind(new InetSocketAddress(8888));
    
    
            // 獲取ServerSocketChannel伺服器通道物件
            ServerSocketChannel ssc3 = ServerSocketChannel.open();
            // 繫結埠號
            ssc3.bind(new InetSocketAddress(9999));
    
    
            // 設定非阻塞
            ssc1.configureBlocking(false);
            ssc2.configureBlocking(false);
            ssc3.configureBlocking(false);
    
            // 獲取Selector選擇器物件
            Selector selector = Selector.open();
    
            // 把伺服器通道的accept()交給選擇器來處理
            // 註冊Channel到Selector選擇器上
            ssc1.register(selector, SelectionKey.OP_ACCEPT);
            ssc2.register(selector, SelectionKey.OP_ACCEPT);
            ssc3.register(selector, SelectionKey.OP_ACCEPT);
    
            // 獲取所有被連線的伺服器Channel物件的Set集合
            // 該Set集合中的元素型別是SelectionKey,該SelectionKey類其實就是對Channel的一個封裝
            Set<SelectionKey> keySet = selector.selectedKeys();
            System.out.println("被連線的伺服器物件有多少個:"+keySet.size());// 0
    
            // 獲取所有被註冊的伺服器Channel物件的Set集合
            // 該Set集合中的元素型別是SelectionKey,該SelectionKey類其實就是對Channel的一個封裝
            Set<SelectionKey> keys = selector.keys();
            System.out.println("被註冊的伺服器物件有多少個:"+keys.size()); // 3
    
    
            // 死迴圈一直接受客戶端的連線請求
            while (true) {
                System.out.println(1);
                // 伺服器等待客戶端的連線
                selector.select();// 阻塞
                System.out.println(2);
                System.out.println("被連線的伺服器物件個數:"+keySet.size());// 有多少個客戶端連線伺服器成功,就列印幾
                System.out.println("被註冊的伺服器物件個數:"+keys.size());// 選擇器上註冊了多少個伺服器Channel,就列印幾
            }
    
        }
    }
    

實操--Selector多路複用

需求

  • 使用Selector進行多路複用,監聽3個伺服器埠

分析

  • 建立3個伺服器通道,設定成非阻塞
  • 獲取Selector選擇器
  • 把Selector註冊到三個伺服器通道上
  • 迴圈去等待客戶端連線
  • 遍歷所有被連線的伺服器通道集合
  • 處理客戶端請求

實現

  • 案例:

    public class Server1 {
        public static void main(String[] args) throws Exception {
            /*
                需求: 使用Selector進行多路複用,監聽3個伺服器埠
                分析:
                    1.建立3個伺服器Channel物件,並繫結埠號
                    2.把3個伺服器Channel物件設定成非阻塞
                    3.獲得Selector選擇器
                    4.把3個個伺服器Channel物件物件註冊到同一個Selector選擇器上,指定監聽事件
                    5.死迴圈去等待客戶端的連線
                    6.獲取所有被連線的伺服器Channel物件的Set集合
                    7.迴圈遍歷所有被連線的伺服器Channel物件
                    8.處理客戶端的請求
             */
            // 1.建立3個伺服器Channel物件,並繫結埠號
            ServerSocketChannel ssc1 = ServerSocketChannel.open();
            ssc1.bind(new InetSocketAddress(7777));
    
            ServerSocketChannel ssc2 = ServerSocketChannel.open();
            ssc2.bind(new InetSocketAddress(8888));
    
            ServerSocketChannel ssc3 = ServerSocketChannel.open();
            ssc3.bind(new InetSocketAddress(9999));
    
            // 2.把3個伺服器Channel物件設定成非阻塞
            ssc1.configureBlocking(false);
            ssc2.configureBlocking(false);
            ssc3.configureBlocking(false);
    
            // 3.獲得Selector選擇器
            Selector selector = Selector.open();
    
            // 4.把3個個伺服器Channel物件物件註冊到同一個Selector選擇器上,指定監聽事件
            ssc1.register(selector, SelectionKey.OP_ACCEPT);
            ssc2.register(selector, SelectionKey.OP_ACCEPT);
            ssc3.register(selector, SelectionKey.OP_ACCEPT);
    
            // 5.死迴圈去等待客戶端的連線
            while (true) {
                // 伺服器等待客戶端連線
                System.out.println(1);
                selector.select();
    
                // 6.獲取所有被連線的伺服器Channel物件的Set集合
                Set<SelectionKey> keySet = selector.selectedKeys(); // 2
    
                // 7.迴圈遍歷所有被連線的伺服器Channel物件,獲取每一個被連線的伺服器Channel物件
                for (SelectionKey key : keySet) {// 遍歷出7777埠  8888埠
                    // 8.由於SelectionKey是對Channel的封裝,所以我們得根據key獲取被連線的伺服器Channel物件
                    ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
                    // 9.處理客戶端的請求
                    // 9.1 獲取連線的客戶端物件
                    SocketChannel sc = ssc.accept();
                    // 9.2 建立ByteBuffer緩衝陣列
                    ByteBuffer b = ByteBuffer.allocate(1024);
                    // 9.3 讀取資料
                    int len = sc.read(b);// 把讀取到的位元組資料儲存到b緩衝陣列中,返回讀取到的位元組個數
                    // 9.4 列印輸出
                    System.out.println(new String(b.array(), 0, len));
                    // 10. 釋放資源
                    sc.close();
                }
            }
            /*
                    - 問題: Selector把所有被連線的伺服器物件放在了一個Set集合中,但是使用完後並沒有刪除,
                            導致在遍歷集合時,遍歷到已經沒用的物件,出現了異常
                    - 解決辦法: 使用完了,應該從集合中刪除,由於遍歷的同時不能刪除,所以使用迭代器進行遍歷
    
             */
        }
    }
    
    
  • 問題: Selector把所有被連線的伺服器物件放在了一個Set集合中,但是使用完後並沒有刪除,導致在遍歷集合時,遍歷到已經沒用的物件,出現了異常

  • 解決辦法: 使用完了,應該從集合中刪除,由於遍歷的同時不能刪除,所以使用迭代器進行遍歷

  • 程式碼如下:

    public class Server2 {
        public static void main(String[] args) throws Exception {
            /*
                需求: 使用Selector進行多路複用,監聽3個伺服器埠
                分析:
                    1.建立3個伺服器Channel物件,並繫結埠號
                    2.把3個伺服器Channel物件設定成非阻塞
                    3.獲得Selector選擇器
                    4.把3個個伺服器Channel物件物件註冊到同一個Selector選擇器上,指定監聽事件
                    5.死迴圈去等待客戶端的連線
                    6.獲取所有被連線的伺服器Channel物件的Set集合
                    7.迴圈遍歷所有被連線的伺服器Channel物件
                    8.處理客戶端的請求
    
                   - 問題: Selector把所有被連線的伺服器物件放在了一個Set集合中,但是使用完後並沒有刪除,
                            導致在遍歷集合時,遍歷到已經沒用的物件,出現了異常
                    - 解決辦法: 使用完了,應該從集合中刪除,由於遍歷的同時不能刪除,所以使用迭代器進行遍歷
             */
            // 1.建立3個伺服器Channel物件,並繫結埠號
            ServerSocketChannel ssc1 = ServerSocketChannel.open();
            ssc1.bind(new InetSocketAddress(7777));
    
            ServerSocketChannel ssc2 = ServerSocketChannel.open();
            ssc2.bind(new InetSocketAddress(8888));
    
            ServerSocketChannel ssc3 = ServerSocketChannel.open();
            ssc3.bind(new InetSocketAddress(9999));
    
            // 2.把3個伺服器Channel物件設定成非阻塞
            ssc1.configureBlocking(false);
            ssc2.configureBlocking(false);
            ssc3.configureBlocking(false);
    
            // 3.獲得Selector選擇器
            Selector selector = Selector.open();
    
            // 4.把3個個伺服器Channel物件物件註冊到同一個Selector選擇器上,指定監聽事件
            ssc1.register(selector, SelectionKey.OP_ACCEPT);
            ssc2.register(selector, SelectionKey.OP_ACCEPT);
            ssc3.register(selector, SelectionKey.OP_ACCEPT);
    
            // 5.死迴圈去等待客戶端的連線
            while (true) {
                // 伺服器等待客戶端連線
                System.out.println(1);
                selector.select();
    
                // 6.獲取所有被連線的伺服器Channel物件的Set集合
                Set<SelectionKey> keySet = selector.selectedKeys();
    
                // 7.迴圈遍歷所有被連線的伺服器Channel物件,獲取每一個被連線的伺服器Channel物件
                Iterator<SelectionKey> it = keySet.iterator();
                // 迭代器的快捷鍵: itit
                while (it.hasNext()){
    
                    // 遍歷出來的SelectionKey
                    SelectionKey key = it.next();
    
                    // 8.由於SelectionKey是對Channel的封裝,所以我們得根據key獲取被連線的伺服器Channel物件
                    ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
                    // 9.處理客戶端的請求
                    // 9.1 獲取連線的客戶端物件
                    SocketChannel sc = ssc.accept();
                    // 9.2 建立ByteBuffer緩衝陣列
                    ByteBuffer b = ByteBuffer.allocate(1024);
                    // 9.3 讀取資料
                    int len = sc.read(b);// 把讀取到的位元組資料儲存到b緩衝陣列中,返回讀取到的位元組個數
                    // 9.4 列印輸出
                    System.out.println(new String(b.array(), 0, len));
                    // 10. 釋放資源
                    sc.close();
    
                    // 用完了就得刪除
                    it.remove();
                }
            }
        }
    }
    
    

第六章 NIO2-AIO(非同步、非阻塞)

知識點--AIO概述

目標

  • 瞭解AIO的概述

路徑

  • 同步,非同步,阻塞,非阻塞概念回顧
  • AIO相關類和方法介紹

講解

同步,非同步,阻塞,非阻塞概念回顧

- 同步:呼叫方法之後,必須要得到一個返回值。

- 非同步:呼叫方法之後,沒有返回值,但是會有回撥函式。回撥函式指的是滿足條件之後會自動執行的方法

- 阻塞:如果沒有達到方法的目的,就一直停在這裡【等待】。  

- 非阻塞:不管有沒有達到目的,都直接【往下執行】。  

AIO相關類和方法介紹

AIO是非同步IO的縮寫,雖然NIO在網路操作中,提供了非阻塞的方法,但是NIO的IO行為還是同步的。對於NIO來說,我們的業務執行緒是在IO操作準備好時,得到通知,接著就由這個執行緒自行進行IO操作,IO操作本身是同步的。

但是對AIO來說,則更加進了一步,它不是在IO準備好時再通知執行緒,而是在IO操作已經完成後,再給執行緒發出通知。因此AIO是不會阻塞的,此時我們的業務邏輯將變成一個回撥函式,等待IO操作完成後,由系統自動觸發。

與NIO不同,當進行讀寫操作時,只須直接呼叫API的read或write方法即可。這兩種方法均為非同步的,對於讀操作而言,當有流可讀取時,作業系統會將可讀的流傳入read方法的緩衝區,並通知應用程式;對於寫操作而言,當作業系統將write方法傳遞的流寫入完畢時,作業系統主動通知應用程式。 即可以理解為,read/write方法都是非同步的,完成後會主動呼叫回撥函式。 在JDK1.7中,這部分內容被稱作NIO.2---->AIO,主要在Java.nio.channels包下增加了下面四個非同步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

在AIO socket程式設計中,服務端通道是AsynchronousServerSocketChannel,這個類提供了一個open()靜態工廠,一個bind()方法用於繫結服務端IP地址(還有埠號),另外還提供了accept()用於接收使用者連線請求。在客戶端使用的通道是AsynchronousSocketChannel,這個通道處理提供open靜態工廠方法外,還提供了read和write方法。

在AIO程式設計中,發出一個事件(accept read write.connect等)之後要指定事件處理類(回撥函式),AIO中的事件處理類是CompletionHandler<V,A>,這個介面定義瞭如下兩個方法,分別在非同步操作成功和失敗時被回撥。

void completed(V result, A attachment); 成功

void failed(Throwable exc, A attachment);

實操--AIO 同步連線同步讀(沒有意義,不要求寫)

需求

  • AIO同步寫法,讀取客戶端寫過來的資料

分析

  • 獲取AsynchronousServerSocketChannel物件,繫結埠
  • 同步接收客戶端請求
  • 讀取資料

實現

  public static void main(String[] args) throws Exception {
        //建立物件
        AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open();

        //繫結埠
        assc.bind(new InetSocketAddress(8888));

        //獲取連線
        //Future裡面放的就是方法的結果
        //********同步********
        System.out.println("準備連線客戶端");
        Future<AsynchronousSocketChannel> future = assc.accept();
        //Future方法需要呼叫get()方法獲取真正的返回值
        AsynchronousSocketChannel sc = future.get();
        System.out.println("連線上了客戶端");
        //讀取客戶端發來的資料
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //讀取
        //以前返回的是讀取到的個數,真正的個數就在Future裡面放著
        //********同步********
        System.out.println("準備讀取資料");
        Future<Integer> future2 = sc.read(buffer);
        //獲取真正的返回值
        Integer len = future2.get();
        System.out.println("讀取到了資料");
        //列印
        System.out.println(new String(buffer.array(),0,len));
    }

實操--AIO 非同步非阻塞連線

需求

  • AIO非同步非阻塞的連線方法

分析

  • 獲取AsynchronousServerSocketChannel物件,繫結埠
  • 非同步接收客戶端請求
    • void accept(A attachment, CompletionHandler<AsynchronousSocketChannel,? super A> handler)
    • 第一個引數: 附件,沒啥用,傳入null即可
    • 第二個引數: CompletionHandler抽象類 ,AIO中的事件處理類
      • void completed(V result, A attachment);非同步連線成功,就會自動呼叫這個方法
      • void failed(Throwable exc, A attachment);非同步連線失敗,就會自動呼叫這個方法

實現

  • 伺服器端:
public static void main(String[] args) throws IOException {
        //伺服器非同步的連線方法
        //建立物件
        AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open();
        //繫結埠
        assc.bind(new InetSocketAddress(8000));
        //【非同步非阻塞】方式!!!!!連線客戶端
        System.out.println("11111111111");
        assc.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            //回撥函式,當成功連線了客戶端之後,會自動回來呼叫這個方法
            public void completed(AsynchronousSocketChannel result, Object attachment) {
                //completed是完成,如果連線成功會自動呼叫這個方法
                System.out.println("completed");
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                //failed是失敗,如果連線失敗會自動呼叫這個方法
            }
        });
        System.out.println("22222222222");

        //寫一個死迴圈讓程式別結束(因為程式結束了無法演示效果)
        while(true){

        }
    }

實操--AIO 非同步非阻塞連線和非同步讀

需求

  • 實現非同步連線,非同步讀

分析

  • 獲取AsynchronousServerSocketChannel物件,繫結埠
  • 非同步接收客戶端請求
  • 在CompletionHandler的completed方法中非同步讀資料

實現

  • 伺服器端程式碼:
//非同步非阻塞連線和讀取
    public static void main(String[] args) throws IOException {
        //建立物件
        AsynchronousServerSocketChannel assc  = AsynchronousServerSocketChannel.open();
        //繫結埠
        assc.bind(new InetSocketAddress(8000));
        //非同步非阻塞連線!!!!
        //第一個引數是一個附件
        System.out.println(1);
        assc.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            public void completed(AsynchronousSocketChannel s, Object attachment) {
                //如果連線客戶端成功,應該獲取客戶端發來的資料
                //completed()的第一個引數表示的是Socket物件.
                System.out.println(5);
                //建立陣列
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //非同步非阻塞讀!!!!!
                System.out.println(3);
                s.read(buffer, null, new CompletionHandler<Integer, Object>() {
                    @Override
                    public void completed(Integer len, Object attachment) {
                        //讀取成功會自動呼叫這個方法
                        //completed()方法的第一個引數是read()讀取到的實際個數
                        //列印資料
                        System.out.println(6);
                        System.out.println(new String(buffer.array(),0,len));
                    }

                    @Override
                    public void failed(Throwable exc, Object attachment) {

                    }
                });
                System.out.println(4);
            }

            @Override
            public void failed(Throwable exc, Object attachment) {

            }
        });

        System.out.println(2);

        //讓程式別結束寫一個死迴圈
        while(true){

        }
    }

實操--AIO 非同步非阻塞客戶端請求連線(沒有意思)

需求

  • 完成非同步非阻塞客戶端請求連線

分析

  • 建立客戶端物件
  • 非同步請求連線伺服器

實現

public class Demo客戶端AIO {
    public static void main(String[] args) throws IOException {
        //客戶端也有AIO物件
        AsynchronousSocketChannel asc = AsynchronousSocketChannel.open();
        System.out.println(1);
        //指定要連線的伺服器的ip和埠
        asc.connect(new InetSocketAddress("123.23.44.3",8000), null, new CompletionHandler<Void, Object>() {
            @Override
            public void completed(Void result, Object attachment) {
                //成功就執行這個方法
                System.out.println(3);
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                //失敗就執行這個方法
                System.out.println(4);

            }
        });
        System.out.println(2);

        //寫一個迴圈讓main方法別結束
        while(true){

        }
    }
}
如果連線成功:
	1
	2
	3
	
如果連線失敗:
	1
	2
	4

總結

- 能夠辨別UDP和TCP協議特點
    UDP: 面向無連線,傳輸資料不安全,速度快
    TCP: 面向連線,傳輸資料安全,速度慢
- 能夠說出TCP協議下兩個常用類名稱
    Socket\ServerSocket
    常用方法
- 能夠編寫TCP協議下字串資料傳輸程式
 - 客戶端實現步驟
  - 建立客戶端Socket物件並指定伺服器地址和埠號
  - 呼叫Socket物件的getOutputStream方法獲得位元組輸出流物件
  - 使用位元組輸出流物件的write方法往伺服器端輸出資料
  - 呼叫Socket物件的getInputStream方法獲得位元組輸入流物件
  - 呼叫位元組輸入流物件的read方法讀取伺服器端返回的資料
  - 關閉Socket物件斷開連線。
- 伺服器實現步驟
  - 建立ServerSocket物件並指定埠號(相當於開啟了一個伺服器)
  - 呼叫ServerSocket物件的accept方法等待客端戶連線並獲得對應Socket物件
  - 呼叫Socket物件的getInputStream方法獲得位元組輸入流物件
  - 呼叫位元組輸入流物件的read方法讀取客戶端傳送的資料
  - 呼叫Socket物件的getOutputStream方法獲得位元組輸出流物件
  - 呼叫位元組輸出流物件的write方法往客戶端輸出資料
  - 關閉Socket和ServerSocket物件

- 能夠理解TCP協議下檔案上傳案例
        1. 【客戶端】輸入流,從硬碟讀取檔案資料到程式中。
        2. 【客戶端】輸出流,寫出檔案資料到服務端。
        3. 【服務端】輸入流,讀取檔案資料到服務端程式。
        4. 【服務端】輸出流,寫出檔案資料到伺服器硬碟中。
        5. 【服務端】獲取輸出流,回寫資料。
        6. 【客戶端】獲取輸入流,解析回寫資料。
    注意: 客戶端上傳檔案的資料後,一定要呼叫shutDownOutput()方法,告訴伺服器不會再寫資料了
- 能夠理解TCP協議下BS案例  瞭解
        1. 準備頁面資料,web資料夾。
		2. 我們模擬伺服器端,ServerSocket類監聽埠,使用瀏覽器訪問,檢視網頁效果

- 能夠說出NIO的優點
     IO: 同步阻塞
     NIO: 同步非阻塞
     NIO2\AIO: 非同步非阻塞