天天看點

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

title: 了解BIO、NIO這一篇就夠了

date: 2021-07-29 00:14:32

此筆記源于B站黑馬BIO、NIO教程

視訊教程

第一章 Java的I/O演進之路

1.1 I/O 模型基本說明

I/O 模型:就是用什麼樣的通道或者說是通信模式和架構進行資料的傳輸和接收,很大程度上決定了程式通信的性能,Java 共支援 3 種網絡程式設計的/IO 模型:BIO、NIO、AIO

實際通信需求下,要根據不同的業務場景和性能需求決定選擇不同的I/O模型

1.2 I/O模型

Java BIO

同步并阻塞(傳統阻塞型),伺服器實作模式為一個連接配接一個線程,即用戶端有連接配接請求時伺服器

端就需要啟動一個線程進行處理,如果這個連接配接不做任何事情會造成不必要的線程開銷 【簡單示意圖

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

Java NIO

Java NIO : 同步非阻塞,伺服器實作模式為一個線程處理多個請求(連接配接),即用戶端發送的連接配接請求都會注

冊到多路複用器上,多路複用器輪詢到連接配接有 I/O 請求就進行處理 【簡單示意圖】

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

Java AIO

Java AIO(NIO.2) : 異步 異步非阻塞,伺服器實作模式為一個有效請求一個線程,用戶端的I/O請求都是由OS先完成了再通知伺服器應用去啟動線程進行處理,一般适用于連接配接數較

多且連接配接時間較長的應用

1.3 BIO、NIO、AIO 适用場景分析

1、BIO 方式适用于連接配接數目比較小且固定的架構,這種方式對伺服器資源要求比較高,并發局限于應用中,JDK1.4以前的唯一選擇,但程式簡單易了解。

2、NIO 方式适用于連接配接數目多且連接配接比較短(輕操作)的架構,比如聊天伺服器,彈幕系統,伺服器間通訊等。

程式設計比較複雜,JDK1.4 開始支援。

3、AIO 方式使用于連接配接數目多且連接配接比較長(重操作)的架構,比如相冊伺服器,充分調用 OS 參與并發操作,

程式設計比較複雜,JDK7 開始支援。

第二章 JAVA BIO深入剖析

2.1 Java BIO 基本介紹

  • Java BIO 就是傳統的 java io 程式設計,其相關的類和接口在 java.io
  • BIO(blocking I/O) : 同步阻塞,伺服器實作模式為一個連接配接一個線程,即用戶端有連接配接請求時伺服器端就需

    要啟動一個線程進行處理,如果這個連接配接不做任何事情會造成不必要的線程開銷,可以通過線程池機制改善(實作多個客戶連接配接伺服器).

2.2 Java BIO 工作機制

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

對 對 BIO 程式設計流程的梳理

  1. 伺服器端啟動一個 ServerSocket,注冊端口,調用accpet方法監聽用戶端的Socket連接配接。
  2. 用戶端啟動 Socket 對伺服器進行通信,預設情況下伺服器端需要對每個客戶 建立一個線程與之通訊

2.3 傳統的BIO程式設計執行個體回顧

​ 網絡程式設計的基本模型是Client/Server模型,也就是兩個程序之間進行互相通信,其中服務端提供位置信(綁定IP位址和端口),用戶端通過連接配接操作向服務端監聽的端口位址發起連接配接請求,基于TCP協定下進行三次握手連接配接,連接配接成功後,雙方通過網絡套接字(Socket)進行通信。

​ 傳統的同步阻塞模型開發中,服務端ServerSocket負責綁定IP位址,啟動監聽端口;用戶端Socket負責發起連接配接操作。連接配接成功後,雙方通過輸入和輸出流進行同步阻塞式通信。

​ 基于BIO模式下的通信,用戶端 - 服務端是完全同步,完全耦合的。

用戶端案例如下

package com.itheima._02bio01;

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
/**
    目标: Socket網絡程式設計。

    Java提供了一個包:java.net下的類都是用于網絡通信。
    Java提供了基于套接字(端口)Socket的網絡通信模式,我們基于這種模式就可以直接實作TCP通信。
    隻要用Socket通信,那麼就是基于TCP可靠傳輸通信。

    功能1:用戶端發送一個消息,服務端接口一個消息,通信結束!!

    建立用戶端對象:
        (1)建立一個Socket的通信管道,請求與服務端的端口連接配接。
        (2)從Socket管道中得到一個位元組輸出流。
        (3)把位元組流改裝成自己需要的流進行資料的發送
    建立服務端對象:
        (1)注冊端口
        (2)開始等待接收用戶端的連接配接,得到一個端到端的Socket管道
        (3)從Socket管道中得到一個位元組輸入流。
        (4)把位元組輸入流包裝成自己需要的流進行資料的讀取。

    Socket的使用:
        構造器:public Socket(String host, int port)
        方法:  public OutputStream getOutputStream():擷取位元組輸出流
               public InputStream getInputStream() :擷取位元組輸入流

    ServerSocket的使用:
        構造器:public ServerSocket(int port)

    小結:
        通信是很嚴格的,對方怎麼發你就怎麼收,對方發多少你就隻能收多少!!

 */
public class ClientDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("==用戶端的啟動==");
        // (1)建立一個Socket的通信管道,請求與服務端的端口連接配接。
        Socket socket = new Socket("127.0.0.1",8888);
        // (2)從Socket通信管道中得到一個位元組輸出流。
        OutputStream os = socket.getOutputStream();
        // (3)把位元組流改裝成自己需要的流進行資料的發送
        PrintStream ps = new PrintStream(os);
        // (4)開始發送消息
        ps.println("我是用戶端,我想約你吃小龍蝦!!!");
        ps.flush();
    }
}
           

服務端案例如下

package com.itheima._02bio01;

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

/**
 * 服務端
 */
public class ServerDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("==伺服器的啟動==");
        // (1)注冊端口
        ServerSocket serverSocket = new ServerSocket(8888);
        //(2)開始在這裡暫停等待接收用戶端的連接配接,得到一個端到端的Socket管道
        Socket socket = serverSocket.accept();
        //(3)從Socket管道中得到一個位元組輸入流。
        InputStream is = socket.getInputStream();
        //(4)把位元組輸入流包裝成自己需要的流進行資料的讀取。
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        //(5)讀取資料
        String line ;
        while((line = br.readLine())!=null){
            System.out.println("服務端收到:"+line);
        }
    }
}
           

小結

  • 在以上通信中,服務端會一緻等待用戶端的消息,如果用戶端沒有進行消息的發送,服務端将一直進入阻塞狀态。
  • 同時服務端是按照行擷取消息的,這意味着用戶端也必須按照行進行消息的發送,否則服務端将進入等待消息的阻塞狀态!

2.4 BIO模式下多發和多收消息

​ 在1.3的案例中,隻能實作用戶端發送消息,服務端接收消息,并不能實作反複的收消息和反複的發消息,我們隻需要在用戶端案例中,加上反複按照行發送消息的邏輯即可!案例代碼如下:

用戶端代碼如下

package com.itheima._03bio02;

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

/**
    目标: Socket網絡程式設計。

    功能1:用戶端可以反複發消息,服務端可以反複收消息

    小結:
        通信是很嚴格的,對方怎麼發你就怎麼收,對方發多少你就隻能收多少!!

 */
public class ClientDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("==用戶端的啟動==");
        // (1)建立一個Socket的通信管道,請求與服務端的端口連接配接。
        Socket socket = new Socket("127.0.0.1",8888);
        // (2)從Socket通信管道中得到一個位元組輸出流。
        OutputStream os = socket.getOutputStream();
        // (3)把位元組流改裝成自己需要的流進行資料的發送
        PrintStream ps = new PrintStream(os);
        // (4)開始發送消息
        Scanner sc = new Scanner(System.in);
        while(true){
            System.out.print("請說:");
            String msg = sc.nextLine();
            ps.println(msg);
            ps.flush();
        }
    }
}
           

服務端代碼如下

package com.itheima._03bio02;

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

/**
 * 服務端
 */
public class ServerDemo {
    public static void main(String[] args) throws Exception {
        String s = "886";
        System.out.println("886".equals(s));
        System.out.println("==伺服器的啟動==");
        //(1)注冊端口
        ServerSocket serverSocket = new ServerSocket(8888);
        //(2)開始在這裡暫停等待接收用戶端的連接配接,得到一個端到端的Socket管道
        Socket socket = serverSocket.accept();
        //(3)從Socket管道中得到一個位元組輸入流。
        InputStream is = socket.getInputStream();
        //(4)把位元組輸入流包裝成  自己需要的流進行資料的讀取。
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        //(5)讀取資料
        String line ;
        while((line = br.readLine())!=null){
            System.out.println("服務端收到:"+line);
        }
    }
}
           

小結

  • 本案例中确實可以實作用戶端多發多收
  • 但是服務端隻能處理一個用戶端的請求,因為服務端是單線程的。一次隻能與一個用戶端進行消息通信。

2.5 BIO模式下接收多個用戶端

概述

​ 在上述的案例中,一個服務端隻能接收一個用戶端的通信請求,那麼如果服務端需要處理很多個用戶端的消息通信請求應該如何處理呢,此時我們就需要在服務端引入線程了,也就是說用戶端每發起一個請求,服務端就建立一個新的線程來處理這個用戶端的請求,這樣就實作了一個用戶端一個線程的模型,圖解模式如下:

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

用戶端案例代碼如下

/**
    目标: Socket網絡程式設計。

    功能1:用戶端可以反複發,一個服務端可以接收無數個用戶端的消息!!

    小結:
         伺服器如果想要接收多個用戶端,那麼必須引入線程,一個用戶端一個線程處理!!

 */
public class ClientDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("==用戶端的啟動==");
        // (1)建立一個Socket的通信管道,請求與服務端的端口連接配接。
        Socket socket = new Socket("127.0.0.1",7777);
        // (2)從Socket通信管道中得到一個位元組輸出流。
        OutputStream os = socket.getOutputStream();
        // (3)把位元組流改裝成自己需要的流進行資料的發送
        PrintStream ps = new PrintStream(os);
        // (4)開始發送消息
        Scanner sc = new Scanner(System.in);
        while(true){
            System.out.print("請說:");
            String msg = sc.nextLine();
            ps.println(msg);
            ps.flush();
        }
    }
}
           

服務端案例代碼如下

/**
    服務端
 */
public class ServerDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("==伺服器的啟動==");
        // (1)注冊端口
        ServerSocket serverSocket = new ServerSocket(7777);
        while(true){
            //(2)開始在這裡暫停等待接收用戶端的連接配接,得到一個端到端的Socket管道
            Socket socket = serverSocket.accept();
            new ServerReadThread(socket).start();
            System.out.println(socket.getRemoteSocketAddress()+"上線了!");
        }
    }
}

class ServerReadThread extends Thread{
    private Socket socket;

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

