超硬核!!!一篇文章搞定BIO、NIO、AIO、Netty(詳細基礎內容+網絡編程內容+代碼示例)【網絡編程 2】
標簽: Java網絡編程全面解刨 網絡 java netty socket nio
BIO、NIO、AIO、Netty
- BIO、NIO、AIO、Netty
BIO、NIO、AIO、Netty
看不懂前面什么是BIO、NIO、AIO、Netty的話往后看,后面有從頭開始全面介紹以及代碼示例
,
文章非常長!!! 非常詳細!!!,讓你不再害怕面試被問到就頭大。此次網絡編程系列分為很多篇文章,后續補齊
同款系列文章地址為:
超硬核!!!一篇文章搞定TCP、UDP、Socket(詳細網絡編程內容+現實解釋三次握手四次揮手+代碼示例)【網絡編程 1】
什么是IO
Java中I/O是以流為基礎進行數據的輸入輸出的,所有數據被串行化(所謂串行化就是數據要按順序進行輸入輸出)寫入輸出流。簡單來說就是java通過io流方式和外部設備進行交互。
在Java類庫中,IO部分的內容是很龐大的,因為它涉及的領域很廣泛:標準輸入輸出,文件的操作,網絡上的數據傳輸流,字符串流,對象流等等等。
比如程序從服務器上下載圖片,就是通過流的方式從網絡上以流的方式到程序中,在到硬盤中
在了解不同的IO之前先了解:同步與異步,阻塞與非阻塞的區別
- 同步,一個任務的完成之前不能做其他操作,必須等待(等于在打電話)
- 異步,一個任務的完成之前,可以進行其他操作(等于在聊QQ)
- 阻塞,是相對于CPU來說的, 掛起當前線程,不能做其他操作只能等待
- 非阻塞,,無須掛起當前線程,可以去執行其他操作
什么是BIO
BIO:同步并阻塞,服務器實現一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,沒處理完之前此線程不能做其他操作(如果是單線程的情況下,我傳輸的文件很大呢?),當然可以通過線程池機制改善。BIO方式適用于連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,并發局限于應用中,JDK1.4以前的唯一選擇,但程序直觀簡單易理解。
什么是NIO
NIO:同步非阻塞,服務器實現一個連接一個線程,即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。NIO方式適用于連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,并發局限于應用中,編程比較復雜,JDK1.4之后開始支持。
什么是AIO
AIO:異步非阻塞,服務器實現模式為一個有效請求一個線程,客戶端的I/O請求都是由操作系統先完成了再通知服務器應用去啟動線程進行處理,AIO方式使用于連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用操作系統參與并發操作,編程比較復雜,JDK1.7之后開始支持。.
AIO屬于NIO包中的類實現,其實IO主要分為BIO和NIO,AIO只是附加品,解決IO不能異步的實現
在以前很少有Linux系統支持AIO,Windows的IOCP就是該AIO模型。但是現在的服務器一般都是支持AIO操作
什么Netty
Netty是由JBOSS提供的一個Java開源框架。Netty提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。
Netty 是一個基于NIO的客戶、服務器端編程框架,使用Netty 可以確保你快速和簡單的開發出一個網絡應用,例如實現了某種協議的客戶,服務端應用。Netty相當簡化和流線化了網絡應用的編程開發過程,例如,TCP和UDP的socket服務開發。
Netty是由NIO演進而來,使用過NIO編程的用戶就知道NIO編程非常繁重,Netty是能夠能跟好的使用NIO
BIO和NIO、AIO的區別
- BIO是阻塞的,NIO是非阻塞的.
- BIO是面向流的,只能單向讀寫,NIO是面向緩沖的, 可以雙向讀寫
- 使用BIO做Socket連接時,由于單向讀寫,當沒有數據時,會掛起當前線程,阻塞等待,為防止影響其它連接,,需要為每個連接新建線程處理.,然而系統資源是有限的,,不能過多的新建線程,線程過多帶來線程上下文的切換,從來帶來更大的性能損耗,因此需要使用NIO進行BIO多路復用,使用一個線程來監聽所有Socket連接,使用本線程或者其他線程處理連接
- AIO是非阻塞 以異步方式發起 I/O 操作。當 I/O 操作進行時可以去做其他操作,由操作系統內核空間提醒IO操作已完成(不懂的可以往下看)
IO流的分類
按照讀寫的單位大小來分:
字符流
:以字符為單位,每次次讀入或讀出是16位數據。其只能讀取字符類型數據。
(Java代碼接收數據為一般為char數組,也可以是別的
)- 字節流:以字節為單位,每次次讀入或讀出是8位數據。可以讀任何類型數據,圖片、文件、音樂視頻等。
(Java代碼接收數據只能為byte數組
)
按照實際IO操作來分:
- 輸出流:從內存讀出到文件。只能進行寫操作。
- 輸入流:從文件讀入到內存。只能進行讀操作。
- 注意:輸出流可以幫助我們創建文件,而輸入流不會。
按照讀寫時是否直接與硬盤,內存等節點連接分:
- 節點流:直接與數據源相連,讀入或讀出。
- 處理流:也叫包裝流,是對一個對于已存在的流的連接進行封裝,通過所封裝的流的功能調用實現數據讀寫。如添加個Buffering緩沖區。(意思就是有個緩存區,等于軟件和mysql中的redis)
- 注意:為什么要有處理流?主要作用是在讀入或寫出時,對數據進行緩存,以減少I/O的次數,以便下次更好更快的讀寫文件,才有了處理流。
什么是內核空間
我們的應用程序是不能直接訪問硬盤的,我們程序沒有權限直接訪問,但是操作系統(Windows、Linux…)會給我們一部分權限較高的內存空間,他叫內核空間,和我們的實際硬盤空間是有區別的
五種IO模型
注意:用戶空間就是應用程序空間
1.阻塞IO(blocking I/O)
A拿著一支魚竿在河邊釣魚,并且一直在魚竿前等,在等的時候不做其他的事情,十分專心。只有魚上鉤的時,才結束掉等的動作,把魚釣上來。
在內核將數據準備好之前,系統調用會一直等待所有的套接字,默認的是阻塞方式。
2.非阻塞IO(noblocking I/O)
B也在河邊釣魚,但是B不想將自己的所有時間都花費在釣魚上,在等魚上鉤這個時間段中,B也在做其他的事情(一會看看書,一會讀讀報紙,一會又去看其他人的釣魚等),但B在做這些事情的時候,每隔一個固定的時間檢查魚是否上鉤。一旦檢查到有魚上鉤,就停下手中的事情,把魚釣上來。 B在檢查魚竿是否有魚,是一個輪詢的過程。
3.異步IO(asynchronous I/O)
C也想釣魚,但C有事情,于是他雇來了D、E、F,讓他們幫他等待魚上鉤,一旦有魚上鉤,就打電話給C,C就會將魚釣上去。
當應用程序請求數據時,內核一方面去取數據報內容返回,另一方面將程序控制權還給應用進程,應用進程繼續處理其他事情,是一種非阻塞的狀態。
4.信號驅動IO(signal blocking I/O)
G也在河邊釣魚,但與A、B、C不同的是,G比較聰明,他給魚竿上掛一個鈴鐺,當有魚上鉤的時候,這個鈴鐺就會被碰響,G就會將魚釣上來。
信號驅動IO模型,應用進程告訴內核:當數據報準備好的時候,給我發送一個信號,對SIGIO信號進行捕捉,并且調用我的信號處理函數來獲取數據報。
5.IO多路轉接(I/O multiplexing)
H同樣也在河邊釣魚,但是H生活水平比較好,H拿了很多的魚竿,一次性有很多魚竿在等,H不斷的查看每個魚竿是否有魚上鉤。增加了效率,減少了等待的時間。
IO多路轉接是多了一個select函數,select函數有一個參數是文件描述符集合,對這些文件描述符進行循環監聽,當某個文件描述符就緒時,就對這個文件描述符進行處理。
IO多路轉接是屬于阻塞IO,但可以對多個文件描述符進行阻塞監聽,所以效率較阻塞IO的高。
IO的常用類和方法,以及如何使用
注意,如果懂IO的普通文件讀寫操作可以直接點擊此處跳過,直接看網絡操作IO編程,那個才是重點,點擊即會跳轉
前面講了那么多廢話,現在我們開始進入主題,后面很長,從開始的文件操作到后面的網絡IO操作都會有例子:
注意,如果懂IO的普通文件讀寫操作可以直接點擊此處跳過,直接看網絡操作IO編程,那個才是重點,點擊即會跳轉
IO基本操作講解
這里的基本操作就是普通的讀取操作,如果想要跟深入的了解不同的IO開發場景必須先了解IO的基本操作
1 按字符
流讀取文件
1.1 按字符流的·節點流方式讀取
如果我們要取的數據基本單位是字符,那么用(字符流)這種方法讀取文件就比較適合。比如:讀取test.txt文件
注釋:
字符流
:以字符為單位,每次次讀入或讀出是16位數據。其只能讀取字符類型數據。
(Java代碼接收數據為一般為char數組,也可以是別的
)- 字節流:以字節為單位,每次次讀入或讀出是8位數據。可以讀任何類型數據,圖片、文件、音樂視頻等。
(Java代碼接收數據只能為byte數組
)
FileReader 類:(字符輸入流)
注意:new FileReader(“D:\test.txt”);//文件必須存在
package com.test.io;
import java.io.FileReader;
import java.io.IOException;
public class TestFileReader {
public static void main(String[] args) throws IOException {
int num=0;
//字符流接收使用的char數組
char[] buf=new char[1024];
//字符流、節點流打開文件類
FileReader fr = new FileReader("D:\\test.txt");//文件必須存在
//FileReader.read():取出字符存到buf數組中,如果讀取為-1代表為空即結束讀取。
//FileReader.read():讀取的是一個字符,但是java虛擬機會自動將char類型數據轉換為int數據,
//如果你讀取的是字符A,java虛擬機會自動將其轉換成97,如果你想看到字符可以在返回的字符數前加(char)強制轉換如
while((num=fr.read(buf))!=-1) { }
//檢測一下是否取到相應的數據
for(int i=0;i<buf.length;i++) {
System.out.print(buf[i]);
}
}
}
運行結果:
1.2 按字符流的·處理流方式讀取
效果是一樣,但是給了我們有不同的選擇操作。進行了一個小封裝,我這只是簡單演示,處理流其實還有很多操作
BufferedReader 類: 字符輸入流使用的類,加緩沖功能,避免頻繁讀寫硬盤
package com.test.io;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TestBufferedReader {
public static void main(String[] args) throws IOException {
int num=0;
//字符流接收使用的String數組
String[] bufstring=new String[1024];
//字符流、節點流打開文件類
FileReader fr = new FileReader("D:\\test.txt");//文件必須存在
//字符流、處理流讀取文件類
BufferedReader br = new BufferedReader(fr);
//臨時接收數據使用的變量
String line=null;
//BufferedReader.readLine():單行讀取,讀取為空返回null
while((line=br.readLine())!=null) {
bufstring[num]=line;
num++;
}
br.close();//關閉文件
for(int i=0;i<num;i++) {
System.out.println(bufstring[i]);
}
}
}
測試效果一樣
2 按字符
流寫出文件
2.1 按字符流的·節點流方式寫出
寫出字符,使用(字符流)這種方法寫出文件比較適合。比如:輸出內容添加到test.txt文件
FileWriter類:(字符輸出流),如果寫出文件不存在會自動創建一個相對應的文件。使用FileWriter寫出文件默認是覆蓋原文件,如果要想在源文件添加內容不覆蓋的話,需要構造參數添加true參數:看示例了解
package com.test.io;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class TestFileWriter {
public static void main(String[] args) throws IOException {
//File是操作文件類
File file = new File("D:\\test.txt");//文件必須存在
//字符流、節點流寫出文件類
//new FileWriter(file,true),這個true代表追加,不寫就代表覆蓋文件
FileWriter out=new FileWriter(file,true);
//寫入的字節,\n代表換行
String str="\nholler";
//寫入
out.write(str);
out.close();
}
}
運行效果:
2.2 按字符流的·處理流方式寫出
BufferedWriter : 增加緩沖功能,避免頻繁讀寫硬盤。
我這里: //new FileWriter(file),這里我只給了他文件位置,我沒加true代表覆蓋源文件
package com.test.io;
import java.io.*;
public class TestBufferedWriter {
public static void main(String[] args) throws IOException {
//File是操作文件類
File file = new File("D:\\test.txt");//文件必須存在
//字符流、節點流寫出文件類
//new FileWriter(file),這個我沒加true代表覆蓋文件
Writer writer = new FileWriter(file);
////字符流、處理流寫出文件類
BufferedWriter bw = new BufferedWriter(writer);
bw.write("\n小心");
bw.close();
writer.close();
}
}
運行效果:
3 按字節
流寫入寫出文件
3.1 按字節流的·節點流寫入寫出文件
如果我們要取的數據 圖片、文件、音樂視頻等類型,就必須使用字節流進行讀取寫出
注釋:
字符流
:以字符為單位,每次次讀入或讀出是16位數據。其只能讀取字符類型數據。
(Java代碼接收數據為一般為char數組,也可以是別的
)- 字節流:以字節為單位,每次次讀入或讀出是8位數據。可以讀任何類型數據,圖片、文件、音樂視頻等。
(Java代碼接收數據只能為byte數組
)
FileInputStream:(字節輸入流)
FileOutputStream:(字節輸出流)
package com.test.io;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestFileOutputStream {
public static void main(String[] args) throws IOException {
//創建字節輸入流、節點流方式讀取文件
FileInputStream fis = new FileInputStream("D:\\Akie秋繪 - Lemon(Cover:米津玄師).mp3");
//創建字節輸入流、節點流方式輸出文件
FileOutputStream fos = new FileOutputStream("D:\\copy.mp3");
//根據文件大小做一個字節數組
byte[] arr = new byte[fis.available()];
//將文件上的所有字節讀取到數組中
fis.read(arr);
//將數組中的所有字節一次寫到了文件上
fos.write(arr);
fis.close();
fos.close();
}
}
運行之前:
運行之后:
3.2 按字節流的·處理流寫入寫出文件
FileInputStream:(字節輸入流)
FileOutputStream:(字節輸出流)
BufferedInputStream:(帶緩沖區字節輸入流)
BufferedOutputStream:(帶緩沖區字節輸入流)
帶緩沖區的處理流,緩沖區的作用的主要目的是:避免每次和硬盤打交道,提高數據訪問的效率。
package com.test.io;
import java.io.*;
public class TestBufferedOutputStream {
//創建文件輸入流對象,關聯致青春.mp3
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("D:\\copy.mp3");
//創建緩沖區對fis裝飾
BufferedInputStream bis = new BufferedInputStream(fis);
//創建輸出流對象,關聯copy.mp3
FileOutputStream fos = new FileOutputStream("D:\\copy2.mp3");
//創建緩沖區對fos裝飾
BufferedOutputStream bos = new BufferedOutputStream(fos);
//循環直接輸出
int i;
while((i = bis.read()) != -1) {
bos.write(i);
}
bis.close();
bos.close();
}
}
運行之前:
運行之后:
網絡操作IO講解
我這使用Socket簡單的來模擬網絡編程IO會帶來的問題
不懂Socket可以看我之前的文章,這個東西很容易懂的,就是基于TCP實現的網絡通信,比http要快,很多實現網絡通信的框架都是基于Socket來實現
網絡操作IO編程演變歷史
1 BIO編程會出現什么問題?
- BIO是阻塞的
例子: 阻塞IO(blocking I/O)
A拿著一支魚竿在河邊釣魚,并且一直在魚竿前等,在等的時候不做其他的事情,十分專心。只有魚上鉤的時,才結束掉等的動作,把魚釣上來。
- 看起來沒問題,但是我很多請求一起發送請求資源怎么辦:
那不是要等待第一個人資源完成后后面的人才可以繼續?
因為BIO是阻塞的所以讀取寫出操作都是非常浪費資源的
BIO代碼示例:(后面有代碼,往后移動一點點,認真看,代碼學習量很足
)
我這有三個類,我模擬啟動服務端,然后啟動客戶端,模擬客戶端操作未完成的時候啟動第二個客戶端
- 啟動服務端(
后面有代碼,我這是教運行順序
)
- 啟動第一個客戶端,發現服務器顯示連接成功
先不要在控制臺 輸入 ,模擬堵塞。(我的代碼輸入了就代表請求完成了)
·
- 啟動第二個客戶端,
發現服務端沒效果
,而客戶端連接成功(在堵塞當中)
我這啟動了倆個Client,注意看,(這倆個代碼是一樣的)
·
- 第一個客戶控制臺輸入,輸入完后就會關閉第一個客戶端,
在看服務端發現第二個客戶端連接上來了
·
BIO通信代碼:
- TCP協議Socket使用BIO進行通信:服務端(先執行)
package com.test.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
//TCP協議Socket使用BIO進行通信:服務端
public class BIOServer {
// 在main線程中執行下面這些代碼
public static void main(String[] args) {
//使用Socket進行網絡通信
ServerSocket server = null;
Socket socket = null;
//基于字節流
InputStream in = null;
OutputStream out = null;
try {
server = new ServerSocket(8000);
System.out.println("服務端啟動成功,監聽端口為8000,等待客戶端連接...");
while (true){
socket = server.accept(); //等待客戶端連接
System.out.println("客戶連接成功,客戶信息為:" + socket.getRemoteSocketAddress());
in = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = 0;
//讀取客戶端的數據
while ((len = in.read(buffer)) > 0) {
System.out.println(new String(buffer, 0, len));
}
//向客戶端寫數據
out = socket.getOutputStream();
out.write("hello!".getBytes());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- TCP協議Socket使用BIO進行通信:客戶端(第二執行)
package com.test.io;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
//TCP協議Socket使用BIO進行通信:客戶端
public class Client01 {
public static void main(String[] args) throws IOException {
//創建套接字對象socket并封裝ip與port
Socket socket = new Socket("127.0.0.1", 8000);
//根據創建的socket對象獲得一個輸出流
//基于字節流
OutputStream outputStream = socket.getOutputStream();
//控制臺輸入以IO的形式發送到服務器
System.out.println("TCP連接成功 \n請輸入:");
String str = new Scanner(System.in).nextLine();
byte[] car = str.getBytes();
outputStream.write(car);
System.out.println("TCP協議的Socket發送成功");
//刷新緩沖區
outputStream.flush();
//關閉連接
socket.close();
}
}
- TCP協議Socket使用BIO進行通信:客戶端(第三執行)
package com.test.io;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
//TCP協議Socket:客戶端
public class Client02 {
public static void main(String[] args) throws IOException {
//創建套接字對象socket并封裝ip與port
Socket socket = new Socket("127.0.0.1", 8000);
//根據創建的socket對象獲得一個輸出流
//基于字節流
OutputStream outputStream = socket.getOutputStream();
//控制臺輸入以IO的形式發送到服務器
System.out.println("TCP連接成功 \n請輸入:");
String str = new Scanner(System.in).nextLine();
byte[] car = str.getBytes();
outputStream.write(car);
System.out.println("TCP協議的Socket發送成功");
//刷新緩沖區
outputStream.flush();
//關閉連接
socket.close();
}
}
為了解決堵塞問題,可以使用多線程,請看下面
2 多線程解決BIO編程會出現的問題
這時有人就會說,我多線程不就解決了嗎?
- 使用多線程是可以解決堵塞等待時間很長的問題,因為他可以充分發揮CPU
- 然而系統資源是有限的,不能過多的新建線程,線程過多帶來線程上下文的切換,從來帶來更大的性能損耗
萬一請求越來越多,線程越來越多那我CPU不就炸了?
多線程BIO代碼示例:
- 四個客戶端,這次我多復制了倆個一樣客戶端類
先啟動服務端,在啟動所有客戶端,測試
,發現連接成功(后面有代碼
)
在所有客戶端輸入消息(Client01、Client02這些是我在客戶端輸入的消息
):發現沒有問題
多線程BIO通信代碼:
服務端的代碼,客戶端的代碼還是上面之前的代碼
package com.test.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
//TCP協議Socket使用多線程BIO進行通行:服務端
public class BIOThreadService {
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(8000);
System.out.println("服務端啟動成功,監聽端口為8000,等待客戶端連接... ");
while (true) {
Socket socket = server.accept();//等待客戶連接
System.out.println("客戶連接成功,客戶信息為:" + socket.getRemoteSocketAddress());
//針對每個連接創建一個線程, 去處理I0操作
//創建多線程創建開始
Thread thread = new Thread(new Runnable() {
public void run() {
try {
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = 0;
//讀取客戶端的數據
while ((len = in.read(buffer)) > 0) {
System.out.println(new String(buffer, 0, len));
}
//向客戶端寫數據
OutputStream out = socket.getOutputStream();
out.write("hello".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
為了解決線程太多,這時又來了,線程池
3 線程池解決多線程BIO編程會出現的問題
這時有人就會說,我TM用線程池?
線程池固然可以解決這個問題,萬一需求量還不夠還要擴大線程池。當是這是我們自己靠著自己的思想完成的IO操作,Socket 上來了就去創建線程去搶奪CPU資源,MD,線程都TM做IO去了,CPU也不舒服呀
這時呢:Jdk官方坐不住了,兄弟BIO的問題交給我,我來給你解決:NIO的誕生
線程池BIO代碼示例:
- 四個客戶端
先啟動服務端,在啟動所有客戶端,測試
,(后面有代碼
)
在所有客戶端輸入消息(Client01、Client02這些是我在客戶端輸入的消息
):發現沒有問題
線程池BIO通信代碼:
服務端的代碼,客戶端的代碼還是上面的代碼
package com.test.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//TCP協議Socket使用線程池BIO進行通行:服務端
public class BIOThreadPoolService {
public static void main(String[] args) {
//創建線程池
ExecutorService executorService = Executors.newFixedThreadPool(30);
try {
ServerSocket server = new ServerSocket(8000);
System.out.println("服務端啟動成功,監聽端口為8000,等待客戶端連接...");
while (true) {
Socket socket = server.accept();//等待客戶連接
System.out.println("客戶連接成功,客戶信息為:" + socket.getRemoteSocketAddress());
//使用線程池中的線程去執行每個對應的任務
executorService.execute(new Thread(new Runnable() {
public void run() {
try {
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = 0;
//讀取客戶端的數據
while ((len = in.read(buffer)) > 0) {
System.out.println(new String(buffer, 0, len));
}
//向客戶端寫數據
OutputStream out = socket.getOutputStream();
out.write("hello".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
})
);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
4 使用NIO實現網絡通信
NIO是JDK1.4提供的操作,他的流還是流,沒有改變,服務器實現的還是一個連接一個線程,當是:客戶端發送的連接請求都會注冊到多路復用器上
,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。NIO方式適用于連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,并發局限于應用中,編程比較復雜,JDK1.4之后開始支持。
看不懂介紹可以認真看看代碼實例,其實不難
什么是通道(Channel)
Channel是一個對象,可以通過它讀取和寫入數據。
通常我們都是將數據寫入包含一個或者多個字節的緩沖區,然后再將緩存區的數據寫入到通道中,將數據從通道讀入緩沖區,再從緩沖區獲取數據。
Channel 類似于原I/O中的流(Stream),但有所區別:
- 流是單向的,通道是雙向的,可讀可寫。
- 流讀寫是阻塞的,通道可以異步讀寫。
什么是選擇器(Selector)
Selector可以稱他為通道的集合,每次客戶端來了之后我們會把Channel注冊到Selector中并且我們給他一個狀態,在用死循環來環判斷(判斷是否做完某個操作,完成某個操作后改變不一樣的狀態
)狀態是否發生變化,知道IO操作完成后在退出死循環
什么是Buffer(緩沖區)
Buffer 是一個緩沖數據的對象, 它包含一些要寫入或者剛讀出的數據。
在普通的面向流的 I/O 中,一般將數據直接寫入或直接讀到 Stream 對象中。當是有了Buffer(緩沖區)后,數據第一步到達的是Buffer(緩沖區)中
緩沖區實質上是一個數組(底層完全是數組實現的,感興趣可以去看一下
)。通常它是一個字節數組,內部維護幾個狀態變量,可以實現在同一塊緩沖區上反復讀寫(不用清空數據再寫)。
代碼實例:
- 目錄結構
- 運行示例,先運行服務端,在運行所有客戶端控制臺輸入消息就好了。:
我這客戶端和服務端代碼有些修該變,后面有代碼
服務端示例,先運行,想要搞定NIO請認真看代碼示例,真的很清楚
package com.test.io;
import com.lijie.iob.RequestHandler;
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws IOException {
//111111111
//Service端的Channel,監聽端口的
ServerSocketChannel serverChannel = ServerSocketChannel.open();
//設置為非阻塞
serverChannel.configureBlocking(false);
//nio的api規定這樣賦值端口
serverChannel.bind(new InetSocketAddress(8000));
//顯示Channel是否已經啟動成功,包括綁定在哪個地址上
System.out.println("服務端啟動成功,監聽端口為8000,等待客戶端連接..."+ serverChannel.getLocalAddress());
//22222222
//聲明selector選擇器
Selector selector = Selector.open();
//這句話的含義,是把selector注冊到Channel上面,
//每個客戶端來了之后,就把客戶端注冊到Selector選擇器上,默認狀態是Accepted
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
//33333333
//創建buffer緩沖區,聲明大小是1024,底層使用數組來實現的
ByteBuffer buffer = ByteBuffer.allocate(1024);
RequestHandler requestHandler = new RequestHandler();
//444444444
//輪詢,服務端不斷輪詢,等待客戶端的連接
//如果有客戶端輪詢上來就取出對應的Channel,沒有就一直輪詢
while (true) {
int select = selector.select();
if (select == 0) {
continue;
}
//有可能有很多,使用Set保存Channel
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//使用SelectionKey來獲取連接了客戶端和服務端的Channel
SelectionKey key = iterator.next();
//判斷SelectionKey中的Channel狀態如何,如果是OP_ACCEPT就進入
if (key.isAcceptable()) {
//從判斷SelectionKey中取出Channel
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//拿到對應客戶端的Channel
SocketChannel clientChannel = channel.accept();
//把客戶端的Channel打印出來
System.out.println("客戶端通道信息打印:" + clientChannel.getRemoteAddress());
//設置客戶端的Channel設置為非阻塞
clientChannel.configureBlocking(false);
//操作完了改變SelectionKey中的Channel的狀態OP_READ
clientChannel.register(selector, SelectionKey.OP_READ);
}
//到此輪訓到的時候,發現狀態是read,開始進行數據交互
if (key.isReadable()) {
//以buffer作為數據橋梁
SocketChannel channel = (SocketChannel) key.channel();
//數據要想讀要先寫,必須先讀取到buffer里面進行操作
channel.read(buffer);
//進行讀取
String request = new String(buffer.array()).trim();
buffer.clear();
//進行打印buffer中的數據
System.out.println(String.format("客戶端發來的消息: %s : %s", channel.getRemoteAddress(), request));
//要返回數據的話也要先返回buffer里面進行返回
String response = requestHandler.handle(request);
//然后返回出去
channel.write(ByteBuffer.wrap(response.getBytes()));
}
iterator.remove();
}
}
}
}
- 客戶端示例:(
我這用的不是之前的了,有修改
)運行起來客戶端控制臺輸入消息就好了。
要模擬測試,請復制粘貼改一下,修改客戶端的類名就行了,四個客戶端代碼一樣的
,
package com.test.io;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
//TCP協議Socket:客戶端
public class Client01 {
public static void main(String[] args) throws IOException {
//創建套接字對象socket并封裝ip與port
Socket socket = new Socket("127.0.0.1", 8000);
//根據創建的socket對象獲得一個輸出流
OutputStream outputStream = socket.getOutputStream();
//控制臺輸入以IO的形式發送到服務器
System.out.println("TCP連接成功 \n請輸入:");
while(true){
byte[] car = new Scanner(System.in).nextLine().getBytes();
outputStream.write(car);
System.out.println("TCP協議的Socket發送成功");
//刷新緩沖區
outputStream.flush();
}
}
}
5 使用Netty實現網絡通信
Netty是由JBOSS提供的一個Java開源框架。Netty提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。
Netty 是一個基于NIO的客戶、服務器端編程框架,使用Netty 可以確保你快速和簡單的開發出一個網絡應用,例如實現了某種協議的客戶,服務端應用。Netty相當簡化和流線化了網絡應用的編程開發過程,例如,TCP和UDP的Socket服務開發。
Netty是由NIO演進而來,使用過NIO編程的用戶就知道NIO編程非常繁重,Netty是能夠能跟好的使用NIO
Netty的原里就是NIO,他是基于NIO的一個完美的封裝,并且優化了NIO,使用他非常方便,簡單快捷
我直接上代碼:
- 1、先添加依賴:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.16.Final</version>
</dependency>
- 2、NettyServer 模板,看起來代碼那么多,
其實只需要添加一行消息就好了
請認真看中間的代碼
package com.lijie.iob;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.codec.string.StringDecoder;
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast("encoder", new ObjectEncoder());
pipeline.addLast(" decoder", new io.netty.handler.codec.serialization.ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
//重點,其他的都是復用的
//這是真正的I0的業務代碼,把他封裝成一個個的個Hand1e類就行了
//把他當成 SpringMVC的Controller
pipeline.addLast(new NettyServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture f = b.bind(8000).sync();
System.out.println("服務端啟動成功,端口號為:" + 8000);
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
- 3、需要做的IO操作,重點是繼承ChannelInboundHandlerAdapter類就好了
package com.lijie.iob;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
RequestHandler requestHandler = new RequestHandler();
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
System.out.println(String.format("客戶端信息: %s", channel.remoteAddress()));
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Channel channel = ctx.channel();
String request = (String) msg;
System.out.println(String.format("客戶端發送的消息 %s : %s", channel.remoteAddress(), request));
String response = requestHandler.handle(request);
ctx.write(response);
ctx.flush();
}
}
- 4 客戶的代碼還是之前NIO的代碼,我在復制下來一下吧
package com.test.io;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
//TCP協議Socket:客戶端
public class Client01 {
public static void main(String[] args) throws IOException {
//創建套接字對象socket并封裝ip與port
Socket socket = new Socket("127.0.0.1", 8000);
//根據創建的socket對象獲得一個輸出流
OutputStream outputStream = socket.getOutputStream();
//控制臺輸入以IO的形式發送到服務器
System.out.println("TCP連接成功 \n請輸入:");
while(true){
byte[] car = new Scanner(System.in).nextLine().getBytes();
outputStream.write(car);
System.out.println("TCP協議的Socket發送成功");
//刷新緩沖區
outputStream.flush();
}
}
}
運行測試,還是之前那樣,啟動服務端,在啟動所有客戶端控制臺輸入就好了:
智能推薦
BIO,NIO,AIO網絡編程——java
一、BIO 阻塞輸入輸出,傳統得tcp和udp編程形式,服務端為每一個客戶端之間建立專線連接。 前面得信息處理會影響后面得信息處理。 二、NIO 非阻塞輸入輸出。一個線程同時管理多個連接,減少線程多的壓力,不是真的異步操作。 Selector:多路選擇器 Channel:通道 Buffer:緩沖區 服務端方法: 1.建立服務端通道并且在...
網絡編程之BIO、NIO、AIO
TCP直連Socket與ServerSocket通信 Server.java ServerHandler.java 啟動Server Client.java Eclispe的client、server輸出如下: 每次建立連接都要新啟動一個線程,而線程會占用一定的資源。如果Client與Server建立的連接很多,就會創建很多的線程,ServerSocket所在的機器可能會出現資源逐步...
結合代碼詳細聊聊 Java 網絡編程中的 BIO、NIO 和 AIO
本文從操作系統的角度來解釋BIO,NIO,AIO的概念,含義和背后的那些事。本文主要分為3篇。 第一篇 講解BIO和NIO以及IO多路復用 第二篇 講解磁盤IO和AIO 第三篇 講解在這些機制上的一些應用的實現方式,比如nginx,nodejs,Java NIO等 到底什么是“IO Block” 很多人說BIO不好,會“block&rdquo...
day25【MappedByteBuffer、網絡編程、Selector選擇器、NIO2-AIO(異步、非阻塞)】課上
1.使用MappedByteBuffer復制超過2G的文件(理解) 1.圖解 2.代碼演示 2.網絡編程收發信息 (掌握) 1.客戶端 小結: 1.創建客戶端對象: int write(ByteBuffer src) 將字節序列從給定的緩沖區中寫入此通道 abstract int read(ByteBuffer dst) 將字節序列從此通道中讀入給定的緩沖區。 2.服務器端 小結: 1.創建服務器...
猜你喜歡
freemarker + ItextRender 根據模板生成PDF文件
1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...
電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!
Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...
requests實現全自動PPT模板
http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...
Linux C系統編程-線程互斥鎖(四)
互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...