一、NIO基本概念
1、NIO介紹
- NIO(New IO)是一個可以替代標準Java IO API的IO API(從Java 1.4開始),Java NIO提供了與標準IO不同的IO工作方式。
- NIO可以理解爲非阻塞IO,傳統的IO的read和write只能阻塞執行,線程在讀寫IO期間不能幹其他事情,比如調用socket.read()時,如果服務器一直沒有數據傳輸過來,線程就一直阻塞,而NIO中可以配置socket爲非阻塞模式。
2、NIO 核心部分組成
- Channels:負責連接
- Buffers: 負責數據的存取
- Selectors
3、與IO的區別
- IO面向流(輸入流、輸出流,單向的),NIO面向緩衝區;
- IO是阻塞式的,NIO是非阻塞式的;
- IO無通道,NIO有通道(Channel),雙向流通,既可讀,也可寫,Channel相當於鐵路(不能運輸東西),必須藉助火車運輸(緩衝區);
- IO無選擇器,NIO有選擇器(Selector)。
二、NIO緩衝區Buffer
1、在java裏爲除boolean外的基本數據類型都提供了一個緩衝區
ByteBuffer
ShortBuffer
IntBuffer
LongBuffer
DoubleBuffer
FloatBuffer
2、Buffer讀寫數據步驟
(1)寫入數據到Buffer;
(2)調用flip()方法;
(3)從Buffer中讀取數據;
(4)調用clear()方法或者compact()方法。
3、Buffer四個屬性
(1)private int mark = -1;標記
(2)private int position = 0;位置
初始值爲0,最大可爲capacity - 1,位置會根據get()和put()發生改變。
當從Buffer的position處讀取數據時,position向前移動到下一個可讀的位置。
當讀取數據時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置爲0。
(3)private int limit;上限
緩衝區第一個不能被讀寫的元素的位置
當在寫模式下,Buffer的limit表示你最多能往Buffer裏寫多少數據。寫模式下,limit等於Buffer的capacity。
當切換Buffer到讀模式時,limit表示你最多能讀到多少數據。因此,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。
(4)private int capacity;容量
緩衝區能夠容納的最大數量,一旦被創建就不可改變。
一旦Buffer滿了,需要將其清空(通過讀數據或者清除數據)才能繼續寫數據往裏寫數據。
4、Buffer基本方法
(1)獲取緩衝區:allocate()
ByteBuffer allocate = ByteBuffer.allocate(1024);
(2)存入元素: put()
allocate.put("abcdef".getBytes())
(3)取出元素:get()
使用get方法前,必須將緩衝區切換成讀取模式,使用flip()方法,切換後,limit會變成已經存入元素的個數,position變爲0。
(4)切換模式:allocate.flip()
取出元素 byte[] bytes = new byte[10]; byte[]裏面的數字不能超過limit
allocate.get(bytes);
(5)重寫讀,做標記: mark() 恢復到標記的位置: reset()
allocate.mark();
allocate.reset();
(6)重複讀取:rewind()
allocate.rewind();
(7)重置緩衝區:clear()
回到最初的狀態,但裏面的數據還在,處於被遺忘的狀態
allocate.clear();
5、注意
(1)當向buffer寫入數據時,buffer會記錄下寫了多少數據。
一旦要讀取數據,需要通過flip()方法將Buffer從寫模式切換到讀模式。
在讀模式下,可以讀取之前寫入到buffer的所有數據。
(2)一旦讀完了所有的數據,就需要清空緩衝區,讓它可以再次被寫入。
有兩種方式能清空緩衝區:調用clear()或compact()方法。
clear()方法會清空整個緩衝,
compact()方法只會清除已經讀過的數據。
任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。
6、直接緩衝區和非直接緩衝區
(1)直接緩衝區將緩衝區建立在物理內存中,可以提高效率。不安全
(2)非直接緩衝區將緩衝區建立在JVM中
創建直接緩衝區: allocateDirect()
ByteBuffer allocateDirect = ByteBuffer.allocateDirect(1024);
7、Demo
public class NIODemo {
public static void main(String[] args) {
//獲取緩衝區 allocate()
ByteBuffer allocate = ByteBuffer.allocate(1024);
//存入元素 put()
allocate.put("abcdef".getBytes());
//取出元素 get(),使用get方法前,必須將緩衝區切換成讀取模式
allocate.flip(); //切換模式
byte[] bytes = new byte[3];
allocate.get(bytes); //get(byte[] dst)
System.out.println(new String(bytes));
System.out.println((char)allocate.get(2)); //get(int index)
//重新讀 做標記mark()
allocate.mark();
重複讀取
allocate.rewind();
//恢復到標記的位置
allocate.reset();
//重置緩衝區 clear(),裏面的數據還在,處於被遺忘的狀態
allocate.clear();
byte[] bytes1 = new byte[3];
allocate.get(bytes1);
}
}
三、NIO通道Channel
1、NIO通道Channel
NIO通道Channel是負責連接的,IO源與目標打開的連接,與傳統IO流類似,只是Channel本身不能直接訪問,必須藉助緩衝區。
2、Java提供的通道
FileChannel
SocketChannel
ServerSocketChannel
DatagramChannel
3、獲取通道:
(1)getChannel()
(2)open()
public class NIOChannelDemo {
public static void main(String[] args) throws IOException {
//getChannelDemo();
//openDemo();
}
//獲取通道方法1:getChannel()方法示例
public static void getChannelDemo() throws IOException {
FileInputStream fis = new FileInputStream("D:\\test\\aa.jpg");
FileOutputStream fos = new FileOutputStream("D:\\test1\\aa.jpg");
//獲取通道
FileChannel inchannel = fis.getChannel();
FileChannel outchannel = fos.getChannel();
//創建緩衝區
ByteBuffer bb = ByteBuffer.allocate(1024);
//循環讀取
while(inchannel.read(bb) != -1) {
//切換成讀模式
bb.flip();
outchannel.write(bb);
//重置緩衝區
bb.clear();
}
inchannel.close();
outchannel.close();
fis.close();
fos.close();
}
//獲取通道方法2:open()方法
public static void openDemo() throws IOException {
FileChannel inChannel = FileChannel.open(Paths.get("D:\\test\\aa.zip"), StandardOpenOption.READ); //讀的操作
FileChannel outChannel = FileChannel.open(Paths.get("D:\\test1\\aa.zip"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); //操作
inChannel.transferTo(0, inChannel.size(), outChannel); //position 位置
inChannel.close();
outChannel.close();
}
}
4、Demo1:NIOChannel文件複製
(1)服務器端
public class SelectorServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel open = ServerSocketChannel.open();
//綁定端口
open.bind(new InetSocketAddress(12306));
FileChannel open2 = FileChannel.open(Paths.get("D:\\test1\\aa.zip"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
ByteBuffer allocate = ByteBuffer.allocate(1024);
SocketChannel accept = open.accept();
while(accept.read(allocate) != -1) {
allocate.flip();
open2.write(allocate);
allocate.clear();
}
//關流
accept.close();
open2.close();
open.close();
}
}
(2)客戶端
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel open = SocketChannel.open(new InetSocketAddress("127.0.0.1",12306));
//文件的通道
FileChannel open2 = FileChannel.open(Paths.get("D:\\test\\aa.zip"), StandardOpenOption.READ);
//創建緩衝區
ByteBuffer bb = ByteBuffer.allocate(1024);
while(open2.read(bb) != -1) {
bb.flip();
open.write(bb);
bb.clear();
}
//關閉連接
open.close();
open2.close();
}
}
5、Demo2:NIOChannel聊天
(1)聊天室服務器端
public class ChatServer {
public static void main(String[] args) throws IOException {
//建立連接,打開通道
ServerSocketChannel open = ServerSocketChannel.open();
//綁定端口號
open.bind(new InetSocketAddress(12306));
//創建緩衝區
ByteBuffer allocate = ByteBuffer.allocate(1024);
System.out.println("服務器已就緒。。。");
Scanner scanner = new Scanner(System.in);
//接收客戶端的連接
SocketChannel sc = open.accept();
//監聽連接
while(true) {
//讀取
sc.read(allocate);
////切換模式
allocate.flip();
System.out.println(new String(allocate.array(), 0, allocate.limit()));
allocate.clear();
//寫回去
String next = "服務器說:" + scanner.next();
//存入緩衝區
allocate.put(next.getBytes());
//切換成寫模式
allocate.flip();
sc.write(allocate);
allocate.clear();
}
}
}
(2)聊天室客戶端
public class ChatClient {
public static void main(String[] args) throws IOException, InterruptedException {
//與服務端建立連接,打開通道
SocketChannel open = SocketChannel.open(new InetSocketAddress("127.0.0.1",12306));
//創建緩衝區
ByteBuffer bb = ByteBuffer.allocate(1024);
//輸入信息
Scanner sc = new Scanner(System.in);
System.out.println("已連接到服務器。。");
//向服務端發送信息
while(true) {
String message = "客戶端說" + sc.next();
bb.put(message.getBytes());
//切換成讀模式
bb.flip();
open.write(bb);
bb.clear();
//接收服務器返回的信息
open.read(bb);
//切換
bb.flip();
System.out.println(new String(bb.array(), 0, bb.limit()));
bb.clear();
}
}
}
四、NIO選擇器Selector
1、NIO選擇器Selector
Selector是NIO的核心,解決阻塞問題,是Channel的多路複用器,用於檢測通道
2、Selector的創建 open()
Selector selector = Selector.open();
3、Demo:NIOSelector聊天室
(1)服務器端
public class SelectorClient {
public static void main(String[] args) throws IOException {
//建立連接,打開通道
ServerSocketChannel open = ServerSocketChannel.open();
//綁定端口號
open.bind(new InetSocketAddress(12306));
//將服務器的通道切換成非阻塞模式
open.configureBlocking(false);
//創建選擇器
Selector select = Selector.open();
//服務端的通道是用來接收客戶端的連接,應該把連接事件註冊到選擇器
open.register(select, SelectionKey.OP_ACCEPT); //sel表示選擇器,ops表示註冊的事件
//輪詢選擇器上所註冊的事件
while(select.select() > 0) {
//取出選擇器上的所有事件,對事件進行判斷
Set<SelectionKey> selectedKeys = select.selectedKeys();
//遍歷Set集合
Iterator<SelectionKey> it = selectedKeys.iterator();
while(it.hasNext()) {
//取出給key
SelectionKey key = it.next();
it.remove();
if(key.isAcceptable()) { //如果該事件是一個接收客戶端連接的事件,就接收客戶端的連接
connect(open, select);
}
if(key.isReadable()) { //如果該事件是一個可讀的事件,則讀取客戶端的數據
read(select, key);
}
}
}
}
//接收連接事件
public static void connect(ServerSocketChannel open,Selector select ) throws IOException {
SocketChannel accept = open.accept();
//將接收到的通道切換成非阻塞模式
accept.configureBlocking(false);
//將該通道的讀取事件註冊到選擇器
accept.register(select, SelectionKey.OP_READ);
}
//讀取數據事件
public static void read(Selector select,SelectionKey key) throws IOException {
//首先取出註冊該事件的通道
SocketChannel channel = (SocketChannel)key.channel(); //SelectableChannel是SocketChannel的父類,所以必須向下轉型
//創建緩衝區
ByteBuffer allocate = ByteBuffer.allocate(1024);
while(channel.read(allocate) > 0) {
allocate.flip();
//拿到數據後,發給所以客戶端,所以要拿到所有客戶端的通道
Set<SelectionKey> keys = select.keys();
//判斷是否是SocketChannel的通道
for(SelectionKey ch: keys) {
SelectableChannel channel2 = ch.channel();
//對這個通道進行判斷 channel2是否是SocketChannel的實例化對象
if(channel2 instanceof SocketChannel) {
//如果是的話要強轉
SocketChannel schannel = (SocketChannel)channel2;
schannel.write(allocate);
//重複寫
allocate.rewind();
}
}
//將數據轉發給所有客戶端後,重置緩衝區
allocate.clear();
}
}
}
(2)客戶端
public class SelectorClient {
public static void main(String[] args) throws IOException {
//與服務端建立連接,打開通道
SocketChannel open = SocketChannel.open(new InetSocketAddress("127.0.0.1",12306));
//將客戶端通道切換成非阻塞模式
open.configureBlocking(false);
//創建緩衝區
ByteBuffer allocate = ByteBuffer.allocate(1024);
//創建Scanner對象
Scanner sc = new Scanner(System.in);
getMessage(open);
while(true) {
String next = sc.next();
//將數據存入緩衝區
allocate.put(next.getBytes());
//切換成讀模式
allocate.flip();
open.write(allocate);
allocate.clear();
}
}
//創建一個定時器,不斷接收服務端發過來的信息
public static void getMessage(SocketChannel open) {
Timer time = new Timer();
ByteBuffer allocate = ByteBuffer.allocate(1024);
time.schedule(new TimerTask() {
@Override
public void run() {
try {
int read = open.read(allocate);
if(read > 0) {
allocate.flip();
System.out.println(new String(allocate.array(), 0, allocate.limit()));
allocate.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}, 50,50);
}
}