    @Override
    public void run() {
        try{
            //(3)從Socket管道中得到一個位元組輸入流。
            InputStream is = socket.getInputStream();
            //(4)把位元組輸入流包裝成自己需要的流進行資料的讀取。
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            //(5)讀取資料
            String line ;
            while((line = br.readLine())!=null){
                System.out.println("服務端收到:"+socket.getRemoteSocketAddress()+":"+line);
            }
        }catch (Exception e){
            System.out.println(socket.getRemoteSocketAddress()+"下線了!");
        }
    }
}
           

小結

  • 1.每個Socket接收到,都會建立一個線程,線程的競争、切換上下文影響性能;
  • 2.每個線程都會占用棧空間和CPU資源;
  • 3.并不是每個socket都進行IO操作,無意義的線程處理;
  • 4.用戶端的并發通路增加時。服務端将呈現1:1的線程開銷,通路量越大,系統将發生線程棧溢出,線程建立失敗,最終導緻程序當機或者僵死,進而不能對外提供服務。

2.6 僞異步I/O程式設計

概述

​ 在上述案例中:用戶端的并發通路增加時。服務端将呈現1:1的線程開銷,通路量越大,系統将發生線程棧溢出,線程建立失敗,最終導緻程序當機或者僵死,進而不能對外提供服務。

​ 接下來我們采用一個僞異步I/O的通信架構,采用線程池和任務隊列實作,當用戶端接入時,将用戶端的Socket封裝成一個Task(該任務實作java.lang.Runnable線程任務接口)交給後端的線程池中進行處理。JDK的線程池維護一個消息隊列和N個活躍的線程,對消息隊列中Socket任務進行處理,由于線程池可以設定消息隊列的大小和最大線程數,是以,它的資源占用是可控的,無論多少個用戶端并發通路,都不會導緻資源的耗盡和當機。

​ 圖示如下:

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

用戶端源碼分析

