目錄
1 nio簡介
Java NIO(New IO)是從Java 1.4版本開始引入的,一個新的IO API,可以替代標準的Java IO API。NIO與原來的IO有同樣的作用和目的,但是使用的方式完全不同,NIO支持面向緩衝區的、基於通道的IO操作。NIO將以更加高效的方式進行文件的讀寫操作。
Java NIO系統的核心在於:通道(Channel)和緩衝區(Buffer)。通道表示打開到 IO 設備(例如:文件、套接字)的連接。若需要使用 NIO 系統,需要獲取用於連接 IO 設備的通道以及用於容納數據的緩衝區。然後操作緩衝區,對數據進行處理。
1.1 緩衝區(Buffer)
在 Java NIO 中負責數據的存取。緩衝區就是數組。用於存儲不同數據類型的數據。根據數據類型不同(boolean 除外),提供了相應類型的緩衝區:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer,上述緩衝區的管理方式幾乎一致,通過 allocate() 獲取緩衝區。
緩衝區存取數據的兩個核心方法:
put() : 存入數據到緩衝區中
get() : 獲取緩衝區中的數據
1.2 通道(Channel)
用於源節點與目標節點的連接。在 Java NIO 中負責緩衝區中數據的傳輸。Channel 本身不存儲數據,因此需要配合緩衝區進行傳輸。
1.2.1 通道的主要實現類
* java.nio.channels.Channel 接口
|--FileChannel
|--SocketChannel
|--ServerSocketChannel
|--DatagramChannel
1.2.2 獲取通道
Java 針對支持通道的類提供了getChannel() 方法
本地 IO:
FileInputStream/FileOutputStream
RandomAccessFile
網絡IO:
Socket
ServerSocket
DatagramSocket
在JDK 1.7中的NIO.2針對各個通道提供了靜態方法open()
在JDK 1.7 中的 NIO.2 的 Files 工具類的 newByteChannel()
1.2.3 通道之間的數據傳輸
transferFrom()
transferTo()
1.2.4 分散(Scatter)與聚集(Gather)
分散讀取(Scattering Reads):將通道中的數據分散到多個緩衝區中
聚集寫入(Gathering Writes):將多個緩衝區中的數據聚集到通道中
1.2.5 字符集:Charset
編碼:字符串 -> 字節數組
解碼:字節數組 -> 字符串
2 緩衝區的4個核心屬性
capacity : 容量,表示緩衝區中最大存儲數據的容量。一旦聲明不能改變。
limit : 界限,表示緩衝區中可以操作數據的大小。(limit 後數據不能進行讀寫)
position : 位置,表示緩衝區中正在操作數據的位置。
mark : 標記,表示記錄當前 position 的位置。可以通過 reset() 恢復到 mark 的位置
0 <= mark <= position <= limit <= capacity
2.1 初始化緩衝區
//分配直接緩衝區
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
@Test
public void testCorePropertity(){
System.out.println("初始化緩衝區");
System.out.println("position:" + buf.position());
System.out.println("limit:" + buf.limit());
System.out.println("capacity:" + buf.capacity());
}
輸出:
初始化緩衝區
position:0
limit:1024
capacity:1024
2.2 利用 put() 存入數據到緩衝區中
buf.put("abcde".getBytes());
System.out.println("存入數據");
System.out.println("position:" + buf.position());
System.out.println("limit:" + buf.limit());
System.out.println("capacity:" + buf.capacity());
輸出:
存入數據
position:5
limit:1024
capacity:1024
2.3 切換讀取數據模式
buf.flip();
System.out.println("開始讀取數據");
System.out.println("position:" + buf.position());
System.out.println("limit:" + buf.limit());
System.out.println("capacity:" + buf.capacity());
開始讀取數據
position:0
limit:5
capacity:1024
2.4 讀取數據
byte[] dst = new byte[buf.limit()];
buf.get(dst);
System.out.println("讀取數據:" + new String(dst, 0, dst.length));
System.out.println("position:" + buf.position());
System.out.println("limit:" + buf.limit());
System.out.println("capacity:" + buf.capacity());
讀取數據:abcde
position:5
limit:5
capacity:1024
2.5 可重複度
buf.rewind();
System.out.println("可重複讀");
System.out.println("position:" + buf.position());
System.out.println("limit:" + buf.limit());
System.out.println("capacity:" + buf.capacity());
可重複讀
position:0
limit:5
capacity:1024
2.6 清空緩衝區
清空緩衝區後緩衝區中的數據依然存在,但是處於“被遺忘”狀態,屬性高全部回到初始化狀態
buf.clear();
System.out.println("清空緩衝區");
System.out.println("position:" + buf.position());
System.out.println("limit:" + buf.limit());
System.out.println("capacity:" + buf.capacity());
清空緩衝區
position:0
limit:1024
capacity:1024
}
2.7 標記緩衝區
String str = "abcde";
buf.put(str.getBytes());
buf.flip();
byte[] dst = new byte[buf.limit()];
buf.get(dst, 0, 2);
System.out.println("讀取兩個數據");
System.out.println("position:" + buf.position());
//mark() : 標記
System.out.println("做標記");
buf.mark();
System.out.println("再讀取兩個數據");
buf.get(dst, 2, 2);
System.out.println("position:" + buf.position());
//恢復到 mark的位置
System.out.println("恢復到mark位置");
buf.reset();
System.out.println("position:" + buf.position());
輸出:
讀取兩個數據
position:2
做標記
再讀取兩個數據
position:4
恢復到mark位置
position:2
3 直接緩衝區與非直接緩衝區
非直接緩衝區:通過 allocate() 方法分配緩衝區,將緩衝區建立在 JVM 的內存中
直接緩衝區:通過 allocateDirect() 方法分配直接緩衝區,將緩衝區建立在物理內存中。可以提高效率
4 利用通道完成文件的複製(非直接緩衝區)
@Test
public void testCopyByIndirectBuffer(){//10874-10953
long start = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
//①獲取通道
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
fis = new FileInputStream("d:/1.mkv");
fos = new FileOutputStream("d:/2.mkv");
inChannel = fis.getChannel();
outChannel = fos.getChannel();
//②分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
//③將通道中的數據存入緩衝區中
while(inChannel.read(buf) != -1){
buf.flip(); //切換讀取數據的模式
//④將緩衝區中的數據寫入通道中
outChannel.write(buf);
buf.clear(); //清空緩衝區
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(outChannel != null){
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(inChannel != null){
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
long end = System.currentTimeMillis();
System.out.println("耗費時間爲:" + (end - start));
}
5 使用直接緩衝區完成文件的複製(內存映射文件)
@Test
public void testCopyByDirectBuffer() throws IOException{
long start = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("d:/1.mkv"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d:/2.mkv"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
//內存映射文件
MappedByteBuffer inMappedBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuf = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接對緩衝區進行數據的讀寫操作
byte[] dst = new byte[inMappedBuf.limit()];
inMappedBuf.get(dst);
outMappedBuf.put(dst);
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
System.out.println("耗費時間爲:" + (end - start));
}
6 通道之間的數據傳輸(直接緩衝區)
@Test
public void testCopyByDirectTransform() throws IOException{
FileChannel inChannel = FileChannel.open(Paths.get("d:/1.mkv"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d:/2.mkv"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
//inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
}
7 分散和聚集
@Test
public void testScatterAndGather() throws IOException{
RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
//1. 獲取通道
FileChannel channel1 = raf1.getChannel();
//2. 分配指定大小的緩衝區
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//3. 分散讀取
ByteBuffer[] bufs = {buf1, buf2};
channel1.read(bufs);
for (ByteBuffer byteBuffer : bufs) {
byteBuffer.flip();
}
System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
System.out.println("-----------------");
System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
//4. 聚集寫入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
}
8、編碼和解碼
@Test
public void testObserveCharacter(){
Map<String, Charset> map = Charset.availableCharsets();
Set<Entry<String, Charset>> set = map.entrySet();
//遍歷操作系統支持的各種編碼
for (Entry<String, Charset> entry : set) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
@Test
public void testEncodeAndDecode() throws IOException{
Charset cs1 = Charset.forName("GBK");
//獲取編碼器
CharsetEncoder ce = cs1.newEncoder();
//獲取解碼器
CharsetDecoder cd = cs1.newDecoder();
CharBuffer cBuf = CharBuffer.allocate(1024);
cBuf.put("尚硅谷威武!");
cBuf.flip();
//編碼
ByteBuffer bBuf = ce.encode(cBuf);
for (int i = 0; i < 12; i++) {
System.out.print(bBuf.get() + " ");
}
//輸出-55 -48 -71 -24 -71 -56 -51 -2 -50 -28 -93 -95
System.out.println();
//解碼
bBuf.flip();
CharBuffer cBuf2 = cd.decode(bBuf);
System.out.println(cBuf2.toString());//尚硅谷威武!
System.out.println("------------------------------------------------------");
Charset cs2 = Charset.forName("UTF-8");
bBuf.flip();
CharBuffer cBuf3 = cs2.decode(bBuf);
System.out.println(cBuf3.toString());//�й�����䣡
}
9 非阻塞網絡通信
傳統的 IO 流都是阻塞式的。也就是說,當一個線程調用 read() 或 write() 時,該線程被阻塞,直到有一些數據被讀取或寫入,該線程在此期間不能執行其他任務。因此,在完成網絡通信進行 IO 操作時,由於線程會阻塞,所以服務器端必須爲每個客戶端都提供一個獨立的線程進行處理,當服務器端需要處理大量客戶端時,性能急劇下降。
l Java NIO 是非阻塞模式的。當線程從某通道進行讀寫數據時,若沒有數據可用時,該線程可以進行其他任務。線程通常將非阻塞 IO 的空閒時間用於在其他通道上執行 IO 操作,所以單獨的線程可以管理多個輸入和輸出通道。因此,NIO 可以讓服務器端使用一個或有限幾個線程來同時處理連接到服務器端的所有客戶端。
nio中提出一個選擇器的概念,所有通道都註冊到選擇器上,該選擇器會監控這些通道的IO狀況(讀、寫、連接、接收數據),當某個通道某個請求事件完全就緒的時候,選擇器會把該任務分配到服務端的一個或多個線程上再去運行。
使用 NIO 完成網絡通信的三個核心:
* 通道(Channel):負責連接
java.nio.channels.Channel 接口:
|--SelectableChannel
|--SocketChannel
|--ServerSocketChannel
|--DatagramChannel
--Pipe.SinkChannel
|--Pipe.SourceChannel
* 緩衝區(Buffer):負責數據的存取
* 選擇器(Selector):是 SelectableChannel 的多路複用器。用於監控 SelectableChannel 的 IO 狀況
10 阻塞IO
@Test
public void client() throws IOException{
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
ByteBuffer buf = ByteBuffer.allocate(1024);
while(inChannel.read(buf) != -1){
buf.flip();
sChannel.write(buf);
buf.clear();
}
//關閉客戶端的輸出流
sChannel.shutdownOutput();
//接收服務端的反饋
int len = 0;
while((len = sChannel.read(buf)) != -1){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
inChannel.close();
sChannel.close();
}
//服務端
@Test
public void server() throws IOException{
ServerSocketChannel ssChannel = ServerSocketChannel.open();
FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
ssChannel.bind(new InetSocketAddress(9898));
SocketChannel sChannel = ssChannel.accept();
ByteBuffer buf = ByteBuffer.allocate(1024);
while(sChannel.read(buf) != -1){
buf.flip();
outChannel.write(buf);
buf.clear();
}
//發送反饋給客戶端
buf.put("服務端接收數據成功".getBytes());
buf.flip();
sChannel.write(buf);
sChannel.close();
outChannel.close();
ssChannel.close();
}
11 非阻塞IO
@Test
public void client() throws IOException{
//1. 獲取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//2. 切換非阻塞模式
sChannel.configureBlocking(false);
//3. 分配指定大小的緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
//4. 發送數據給服務端
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + "\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
//5. 關閉通道
sChannel.close();
}
//服務端
@Test
public void server() throws IOException{
//1. 獲取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切換非阻塞模式
ssChannel.configureBlocking(false);
//3. 綁定連接
ssChannel.bind(new InetSocketAddress(9898));
//4. 獲取選擇器
Selector selector = Selector.open();
//5. 將通道註冊到選擇器上, 並且指定“監聽接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//6. 輪詢式的獲取選擇器上已經“準備就緒”的事件
while(selector.select() > 0){
//7. 獲取當前選擇器中所有註冊的“選擇鍵(已就緒的監聽事件)”
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
//8. 獲取準備“就緒”的是事件
SelectionKey sk = it.next();
//9. 判斷具體是什麼事件準備就緒
if(sk.isAcceptable()){
//10. 若“接收就緒”,獲取客戶端連接
SocketChannel sChannel = ssChannel.accept();
//11. 切換非阻塞模式
sChannel.configureBlocking(false);
//12. 將該通道註冊到選擇器上
sChannel.register(selector, SelectionKey.OP_READ);
}else if(sk.isReadable()){
//13. 獲取當前選擇器上“讀就緒”狀態的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14. 讀取數據
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = sChannel.read(buf)) > 0 ){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
//15. 取消選擇鍵 SelectionKey
it.remove();
}
}
}
12、非阻塞IO(DatagramChannel)
@Test
public void send() throws IOException{
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + ":\n" + str).getBytes());
buf.flip();
dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
buf.clear();
}
dc.close();
}
@Test
public void receive() throws IOException{
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
dc.register(selector, SelectionKey.OP_READ);
while(selector.select() > 0){
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
SelectionKey sk = it.next();
if(sk.isReadable()){
ByteBuffer buf = ByteBuffer.allocate(1024);
dc.receive(buf);
buf.flip();
System.out.println(new String(buf.array(), 0, buf.limit()));
buf.clear();
}
}
it.remove();
}
}
13、Pipe管道的使用
@Test
public void test1() throws IOException{
//1. 獲取管道
Pipe pipe = Pipe.open();
//2. 將緩衝區中的數據寫入管道
ByteBuffer buf = ByteBuffer.allocate(1024);
Pipe.SinkChannel sinkChannel = pipe.sink();
buf.put("通過單向管道發送數據".getBytes());
buf.flip();
sinkChannel.write(buf);
//3. 讀取緩衝區中的數據
Pipe.SourceChannel sourceChannel = pipe.source();
buf.flip();
int len = sourceChannel.read(buf);
System.out.println(new String(buf.array(), 0, len));
sourceChannel.close();
sinkChannel.close();
}