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協議中,在傳送資料的準備階段,客戶端與伺服器之間的三次互動,以保證連線的可靠。
完成三次握手,連線建立後,客戶端和伺服器就可以開始進行資料傳輸了。由於這種面向連線的特性,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.1
、localhost
。
埠號
網路的通訊,本質上是兩個程序(應用程式)的通訊。每臺計算機都有很多的程序,那麼在網路通訊時,如何區分這些程序呢?
如果說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, 會自動關閉相關的流
- Socket(String host, int port) 根據ip地址字串和埠號建立客戶端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協議, 通過客戶端向伺服器上傳一個檔案
分析
-
【客戶端】輸入流,從硬碟讀取檔案資料到程式中。
-
【客戶端】輸出流,寫出檔案資料到服務端。
-
【服務端】輸入流,讀取檔案資料到服務端程式。
-
【服務端】輸出流,寫出檔案資料到伺服器硬碟中。
-
【服務端】獲取輸出流,回寫資料。
-
【客戶端】獲取輸入流,解析回寫資料。
實現
拷貝檔案
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伺服器 擴充套件
需求
- 模擬網站伺服器,使用瀏覽器訪問自己編寫的服務端程式,檢視網頁效果。
分析
- 準備頁面資料,web資料夾。
- 我們模擬伺服器端,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的四個常量表示:
-
連線就緒--常量:SelectionKey.OP_CONNECT
-
接收就緒--常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在註冊時只能使用此項)
-
讀就緒--常量:SelectionKey.OP_READ
-
寫就緒--常量: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: 非同步非阻塞