public class Client {
   public static void main(String[] args) {
      try {
         // 1.履歷一個與服務端的Socket對象:套接字
         Socket socket = new Socket("127.0.0.1", 9999);
         // 2.從socket管道中擷取一個輸出流,寫資料給服務端 
         OutputStream os = socket.getOutputStream() ;
         // 3.把輸出流包裝成一個列印流 
         PrintWriter pw = new PrintWriter(os);
         // 4.反複接收使用者的輸入 
         BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
         String line = null ;
         while((line = br.readLine()) != null){
            pw.println(line);
            pw.flush();
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}
           

線程池處理類

// 線程池處理類
public class HandlerSocketThreadPool {
   
   // 線程池 
   private ExecutorService executor;
   
   public HandlerSocketThreadPool(int maxPoolSize, int queueSize){
      
      this.executor = new ThreadPoolExecutor(
            3, // 8
            maxPoolSize,  
            120L, 
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(queueSize) );
   }
   
   public void execute(Runnable task){
      this.executor.execute(task);
   }
}
           

服務端源碼分析

public class Server {
   public static void main(String[] args) {
      try {
         System.out.println("----------服務端啟動成功------------");
         ServerSocket ss = new ServerSocket(9999);

         // 一個服務端隻需要對應一個線程池
         HandlerSocketThreadPool handlerSocketThreadPool =
               new HandlerSocketThreadPool(3, 1000);

         // 用戶端可能有很多個
         while(true){
            Socket socket = ss.accept() ; // 阻塞式的!
            System.out.println("有人上線了!!");
            // 每次收到一個用戶端的socket請求,都需要為這個用戶端配置設定一個
            // 獨立的線程 專門負責對這個用戶端的通信!!
            handlerSocketThreadPool.execute(new ReaderClientRunnable(socket));
         }

      } catch (Exception e) {
         e.printStackTrace();
      }
   }

}
           
class ReaderClientRunnable implements Runnable{

   private Socket socket ;

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

   @Override
   public void run() {
      try {
         // 讀取一行資料
         InputStream is = socket.getInputStream() ;
         // 轉成一個緩沖字元流
         Reader fr = new InputStreamReader(is);
         BufferedReader br = new BufferedReader(fr);
         // 一行一行的讀取資料
         String line = null ;
         while((line = br.readLine())!=null){ // 阻塞式的!!
            System.out.println("服務端收到了資料:"+line);
         }
      } catch (Exception e) {
         System.out.println("有人下線了");
      }

   }
}
           

小結

  • 僞異步io采用了線程池實作,是以避免了為每個請求建立一個獨立線程造成線程資源耗盡的問題,但由于底層依然是采用的同步阻塞模型,是以無法從根本上解決問題。
  • 如果單個消息處理的緩慢,或者伺服器線程池中的全部線程都被阻塞,那麼後續socket的i/o消息都将在隊列中排隊。新的Socket請求将被拒絕,用戶端會發生大量連接配接逾時。

2.7 基于BIO形式下的檔案上傳

目标

支援任意類型檔案形式的上傳。

用戶端開發

package com.itheima.file;

import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.Socket;

/**
    目标:實作用戶端上傳任意類型的檔案資料給服務端儲存起來。

 */
public class Client {
    public static void main(String[] args) {
        try(
                InputStream is = new FileInputStream("C:\\Users\\dlei\\Desktop\\BIO,NIO,AIO\\檔案\\java.png");
        ){
            //  1、請求與服務端的Socket連結
            Socket socket = new Socket("127.0.0.1" , 8888);
            //  2、把位元組輸出流包裝成一個資料輸出流
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            //  3、先發送上傳檔案的字尾給服務端
            dos.writeUTF(".png");
            //  4、把檔案資料發送給服務端進行接收
            byte[] buffer = new byte[1024];
            int len;
            while((len = is.read(buffer)) > 0 ){
                dos.write(buffer , 0 , len);
            }
            dos.flush();
            Thread.sleep(10000);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
           

服務端開發

package com.itheima.file;

import java.net.ServerSocket;
import java.net.Socket;

/**
    目标:服務端開發,可以實作接收用戶端的任意類型檔案,并儲存到服務端磁盤。
 */
public class Server {
    public static void main(String[] args) {
        try{
            ServerSocket ss = new ServerSocket(8888);
            while (true){
                Socket socket = ss.accept();
                // 交給一個獨立的線程來處理與這個用戶端的檔案通信需求。
                new ServerReaderThread(socket).start();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
           
package com.itheima.file;

import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.UUID;

public class ServerReaderThread extends Thread {
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try{
            // 1、得到一個資料輸入流讀取用戶端發送過來的資料
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            // 2、讀取用戶端發送過來的檔案類型
            String suffix = dis.readUTF();
            System.out.println("服務端已經成功接收到了檔案類型:" + suffix);
            // 3、定義一個位元組輸出管道負責把用戶端發來的檔案資料寫出去
            OutputStream os = new FileOutputStream("C:\\Users\\dlei\\Desktop\\BIO,NIO,AIO\\檔案\\server\\"+
                    UUID.randomUUID().toString()+suffix);
            // 4、從資料輸入流中讀取檔案資料,寫出到位元組輸出流中去
            byte[] buffer = new byte[1024];
            int len;
            while((len = dis.read(buffer)) > 0){
                os.write(buffer,0, len);
            }
            os.close();
            System.out.println("服務端接收檔案儲存成功!");

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
           

小結

用戶端怎麼發,服務端就怎麼接收

2.8 Java BIO模式下的端口轉發思想

需求:需要實作一個用戶端的消息可以發送給所有的用戶端去接收。(群聊實作)

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

用戶端開發

package com.itheima.file;

import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.Socket;

/**
    目标:實作用戶端上傳任意類型的檔案資料給服務端儲存起來。

 */
public class Client {
    public static void main(String[] args) {
        try(
                InputStream is = new FileInputStream("C:\\Users\\dlei\\Desktop\\BIO,NIO,AIO\\檔案\\java.png");
        ){
            //  1、請求與服務端的Socket連結
            Socket socket = new Socket("127.0.0.1" , 8888);
            //  2、把位元組輸出流包裝成一個資料輸出流
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            //  3、先發送上傳檔案的字尾給服務端
            dos.writeUTF(".png");
            //  4、把檔案資料發送給服務端進行接收
            byte[] buffer = new byte[1024];
            int len;
            while((len = is.read(buffer)) > 0 ){
                dos.write(buffer , 0 , len);
            }
            dos.flush();
            Thread.sleep(10000);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
           

服務端實作

小結

2.9 基于BIO模式下即時通信

基于BIO模式下的即時通信,我們需要解決用戶端到用戶端的通信,也就是需要實作用戶端與用戶端的端口消息轉發邏輯。

項目功能示範

項目案例說明

本項目案例為即時通信的軟體項目,适合基礎加強的大案例,具備綜合性。學習本項目案例至少需要具備如下Java SE技術點:

    1. Java 面向對象設計,文法設計。
    1. 多線程技術。
    1. IO流技術。
    1. 網絡通信相關技術。
    1. 集合架構。
    1. 項目開發思維。
    1. Java 常用 api 使用。

​ …

功能清單簡單說明:

1.用戶端登陸功能

  • 可以啟動用戶端進行登入,用戶端登陸隻需要輸入使用者名和服務端ip位址即可。

2.線上人數實時更新。

  • 用戶端使用者戶登陸以後,需要同步更新所有用戶端的聯系人資訊欄。

3.離線人數更新

  • 檢測到有用戶端下線後,需要同步更新所有用戶端的聯系人資訊欄。

4.群聊

  • 任意一個用戶端的消息,可以推送給目前所有用戶端接收。

5.私聊

  • 可以選擇某個員工,點選私聊按鈕,然後發出的消息可以被該用戶端單獨接收。

[email protected]消息

  • 可以選擇某個員工,然後發出的消息可以@該使用者,但是其他所有人都能

7.消息使用者和消息時間點

  • 服務端可以實時記錄該使用者的消息時間點,然後進行消息的多路轉發或者選擇。

項目啟動與示範

項目代碼結構示範。

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

項目啟動步驟:

  • 1.首先需要啟動服務端,點選ServerChat類直接右鍵啟動,顯示服務端啟動成功!
  • 2.其次,點選用戶端類ClientChat類,在彈出的方框中輸入服務端的ip和目前用戶端的昵稱
    了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結
  • 3.登陸進入後的聊天界面如下,即可進行相關操作。
    • 如果直接點選發送,預設發送群聊消息
  • 如果選中右側線上清單某個使用者,預設發送@消息
    • 如果選中右側線上清單某個使用者,然後選擇右下側私聊按鈕默,認發送私聊消息。
    了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結
    了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

技術選型分析

本項目案例涉及到Java基礎加強的案例,具體涉及到的技術點如下:

    1. Java 面向對象設計,文法設計。
    1. 多線程技術。
    1. IO流技術。
    1. 網絡通信相關技術。
    1. 集合架構。
    1. 項目開發思維。
    1. Java 常用 api 使用。

服務端設計

服務端接收多個用戶端邏輯

目标

服務端需要接收多個用戶端的接入。

實作步驟
  • 1.服務端需要接收多個用戶端,目前我們采取的政策是一個用戶端對應一個服務端線程。
  • 2.服務端除了要注冊端口以外,還需要為每個用戶端配置設定一個獨立線程處理與之通信。
代碼實作
  • 服務端主體代碼,主要進行端口注冊,和接收用戶端,配置設定線程處理該用戶端請求
public class ServerChat {
    
    /** 定義一個集合存放所有線上的socket  */
	public static Map<Socket, String> onLineSockets = new HashMap<>();

   public static void main(String[] args) {
      try {
         /** 1.注冊端口   */
         ServerSocket serverSocket = new ServerSocket(Constants.PORT);

         /** 2.循環一直等待所有可能的用戶端連接配接 */
         while(true){
            Socket socket = serverSocket.accept();
            /**3. 把用戶端的socket管道單獨配置一個線程來處理 */
            new ServerReader(socket).start();
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}
           
  • 服務端配置設定的獨立線程類負責處理該用戶端Socket的管道請求。
class ServerReader extends Thread {
   private Socket socket;
   public ServerReader(Socket socket) {
      this.socket = socket;
   }
   @Override
   public void run() {
      try {
       
      } catch (Exception e) {
            e.printStackTrace();
      }
   }
}
           

常量包負責做端口配置

public class Constants {
   /** 常量 */
   public static final int PORT = 7778 ;

}
           
小結

​ 本節實作了服務端可以接收多個用戶端請求。

服務端接收登陸消息以及監測離線

目标

在上節我們實作了服務端可以接收多個用戶端,然後服務端可以接收多個用戶端連接配接,接下來我們要接收用戶端的登陸消息。

實作步驟
  • 需要在服務端處理用戶端的線程的登陸消息。
  • 需要注意的是,服務端需要接收用戶端的消息可能有很多種。
    • 分别是登陸消息,群聊消息,私聊消息 和@消息。
    • 這裡需要約定如果用戶端發送消息之前需要先發送消息的類型,類型我們使用信号值标志(1,2,3)。
      • 1代表接收的是登陸消息
      • 2代表群發| @消息
      • 3代表了私聊消息
  • 服務端的線程中有異常校驗機制,一旦發現用戶端下線會在異常機制中處理,然後移除目前用戶端使用者,把最新的使用者清單發回給全部用戶端進行線上人數更新。
代碼實作
public class ServerReader extends Thread {
	private Socket socket;
	public ServerReader(Socket socket) {
		this.socket = socket;
	}

	@Override
	public void run() {
		DataInputStream dis = null;
		try {
			dis = new DataInputStream(socket.getInputStream());
			/** 1.循環一直等待用戶端的消息 */
			while(true){
				/** 2.讀取目前的消息類型 :登入,群發,私聊 , @消息 */
				int flag = dis.readInt();
				if(flag == 1){
					/** 先将目前登入的用戶端socket存到線上人數的socket集合中   */
					String name = dis.readUTF() ;
					System.out.println(name+"---->"+socket.getRemoteSocketAddress());
					ServerChat.onLineSockets.put(socket, name);
				}
				writeMsg(flag,dis);
			}
		} catch (Exception e) {
			System.out.println("--有人下線了--");
			// 從線上人數中将目前socket移出去  
			ServerChat.onLineSockets.remove(socket);
			try {
				// 從新更新線上人數并發給所有用戶端 
				writeMsg(1,dis);
			} catch (Exception e1) {
				e1.printStackTrace();
			}
		}

	}

	private void writeMsg(int flag, DataInputStream dis) throws Exception {
        // DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); 
		// 定義一個變量存放最終的消息形式 
		String msg = null ;
		if(flag == 1){
			/** 讀取所有線上人數發給所有用戶端去更新自己的線上人數清單 */
			/** onlineNames = [波仔,zhangsan,波妞]*/
			StringBuilder rs = new StringBuilder();
			Collection<String> onlineNames = ServerChat.onLineSockets.values();
			// 判斷是否存在線上人數 
			if(onlineNames != null && onlineNames.size() > 0){
				for(String name : onlineNames){
					rs.append(name+ Constants.SPILIT);
				}
				// 波仔003197♣♣㏘♣④④♣zhangsan003197♣♣㏘♣④④♣波妞003197♣♣㏘♣④④♣
				// 去掉最後的一個分隔符 
				msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));

				/** 将消息發送給所有的用戶端 */
				sendMsgToAll(flag,msg);
			}
		}else if(flag == 2 || flag == 3){
			
			}
		}
	}
	
	private void sendMsgToAll(int flag, String msg) throws Exception {
		// 拿到所有的線上socket管道 給這些管道寫出消息
		Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
		for(Socket sk :  allOnLineSockets){
			DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
			dos.writeInt(flag); // 消息類型
			dos.writeUTF(msg);
			dos.flush();
		}
	}
}
           
小結
  • 此處實作了接收用戶端的登陸消息,然後提取目前線上的全部的使用者名稱和目前登陸的使用者名稱發送給全部線上使用者更新自己的線上人數清單。

服務端接收群聊消息

目标

在上節實作了接收用戶端的登陸消息,然後提取目前線上的全部的使用者名稱和目前登陸的使用者名稱發送給全部線上使用者更新自己的線上人數清單。接下來要接收用戶端發來的群聊消息推送給目前線上的所有用戶端

實作步驟
  • 接下來要接收用戶端發來的群聊消息。
  • 需要注意的是,服務端需要接收用戶端的消息可能有很多種。
    • 分别是登陸消息,群聊消息,私聊消息 和@消息。
    • 這裡需要約定如果用戶端發送消息之前需要先發送消息的類型,類型我們使用信号值标志(1,2,3)。
      • 1代表接收的是登陸消息
      • 2代表群發| @消息
      • 3代表了私聊消息
代碼實作
public class ServerReader extends Thread {
	private Socket socket;
	public ServerReader(Socket socket) {
		this.socket = socket;
	}

	@Override
	public void run() {
		DataInputStream dis = null;
		try {
			dis = new DataInputStream(socket.getInputStream());
			/** 1.循環一直等待用戶端的消息 */
			while(true){
				/** 2.讀取目前的消息類型 :登入,群發,私聊 , @消息 */
				int flag = dis.readInt();
				if(flag == 1){
					/** 先将目前登入的用戶端socket存到線上人數的socket集合中   */
					String name = dis.readUTF() ;
					System.out.println(name+"---->"+socket.getRemoteSocketAddress());
					ServerChat.onLineSockets.put(socket, name);
				}
				writeMsg(flag,dis);
			}
		} catch (Exception e) {
			System.out.println("--有人下線了--");
			// 從線上人數中将目前socket移出去  
			ServerChat.onLineSockets.remove(socket);
			try {
				// 從新更新線上人數并發給所有用戶端 
				writeMsg(1,dis);
			} catch (Exception e1) {
				e1.printStackTrace();
			}
		}

	}

	private void writeMsg(int flag, DataInputStream dis) throws Exception {
        // DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); 
		// 定義一個變量存放最終的消息形式 
		String msg = null ;
		if(flag == 1){
			/** 讀取所有線上人數發給所有用戶端去更新自己的線上人數清單 */
			/** onlineNames = [波仔,zhangsan,波妞]*/
			StringBuilder rs = new StringBuilder();
			Collection<String> onlineNames = ServerChat.onLineSockets.values();
			// 判斷是否存在線上人數 
			if(onlineNames != null && onlineNames.size() > 0){
				for(String name : onlineNames){
					rs.append(name+ Constants.SPILIT);
				}
				// 波仔003197♣♣㏘♣④④♣zhangsan003197♣♣㏘♣④④♣波妞003197♣♣㏘♣④④♣
				// 去掉最後的一個分隔符 
				msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));

				/** 将消息發送給所有的用戶端 */
				sendMsgToAll(flag,msg);
			}
		}else if(flag == 2 || flag == 3){
			// 讀到消息  群發的 或者 @消息
			String newMsg = dis.readUTF() ; // 消息
			// 得到發件人 
			String sendName = ServerChat.onLineSockets.get(socket);
	
			// 内容
			StringBuilder msgFinal = new StringBuilder();
			// 時間  
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE");
			if(flag == 2){
				msgFinal.append(sendName).append("  ").append(sdf.format(System.currentTimeMillis())).append("\r\n");
				msgFinal.append("    ").append(newMsg).append("\r\n");
				sendMsgToAll(flag,msgFinal.toString());
			}else if(flag == 3){
	
			}
		}
	}
	

	private void sendMsgToAll(int flag, String msg) throws Exception {
		// 拿到所有的線上socket管道 給這些管道寫出消息
		Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
		for(Socket sk :  allOnLineSockets){
			DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
			dos.writeInt(flag); // 消息類型
			dos.writeUTF(msg);
			dos.flush();
		}
	}
}
           
小結
  • 此處根據消息的類型判斷為群聊消息,然後把群聊消息推送給目前線上的所有用戶端。

服務端接收私聊消息

目标

在上節我們接收了用戶端發來的群聊消息推送給目前線上的所有用戶端,接下來要解決私聊消息的推送邏輯

實作步驟
  • 解決私聊消息的推送邏輯,私聊消息需要知道推送給某個具體的用戶端
  • 我們可以接收到用戶端發來的私聊使用者名稱,根據使用者名稱定位該使用者的Socket管道,然後單獨推送消息給該Socket管道。
  • 需要注意的是,服務端需要接收用戶端的消息可能有很多種。
    • 分别是登陸消息,群聊消息,私聊消息 和@消息。
    • 這裡需要約定如果用戶端發送消息之前需要先發送消息的類型,類型我們使用信号值标志(1,2,3)。
      • 1代表接收的是登陸消息
      • 2代表群發| @消息
      • 3代表了私聊消息
代碼實作
public class ServerReader extends Thread {
	private Socket socket;
	public ServerReader(Socket socket) {
		this.socket = socket;
	}

	@Override
	public void run() {
		DataInputStream dis = null;
		try {
			dis = new DataInputStream(socket.getInputStream());
			/** 1.循環一直等待用戶端的消息 */
			while(true){
				/** 2.讀取目前的消息類型 :登入,群發,私聊 , @消息 */
				int flag = dis.readInt();
				if(flag == 1){
					/** 先将目前登入的用戶端socket存到線上人數的socket集合中   */
					String name = dis.readUTF() ;
					System.out.println(name+"---->"+socket.getRemoteSocketAddress());
					ServerChat.onLineSockets.put(socket, name);
				}
				writeMsg(flag,dis);
			}
		} catch (Exception e) {
			System.out.println("--有人下線了--");
			// 從線上人數中将目前socket移出去  
			ServerChat.onLineSockets.remove(socket);
			try {
				// 從新更新線上人數并發給所有用戶端 
				writeMsg(1,dis);
			} catch (Exception e1) {
				e1.printStackTrace();
			}
		}

	}

	private void writeMsg(int flag, DataInputStream dis) throws Exception {
        // DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); 
		// 定義一個變量存放最終的消息形式 
		String msg = null ;
		if(flag == 1){
			/** 讀取所有線上人數發給所有用戶端去更新自己的線上人數清單 */
			/** onlineNames = [波仔,zhangsan,波妞]*/
			StringBuilder rs = new StringBuilder();
			Collection<String> onlineNames = ServerChat.onLineSockets.values();
			// 判斷是否存在線上人數 
			if(onlineNames != null && onlineNames.size() > 0){
				for(String name : onlineNames){
					rs.append(name+ Constants.SPILIT);
				}
				// 波仔003197♣♣㏘♣④④♣zhangsan003197♣♣㏘♣④④♣波妞003197♣♣㏘♣④④♣
				// 去掉最後的一個分隔符 
				msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));

				/** 将消息發送給所有的用戶端 */
				sendMsgToAll(flag,msg);
			}
		}else if(flag == 2 || flag == 3){
			// 讀到消息  群發的 或者 @消息
			String newMsg = dis.readUTF() ; // 消息
			// 得到發件人 
			String sendName = ServerChat.onLineSockets.get(socket);
	
			// 内容
			StringBuilder msgFinal = new StringBuilder();
			// 時間  
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE");
			if(flag == 2){
				msgFinal.append(sendName).append("  ").append(sdf.format(System.currentTimeMillis())).append("\r\n");
				msgFinal.append("    ").append(newMsg).append("\r\n");
				sendMsgToAll(flag,msgFinal.toString());
			}else if(flag == 3){
			msgFinal.append(sendName).append("  ").append(sdf.format(System.currentTimeMillis())).append("對您私發\r\n");
				msgFinal.append("    ").append(newMsg).append("\r\n");
				// 私發 
				// 得到給誰私發 
				String destName = dis.readUTF();
				sendMsgToOne(destName,msgFinal.toString());
			}
		}
	}
	/**
	 * @param destName 對誰私發 
	 * @param msg 發的消息内容 
	 * @throws Exception
	 */
	private void sendMsgToOne(String destName, String msg) throws Exception {
		// 拿到所有的線上socket管道 給這些管道寫出消息
		Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
		for(Socket sk :  allOnLineSockets){
			// 得到目前需要私發的socket 
			// 隻對這個名字對應的socket私發消息
			if(ServerChat.onLineSockets.get(sk).trim().equals(destName)){
				DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
				dos.writeInt(2); // 消息類型
				dos.writeUTF(msg);
				dos.flush();
			}
		}

	}
	

	private void sendMsgToAll(int flag, String msg) throws Exception {
		// 拿到所有的線上socket管道 給這些管道寫出消息
		Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
		for(Socket sk :  allOnLineSockets){
			DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
			dos.writeInt(flag); // 消息類型
			dos.writeUTF(msg);
			dos.flush();
		}
	}
}
           
小結
  • 本節我們解決了私聊消息的推送邏輯,私聊消息需要知道推送給某個具體的用戶端Socket管道
  • 我們可以接收到用戶端發來的私聊使用者名稱,根據使用者名稱定位該使用者的Socket管道,然後單獨推送消息給該Socket管道。

用戶端設計

啟動用戶端界面 ,登陸,重新整理線上

目标

啟動用戶端界面,登陸,重新整理線上人數清單

實作步驟
  • 用戶端界面主要是GUI設計,主體頁面分為登陸界面和聊天視窗,以及線上使用者清單。
  • GUI界面讀者可以自行複制使用。
  • 登陸輸入服務端ip和使用者名後,要請求與服務端的登陸,然後立即為目前用戶端配置設定一個讀線程處理用戶端的讀資料消息。因為用戶端可能随時會接收到服務端那邊轉發過來的各種即時消息資訊。
  • 用戶端登陸完成,服務端收到登陸的使用者名後,會立即發來最新的使用者清單給用戶端更新。
代碼實作

用戶端主體代碼:

public class ClientChat implements ActionListener {
   /** 1.設計界面  */
   private JFrame win = new JFrame();
   /** 2.消息内容架構 */
   public JTextArea smsContent =new JTextArea(23 , 50);
   /** 3.發送消息的框  */
   private JTextArea smsSend = new JTextArea(4,40);
   /** 4.線上人數的區域  */
   /** 存放人的資料 */
   /** 展示線上人數的視窗 */
   public JList<String> onLineUsers = new JList<>();

   // 是否私聊按鈕
   private JCheckBox isPrivateBn = new JCheckBox("私聊");
   // 消息按鈕
   private JButton sendBn  = new JButton("發送");

   // 登入界面
   private JFrame loginView;

   private JTextField ipEt , nameEt , idEt;

   private Socket socket ;

   public static void main(String[] args) {
      new ClientChat().initView();

   }

   private void initView() {
      /** 初始化聊天視窗的界面 */
      win.setSize(650, 600);

      /** 展示登入界面  */
      displayLoginView();

      /** 展示聊天界面 */
      //displayChatView();

   }

   private void displayChatView() {

      JPanel bottomPanel = new JPanel(new BorderLayout());
      //-----------------------------------------------
      // 将消息框和按鈕 添加到視窗的底端
      win.add(bottomPanel, BorderLayout.SOUTH);
      bottomPanel.add(smsSend);
      JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
      btns.add(sendBn);
      btns.add(isPrivateBn);
      bottomPanel.add(btns, BorderLayout.EAST);
      //-----------------------------------------------
      // 給發送消息按鈕綁定點選事件監聽器
      // 将展示消息區centerPanel添加到視窗的中間
      smsContent.setBackground(new Color(0xdd,0xdd,0xdd));
      // 讓展示消息區可以滾動。
      win.add(new JScrollPane(smsContent), BorderLayout.CENTER);
      smsContent.setEditable(false);
      //-----------------------------------------------
      // 使用者清單和是否私聊放到視窗的最右邊
      Box rightBox = new Box(BoxLayout.Y_AXIS);
      onLineUsers.setFixedCellWidth(120);
      onLineUsers.setVisibleRowCount(13);
      rightBox.add(new JScrollPane(onLineUsers));
      win.add(rightBox, BorderLayout.EAST);
      //-----------------------------------------------
      // 關閉視窗退出目前程式
      win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      win.pack();  // swing 加上這句 就可以擁有關閉視窗的功能
      /** 設定視窗居中,顯示出來  */
      setWindowCenter(win,650,600,true);
      // 發送按鈕綁定點選事件
      sendBn.addActionListener(this);
   }

   private void displayLoginView(){

      /** 先讓使用者進行登入
       *  服務端ip
       *  使用者名
       *  id
       *  */
      /** 顯示一個qq的登入框     */
      loginView = new JFrame("登入");
      loginView.setLayout(new GridLayout(3, 1));
      loginView.setSize(400, 230);

      JPanel ip = new JPanel();
      JLabel label = new JLabel("   IP:");
      ip.add(label);
      ipEt = new JTextField(20);
      ip.add(ipEt);
      loginView.add(ip);

      JPanel name = new JPanel();
      JLabel label1 = new JLabel("姓名:");
      name.add(label1);
      nameEt = new JTextField(20);
      name.add(nameEt);
      loginView.add(name);

      JPanel btnView = new JPanel();
      JButton login = new JButton("登陸");
      btnView.add(login);
      JButton cancle = new JButton("取消");
      btnView.add(cancle);
      loginView.add(btnView);
      // 關閉視窗退出目前程式
      loginView.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setWindowCenter(loginView,400,260,true);

      /** 給登入和取消綁定點選事件 */
      login.addActionListener(this);
      cancle.addActionListener(this);

   }

   private static void setWindowCenter(JFrame frame, int width , int height, boolean flag) {
      /** 得到所在系統所在螢幕的寬高 */
      Dimension ds = frame.getToolkit().getScreenSize();

      /** 拿到電腦的寬 */
      int width1 = ds.width;
      /** 高 */
      int height1 = ds.height ;

      System.out.println(width1 +"*" + height1);
      /** 設定視窗的左上角坐标 */
      frame.setLocation(width1/2 - width/2, height1/2 -height/2);
      frame.setVisible(flag);
   }

   @Override
   public void actionPerformed(ActionEvent e) {
      /** 得到點選的事件源 */
      JButton btn = (JButton) e.getSource();
      switch(btn.getText()){
         case "登陸":
            String ip = ipEt.getText().toString();
            String name = nameEt.getText().toString();
            // 校驗參數是否為空
            // 錯誤提示
            String msg = "" ;
            // 12.1.2.0
            // \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\
            if(ip==null || !ip.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){
               msg = "請輸入合法的服務端ip位址";
            }else if(name==null || !name.matches("\\S{1,}")){
               msg = "姓名必須1個字元以上";
            }

            if(!msg.equals("")){
               /** msg有内容說明參數有為空 */
               // 參數一:彈出放到哪個視窗裡面
               JOptionPane.showMessageDialog(loginView, msg);
            }else{
               try {
                  // 參數都合法了
                  // 目前登入的使用者,去服務端登陸
                  /** 先把目前使用者的名稱展示到界面 */
                  win.setTitle(name);
                  // 去服務端登陸連接配接一個socket管道
                  socket = new Socket(ip, Constants.PORT);

                  //為用戶端的socket配置設定一個線程 專門負責收消息
                  new ClientReader(this,socket).start();

                  // 帶上使用者資訊過去
                  DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                  dos.writeInt(1); // 登入消息
                  dos.writeUTF(name.trim());
                  dos.flush();

                  // 關系目前視窗 彈出聊天界面
                  loginView.dispose(); // 登入視窗銷毀
                  displayChatView(); // 展示了聊天視窗了


               } catch (Exception e1) {
                  e1.printStackTrace();
               }
            }
            break;
         case "取消":
            /** 退出系統 */
            System.exit(0);
            break;
         case "發送":
            
            break;

      }

   }
}
           

用戶端socket處理線程:

public class ClientReader extends Thread {

   private Socket socket;
    // 接收用戶端界面,友善收到消息後,更新界面資料。
   private ClientChat clientChat ;

   public ClientReader(ClientChat clientChat, Socket socket) {
      this.clientChat = clientChat;
      this.socket = socket;
   }

   @Override
   public void run() {
      try {
         DataInputStream dis = new DataInputStream(socket.getInputStream());
         /** 循環一直等待用戶端的消息 */
         while(true){
            /** 讀取目前的消息類型 :登入,群發,私聊 , @消息 */
            int flag = dis.readInt();
            if(flag == 1){
               // 線上人數消息回來了
               String nameDatas = dis.readUTF();
               // 展示到線上人數的界面
               String[] names = nameDatas.split(Constants.SPILIT);

               clientChat.onLineUsers.setListData(names);
            }else if(flag == 2){
              
            }
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}
           
小結
  • 此處說明了如果啟動用戶端界面,以及登陸功能後,服務端收到新的登陸消息後,會響應一個線上清單使用者回來給用戶端更新線上人數!

用戶端發送消息邏輯

目标

用戶端發送群聊消息,@消息,以及私聊消息。

實作步驟
  • 用戶端啟動後,在聊天界面需要通過發送按鈕推送群聊消息,@消息,以及私聊消息。
  • 了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結
  • 如果直接點選發送,預設發送群聊消息
  • 如果選中右側線上清單某個使用者,預設發送@消息
  • 如果選中右側線上清單某個使用者,然後選擇右下側私聊按鈕默,認發送私聊消息。
代碼實作

用戶端主體代碼:

public class ClientChat implements ActionListener {
	/** 1.設計界面  */
	private JFrame win = new JFrame();
	/** 2.消息内容架構 */
	public JTextArea smsContent =new JTextArea(23 , 50);
	/** 3.發送消息的框  */
	private JTextArea smsSend = new JTextArea(4,40);
	/** 4.線上人數的區域  */
	/** 存放人的資料 */
	/** 展示線上人數的視窗 */
	public JList<String> onLineUsers = new JList<>();

	// 是否私聊按鈕
	private JCheckBox isPrivateBn = new JCheckBox("私聊");
	// 消息按鈕
	private JButton sendBn  = new JButton("發送");

	// 登入界面
	private JFrame loginView;

	private JTextField ipEt , nameEt , idEt;

	private Socket socket ;

	public static void main(String[] args) {
		new ClientChat().initView();

	}

	private void initView() {
		/** 初始化聊天視窗的界面 */
		win.setSize(650, 600);

		/** 展示登入界面  */
		displayLoginView();

		/** 展示聊天界面 */
		//displayChatView();

	}

	private void displayChatView() {

		JPanel bottomPanel = new JPanel(new BorderLayout());
		//-----------------------------------------------
		// 将消息框和按鈕 添加到視窗的底端
		win.add(bottomPanel, BorderLayout.SOUTH);
		bottomPanel.add(smsSend);
		JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
		btns.add(sendBn);
		btns.add(isPrivateBn);
		bottomPanel.add(btns, BorderLayout.EAST);
		//-----------------------------------------------
		// 給發送消息按鈕綁定點選事件監聽器
		// 将展示消息區centerPanel添加到視窗的中間
		smsContent.setBackground(new Color(0xdd,0xdd,0xdd));
		// 讓展示消息區可以滾動。
		win.add(new JScrollPane(smsContent), BorderLayout.CENTER);
		smsContent.setEditable(false);
		//-----------------------------------------------
		// 使用者清單和是否私聊放到視窗的最右邊
		Box rightBox = new Box(BoxLayout.Y_AXIS);
		onLineUsers.setFixedCellWidth(120);
		onLineUsers.setVisibleRowCount(13);
		rightBox.add(new JScrollPane(onLineUsers));
		win.add(rightBox, BorderLayout.EAST);
		//-----------------------------------------------
		// 關閉視窗退出目前程式
		win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		win.pack();  // swing 加上這句 就可以擁有關閉視窗的功能
		/** 設定視窗居中,顯示出來  */
		setWindowCenter(win,650,600,true);
		// 發送按鈕綁定點選事件
		sendBn.addActionListener(this);
	}

	private void displayLoginView(){

		/** 先讓使用者進行登入
		 *  服務端ip
		 *  使用者名
		 *  id
		 *  */
		/** 顯示一個qq的登入框     */
		loginView = new JFrame("登入");
		loginView.setLayout(new GridLayout(3, 1));
		loginView.setSize(400, 230);

		JPanel ip = new JPanel();
		JLabel label = new JLabel("   IP:");
		ip.add(label);
		ipEt = new JTextField(20);
		ip.add(ipEt);
		loginView.add(ip);

		JPanel name = new JPanel();
		JLabel label1 = new JLabel("姓名:");
		name.add(label1);
		nameEt = new JTextField(20);
		name.add(nameEt);
		loginView.add(name);

		JPanel btnView = new JPanel();
		JButton login = new JButton("登陸");
		btnView.add(login);
		JButton cancle = new JButton("取消");
		btnView.add(cancle);
		loginView.add(btnView);
		// 關閉視窗退出目前程式
		loginView.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setWindowCenter(loginView,400,260,true);

		/** 給登入和取消綁定點選事件 */
		login.addActionListener(this);
		cancle.addActionListener(this);

	}

	private static void setWindowCenter(JFrame frame, int width , int height, boolean flag) {
		/** 得到所在系統所在螢幕的寬高 */
		Dimension ds = frame.getToolkit().getScreenSize();

		/** 拿到電腦的寬 */
		int width1 = ds.width;
		/** 高 */
		int height1 = ds.height ;

		System.out.println(width1 +"*" + height1);
		/** 設定視窗的左上角坐标 */
		frame.setLocation(width1/2 - width/2, height1/2 -height/2);
		frame.setVisible(flag);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		/** 得到點選的事件源 */
		JButton btn = (JButton) e.getSource();
		switch(btn.getText()){
			case "登陸":
				String ip = ipEt.getText().toString();
				String name = nameEt.getText().toString();
				// 校驗參數是否為空
				// 錯誤提示
				String msg = "" ;
				// 12.1.2.0
				// \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\
				if(ip==null || !ip.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){
					msg = "請輸入合法的服務端ip位址";
				}else if(name==null || !name.matches("\\S{1,}")){
					msg = "姓名必須1個字元以上";
				}

				if(!msg.equals("")){
					/** msg有内容說明參數有為空 */
					// 參數一:彈出放到哪個視窗裡面
					JOptionPane.showMessageDialog(loginView, msg);
				}else{
					try {
						// 參數都合法了
						// 目前登入的使用者,去服務端登陸
						/** 先把目前使用者的名稱展示到界面 */
						win.setTitle(name);
						// 去服務端登陸連接配接一個socket管道
						socket = new Socket(ip, Constants.PORT);

						//為用戶端的socket配置設定一個線程 專門負責收消息
						new ClientReader(this,socket).start();

						// 帶上使用者資訊過去
						DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
						dos.writeInt(1); // 登入消息
						dos.writeUTF(name.trim());
						dos.flush();

						// 關系目前視窗 彈出聊天界面
						loginView.dispose(); // 登入視窗銷毀
						displayChatView(); // 展示了聊天視窗了


					} catch (Exception e1) {
						e1.printStackTrace();
					}
				}
				break;
			case "取消":
				/** 退出系統 */
				System.exit(0);
				break;
			case "發送":
				// 得到發送消息的内容
				String msgSend = smsSend.getText().toString();
				if(!msgSend.trim().equals("")){
					/** 發消息給服務端 */
					try {
						// 判斷是否對誰發消息
						String selectName = onLineUsers.getSelectedValue();
						int flag = 2 ;// 群發 @消息
						if(selectName!=null&&!selectName.equals("")){
							msgSend =("@"+selectName+","+msgSend);
							/** 判斷是否選中了私法 */
							if(isPrivateBn.isSelected()){
								/** 私法 */
								flag = 3 ;//私發消息
							}

						}

						DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
						dos.writeInt(flag); // 群發消息  發送給所有人
						dos.writeUTF(msgSend);
						if(flag == 3){
							// 告訴服務端我對誰私發
							dos.writeUTF(selectName.trim());
						}
						dos.flush();

					} catch (Exception e1) {
						e1.printStackTrace();
					}

				}
				smsSend.setText(null);
				break;

		}

	}
}
           

用戶端socket處理線程:

class ClientReader extends Thread {

	private Socket socket;
	private ClientChat clientChat ;

	public ClientReader(ClientChat clientChat, Socket socket) {
		this.clientChat = clientChat;
		this.socket = socket;
	}

	@Override
	public void run() {
		try {
			DataInputStream dis = new DataInputStream(socket.getInputStream());
			/** 循環一直等待用戶端的消息 */
			while(true){
				/** 讀取目前的消息類型 :登入,群發,私聊 , @消息 */
				int flag = dis.readInt();
				if(flag == 1){
					// 線上人數消息回來了
					String nameDatas = dis.readUTF();
					// 展示到線上人數的界面
					String[] names = nameDatas.split(Constants.SPILIT);

					clientChat.onLineUsers.setListData(names);
				}else if(flag == 2){
					//群發,私聊 , @消息 都是直接顯示的。
					String msg = dis.readUTF() ;
					clientChat.smsContent.append(msg);
					// 讓消息界面滾動到底端
					clientChat.smsContent.setCaretPosition(clientChat.smsContent.getText().length());
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
           
小結
  • 此處實作了用戶端發送群聊消息,@消息,以及私聊消息。
  • 如果直接點選發送,預設發送群聊消息
  • 如果選中右側線上清單某個使用者,預設發送@消息
  • 如果選中右側線上清單某個使用者,然後選擇右下側私聊按鈕默,認發送私聊消息。

第三章 JAVA NIO深入剖析

在講解利用NIO實作通信架構之前,我們需要先來了解一下NIO的基本特點和使用。

3.1 Java NIO 基本介紹

  • Java NIO(New IO)也有人稱之為 java non-blocking IO是從Java 1.4版本開始引入的一個新的IO API,可以替代标準的Java IO API。NIO與原來的IO有同樣的作用和目的,但是使用的方式完全不同,NIO支援面向緩沖區的、基于通道的IO操作。NIO将以更加高效的方式進行檔案的讀寫操作。NIO可以了解為非阻塞IO,傳統的IO的read和write隻能阻塞執行,線程在讀寫IO期間不能幹其他事情,比如調用socket.read()時,如果伺服器一直沒有資料傳輸過來,線程就一直阻塞,而NIO中可以配置socket為非阻塞模式。
  • NIO 相關類都被放在 java.nio 包及子包下,并且對原 java.io 包中的很多類進行改寫。
  • NIO 有三大核心部分:Channel( 通道) ,Buffer( 緩沖區), Selector( 選擇器)
  • Java NIO 的非阻塞模式,使一個線程從某通道發送請求或者讀取資料,但是它僅能得到目前可用的資料,如果目前沒有資料可用時,就什麼都不會擷取,而不是保持線程阻塞,是以直至資料變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此,一個線程請求寫入一些資料到某通道,但不需要等待它完全寫入,這個線程同時可以去做别的事情。
  • 通俗了解:NIO 是可以做到用一個線程來處理多個操作的。假設有 1000 個請求過來,根據實際情況,可以配置設定20 或者 80個線程來處理。不像之前的阻塞 IO 那樣,非得配置設定 1000 個。

3.2 NIO 和 BIO 的比較

  • BIO 以流的方式處理資料,而 NIO 以塊的方式處理資料,塊 I/O 的效率比流 I/O 高很多
  • BIO 是阻塞的,NIO 則是非阻塞的
  • BIO 基于位元組流和字元流進行操作,而 NIO 基于 Channel(通道)和 Buffer(緩沖區)進行操作,資料總是從通道

    讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇器)用于監聽多個通道的事件(比如:連接配接請求,資料到達等),是以使用單個線程就可以監聽多個用戶端通道

NIO BIO
面向緩沖區(Buffer) 面向流(Stream)
非阻塞(Non Blocking IO) 阻塞IO(Blocking IO)
選擇器(Selectors)

3.3 NIO 三大核心原理示意圖

NIO 有三大核心部分:Channel( 通道) ,Buffer( 緩沖區), Selector( 選擇器)

Buffer緩沖區

緩沖區本質上是一塊可以寫入資料,然後可以從中讀取資料的記憶體。這塊記憶體被包裝成NIO Buffer對象,并提供了一組方法,用來友善的通路該塊記憶體。相比較直接對數組的操作,Buffer API更加容易操作和管理。

Channel(通道)

Java NIO的通道類似流,但又有些不同:既可以從通道中讀取資料,又可以寫資料到通道。但流的(input或output)讀寫通常是單向的。 通道可以非阻塞讀取和寫入通道,通道可以支援讀取或寫入緩沖區,也支援異步地讀寫。

Selector選擇器

Selector是 一個Java NIO元件,可以能夠檢查一個或多個 NIO 通道,并确定哪些通道已經準備好進行讀取或寫入。這樣,一個單獨的線程可以管理多個channel,進而管理多個網絡連接配接,提高效率

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結
  • 每個 channel 都會對應一個 Buffer
  • 一個線程對應Selector , 一個Selector對應多個 channel(連接配接)
  • 程式切換到哪個 channel 是由事件決定的
  • Selector 會根據不同的事件,在各個通道上切換
  • Buffer 就是一個記憶體塊 , 底層是一個數組
  • 資料的讀取寫入是通過 Buffer完成的 , BIO 中要麼是輸入流,或者是輸出流, 不能雙向,但是 NIO 的 Buffer 是可以讀也可以寫。
  • Java NIO系統的核心在于:通道(Channel)和緩沖區 (Buffer)。通道表示打開到 IO 裝置(例如:檔案、 套接字)的連接配接。若需要使用 NIO 系統,需要擷取 用于連接配接 IO 裝置的通道以及用于容納資料的緩沖 區。然後操作緩沖區,對資料進行處理。簡而言之,Channel 負責傳輸, Buffer 負責存取資料

3.4 NIO核心一:緩沖區(Buffer)

緩沖區(Buffer)

一個用于特定基本資料類 型的容器。由 java.nio 包定義的,所有緩沖區 都是 Buffer 抽象類的子類.。Java NIO 中的 Buffer 主要用于與 NIO 通道進行 互動,資料是從通道讀入緩沖區,從緩沖區寫入通道中的

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

Buffer 類及其子類

Buffer 就像一個數組,可以儲存多個相同類型的資料。根 據資料類型不同 ,有以下 Buffer 常用子類:

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

上述 Buffer 類 他們都采用相似的方法進行管理資料,隻是各自 管理的資料類型不同而已。都是通過如下方法擷取一個 Buffer 對象:

static XxxBuffer allocate(int capacity) : 建立一個容量為capacity 的 XxxBuffer 對象
           

緩沖區的基本屬性

Buffer 中的重要概念:

  • 容量 (capacity) :作為一個記憶體塊,Buffer具有一定的固定大小,也稱為"容量",緩沖區容量不能為負,并且建立後不能更改。
  • 限制 (limit):表示緩沖區中可以操作資料的大小(limit 後資料不能進行讀寫)。緩沖區的限制不能為負,并且不能大于其容量。 寫入模式,限制等于buffer的容量。讀取模式下,limit等于寫入的資料量。
  • 位置 (position):下一個要讀取或寫入的資料的索引。緩沖區的位置不能為 負,并且不能大于其限制
  • 标記 (mark)與重置 (reset):标記是一個索引,通過 Buffer 中的 mark() 方法 指定 Buffer 中一個特定的 position,之後可以通過調用 reset() 方法恢複到這 個 position.

    标記、位置、限制、容量遵守以下不變式: 0 <= mark <= position <= limit <= capacity

  • 圖示:
  • 了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

Buffer常見方法

Buffer clear() 清空緩沖區并傳回對緩沖區的引用
Buffer flip() 為 将緩沖區的界限設定為目前位置,并将目前位置充值為 0
int capacity() 傳回 Buffer 的 capacity 大小
boolean hasRemaining() 判斷緩沖區中是否還有元素
int limit() 傳回 Buffer 的界限(limit) 的位置
Buffer limit(int n) 将設定緩沖區界限為 n, 并傳回一個具有新 limit 的緩沖區對象
Buffer mark() 對緩沖區設定标記
int position() 傳回緩沖區的目前位置 position
Buffer position(int n) 将設定緩沖區的目前位置為 n , 并傳回修改後的 Buffer 對象
int remaining() 傳回 position 和 limit 之間的元素個數
Buffer reset() 将位置 position 轉到以前設定的 mark 所在的位置
Buffer rewind() 将位置設為為 0, 取消設定的 mark
           

緩沖區的資料操作

Buffer 所有子類提供了兩個用于資料操作的方法:get()put() 方法
取擷取 Buffer中的資料
get() :讀取單個位元組
get(byte[] dst):批量讀取多個位元組到 dst 中
get(int index):讀取指定索引位置的位元組(不會移動 position)
    
放到 入資料到 Buffer 中 中
put(byte b):将給定單個位元組寫入緩沖區的目前位置
put(byte[] src):将 src 中的位元組寫入緩沖區的目前位置
put(int index, byte b):将指定位元組寫入緩沖區的索引位置(不會移動 position)
           

使用Buffer讀寫資料一般遵循以下四個步驟:

  • 1.寫入資料到Buffer
  • 2.調用flip()方法,轉換為讀取模式
  • 3.從Buffer中讀取資料
  • 4.調用buffer.clear()方法或者buffer.compact()方法清除緩沖區

案例示範

public class TestBuffer {
   @Test
   public void test3(){
      //配置設定直接緩沖區
      ByteBuffer buf = ByteBuffer.allocateDirect(1024);
      System.out.println(buf.isDirect());
   }
   
   @Test
   public void test2(){
      String str = "itheima";
      
      ByteBuffer buf = ByteBuffer.allocate(1024);
      
      buf.put(str.getBytes());
      
      buf.flip();
      
      byte[] dst = new byte[buf.limit()];
      buf.get(dst, 0, 2);
      System.out.println(new String(dst, 0, 2));
      System.out.println(buf.position());
      
      //mark() : 标記
      buf.mark();
      
      buf.get(dst, 2, 2);
      System.out.println(new String(dst, 2, 2));
      System.out.println(buf.position());
      
      //reset() : 恢複到 mark 的位置
      buf.reset();
      System.out.println(buf.position());
      
      //判斷緩沖區中是否還有剩餘資料
      if(buf.hasRemaining()){
         //擷取緩沖區中可以操作的數量
         System.out.println(buf.remaining());
      }
   }
    
   @Test
   public void test1(){
      String str = "itheima";
      //1. 配置設定一個指定大小的緩沖區
      ByteBuffer buf = ByteBuffer.allocate(1024);
      System.out.println("-----------------allocate()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      
      //2. 利用 put() 存入資料到緩沖區中
      buf.put(str.getBytes());
      System.out.println("-----------------put()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      
      //3. 切換讀取資料模式
      buf.flip();
      System.out.println("-----------------flip()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      
      //4. 利用 get() 讀取緩沖區中的資料
      byte[] dst = new byte[buf.limit()];
      buf.get(dst);
      System.out.println(new String(dst, 0, dst.length));

      System.out.println("-----------------get()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      //5. rewind() : 可重複讀
      buf.rewind();
      System.out.println("-----------------rewind()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      
      //6. clear() : 清空緩沖區. 但是緩沖區中的資料依然存在,但是處于“被遺忘”狀态
      buf.clear();
      System.out.println("-----------------clear()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      System.out.println((char)buf.get());
      
   }

}
           

直接與非直接緩沖區

什麼是直接記憶體與非直接記憶體

根據官方文檔的描述:

byte byffer

可以是兩種類型,一種是基于直接記憶體(也就是非堆記憶體);另一種是非直接記憶體(也就是堆記憶體)。對于直接記憶體來說,JVM将會在IO操作上具有更高的性能,因為它直接作用于本地系統的IO操作。而非直接記憶體,也就是堆記憶體中的資料,如果要作IO操作,會先從本程序記憶體複制到直接記憶體,再利用本地IO處理。

從資料流的角度,非直接記憶體是下面這樣的作用鍊:

本地IO-->直接記憶體-->非直接記憶體-->直接記憶體-->本地IO
           

而直接記憶體是:

本地IO-->直接記憶體-->本地IO
           

很明顯,在做IO處理時,比如網絡發送大量資料時,直接記憶體會具有更高的效率。直接記憶體使用allocateDirect建立,但是它比申請普通的堆記憶體需要耗費更高的性能。不過,這部分的資料是在JVM之外的,是以它不會占用應用的記憶體。是以呢,當你有很大的資料要緩存,并且它的生命周期又很長,那麼就比較适合使用直接記憶體。隻是一般來說,如果不是能帶來很明顯的性能提升,還是推薦直接使用堆記憶體。位元組緩沖區是直接緩沖區還是非直接緩沖區可通過調用其 isDirect() 方法來确定。

使用場景

  • 1 有很大的資料需要存儲,它的生命周期又很長
  • 2 适合頻繁的IO操作,比如網絡并發場景

3.5 NIO核心二:通道(Channel)

通道Channe概述

通道(Channel):由 java.nio.channels 包定義 的。Channel 表示 IO 源與目标打開的連接配接。 Channel 類似于傳統的“流”。隻不過 Channel 本身不能直接通路資料,Channel 隻能與 Buffer 進行互動。

1、 NIO 的通道類似于流,但有些差別如下:

  • 通道可以同時進行讀寫,而流隻能讀或者隻能寫
  • 通道可以實作異步讀寫資料
  • 通道可以從緩沖讀資料,也可以寫資料到緩沖:

2、BIO 中的 stream 是單向的,例如 FileInputStream 對象隻能進行讀取資料的操作,而 NIO 中的通道(Channel)

是雙向的,可以讀操作,也可以寫操作。

3、Channel 在 NIO 中是一個接口

常用的Channel實作類

  • FileChannel:用于讀取、寫入、映射和操作檔案的通道。
  • DatagramChannel:通過 UDP 讀寫網絡中的資料通道。
  • SocketChannel:通過 TCP 讀寫網絡中的資料。
  • ServerSocketChannel:可以監聽新進來的 TCP 連接配接,對每一個新進來的連接配接都會建立一個 SocketChannel。 【ServerSocketChanne 類似 ServerSocket , SocketChannel 類似 Socket】

FileChannel 類

擷取通道的一種方式是對支援通道的對象調用getChannel() 方法。支援通道的類如下:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket

    擷取通道的其他方式是使用 Files 類的靜态方法 newByteChannel() 擷取位元組通道。或者通過通道的靜态方法 open() 打開并傳回指定通道

FileChannel的常用方法

int read(ByteBuffer dst) 從 從  Channel 到 中讀取資料到  ByteBuffer
long  read(ByteBuffer[] dsts) 将 将  Channel 到 中的資料“分散”到  ByteBuffer[]
int  write(ByteBuffer src) 将 将  ByteBuffer 到 中的資料寫入到  Channel
long write(ByteBuffer[] srcs) 将 将  ByteBuffer[] 到 中的資料“聚集”到  Channel
long position() 傳回此通道的檔案位置
FileChannel position(long p) 設定此通道的檔案位置
long size() 傳回此通道的檔案的目前大小
FileChannel truncate(long s) 将此通道的檔案截取為給定大小
void force(boolean metaData) 強制将所有對此通道的檔案更新寫入到儲存設備中
           

案例1-本地檔案寫資料

需求:使用前面學習後的 ByteBuffer(緩沖) 和 FileChannel(通道), 将 “hello,黑馬Java程式員!” 寫入到 data.txt 中.

package com.itheima;


import org.junit.Test;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class ChannelTest {
    @Test
    public void write(){
        try {
            // 1、位元組輸出流通向目标檔案
            FileOutputStream fos = new FileOutputStream("data01.txt");
            // 2、得到位元組輸出流對應的通道Channel
            FileChannel channel = fos.getChannel();
            // 3、配置設定緩沖區
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put("hello,黑馬Java程式員!".getBytes());
            // 4、把緩沖區切換成寫出模式
            buffer.flip();
            channel.write(buffer);
            channel.close();
            System.out.println("寫資料到檔案中!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
           

案例2-本地檔案讀資料

需求:使用前面學習後的 ByteBuffer(緩沖) 和 FileChannel(通道), 将 data01.txt 中的資料讀入到程式,并顯示在控制台螢幕

public class ChannelTest {

    @Test
    public void read() throws Exception {
        // 1、定義一個檔案位元組輸入流與源檔案接通
        FileInputStream is = new FileInputStream("data01.txt");
        // 2、需要得到檔案位元組輸入流的檔案通道
        FileChannel channel = is.getChannel();
        // 3、定義一個緩沖區
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 4、讀取資料到緩沖區
        channel.read(buffer);
        buffer.flip();
        // 5、讀取出緩沖區中的資料并輸出即可
        String rs = new String(buffer.array(),0,buffer.remaining());
        System.out.println(rs);

    }
           

案例3-使用Buffer完成檔案複制

使用 FileChannel(通道) ,完成檔案的拷貝。

@Test
public void copy() throws Exception {
    // 源檔案
    File srcFile = new File("C:\\Users\\dlei\\Desktop\\BIO,NIO,AIO\\檔案\\桌面.jpg");
    File destFile = new File("C:\\Users\\dlei\\Desktop\\BIO,NIO,AIO\\檔案\\桌面new.jpg");
    // 得到一個位元組位元組輸入流
    FileInputStream fis = new FileInputStream(srcFile);
    // 得到一個位元組輸出流
    FileOutputStream fos = new FileOutputStream(destFile);
    // 得到的是檔案通道
    FileChannel isChannel = fis.getChannel();
    FileChannel osChannel = fos.getChannel();
    // 配置設定緩沖區
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while(true){
        // 必須先清空緩沖然後再寫入資料到緩沖區
        buffer.clear();
        // 開始讀取一次資料
        int flag = isChannel.read(buffer);
        if(flag == -1){
            break;
        }
        // 已經讀取了資料 ,把緩沖區的模式切換成可讀模式
        buffer.flip();
        // 把資料寫出到
        osChannel.write(buffer);
    }
    isChannel.close();
    osChannel.close();
    System.out.println("複制完成!");
}
           

案例4-分散 (Scatter) 和聚集 (Gather)

分散讀取(Scatter ):是指把Channel通道的資料讀入到多個緩沖區中去

聚集寫入(Gathering )是指将多個 Buffer 中的資料“聚集”到 Channel。

//分散和聚集
@Test
public void test() throws IOException{
		RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
	//1. 擷取通道
	FileChannel channel1 = raf1.getChannel();
	
	//2. 配置設定指定大小的緩沖區
	ByteBuffer buf1 = ByteBuffer.allocate(100);
	ByteBuffer buf2 = ByteBuffer.allocate(1024);
	
	//3. 分散讀取
	ByteBuffer[] bufs = {buf1, buf2};
	channel1.read(bufs);
	
	for (ByteBuffer byteBuffer : bufs) {
		byteBuffer.flip();
	}
	
	System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
	System.out.println("-----------------");
	System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
	
	//4. 聚集寫入
	RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
	FileChannel channel2 = raf2.getChannel();
	
	channel2.write(bufs);
}
           

案例5-transferFrom()

從目标通道中去複制原通道資料

@Test
public void test02() throws Exception {
    // 1、位元組輸入管道
    FileInputStream is = new FileInputStream("data01.txt");
    FileChannel isChannel = is.getChannel();
    // 2、位元組輸出流管道
    FileOutputStream fos = new FileOutputStream("data03.txt");
    FileChannel osChannel = fos.getChannel();
    // 3、複制
    osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size());
    isChannel.close();
    osChannel.close();
}
           

案例6-transferTo()

把原通道資料複制到目标通道

@Test
public void test02() throws Exception {
    // 1、位元組輸入管道
    FileInputStream is = new FileInputStream("data01.txt");
    FileChannel isChannel = is.getChannel();
    // 2、位元組輸出流管道
    FileOutputStream fos = new FileOutputStream("data04.txt");
    FileChannel osChannel = fos.getChannel();
    // 3、複制
    isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel);
    isChannel.close();
    osChannel.close();
}
           

3.6 NIO核心三:選擇器(Selector)

選擇器(Selector)概述

選擇器(Selector) 是 SelectableChannle 對象的多路複用器,Selector 可以同時監控多個 SelectableChannel 的 IO 狀況,也就是說,利用 Selector可使一個單獨的線程管理多個 Channel。Selector 是非阻塞 IO 的核心

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結
  • Java 的 NIO,用非阻塞的 IO 方式。可以用一個線程,處理多個的用戶端連接配接,就會使用到 Selector(選擇器)
  • Selector 能夠檢測多個注冊的通道上是否有事件發生(注意:多個 Channel 以事件的方式可以注冊到同一個

    Selector),如果有事件發生,便擷取事件然後針對每個事件進行相應的處理。這樣就可以隻用一個單線程去管

    理多個通道,也就是管理多個連接配接和請求。

  • 隻有在 連接配接/通道 真正有讀寫事件發生時,才會進行讀寫,就大大地減少了系統開銷,并且不必為每個連接配接都

    建立一個線程,不用去維護多個線程

  • 避免了多線程之間的上下文切換導緻的開銷

選擇 器(Selector)的應用

建立 Selector :通過調用 Selector.open() 方法建立一個 Selector。

向選擇器注冊通道:SelectableChannel.register(Selector sel, int ops)

//1. 擷取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切換非阻塞模式
ssChannel.configureBlocking(false);
//3. 綁定連接配接
ssChannel.bind(new InetSocketAddress(9898));
//4. 擷取選擇器
Selector selector = Selector.open();
//5. 将通道注冊到選擇器上, 并且指定“監聽接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
           

當調用 register(Selector sel, int ops) 将通道注冊選擇器時,選擇器對通道的監聽事件,需要通過第二個參數 ops 指定。可以監聽的事件類型(用 可使用 SelectionKey 的四個常量 表示):

  • 讀 : SelectionKey.OP_READ (1)
  • 寫 : SelectionKey.OP_WRITE (4)
  • 連接配接 : SelectionKey.OP_CONNECT (8)
  • 接收 : SelectionKey.OP_ACCEPT (16)
  • 若注冊時不止監聽一個事件,則可以使用“位或”操作符連接配接。
int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE 
           

3.7 NIO非阻塞式網絡通信原理分析

Selector 示意圖和特點說明

Selector可以實作: 一個 I/O 線程可以并發處理 N 個用戶端連接配接和讀寫操作,這從根本上解決了傳統同步阻塞 I/O 一連接配接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。

了解BIO、NIO這一篇就夠了第一章 Java的I/O演進之路第二章 JAVA BIO深入剖析第三章 JAVA NIO深入剖析第四章 JAVA AIO深入剖析第五章 BIO,NIO,AIO總結

服務端流程

  • 1、當用戶端連接配接服務端時,服務端會通過 ServerSocketChannel 得到 SocketChannel:1. 擷取通道
  • 2、切換非阻塞模式
    ssChannel.configureBlocking(false);
               
  • 3、綁定連接配接
    ssChannel.bind(new InetSocketAddress(9999));
               
  • 4、 擷取選擇器
    Selector selector = Selector.open();
               
  • 5、 将通道注冊到選擇器上, 并且指定“監聽接收事件”
    ssChannel.register(selector, SelectionKey.OP_ACCEPT);
               
    1. 輪詢式的擷取選擇器上已經“準備就緒”的事件
  • //輪詢式的擷取選擇器上已經“準備就緒”的事件
     while (selector.select() > 0) {
            System.out.println("輪一輪");
            //7. 擷取目前選擇器中所有注冊的“選擇鍵(已就緒的監聽事件)”
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                //8. 擷取準備“就緒”的是事件
                SelectionKey sk = it.next();
                //9. 判斷具體是什麼事件準備就緒
                if (sk.isAcceptable()) {
                    //10. 若“接收就緒”,擷取用戶端連接配接
                    SocketChannel sChannel = ssChannel.accept();
                    //11. 切換非阻塞模式
                    sChannel.configureBlocking(false);
                    //12. 将該通道注冊到選擇器上
                    sChannel.register(selector, SelectionKey.OP_READ);
                } else if (sk.isReadable()) {
                    //13. 擷取目前選擇器上“讀就緒”狀态的通道
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    //14. 讀取資料
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len = 0;
                    while ((len = sChannel.read(buf)) > 0) {
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, len));
                        buf.clear();
                    }
                }
                //15. 取消選擇鍵 SelectionKey
                it.remove();
            }
        }
    }
               

用戶端流程

    1. 擷取通道
      SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
                 
    1. 切換非阻塞模式
      sChannel.configureBlocking(false);
                 
    1. 配置設定指定大小的緩沖區
    ByteBuffer buf = ByteBuffer.allocate(1024);
               
    1. 發送資料給服務端

    Scanner scan = new Scanner(System.in);

    while(scan.hasNext()){

    String str = scan.nextLine();

    buf.put((new SimpleDateFormat(“yyyy/MM/dd HH:mm:ss”).format(System.currentTimeMillis())

    + “\n” + str).getBytes());

    buf.flip();

    sChannel.write(buf);

    buf.clear();

    }

    //關閉通道

    sChannel.close();

3.8 NIO非阻塞式網絡通信入門案例

需求:服務端接收用戶端的連接配接請求,并接收多個用戶端發送過來的事件。

代碼案例

/**
  用戶端
 */
public class Client {

	public static void main(String[] args) throws Exception {
		//1. 擷取通道
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
		//2. 切換非阻塞模式
		sChannel.configureBlocking(false);
		//3. 配置設定指定大小的緩沖區
		ByteBuffer buf = ByteBuffer.allocate(1024);
		//4. 發送資料給服務端
		Scanner scan = new Scanner(System.in);
		while(scan.hasNext()){
			String str = scan.nextLine();
			buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())
					+ "\n" + str).getBytes());
			buf.flip();
			sChannel.write(buf);
			buf.clear();
		}
		//5. 關閉通道
		sChannel.close();
	}
}

/**
 服務端
 */
public class Server {
    public static void main(String[] args) throws IOException {
        //1. 擷取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        //2. 切換非阻塞模式
        ssChannel.configureBlocking(false);
        //3. 綁定連接配接
        ssChannel.bind(new InetSocketAddress(9999));
        //4. 擷取選擇器
        Selector selector = Selector.open();
        //5. 将通道注冊到選擇器上, 并且指定“監聽接收事件”
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        //6. 輪詢式的擷取選擇器上已經“準備就緒”的事件
        while (selector.select() > 0) {
            System.out.println("輪一輪");
            //7. 擷取目前選擇器中所有注冊的“選擇鍵(已就緒的監聽事件)”
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                //8. 擷取準備“就緒”的是事件
                SelectionKey sk = it.next();
                //9. 判斷具體是什麼事件準備就緒
                if (sk.isAcceptable()) {
                    //10. 若“接收就緒”,擷取用戶端連接配接
                    SocketChannel sChannel = ssChannel.accept();
                    //11. 切換非阻塞模式
                    sChannel.configureBlocking(false);
                    //12. 将該通道注冊到選擇器上
                    sChannel.register(selector, SelectionKey.OP_READ);
                } else if (sk.isReadable()) {
                    //13. 擷取目前選擇器上“讀就緒”狀态的通道
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    //14. 讀取資料
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len = 0;
                    while ((len = sChannel.read(buf)) > 0) {
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, len));
                        buf.clear();
                    }
                }
                //15. 取消選擇鍵 SelectionKey
                it.remove();
            }
        }
    }
}

           

3.9 NIO 網絡程式設計應用執行個體-群聊系統

目标

需求:進一步了解 NIO 非阻塞網絡程式設計機制,實作多人群聊

  • 編寫一個 NIO 群聊系統,實作用戶端與用戶端的通信需求(非阻塞)
  • 伺服器端:可以監測使用者上線,離線,并實作消息轉發功能
  • 用戶端:通過 channel 可以無阻塞發送消息給其它所有用戶端使用者,同時可以接受其它用戶端使用者通過服務端轉發來的消息

服務端代碼實作

public class Server {
    //定義屬性
    private Selector selector;
    private ServerSocketChannel ssChannel;
    private static final int PORT = 9999;
    //構造器
    //初始化工作
    public Server() {
        try {
            // 1、擷取通道
            ssChannel = ServerSocketChannel.open();
            // 2、切換為非阻塞模式
            ssChannel.configureBlocking(false);
            // 3、綁定連接配接的端口
            ssChannel.bind(new InetSocketAddress(PORT));
            // 4、擷取選擇器Selector
            selector = Selector.open();
            // 5、将通道都注冊到選擇器上去,并且開始指定監聽接收事件
            ssChannel.register(selector , SelectionKey.OP_ACCEPT);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
    //監聽
    public void listen() {
        System.out.println("監聽線程: " + Thread.currentThread().getName());
        try {
            while (selector.select() > 0){
                System.out.println("開始一輪事件處理~~~");
                // 7、擷取選擇器中的所有注冊的通道中已經就緒好的事件
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                // 8、開始周遊這些準備好的事件
                while (it.hasNext()){
                    // 提取目前這個事件
                    SelectionKey sk = it.next();
                    // 9、判斷這個事件具體是什麼
                    if(sk.isAcceptable()){
                        // 10、直接擷取目前接入的用戶端通道
                        SocketChannel schannel = ssChannel.accept();
                        // 11 、切換成非阻塞模式
                        schannel.configureBlocking(false);
                        // 12、将本用戶端通道注冊到選擇器
                        System.out.println(schannel.getRemoteAddress() + " 上線 ");
                        schannel.register(selector , SelectionKey.OP_READ);
                        //提示
                    }else if(sk.isReadable()){
                        //處理讀 (專門寫方法..)
                        readData(sk);
                    }

                    it.remove(); // 處理完畢之後需要移除目前事件
                }
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //發生異常處理....

        }
    }

    //讀取用戶端消息
    private void readData(SelectionKey key) {
        //取到關聯的channle
        SocketChannel channel = null;
        try {
           //得到channel
            channel = (SocketChannel) key.channel();
            //建立buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = channel.read(buffer);
            //根據count的值做處理
            if(count > 0) {
                //把緩存區的資料轉成字元串
                String msg = new String(buffer.array());
                //輸出該消息
                System.out.println("form 用戶端: " + msg);
                //向其它的用戶端轉發消息(去掉自己), 專門寫一個方法來處理
                sendInfoToOtherClients(msg, channel);
            }
        }catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 離線了..");
                e.printStackTrace();
                //取消注冊
                key.cancel();
                //關閉通道
                channel.close();
            }catch (IOException e2) {
                e2.printStackTrace();;
            }
        }
    }

    //轉發消息給其它客戶(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self ) throws  IOException{
        System.out.println("伺服器轉發消息中...");
        System.out.println("伺服器轉發資料給用戶端線程: " + Thread.currentThread().getName());
        //周遊 所有注冊到selector 上的 SocketChannel,并排除 self
        for(SelectionKey key: selector.keys()) {
            //通過 key  取出對應的 SocketChannel
            Channel targetChannel = key.channel();
            //排除自己
            if(targetChannel instanceof  SocketChannel && targetChannel != self) {
                //轉型
                SocketChannel dest = (SocketChannel)targetChannel;
                //将msg 存儲到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //将buffer 的資料寫入 通道
                dest.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        //建立伺服器對象
        Server groupChatServer = new Server();
        groupChatServer.listen();
    }
}
           

用戶端代碼實作

package com.itheima.chat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

public class Client {
    //定義相關的屬性
    private final String HOST = "127.0.0.1"; // 伺服器的ip
    private final int PORT = 9999; //伺服器端口
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    //構造器, 完成初始化工作
    public Client() throws IOException {

        selector = Selector.open();
        //連接配接伺服器
        socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
        //設定非阻塞
        socketChannel.configureBlocking(false);
        //将channel 注冊到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        //得到username
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " is ok...");

    }

    //向伺服器發送消息
    public void sendInfo(String info) {
        info = username + " 說:" + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    //讀取從伺服器端回複的消息
    public void readInfo() {
        try {

            int readChannels = selector.select();
            if(readChannels > 0) {//有可以用的通道

                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {

                    SelectionKey key = iterator.next();
                    if(key.isReadable()) {
                        //得到相關的通道
                       SocketChannel sc = (SocketChannel) key.channel();
                       //得到一個Buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        //讀取
                        sc.read(buffer);
                        //把讀到的緩沖區的資料轉成字元串
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    }
                }
                iterator.remove(); //删除目前的selectionKey, 防止重複操作
            } else {
                //System.out.println("沒有可以用的通道...");

            }

        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        //啟動我們用戶端
        Client chatClient = new Client();
        //啟動一個線程, 每個3秒,讀取從伺服器發送資料
        new Thread() {
            public void run() {

                while (true) {
                    chatClient.readInfo();
                    try {
                        Thread.currentThread().sleep(3000);
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        //發送資料給伺服器端
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            chatClient.sendInfo(s);
        }
    }
}
           

小結

第四章 JAVA AIO深入剖析

4.1 AIO程式設計

  • Java AIO(NIO.2) : 異步非阻塞,伺服器實作模式為一個有效請求一個線程,用戶端的I/O請求都是由OS先完成了再通知伺服器應用去啟動線程進行處理。
AIO
異步非阻塞,基于NIO的,可以稱之為NIO2.0
    BIO                   NIO                              AIO        
Socket                SocketChannel                    AsynchronousSocketChannel
ServerSocket          ServerSocketChannel	       AsynchronousServerSocketChannel
           

與NIO不同,當進行讀寫操作時,隻須直接調用API的read或write方法即可, 這兩種方法均為異步的,對于讀操作而言,當有流可讀取時,作業系統會将可讀的流傳入read方法的緩沖區,對于寫操作而言,當作業系統将write方法傳遞的流寫入完畢時,作業系統主動通知應用程式

即可以了解為,read/write方法都是異步的,完成後會主動調用回調函數。在JDK1.7中,這部分内容被稱作NIO.2,主要在Java.nio.channels包下增加了下面四個異步通道:

AsynchronousSocketChannel
	AsynchronousServerSocketChannel
	AsynchronousFileChannel
	AsynchronousDatagramChannel
           

第五章 BIO,NIO,AIO總結

BIO、NIO、AIO:

  • Java BIO : 同步并阻塞,伺服器實作模式為一個連接配接一個線程,即用戶端有連接配接請求時伺服器端就需要啟動一個線程進行處理,如果這個連接配接不做任何事情會造成不必要的線程開銷,當然可以通過線程池機制改善。
  • Java NIO : 同步非阻塞,伺服器實作模式為一個請求一個線程,即用戶端發送的連接配接請求都會注冊到多路複用器上,多路複用器輪詢到連接配接有I/O請求時才啟動一個線程進行處理。
  • Java AIO(NIO.2) : 異步非阻塞,伺服器實作模式為一個有效請求一個線程,用戶端的I/O請求都是由OS先完成了再通知伺服器應用去啟動線程進行處理。

BIO、NIO、AIO适用場景分析:

  • BIO方式适用于連接配接數目比較小且固定的架構,這種方式對伺服器資源要求比較高,并發局限于應用中,JDK1.4以前的唯一選擇,但程式直覺簡單易了解。
  • NIO方式适用于連接配接數目多且連接配接比較短(輕操作)的架構,比如聊天伺服器,并發局限于應用中,程式設計比較複雜,JDK1.4開始支援。
  • AIO方式使用于連接配接數目多且連接配接比較長(重操作)的架構,比如相冊伺服器,充分調用OS參與并發操作,程式設計比較複雜,JDK7開始支援。Netty!

繼續閱讀