一、什么是NIO
1.概念
NIO是JAVA1.4中引入的,被稱為new I/O,也有說是non-blocking I/O,NIO被成為同步非阻塞的IO。
2.跟BIO流的區別
- BIO是面向流的,NIO是面向塊(緩沖區)的。
- BIO的流都是同步阻塞的,而NIO是同步非阻塞的。
- NIO會等待數據全部傳輸過來再讓線程處理,BIO是直接讓線程等待。
- NIO有選擇器,而BIO沒有。
- NIO是采用管道和緩存區的形式來處理數據的,而BIO是采用輸入輸出流來處理的。
- NIO是可以雙向的,BIO只能夠單向。
二、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);
}
讓我們趕緊開始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();
//啟動該線程
}
}
}
搞完了代碼,讓我們來看看代碼的演示效果————從客戶端發送數據到服務端,下面展示一下效果:
先后啟動BIO的TCPServer和TCPClient方法;
TCPClient:
TCPServer:
步驟
畫了個圖來表示,這是關于selector的配置流程,在循環中根據不同key值所進行的操作,跟上面文件復制的例子差不多了,只不過這里的Channel是通過 key.channel()獲得的。
差別
我們來看看一下BIO和NIO的差別。
BIO
我們用IDEA的debug啟動BIO的服務端,然后在啟動多個客戶端。
我這里啟動了三個客戶端,可以看到有三個線程已經創建好了,然而我這時還沒有發送數據到服務端。
NIO
我們用IDEA的debug啟動NIO的服務端,然后在啟動多個BIO客戶端。
這里啟動了多個客戶端,服務器上沒有多余的幾個線程。
修改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;
// }
}
}
復制代碼
休眠時間記得調高點!!!宕機警告!
這樣客戶端就會在讀取到第一次時,一直發送這個數據,可以看到一些線程,也是只有在收到數據之后才會創建這個線程去打印這個數據。如果休眠時間調高一點的話,就會看到有時候這里會一閃一閃的,調低后就會出現一閃而過的很多線程,如下圖。
四、總結
BIO的話,每次網絡請求過來之后,服務器都是會為這個請求創建一個線程,這個線程會一直等待這個請求后續的數據,等處理完成后才會銷毀這個線程;而NIO,當每次網絡請求過來時,服務器不會馬上創建一個線程去處理這個請求,而是會交給一個Selector線程,只有這個請求后續的數據全部傳輸過來后,Selector才會去通知其他其他線程或者創建一個線程來處理這個請求。
這就是NIO和BIO最大的差別,只有數據傳輸到服務器時才會讓線程去處理,減少了線程的空等待,大部分情況下可以采用線程池的方式來處理數據,可以提高線程的利用率。