java基礎之NIO介紹及使用
一、NIO
java.nio全稱java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO) ,為所有的原始類型(boolean類型除外)提供緩存支持的數(shù)據(jù)容器,使用它可以提供非阻塞式的高伸縮性網(wǎng)絡。
二、三大組件
NIO三大組件:Channel、Buffer、Selector
1.Channel 和Buffer
Channel是一個對象,可以通過它讀取和寫入數(shù)據(jù)。拿 NIO 與原來的 I/O 做個比較,通道就像是流,而且他們面向緩沖區(qū)(Buffer)的。所有數(shù)據(jù)都通過Buffer對象來處理,永遠不會將字節(jié)直接寫入通道中,而是將數(shù)據(jù)寫入包含一個或多個字節(jié)的緩沖區(qū)。也不會直接從通道中讀取字節(jié),而是將數(shù)據(jù)從通道讀入緩沖區(qū),再從緩沖區(qū)獲取這個字節(jié)。
Channel是讀寫數(shù)據(jù)的雙向通道,可以從Channel將數(shù)據(jù)讀取Buffer,也可將Buffer的數(shù)據(jù)寫入Channel,而之前的Stream要么是輸入(InputStream)、要么是輸出(OutputStream),只在一個方向上流通。 而通道(Channel)可以用于讀、寫或者同時用于讀寫
常見的Channel
1.FileChannel
2.DatagramChannel
3.SocketChannel
4.ServerSocketChannel
Buffer緩沖區(qū)用來讀寫數(shù)據(jù),常見的Buffer
1.ByteBuffer
2.ShortBuffer
3.IntBuffer
4.LongBuffer
5.FloatBuffer
6.DoubleBuffer
7.CharBuffer
2.Selector
在多線程模式下,阻塞IO時,一個線程只能處理一個請求,比如http請求,當請求響應式關閉連接,釋放線程資源。Selector選擇器的作用就是配合一個線程來管理多個Channel,獲取這些Channel上發(fā)生的事件,這些Channel工作在非阻塞模式下,不會讓線程一直在一個Channel上,適合連接數(shù)特別多,但流量低的場景。

