JAVA SE(二十一)—— I/O 流3(文件流:NIO)

一、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);
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章