前言
前面通過Socket實現了一個簡單的聊天系統,且對Socket進行了一定的瞭解
Java網絡編程(3)Socket實現一個簡單的聊天系統
而前面的Socket都是通過IO實現的
現在來系統的瞭解IO與NIO
目錄
- Java的IO演變
1.1. BIO
1.2. 僞異步IO
1.3. NIO
1.4. AIO - NIO結構
2.1. 緩衝區Buffer
2.2. 通道Channel
2.3. 多複用選擇器Selector - 緩衝區操作
3.1. ByteBuffer - 通道Channel
4.1. 常用操作 - 緩衝區與通道:分散、聚集
5.1. 案例 - 選擇器Selector
6.1. 常用方法
6.2. Selector的使用
6.3. Selector案例 - 總結
Java的IO演變
BIO
在jdk1.4之前,Java的Socket通信都是通過同步阻塞模式BIO(block-IO)
同步阻塞式模式在應用時性能和可靠性是非常差的
在前面的應用也可以看出:因爲是阻塞式,一個線程只能實現一個通信,在高併發會消耗太多資源
一客戶端一線程形式
僞異步IO
前面使用線程池完成多客戶端連接服務器,就是這種僞異步IO
//創建線程池:限定最多50個線程
ExecutorService executor= Executors.newFixedThreadPool(50);
while (true) {
//接受連接,創建socket
Socket socket = serverSocket.accept();
System.out.println("IP地址:" + socket.getLocalAddress());
//線程
Runnable runnable=()->{
try {
BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
String str=null;
while ((str=reader.readLine())!=null){
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
};
executor.submit(runnable);
}
JDK的線程池維護一個消息隊列和N個活躍線程對消息隊列中的任務進行處理,有一定的效率但本質上還是BIO
線程池的運用有一定的弊端:當網絡傳輸慢會阻塞線程,阻塞的線程過多會影響線程池效率甚至崩潰
NIO
爲了解決網絡通信問題,jdk1.4推出了非阻塞模式NIO
NIO可以稱New IO,也可以稱Non-block IO
NIO在Java代碼提供了高速的、面向塊的IO,提供了很多API和類庫
AIO
jdk1.7提供了異步非阻塞IO - AIO,支持文件的異步IO操作和針對網絡套接字的異步操作等等
一步步來,慢慢理解
NIO結構
NIO:非阻塞式IO,可以稱爲New IO,或者Non-block IO
它與BIO不同在與通道Channel與緩衝區Buffer、多複用選擇器Selector三個重要組件
緩衝區Buffer
緩衝區Buffer是一個對象,包含了一些要寫入讀出的數據
以前的IO是面向流,通過直接流讀寫數據
Java程序直接讀出或寫入流就可以通信了
當然現在的IO也有緩衝,例如BufferedReader等,這些都是NIO重新實現過了
在NIO庫中,所有的數據都是緩衝區處理,緩衝區實質上是一個數組,常用的是字節數組ByteBuffer
所有的緩衝區類型都繼承於抽象類Buffer,對於Java中的基本類型,都有一個具體Buffer類型與之相對應(除了Boolean類型)
通道Channel
在Java NIO中,通道是在實體和字節緩衝區之間有效傳輸數據的媒介
通道在實體與緩衝區之間,通過通道來讀取、寫入數據
通道的作用於流相似,但不同的是通道是雙工的,可以同時進行讀、寫
和傳統IO分爲 File IO與Stream IO類似,NIO有兩種類型的通道:文件通道(file)和套接字通道(socket)
多複用選擇器Selector
多複用選擇器Selector是NIO編程的重點
選擇器用於使用單個線程處理多個通道,它會輪詢註冊在其上的通道,確定哪個通道準備好通信,通過SelectionKey獲得就緒Channel的集合,然後進行IO操作
選擇器只能管理非阻塞的通道
這就比僞異步IO的線程池方便多了,通過選擇器單線程即可處理多個Channel
緩衝區操作
所有緩衝區類型繼承抽象類Buffer,大部分緩衝區類型的操作都類似,僅學習一下最常用的ByteBuffer的操作(能與channel交互的只有ByteBuffer)
ByteBuffer
實例化:
Buffer、ByteBuffer等類都是抽象類
抽象類無法實例化
ByteBuffer提供了四個靜態工廠方法得到ByteBuffer實例
這四個方法:
-
allocate(int capacity)
從堆空間中分配一個容量大小爲capacity的byte數組作爲緩衝區的byte數據存儲器(HeapByteBuffer實例) -
allocateDirect(int capacity)
是不使用JVM堆棧而是通過操作系統來創建內存塊用作緩衝區,它與當前操作系統能夠更好的耦合,因此能進一步提高I/O操作速度。但是分配直接緩衝區的系統開銷很大,因此只有在緩衝區較大並長期存在,或者需要經常重用時,才使用這種緩衝區 -
wrap(byte[] array)
這個緩衝區的數據會存放在byte數組中,bytes數組或buff緩衝區任何一方中數據的改動都會影響另一方。其實ByteBuffer底層本來就有一個bytes數組負責來保存buffer緩衝區中的數據,通過allocate方法系統會幫你構造一個byte數組(本質也是HeapByteBuffer實例) -
wrap(byte[] array, int offset, int length)
在上一個方法的基礎上可以指定偏移量和長度,這個offset也就是包裝後byteBuffer的position,而length呢就是limit-position的大小,從而我們可以得到limit的位置爲length+position(offset)
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
ByteBuffer byteBuffer1=ByteBuffer.allocateDirect(1024);
ByteBuffer byteBuffer2=ByteBuffer.wrap(new byte[]{});
ByteBuffer byteBuffer3=ByteBuffer.wrap(new byte[]{},0,100);
get方法:
四種參數四種get方法:
- get():相對方法,讀取當前位置的byte,然後position +1
- get(byte[] dst):相對體積方法,將此緩衝區傳輸到給定的目標數組中的字節數
- get(byte[] dst, int offset, int length) :從當前位置開始相對讀,讀length個byte,並寫入dst下標從offset到offset+length的區域
- get(int index) : 絕對方法,從指定下標開始讀取
其他方法:
asIntBuffer()等:輸入的數據可能是其他類型,可以使用這類方法將ByteBuffer轉化成想要的類型
flip():翻轉
put():放置
等等
通道Channel
channel類繼承結構:
有很多種channel,分爲兩種類型:文件通道、套接字通道
常用的有:
- FileChannel:用於讀取、寫入、映射和操作文件的通道
- DatagramChannel:讀寫UDP通信的數據,對應DatagramSocket類
- SocketChannel:讀寫TCP通信的數據,對應Socket類
- ServerSocketChannel:監聽新的TCP連接,並且會創建一個可讀寫的SocketChannel,對應ServerSocket類(服務器)
- ScatteringByteChannel和GatheringByteChannel:分散聚集通道,由操作系統完成
- WritableByteChannel和ReadableByteChannel:接口提供讀寫API
常用操作:
- 實例化:文件通道使用流的getChannel()方法創建,套接字通道使用open()方法直接打開
- isOpen():Channel自帶的方法,告訴這個通道是否打開
- close: Channel自帶的方法,關閉通道
- read() : Channel大部分子類擁有的方法,從通道讀取數據到緩衝區,不同的參數有不同的作用,FileChannel有四種read方法
- write():Channel大部分子類擁有的方法,從緩衝區寫入數據到通道,FileChannel有四種write方法
緩衝區與通道:分散、聚集
前面知道了通道類似與流,緩衝區暫時保存數據
那麼程序與實體間數據交流就是通過緩衝區與通道的分散讀取、聚集寫入
分散讀取:將數據從通道中讀取到多個緩衝區(read方法)
聚集寫入:將多個緩衝區的數據寫入到單個通道中(write方法)
案例
聚集寫入文件,分散讀出文件
package com.company.ScatterGather;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.FileChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ScatteringByteChannel;
public class ScatterGatherIO {
//聚集寫入
public static void Gather(String data) throws FileNotFoundException {
//創建兩個ByteBuffer存數據
ByteBuffer byteBuffer1=ByteBuffer.allocate(20);
ByteBuffer byteBuffer2=ByteBuffer.allocate(400);
//把整數放入byteBuffer1
byteBuffer1.asIntBuffer().put(1024);
//把輸入的String變量放入byteBuffer2
byteBuffer2.asCharBuffer().put(data);
//GatheringByteChannel接口允許委託操作系統完成任務
//CreatChanner使用文件寫入流
GatheringByteChannel gatherChannel=CreatChanner("TestOut.txt",true);
//聚集寫入通道
try {
//write只允許一個ByteBuffer
gatherChannel.write(new ByteBuffer[]{byteBuffer1,byteBuffer2});
} catch (IOException e) {
e.printStackTrace();
}
}
//分散寫出
public static void Scatter() throws FileNotFoundException {
//創建兩個ByteBuffer存數據
ByteBuffer byteBuffer1=ByteBuffer.allocate(20);
ByteBuffer byteBuffer2=ByteBuffer.allocate(400);
//讀取文件通道
ScatteringByteChannel scatterChannel=CreatChanner("TestOut.txt",false);
try {
scatterChannel.read(new ByteBuffer[]{byteBuffer1,byteBuffer2});
} catch (IOException e) {
e.printStackTrace();
}
//buffer位置置0
byteBuffer1.rewind();
byteBuffer2.rewind();
System.out.println(byteBuffer1.asIntBuffer().get());
System.out.println(byteBuffer2.asCharBuffer().toString());
}
//輸入文件地址和輸入方向,決定通道方向
public static FileChannel CreatChanner(String fileUrl,boolean out) throws FileNotFoundException {
FileChannel fileChannel=null;
if (out){
fileChannel=new FileOutputStream(fileUrl).getChannel();
}
else
fileChannel=new FileInputStream(fileUrl).getChannel();
return fileChannel;
}
public static void main(String[] args) throws FileNotFoundException {
String data="hello,welcome to ScatterGatherIO";
Gather(data);
Scatter();
}
}
上面展示將通道與緩衝區的使用
選擇器Selector
選擇器讓一個線程能夠處理多個通道,選擇器輪詢註冊在其上的通道,Selector只能管理非阻塞的通道,文件通道(FileChannel等等)是阻塞的,無法管理
常用方法
- open():Selector是抽象類,實例化要通過Selector.open()方法
- select():選擇一組鍵,該通道爲IO操作準備,這個方法會阻塞, 直到註冊在 Selector 中的 Channel 發送可讀寫事件,當這個方法返回後, 當前線程就可以處理 Channel 的事件(返回int型數據,大於0即有多少個通道就緒)
- selectedKeys():返回準備好的通道集合,返回值是Set< SelectionKey>集合型,SelectionKey是就緒通道的標識
- wakeup():喚醒在select()方法中阻塞的線程
Selector的使用
案例
服務器:
package com.company.Selector;
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;
public class Server {
public static void main(String[] args) throws IOException {
//打開ServerSocketChannel通道,等待連接
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//設置非阻塞
serverSocketChannel.configureBlocking(false);
//綁定端口號
serverSocketChannel.bind(new InetSocketAddress(8080));
//打開選擇器
Selector selector = Selector.open();
//將通道註冊到選擇器上,監聽接收事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
//輪詢式的獲取選擇器上已經‘準備就緒’的事件
while (selector.select()>0){
//獲取當前選擇器中所有註冊的"選擇健(已就緒的監聽事件)"
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
//SelectionKey表示註冊的標識
SelectionKey selectionKey = iterator.next();
//判斷具體事件,就緒
if (selectionKey.isAcceptable()){
//serverSocketChannel接受客戶端連接,返回SocketChannel通道
SocketChannel socketChannel = serverSocketChannel.accept();
//設置非阻塞
socketChannel.configureBlocking(false);
//將客戶端通道註冊到選擇器上
//OP_READ表示通道可讀
socketChannel.register(selector,SelectionKey.OP_READ);
}else if (selectionKey.isReadable()){
//獲取當前選擇器上“讀就緒”狀態的通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//讀取客戶端傳過來的數據
int len = 0;
while ((len = socketChannel.read(buffer))>0){
buffer.flip();
System.out.println(new String(buffer.array(),0,len));
buffer.clear();
}
}
//取消選擇鍵selectionKey
iterator.remove();
}
}
}
}
客戶端:
package com.company.Selector;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
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 {
public static void main(String[] args) throws IOException {
//打開客戶端通道SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
//設置爲非堵塞模式
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//發送數據給服務端
//控制檯輸入數據
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String msg = scanner.next();
//寫入緩存
byteBuffer.put(msg.getBytes());
//byteBuffer切換模式:讀模式
byteBuffer.flip();
//讀取byteBuffer的數據
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
//關閉連接
socketChannel.close();
}
}
這裏通過選擇器實現了服務器,當然客戶端沒有使用選擇器,即這個聊天系統只能客戶端寫入,服務器讀
總結
- 大致瞭解了Java IO的發展
- BIO:阻塞式IO;僞異步IO:通過線程池完成BIO;NIO:非阻塞式IO;AIO:異步非阻塞式IO
- NIO有三個重要的部件:緩衝區、通道、選擇器
- 緩衝區是一個數組,保存要輸入輸出的數據
- 通道與流類似,是在實體和字節緩衝區之間有效傳輸數據的媒介,可以雙向傳輸
- 選擇器讓一個線程能夠處理多個通道,只能管理非阻塞的通道