一、什麼是NIO
1.概念
NIO是java1.4中引入的,被稱為new I/O,也有說是non-blocking I/O,NIO被成為同步非阻塞的IO。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5yMzMTOzATN2QGOmZGZxYzXyAzMwgTMzEzLcFDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
image.png
2.跟BIO流的差別
-
- BIO是面向流的,NIO是面向塊(緩沖區)的。
- BIO的流都是同步阻塞的,而NIO是同步非阻塞的。
- NIO會等待資料全部傳輸過來再讓線程處理,BIO是直接讓線程等待。
- NIO有選擇器,而BIO沒有。
- NIO是采用管道和緩存區的形式來處理資料的,而BIO是采用輸入輸出流來處理的。
- NIO是可以雙向的,BIO隻能夠單向。
image
二、NIO常用元件Channel和Buffer的使用
1.代碼
這裡以檔案複制為例
public class test {
public static void main(String[] args){
try{
//存在的照片
File inFile=new File("C:\\Users\\Administrator\\Desktop\\study.PNG");
//複制後要存放照片的位址
File outFile=new File("C:\\Users\\Administrator\\Desktop\\study1.PNG");
//打開流
FileInputStream fileInputStream=new FileInputStream(inFile);
FileOutputStream fileOutputStream=new FileOutputStream(outFile);
/**
* RandomAccessFile accessFile=new RandomAccessFile(inFile,"wr");
* FileChannel inFileChannel=accessFile.getChannel();
* 和下面兩行代碼是一樣的,都是可以拿到FileChannel
*/
//擷取Channel
FileChannel inFileChannel=fileInputStream.getChannel();
FileChannel outFileChannel=fileOutputStream.getChannel();
//建立buffer
ByteBuffer buffer=ByteBuffer.allocate(1024*1024);
//讀取到buffer中
while (inFileChannel.read(buffer)!=-1){
//翻轉一下,就可以讀取到全部資料了
buffer.flip();
outFileChannel.write(buffer);
//讀取完後要clear
buffer.clear();
}
//關閉
inFileChannel.close();
outFileChannel.close();
fileInputStream.close();
fileOutputStream.close();
}catch (Exception e){}
}
}
我的桌面上的确多了一張一模一樣的圖檔
2.解釋
使用NIO的話,需要注意幾個步驟:
-
- 打開流
- 擷取通道
- 建立Buffer
- 切換到讀模式 buffer.flip()
- 切換到寫模式 buffer.clear(); 其實這裡也看不出來它是怎麼使用緩沖區的,上面這段代碼中的while循環的作用和下面的代碼是一樣的
while ((i=fileInputStream.read())!=-1){
fileOutputStream.write(i);
}
image.png
讓我們趕緊開始NIO的程式設計
三、BIO和NIO的差別
學習了Channel和Buffer的使用,我們就可以正式進入NIO的開發了
代碼
NIO
NIO服務端:隻是接受用戶端發送過來的資料,然後列印在控制台
/**
* NIO
* @author xuxiaobai
*/
public class NIOTest {
private final static int port = 8080;
public static void main(String[] args) throws IOException {
//啟動服務端
TCPServer();
}
/**
* TCP服務端
* 接受TCP
*
* @throws IOException
*/
public static void TCPServer() throws IOException {
//建立服務端多路複用選擇器
Selector selector = Selector.open();
//建立服務端SocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//定義位址
InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
//綁定位址
serverSocketChannel.bind(inetSocketAddress);
System.out.println("綁定成功:" + inetSocketAddress);
//設定為非阻塞
serverSocketChannel.configureBlocking(false);
//注冊服務端選擇端,隻接受accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//加上延時,什麼原理我忘記了,隻知道是為了防止死鎖
selector.select(500);
//周遊服務端選擇器的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
if (!next.isValid()) {
//該key無效直接跳過
continue;
}
//注意
if (next.isAcceptable()) {
//1\. accept事件
//接收到accept事件,拿到channel,這個是服務端SocketChannel
ServerSocketChannel channel = (ServerSocketChannel) next.channel();
//accept得到連接配接用戶端的channel
SocketChannel accept = channel.accept();
accept.configureBlocking(false);
//注冊write事件
accept.register(selector, SelectionKey.OP_READ);
iterator.remove();
} else if (next.isReadable()) {
//2\. read事件
//開啟一個新的線程
Thread thread = new Thread(() -> {
SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.clear();
try {
channel.read(byteBuffer);
//開始處理資料
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String x = new String(bytes);
if(x.equals("")){
//老是會莫名其妙地列印一些空行,打個更新檔
return;
}
System.out.println(x);
if ("exit".equals(x)) {
//關閉通道
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
next.cancel();
}
} catch (IOException e) {
//出現異常的處理
e.printStackTrace();
try {
channel.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
next.cancel();
}
});
iterator.remove();
thread.start();
}
}
}
}
}
BIO
BIO服務端:接受用戶端的資料,然後列印在控制台
BIO用戶端:向服務端發送資料。NIO的測試中也使用這個用戶端進行測試
/**
* BIO
* @author xuxiaobai
*/
public class BIOTest {
private final static int port = 8080;
public static void main(String[] args) throws IOException {
TCPClient();
// TCPServer();
}
/**
* TCP用戶端
* 發送TCP
* @throws IOException
*/
private static void TCPClient() throws IOException {
SocketChannel socketChannel = SocketChannel.open();
//定義位址
InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
//連接配接
socketChannel.connect(inetSocketAddress);
System.out.println("連接配接成功:"+inetSocketAddress);
Scanner scanner = new Scanner(System.in);
while (true) {
String next = scanner.next();
//直接包裝一個buffer
ByteBuffer wrap = ByteBuffer.wrap(next.getBytes());
//寫入
socketChannel.write(wrap);
if ("exit".equals(next)) {
//等于exit時關閉channel
socketChannel.close();
break;
}
}
}
/**
* TCP服務端
* 接受TCP
* @throws IOException
*/
private static void TCPServer() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//定義位址
InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
//綁定
serverSocketChannel.bind(inetSocketAddress);
System.out.println("綁定成功:"+inetSocketAddress);
while (true) {
//接受連接配接
SocketChannel accept = serverSocketChannel.accept();
new Thread(new Runnable() {
@Override
public void run() {
//定義一個緩沖區,讀出來的資料超出緩沖區的大小時會被丢棄
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {
try {
//每次使用前都要清空,但這裡沒有真的區clear資料,隻是移動了buffer裡面的下标
byteBuffer.clear();
//讀取資料到緩沖區
accept.read(byteBuffer);
//每次讀取資料前都要flip一下,這裡都移動下标
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
//擷取資料
byteBuffer.get(bytes);
String x = new String(bytes);
System.out.println(x);
if (x.equals("exit")) {
//當讀出來的資料等于exit時退出
accept.close();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
//啟動該線程
}
}
}
image.png
搞完了代碼,讓我們來看看代碼的示範效果————從用戶端發送資料到服務端,下面展示一下效果:
先後啟動BIO的TCPServer和TCPClient方法;
TCPClient:
image
TCPServer:
image
步驟
image
畫了個圖來表示,這是關于selector的配置流程,在循環中根據不同key值所進行的操作,跟上面檔案複制的例子差不多了,隻不過這裡的Channel是通過 key.channel()獲得的。
image
差别
我們來看看一下BIO和NIO的差别。
BIO
我們用IDEA的debug啟動BIO的服務端,然後在啟動多個用戶端。
image
我這裡啟動了三個用戶端,可以看到有三個線程已經建立好了,然而我這時還沒有發送資料到服務端。
NIO
我們用IDEA的debug啟動NIO的服務端,然後在啟動多個BIO用戶端。
image
這裡啟動了多個用戶端,伺服器上沒有多餘的幾個線程。
修改BIO的TCPClient方法
private static void TCPClient() throws IOException {
SocketChannel socketChannel = SocketChannel.open();
//定義位址
InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), port);
//連接配接
socketChannel.connect(inetSocketAddress);
System.out.println("連接配接成功:" + inetSocketAddress);
Scanner scanner = new Scanner(System.in);
while (true) {
String next = scanner.next();
//直接包裝一個buffer
// ByteBuffer wrap = ByteBuffer.wrap(next.getBytes());
//寫入
while (true) {
try {
//休眠
//注意,休眠時間建議調高一點
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
socketChannel.write(ByteBuffer.wrap(next.getBytes()));
}
// if ("exit".equals(next)) {
// //等于exit時關閉channel
// socketChannel.close();
// break;
// }
}
}
休眠時間記得調高點!!!當機警告!
image.png
這樣用戶端就會在讀取到第一次時,一直發送這個資料,可以看到一些線程,也是隻有在收到資料之後才會建立這個線程去列印這個資料。如果休眠時間調高一點的話,就會看到有時候這裡會一閃一閃的,調低後就會出現一閃而過的很多線程,如下圖。