調(diào)用Selector的select()方法會阻塞直到Channel發(fā)送了讀寫就緒事件,這些事件發(fā)生,select()方法就會
返回這些事件交給thread來處理。
三、ByteBuffer的使用
屬性:
Buffer的讀寫操作就是通過改變position,mark,limit的值來實現(xiàn)。注意他們之間的關系可以很輕松的讀、寫、覆蓋。
- position:對于寫入模式,表示當前可寫入數(shù)據(jù)的下標,對于讀取模式,表示接下來可以讀取的數(shù)據(jù)的下標。當前操作位置的下標。position()方法獲取。
- mark:用來標志當前操作位置,調(diào)用mark()方法,使mark = position,可以在讀和寫切換過程中標記前一個操作下標位置。
- limit:Buffer的限界值。對于寫模式,相當于可寫入的最大值,數(shù)組長度。對于讀模式,表示最多可以讀取的數(shù)據(jù)的位置下標,通過flip()方法進行讀寫切換,原理改變position,mark,limit的值。
- capacity:數(shù)組容量大小
方法:
Buffer的方法全是根據(jù)改變position的值進行操作的。
- put():put方法寫入數(shù)據(jù),可以單個byte字節(jié),或者byte數(shù)組。或者其它類型,根據(jù)Buffer實例而定。
- get():get方法讀取數(shù)據(jù),可以傳入byte數(shù)組和不傳參讀取一個字節(jié)。
- mark():標記當前下標position位置,mark = position 。讀寫操作切換或者特殊要求時,標記當前的下標位置。
- reset():將position 值重置為mark()方法標記的值。
- array():Buffer內(nèi)數(shù)據(jù)的byte數(shù)組。沒有值的位用0補位。
- flip():limit為position值,將position置為0,mark初始值,寫入操作切換為讀操作。
- rewind():將position 和 mark設為初始值,調(diào)用這個可以一直讀取內(nèi)容或者一直寫入覆蓋之前內(nèi)容,從第一位開始讀/寫。
- clear():將屬性值還原。之前array()數(shù)組的值還在。
- hasRemaining():判斷是否到最后
四、測試Demo
private static void buffer1() {
String data = "abc";
//byte[] bytes = new byte[1024];
//創(chuàng)建一個字節(jié)緩沖區(qū),1024byte
ByteBuffer byteBuffer = ByteBuffer.allocate(15);
System.out.println(byteBuffer.toString());
// 標記當前下標position位置,mark = position ,返回當前對象
System.out.println(byteBuffer.mark());
//對于寫入模式,表示當前可以寫入的數(shù)組大小,默認為數(shù)組的最大長度,對于讀取模式,表示當前最多可以讀取的數(shù)據(jù)的位置下標
System.out.println(byteBuffer.limit());
// 對于寫入模式,表示當前可寫入數(shù)據(jù)的下標,對于讀取模式,表示接下來可以讀取的數(shù)據(jù)的下標
System.out.println(byteBuffer.position());
//capacity 表示當前數(shù)組的容量大小
System.out.println(byteBuffer.capacity());
//將字節(jié)數(shù)據(jù)寫入 byteBuffer
byteBuffer.put(data.getBytes());
//保存了當前寫入的數(shù)據(jù)
for(Byte b : byteBuffer.array()){
System.out.print(b + " ");
}
System.out.println();
System.out.println(new String(byteBuffer.array()));
//讀寫模式切換 改變 limit,position ,mark的值
byteBuffer.flip();
//切換為寫模式,將第一個字節(jié)覆蓋
byteBuffer.put("n".getBytes()); // abc 改為 nbc
System.out.println(new String(byteBuffer.array()));
//讓position為之前標記的值,調(diào)用mark()方法是將mark = position,這里將position = mark,mark為初始值拋出異常
byteBuffer.mark();
byteBuffer.reset();
//將position 和 mark設為初始值,調(diào)用這個可以一直讀取內(nèi)容或者一直寫入覆蓋之前內(nèi)容,從第一位開始讀/寫
byteBuffer.rewind();
for(Byte b : byteBuffer.array()){
System.out.print(b + " ");
}
System.out.println();
System.out.println(byteBuffer.toString());
//找到可寫入的開始位置,不覆蓋之前的數(shù)據(jù)
byteBuffer.compact();
System.out.println(byteBuffer.toString());
}
寫入讀取完整操作
private static void buffer(){
//寫入的數(shù)據(jù)
String data = "1234";
//創(chuàng)建一個字節(jié)緩沖區(qū),1024byte
ByteBuffer byteBuffer = ByteBuffer.allocate(15);
//寫入數(shù)據(jù)
byteBuffer.put(data.getBytes());
//打輸出 49 50 51 52 0 0 0 0 0 0 0 0 0 0 0
println(byteBuffer.array());
byteBuffer.put((byte) 5);
//追加寫入 輸出: 49 50 51 52 5 0 0 0 0 0 0 0 0 0 0
println(byteBuffer.array());
//覆蓋寫入
byteBuffer.flip(); //將寫入下標的 position = 0
byteBuffer.put((byte) 1);
byteBuffer.put((byte) 2);
byteBuffer.put((byte) 3);
byteBuffer.put((byte) 4);
byteBuffer.put((byte) 5);
// 打印輸出 : 1 2 3 4 5 0 0 0 0 0 0 0 0 0 0
println(byteBuffer.array());
//讀取數(shù)據(jù)操作
byteBuffer.flip();//從頭開始讀
while (byteBuffer.position() != byteBuffer.limit()){
System.out.println(byteBuffer.get());
}
//此時 position 和 數(shù)據(jù)數(shù) limit相等
System.out.println(byteBuffer.toString());
//批量讀取
byteBuffer.flip(); //將 position 置位0,從頭開始操作
//創(chuàng)建一個byte數(shù)組,數(shù)組大小為可讀取的大小
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
//輸出bytes 1 2 3 4 5
println(bytes);
}
private static void println(byte[] bytes){
for(Byte b : bytes){
System.out.print(b + " ");
}
System.out.println();
}
字符串跟ByteBuffer之間的轉(zhuǎn)換
public static void main(String[] args) {
/*======================字符串轉(zhuǎn)buffer===========================*/
//1.字符串 轉(zhuǎn)為buytebuffer 需要轉(zhuǎn)為讀模式再進行讀取操作
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("nio".getBytes());
//2.charset 自動轉(zhuǎn)為讀模式
ByteBuffer buffer1 = StandardCharsets.UTF_8.encode("nio");
//3, warp 自動轉(zhuǎn)為讀模式
ByteBuffer buffer2 = ByteBuffer.wrap("nio".getBytes());
/*======================buffer轉(zhuǎn)字符串===========================*/
String str = StandardCharsets.UTF_8.decode(buffer1).toString();
System.out.println(str);
}
五、Channel的使用
文件編程FileChannel
FileChannel只能工作在阻塞模式下,不能配合在Selector使用,Channel是雙向通道,但是FileChannel根據(jù)獲取源頭判定可讀或可寫
- FileInputStream獲取,只可讀
- FileOutputStream獲取,只可寫
- RandomAccessFile獲取,可讀、可寫(雙向通道)
**
* 文件流對象打開Channel,F(xiàn)ileChannel是阻塞的
* @throws Exception
*/
private static void channel() throws Exception{
FileInputStream in = new FileInputStream("C:\\Users\\zqq\\Desktop\\123.txt");
FileOutputStream out = new FileOutputStream("C:\\Users\\zqq\\Desktop\\321.txt");
//通過文件輸入流創(chuàng)建通道channel
FileChannel channel = in.getChannel();
//獲取FileChannel
FileChannel outChannel = out.getChannel();
//創(chuàng)建緩沖區(qū)byteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//將管道channel中數(shù)據(jù)讀取緩存區(qū)byteBuffer中,channel只能與Buffer交互
while (channel.read(byteBuffer) != -1){
//position設置為0,從頭開始讀
byteBuffer.flip();
outChannel.write(byteBuffer);
//byteBuffer 屬性還原
byteBuffer.clear();
}
//關閉各種資源
channel.close();
out.flush();
out.close();
in.close();
out.close();
}
/**
* 靜態(tài)方法打開Channel
* @throws Exception
*/
public static void channel1() throws Exception{
// StandardOpenOption.READ :讀取一個已存在的文件,如果不存在或被占用拋出異常
// StandardOpenOption.WRITE :以追加到文件頭部的方式,寫入一個已存在的文件,如果不存在或被占用拋出異常
// StandardOpenOption.APPEND:以追加到文件尾部的方式,寫入一個已存在的文件,如果不存在或被占用拋出異常
// StandardOpenOption.CREATE:創(chuàng)建一個空文件,如果文件存在則不創(chuàng)建
// StandardOpenOption.CREATE_NEW:創(chuàng)建一個空文件,如果文件存在則報錯
// StandardOpenOption.DELETE_ON_CLOSE:管道關閉時刪除文件
//創(chuàng)建讀通道
FileChannel inChannel = FileChannel.open(Paths.get("C:\\Users\\zqq\\Desktop\\123.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("C:\\Users\\zqq\\Desktop\\321.txt"),
StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//內(nèi)存映射
MappedByteBuffer inByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY,0,inChannel.size());
MappedByteBuffer outByteBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE,0,inChannel.size());
//直接對緩沖區(qū)數(shù)據(jù)讀寫
byte[] bytes = new byte[inByteBuffer.limit()];
inByteBuffer.get(bytes);//讀取inByteBuffer內(nèi)的數(shù)據(jù),讀到bytes數(shù)組中
outByteBuffer.put(bytes);//寫入到outByteBuffer
inChannel.close();
outChannel.close();
}
RandomAccessFile打開通道Channel
/**
* 通過RandomAccessFile獲取雙向通道
* @throws Exception
*/
private static void random() throws Exception{
try (RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\workspace\\Demo\\test.txt","rw")){
//獲取Channel
FileChannel fileChannel = randomAccessFile.getChannel();
//截取字節(jié)
//fileChannel.truncate(10);
//創(chuàng)建ByteBuffer,注意文件大小
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
fileChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(),"GBK"));
byteBuffer.clear();
String data = "this is data\r";
byteBuffer.put(data.getBytes());
byteBuffer.flip();//讀寫切換
while (byteBuffer.hasRemaining()){
fileChannel.write(byteBuffer);
}
//將通道數(shù)據(jù)強制寫到磁盤
fileChannel.force(true);
}
}
FileChannel數(shù)據(jù)傳輸transferTo
/**
* 一個文件向另一個文件傳輸(copy)
*/
private static void transferTo() {
try (
FileChannel inChannel = new FileInputStream("C:\\Users\\44141\\Desktop\\demo\\in.txt").getChannel();
FileChannel outChannel = new FileOutputStream("C:\\Users\\44141\\Desktop\\demo\\out.txt").getChannel()
){
//底層使用操作系統(tǒng)零拷貝進行優(yōu)化,效率高。限制2g
inChannel.transferTo(0,inChannel.size(),outChannel);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 大于2g數(shù)據(jù)
*/
private static void transferToGt2g() {
try (
FileChannel inChannel = new FileInputStream("C:\\Users\\44141\\Desktop\\demo\\in.txt").getChannel();
FileChannel outChannel = new FileOutputStream("C:\\Users\\44141\\Desktop\\demo\\out1.txt").getChannel()
){
//記錄inChannel初始化大小
long size = inChannel.size();
//多次傳輸
for(long rsize = size; rsize > 0;){
//transferTo返回位移的字節(jié)數(shù)
rsize -= inChannel.transferTo((size-rsize),rsize,outChannel);
}
}catch (Exception e){
e.printStackTrace();
}
}
六、網(wǎng)絡編程
阻塞模式
阻塞模式服務端每個方法都會阻塞等待客戶端操作。比如accept()方法阻塞等待客戶端連接,read()方法阻塞等待客戶端傳送數(shù)據(jù),并發(fā)訪問下沒法高效的進行工作。
1.服務端代碼
private static void server() throws Exception{
//1.創(chuàng)建服務器
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2.綁定監(jiān)聽端口
serverSocketChannel.bind(new InetSocketAddress(8080));
while (true){
System.out.println("開始監(jiān)聽連接=============" );
//4.accept 監(jiān)聽進來的連接 返回SocketChannel對象,accept默認阻塞
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("有連接連入===============" );
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// read()是阻塞方法,等客戶端發(fā)送數(shù)據(jù)才會執(zhí)行
socketChannel.read(byteBuffer);
byteBuffer.flip();
String str = StandardCharsets.UTF_8.decode(byteBuffer).toString();
System.out.println("接收到數(shù)據(jù)=============:" + str);
}
}
2.客戶端代碼
private static void client() throws Exception {
//1.創(chuàng)建客戶端
SocketChannel socketChannel = SocketChannel.open();
//連接服務端
socketChannel.connect(new InetSocketAddress("localhost",8080));
//2 秒后寫入數(shù)據(jù)
Thread.sleep(2 * 1000);
socketChannel.write(StandardCharsets.UTF_8.encode("nio"));
System.out.println();
}
非阻塞模式
服務端設置成非阻塞模式??蛻舳瞬挥脛?。
private static void server() throws Exception{
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//設置成非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
//非阻塞模式下,SocketChannel會返回為null
if(socketChannel != null){
//非阻塞模式
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socketChannel.read(byteBuffer);
byteBuffer.flip();
String str = StandardCharsets.UTF_8.decode(byteBuffer).toString();
System.out.println("接收到數(shù)據(jù)=============:" + str);
}
}
}
七、Selector
Selector選擇器的作用就是配合一個線程來管理多個Channel,被Selector管理的Channel必須是非阻塞模式下的,所以Selector沒法與FileChannel(FileChannel只有阻塞模式)一起使用。
創(chuàng)建Selector
創(chuàng)建Selector只需要調(diào)用一個靜態(tài)方法
//創(chuàng)建Selector Selector selector = Selector.open();
Selector可以用來監(jiān)聽Channel的事件,總共有四個事件:
- accept:會在有連接請求時觸發(fā),靜態(tài)常量 SelectionKey.OP_ACCEPT
- connect:客戶端建立連接后觸發(fā),靜態(tài)常量 SelectionKey.OP_CONNECT
- read:可讀時觸發(fā),靜態(tài)常量 SelectionKey.OP_READ
- write:可寫時觸發(fā),靜態(tài)常量 SelectionKey.OP_WRITE
Selector與Channel綁定
//設置成非阻塞模式 serverSocketChannel.configureBlocking(false); SelectionKey selectionKey = serverSocketChannel.register(selector,0,null); //綁定關注的事件 selectionKey.interestOps(SelectionKey.OP_ACCEPT);
八、網(wǎng)絡編程(多路復用)
1.客戶端代碼 SocketChannel
public static void main(String[] args) throws Exception {
client();
}
private static void client() throws Exception {
//1.創(chuàng)建客戶端
SocketChannel socketChannel = SocketChannel.open();
//連接服務端
socketChannel.connect(new InetSocketAddress("localhost",8080));
//2 秒后寫入數(shù)據(jù)
Thread.sleep(2 * 1000);
socketChannel.write(StandardCharsets.UTF_8.encode("nio"));
//3.讀取服務端返回數(shù)據(jù)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socketChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println("服務端返回數(shù)據(jù)=======:" + StandardCharsets.UTF_8.decode(byteBuffer).toString());
//斷開連接
socketChannel.close();
}
2.服務端代碼
public static void main(String[] args) throws Exception{
server();
}
private static void server() throws Exception{
//創(chuàng)建Selector
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//設置成非阻塞模式
serverSocketChannel.configureBlocking(false);
//將Channel注冊到selector上(綁定關系)
//當事件發(fā)生后可以根據(jù)SelectionKey知道哪個事件和哪個Channel的事件
SelectionKey selectionKey = serverSocketChannel.register(selector,0,null);
//selectionKey 關注ACCEPT事件(也可以在上面注冊時用第二個參數(shù)設置)
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
serverSocketChannel.bind(new InetSocketAddress(8080));
while (true){
System.out.println("阻塞====================");
// select方法沒有事件發(fā)生時阻塞線程,有事件線程會恢復運行
selector.select();
System.out.println("開始處理事件=================");
// 處理事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey sk = iterator.next();
//獲取到SelectionKey對象之后,將集合內(nèi)的引用刪掉(Selecotr不會自動刪除)
iterator.remove();
//取消事件,不操作(不處理或者不取消事件,selector.select()方法不會阻塞)
//sk.cancel();
//區(qū)分不同的事件做不同的動作
if(sk.isAcceptable()){ //有連接請求事件
//通過SelectionKey 獲取對應的channel
ServerSocketChannel channel = (ServerSocketChannel) sk.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
//讀取數(shù)據(jù)內(nèi)容,bytebuffer大小注意消息邊界(一個字符串被分割讀取多次出來結果不準確)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//將socketChannel綁定Selector
SelectionKey socketKey = socketChannel.register(selector,0,byteBuffer);
//關注可讀事件
socketKey.interestOps(SelectionKey.OP_READ);
}else if(sk.isReadable()){//可讀事件
try {
//取到Channel
SocketChannel socketChannel = (SocketChannel) sk.channel();
//獲取到綁定的ByteBuffer
ByteBuffer byteBuffer = (ByteBuffer) sk.attachment();
int read = socketChannel.read(byteBuffer);
//如果是正常斷開 read = -1
if(read == -1){
//取消事件
sk.cancel();
continue;
}
byteBuffer.flip();
String str = StandardCharsets.UTF_8.decode(byteBuffer).toString();
System.out.println("服務端讀取到數(shù)據(jù)===========:" + str);
//寫數(shù)據(jù)回客戶端
ByteBuffer writeBuffer = StandardCharsets.UTF_8.encode("this is result");
socketChannel.write(writeBuffer);
//如果數(shù)據(jù)一次沒寫完關注可寫事件進行再次寫入(大數(shù)據(jù)一次寫不完的情況)
if(writeBuffer.hasRemaining()){
//關注可寫事件,添加事件,用interestOps()方法獲取到之前的事件加上可寫事件(類似linux系統(tǒng)的賦權限 777)
sk.interestOps(sk.interestOps() + SelectionKey.OP_WRITE);
sk.attach(writeBuffer);
//位運算符也可以
//sk.interestOps(sk.interestOps() | SelectionKey.OP_WRITE);
}
}catch (IOException e){
e.printStackTrace();
//客戶端異常斷開連接 ,取消事件
sk.cancel();
}
}else if(sk.isWritable()){
//取到Channel
SocketChannel socketChannel = (SocketChannel) sk.channel();
//獲取到綁定的ByteBuffer
ByteBuffer writeBuffer = (ByteBuffer) sk.attachment();
socketChannel.write(writeBuffer);
//如果全部寫完,取消可寫事件綁定,解除writeBuffer綁定
if(!writeBuffer.hasRemaining()){
sk.attach(null);
//取消可寫事件
sk.interestOps(sk.interestOps() - SelectionKey.OP_WRITE);
}
}
}
}
}
到此這篇關于java基礎之NIO介紹及使用的文章就介紹到這了,更多相關Java NIO詳解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot接收日期字符串參數(shù)與返回日期字符串類型格式化
這篇文章主要介紹了springboot接收日期字符串參數(shù)與返回日期字符串類型格式化,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01
Mybatis注解方式完成輸入?yún)?shù)為list的SQL語句拼接方式
這篇文章主要介紹了Mybatis注解方式完成輸入?yún)?shù)為list的SQL語句拼接方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
淺談Spring AOP中args()和argNames的含義
這篇文章主要介紹了Spring AOP中args()和argNames的含義,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07

