NIO學習
NIO 三個重要元件 Buffer Channel Selector
1.Buffer
Buffer,底層數組,通過4個标志維護
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL1YTNwMjN1cTMwEjNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
- position表示目前指針位置/數組下标
- limit表示緩沖區目前最多處理的資料
- capacity容量
- mark标志位
重要的api
- get() 擷取目前位置的資料,指針右移
- put(資料) 向目前位置填入資料
- allocate(int n)初識化,建立一個大小為n的緩沖區
//Buffer的使用
public static void main(String[] args) {
IntBuffer intBuffer = IntBuffer.allocate(5);
for(int i = 0;i<intBuffer.capacity();i++){
intBuffer.put(i*3);//position++
}
//Buffer是雙向的,既可以讀,也可以寫
//讀寫切換 "轉向"
//limit = position
//position = 0
intBuffer.flip();
//檢查下一個位置是否有資料
while(intBuffer.hasRemaining()){
//get()方法擷取該位置的值,并且将向後移動指針
System.out.println(intBuffer.get());//position++
}
}
filp()和clear()兩者都會把position改為0,不過flip首先将limit=position,再将position置為0,clear将limit = capacity,position = 0
Buffer的聚合和分散
- 可以建立一個Buffer數組進行資料的讀取寫入,這樣會更靈活。
- read() write()接受Buffer[]參數
/*簡單的聚合和分散展示*/
public static void main(String[] args) throws Exception{
ByteBuffer [] byteBuffers = new ByteBuffer[2];
byteBuffers[0]=ByteBuffer.allocate(5);
byteBuffers[1]=ByteBuffer.allocate(5);
//建立Socket步驟
//1.打開一個ServerSocketChannel用來建立SocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(30000);
//2.綁定監聽端口号
serverSocketChannel.socket().bind(inetSocketAddress);
//3.等待用戶端連接配接(類似于ServerSocket的accept方法) 阻塞
SocketChannel accept = serverSocketChannel.accept();
int msgLength = 10;//最大輸入長度
while(true){
//記錄輸入長度
long byteRead = 0;
while(byteRead < msgLength){
//阻塞
long read = accept.read(byteBuffers);
byteRead+=read;
System.out.println("byteRead="+byteRead);
//列印必要資訊
Arrays.asList(byteBuffers).stream().map(byteBuffer ->"position="+byteBuffer.position()+" ,limit="+byteBuffer.limit()).forEach(System.out::println);
}
Arrays.asList(byteBuffers).forEach(byteBuffer -> {byteBuffer.flip();});
long byteWrite = 0;
while (byteWrite<msgLength){
long write = accept.write(byteBuffers);
byteWrite+=write;
}
Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());
System.out.println("byteread="+byteRead+" bytewrite="+byteWrite+" msg length="+msgLength);
}
}
發送至少10個才調用write
使用telnet進行測試:
- 發送4個
NIO 學習筆記NIO學習 - 發送6個
NIO 學習筆記NIO學習 - 發送10個,執行一次read,一次write
發送12個
讀兩次,一次10個,一次2個,同時執行一次write
2.Channel
FileChannel
是FileOutputStream/FileInPutStream的成員變量,即被包裹在io類中
api:
FileChannel寫入檔案
public static void main(String[] args) {
String str = "hello world!";
try(FileOutputStream out = new FileOutputStream("myText.txt")){
//擷取Channel
FileChannel channel = out.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(str.getBytes("UTF-8"));
//filp()将指針position置0,修改limit
buffer.flip();//此方法不要忘記
//将buffer的資料寫入到myText.txt檔案中
channel.write(buffer);
}catch (IOException e){
e.printStackTrace();
}
}
channel.write()方法将從position所指向的位置進行寫入,如果不執行flip()方法,可以想象write會寫入錯誤的資料
FileChannel讀取檔案
public static void main(String[] args) {
File f = new File("myText.txt");
try(FileInputStream input = new FileInputStream(f)){
FileChannel fileChannel = input.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate((int)f.length());
fileChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(),"UTF-8"));
}catch (IOException ie){
ie.printStackTrace();
}
}
byteBuffer.array()方法傳回對應類型Buffer的數組,ByteBuffer就傳回 byte[]
FileChannel讀取并寫入檔案 (檔案拷貝)
public static void main(String[] args) {
try(FileInputStream input = new FileInputStream("text.txt");FileOutputStream output = new FileOutputStream("copy.txt")){
FileChannel channel = input.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
FileChannel channel1 = output.getChannel();
while(true){
//clear()方法不能忘記寫!!!
byteBuffer.clear();
if(channel.read(byteBuffer)==-1){
break;
}
byteBuffer.flip();
channel1.write(byteBuffer);
}
}catch (IOException ie){
ie.printStackTrace();
}
}
channel.read(byteBuffer);
方法調用将會傳回讀取的資料數量,有個神奇的地方就是當position==limit時,會傳回0,永遠不會傳回-1。
也就是說,忘記寫clear()或者自作聰明把clear()用flip()代替(比如我),該程式會陷入死循環。
未寫clear,無限循環寫入buffer的資料;把clear()寫成flip(),無限循環,但是因為第一次執行就把position=limit=0,不會寫入任何資料。
FileChannel提供了檔案拷貝的函數
public static void main(String[] args) {
try(FileInputStream in = new FileInputStream("1.jpeg"); FileOutputStream out = new FileOutputStream("2.jpeg")){
//來源Channel
FileChannel inChannel = in.getChannel();
//寫入Channel
FileChannel outChannel = out.getChannel();
//第一個參數是來源Channel,第二個參數是位置,第三個參數是大小
outChannel.transferFrom(inChannel,0,inChannel.size());
}catch (IOException e){
e.printStackTrace();
}
}
Selector 多路複用器
- Selector可以檢測多個注冊的通道上是否有事件發生
- 事件驅動,有事件的時候才會執行連接配接/讀寫
- 避免線程頻繁切換
- 1個SelectionKey 對應 1個Channel,可以通過SelectionKey得到監聽的管道。
NIO寫法
NIO服務端基本步驟:
- 建立ServerSocketChannel,設定為非阻塞
- 将ServerSocketChannel注冊到Selector中監聽事件
- 每隔1s調用select()方法監聽有沒有連接配接事件/讀寫事件
- 如果有select()!=0,有事件發生,進行判斷,如果是連接配接事件,就把這個Channel注冊,并且開始監聽資料寫入
- 當有讀寫事件時擷取SelectionKey并且得到對應Channel,讀取Channel的資料
服務端代碼
//建立ServerSocketChannel
ServerSocketChannel socketChannel = ServerSocketChannel.open();
//擷取Selector對象
Selector selector = Selector.open();
//綁定端口
InetSocketAddress i = new InetSocketAddress(30000);
socketChannel.bind(i);
//設定為非阻塞
socketChannel.configureBlocking(false);
//把ServerSocketChannel 注冊到 Selector
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循環等待用戶端連接配接
while(true){
//select監聽哪個注冊的Channel有新的事件 Read/Write/Accept
if(selector.select(1000)==0){
//沒有事件發生
System.out.println("無連接配接");
continue;
}
//擷取到相關的Selection集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey next = iterator.next();
if(next.isAcceptable()){
//已經有連接配接,accept()是阻塞方法,但是此時一定有連接配接,不會阻塞
SocketChannel accept = socketChannel.accept();
accept.configureBlocking(false);
System.out.println("一個連接配接,"+accept.hashCode());
//将新加入的連接配接設定為Read,注冊到selector
accept.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if(next.isReadable()){
SocketChannel channel = (SocketChannel)next.channel();
ByteBuffer attachment = (ByteBuffer)next.attachment();
channel.read(attachment);
System.out.println("from 用戶端 "+ new String(attachment.array()));
}
//防止多線程重複通路
iterator.remove();
}
}
所有的Channel都要進行注冊,因為要監聽讀寫事件
ServerSocketChannel主要用來擷取SocketChannel,用于擷取連接配接,SocketChannel主要用于處理讀寫事件(是嗎?)
用戶端代碼
// 連接配接後發送一條hello world!
public static void main(String[] args) throws Exception{
SocketChannel socketChannel = SocketChannel.open();
//socketChannel.configureBlocking(false);
//伺服器 ip 和 port
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",30000);
//連接配接伺服器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()){
System.out.println("等待...");
}
}
//連接配接成功
String str = "hello world!";
//Warps a byte array into a buffer
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//發送資料
socketChannel.write(buffer);
System.in.read();
}
(未完待續)