概述
一、NIO简介
- NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
- NIO基于 Channel 和 Buffer 进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中
- Selector用于监听多个通道的事件(比如:连接打开,数据到达),单个线程可以监听多个数据通道
二、NIO VS 传统IO
- IO是面向流的,NIO是面向缓冲区的:NIO中的缓冲区的存在使我们可以在其中对数据进行操作,增加了灵活性
- IO的各种流是阻塞的,NIO是非阻塞模式:Selector用于监听多个通道的事件,从而实现一个线程管理多个输入和输出通道
三、Channel & Buffer & Selector
1、channel
- 传统IO中的stream是单向的,而 NIO 中的channel是双向的,既可读又可写;
- NIO中的Channel的主要实现有:这几个实现分别对应 IO、UDP、TCP
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
2、buffer
- NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应几种基本的数据类型
- NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等
3、selector
- 要使用Selector, 得向Selector注册Channel,然后调用select()方法,这个方法会一直阻塞到某个注册的通道有事件就绪
- 事件就绪后这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等
FileChannel
一、NIO的实例
public static void method1(){
RandomAccessFile aFile = null;
try{
aFile = new RandomAccessFile("d:\\123.txt","rw");
FileChannel fileChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = fileChannel.read(buf);
System.out.println(bytesRead);
while(bytesRead != -1)
{
buf.flip();
while(buf.hasRemaining())
{
System.out.print((char)buf.get());
}
buf.compact();
bytesRead = fileChannel.read(buf);
}
}catch (IOException e){
e.printStackTrace();
}finally{
try{
if(aFile != null){
aFile.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
二、buffer 使用
1、基本原理
- buffer 的本质是一个容器,是一个连续的数组;
- NIO 数据传递的基本过程如图:
- 向Buffer中写数据:
从Channel写到Buffer (fileChannel.read(buf))
通过Buffer的put()方法 (buf.put(…)) - 从Buffer中读数据:
从Buffer读取到Channel (channel.write(buf))
使用get()方法从Buffer中读取数据 (buf.get())
2、详细使用步骤描述
buffer的几个参数
- capacity:指定了可以存储在缓冲区中的最大数据容量
- position:指的是下一个要被读写的元素的数组下标索引,该值会随get()和put()的调用自动更新
- limit:指的是缓冲区中第一个不能读写的元素的数组下标索引,也可以认为是缓冲区中实际元素的数量
- mark:一个备忘位置,调用mark()方法的话,mark值将存储当前position的值,等下次调用reset()方法时,会设定position的值为之前的标记值
- 四个参数之间的大小关系为:0 <= mark <= position <= limit <= capacity
buffer的实际使用过程
- 创建一个容量大小为10的字符缓冲区:
ByteBuffer bf = ByteBuffer.allocate(10);
- 往缓冲区中put()五个字节:
bf.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'0');
- 调用flip()方法,切换为读就绪状态:
bf.flip()
- 读取两个元素:
System.out.println("" + (char) bf.get() + (char) bf.get());
- 标记此时的position位置:
bf.mark()
- 读取两个元素后,恢复到之前mark的位置处:
System.out.println("" + (char) bf.get() + (char) bf.get());
bf.reset();
- 调用compact()方法,释放已读数据的空间,准备重新填充缓存区:bf.compact() ; 这里要是调用 clear 方法的话,position将被设回0,limit设置成capacity
SocketChannel
一、概述
- NIO的channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道
channel.configureBlocking(false)
二、TCP示例
1、client 使用 NIO
public static void client(){
ByteBuffer buffer = ByteBuffer.allocate(1024);
SocketChannel socketChannel = null;
try
{
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
if(socketChannel.finishConnect())
{
int i=0;
while(true)
{
TimeUnit.SECONDS.sleep(1);
String info = "I'm "+i+++"-th information from client";
buffer.clear();
buffer.put(info.getBytes());
buffer.flip();
while(buffer.hasRemaining()){
System.out.println(buffer);
socketChannel.write(buffer);
}
}
}
}
catch (IOException | InterruptedException e)
{
e.printStackTrace();
}
finally{
try{
if(socketChannel!=null){
socketChannel.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
2、server 使用 BIO
public static void server(){
ServerSocket serverSocket = null;
InputStream in = null;
try
{
serverSocket = new ServerSocket(8080);
int recvMsgSize = 0;
byte[] recvBuf = new byte[1024];
while(true){
Socket clntSocket = serverSocket.accept();
SocketAddress clientAddress = clntSocket.getRemoteSocketAddress();
System.out.println("Handling client at "+clientAddress);
in = clntSocket.getInputStream();
while((recvMsgSize=in.read(recvBuf))!=-1){
byte[] temp = new byte[recvMsgSize];
System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
System.out.println(new String(temp));
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally{
try{
if(serverSocket!=null){
serverSocket.close();
}
if(in!=null){
in.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
3、示例说明
- channel 的 write()方法无法保证能写多少字节到SocketChannel,所以在死循环里重复调用write()直到Buffer没有要写的字节为止
TCP服务端NIO写法
一、TCP服务端 NIO 代码
public class ServerConnect
{
private static final int BUF_SIZE=1024;
private static final int PORT = 8080;
private static final int TIMEOUT = 3000;
public static void main(String[] args)
{
selector();
}
public static void handleAccept(SelectionKey key) throws IOException{
ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
SocketChannel sc = ssChannel.accept();
sc.configureBlocking(false);
sc.register(key.selector(), SelectionKey.OP_READ,ByteBuffer.allocateDirect(BUF_SIZE));
}
public static void handleRead(SelectionKey key) throws IOException{
SocketChannel sc = (SocketChannel)key.channel();
ByteBuffer buf = (ByteBuffer)key.attachment();
long bytesRead = sc.read(buf);
while(bytesRead>0){
buf.flip();
while(buf.hasRemaining()){
System.out.print((char)buf.get());
}
System.out.println();
buf.clear();
bytesRead = sc.read(buf);
}
if(bytesRead == -1){
sc.close();
}
}
public static void handleWrite(SelectionKey key) throws IOException{
ByteBuffer buf = (ByteBuffer)key.attachment();
buf.flip();
SocketChannel sc = (SocketChannel) key.channel();
while(buf.hasRemaining()){
sc.write(buf);
}
buf.compact();
}
public static void selector() {
Selector selector = null;
ServerSocketChannel ssc = null;
try{
selector = Selector.open();
ssc= ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(PORT));
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true){
if(selector.select(TIMEOUT) == 0){
System.out.println("==");
continue;
}
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
if(key.isAcceptable()){
handleAccept(key);
}
if(key.isReadable()){
handleRead(key);
}
if(key.isWritable() && key.isValid()){
handleWrite(key);
}
if(key.isConnectable()){
System.out.println("isConnectable = true");
}
iter.remove();
}
}
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(selector!=null){
selector.close();
}
if(ssc!=null){
ssc.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
二、示例代码说明
1、ServerSocketChannel
- ServerSocketChannel 的打开、关闭和监听(open、close、accept)
- 在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null,因此需要进行非空检查
2、selector
管理多信道的机制(主动通知)
- Selector 在内部可以同时管理多个channel,当一个信道有I/O操作的时候,他会通知Selector,Selector就知道这个信道有I/O操作以及具体是何种操作;
- Selector 的 select 方法返回的结果为 0,代表在你调用的时刻没有任何客户端需要I/O操作,不为 0 代表有客户端准备就绪了,返回的就是可操作的信道数量;
Channel和Selector配合使用
- 二者配合使用必须将Channel注册到Selector上,通过SelectableChannel.register()方法来实现,注册 channel 的时候可以指定监听事件的类型;
- 监听事件的类型无外乎就四种(连接、接受、读、写)这四种事件用SelectionKey的四个常量来表示:
1. SelectionKey.OP_CONNECT
2. SelectionKey.OP_ACCEPT
3. SelectionKey.OP_READ
4. SelectionKey.OP_WRITE
- selecet 方法返回非 0 ,通过调用如下的方法可以返回一个 SelectionKey 的集合,每一个SK对象都代表着注册到 selector 上的信道,遍历此集合获得SK对象后即可进行后续处理
Set selectedKeys = selector.selectedKeys();
- Selector不会自己从已选择键集中移除SelectionKey实例,所以每次迭代末尾都要keyIterator.remove()
3、SelectionKey
-
ServerSocketChannel 的 register()方法会返回一个SelectionKey对象,这个对象包含如下属性:
- interest集合:监听事件的集合,SK对象的 interestOps 方法可以得到这个集合
- ready集合:是通道已经准备就绪的操作的集合,readyOps方法可以得到这个集合
selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
- Channel 和 Selector:通过 channel 和 selector 方法即可获得
- 附加的对象(可选):有两种添加附加信息的方法
selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment(); ----------------------------------------------- SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
内存映射文件
一、byteBuffer的两种模式
- 间接模式:就是HeapByteBuffer,即操作堆内存 (byte[]),但是如果文件很大的话可能会出现内存溢出的情况;
- 直接模式:MappedByteBuffer 将文件直接映射到虚拟内存,这种模式读写性能很高;
二、示例代码
public static void method4(){
RandomAccessFile aFile = null;
FileChannel fc = null;
try{
aFile = new RandomAccessFile("src/1.ppt","rw");
fc = aFile.getChannel();
long timeBegin = System.currentTimeMillis();
ByteBuffer buff = ByteBuffer.allocate((int) aFile.length());
buff.clear();
fc.read(buff);
//System.out.println((char)buff.get((int)(aFile.length()/2-1)));
//System.out.println((char)buff.get((int)(aFile.length()/2)));
//System.out.println((char)buff.get((int)(aFile.length()/2)+1));
long timeEnd = System.currentTimeMillis();
System.out.println("Read time: "+(timeEnd-timeBegin)+"ms");
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(aFile!=null){
aFile.close();
}
if(fc!=null){
fc.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
public static void method3(){
RandomAccessFile aFile = null;
FileChannel fc = null;
try{
aFile = new RandomAccessFile("src/1.ppt","rw");
fc = aFile.getChannel();
long timeBegin = System.currentTimeMillis();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, aFile.length());
// System.out.println((char)mbb.get((int)(aFile.length()/2-1)));
// System.out.println((char)mbb.get((int)(aFile.length()/2)));
//System.out.println((char)mbb.get((int)(aFile.length()/2)+1));
long timeEnd = System.currentTimeMillis();
System.out.println("Read time: "+(timeEnd-timeBegin)+"ms");
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(aFile!=null){
aFile.close();
}
if(fc!=null){
fc.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
1、示例说明
- 示例代码通过 FileChannel 的 map 方法实现,这个方法有三个参数(模式,position,size)
- map方法的模式参数有三个选项:READ_ONLY(只读)、READ_WRITE(读/写:缓冲区修改会传播到文件)、PRIVATE(专用:修改不会传播到文件,会建立一个专用副本)
2、MappedByteBuffer的特有方法
- force():缓冲区是READ_WRITE模式下,此方法对缓冲区内容的修改强行写入文件;
- load():将缓冲区的内容载入内存,并返回该缓冲区的引用;
- isLoaded():如果缓冲区的内容在物理内存中,则返回真,否则返回假;
知识点补充
一、scatter & gatter
1、简介
- 分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中
- 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel
2、示例代码
- 这是一个 gather 的示例代码,实际上是通过 buffer 数组来实现的聚集,channel 调用 write 方法直接使用 buffer 数组作为参数
public class ScattingAndGather
{
public static void main(String args[]){
gather();
}
public static void gather()
{
ByteBuffer header = ByteBuffer.allocate(10);
ByteBuffer body = ByteBuffer.allocate(10);
byte [] b1 = {'0', '1'};
byte [] b2 = {'2', '3'};
header.put(b1);
body.put(b2);
ByteBuffer [] buffs = {header, body};
try
{
FileOutputStream os = new FileOutputStream("src/scattingAndGather.txt");
FileChannel channel = os.getChannel();
channel.write(buffs);
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
二、transferFrom & transferTo
1、transferFrom
- FileChannel的transferFrom()方法可以将数据从其他通道传输到FileChannel中;
- SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中,只会传输此刻准备好的数据(可能不足count字节);
public static void method1(){
RandomAccessFile fromFile = null;
RandomAccessFile toFile = null;
try
{
fromFile = new RandomAccessFile("src/fromFile.xml","rw");
FileChannel fromChannel = fromFile.getChannel();
toFile = new RandomAccessFile("src/toFile.txt","rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
System.out.println(count);
toChannel.transferFrom(fromChannel, position, count);
}
catch (IOException e)
{
e.printStackTrace();
}
finally{
try{
if(fromFile != null){
fromFile.close();
}
if(toFile != null){
toFile.close();
}
}
catch(IOException e){
e.printStackTrace();
}
}
}
2、transferTo
- transferTo()方法将数据从FileChannel传输到其他的channel中
public static void method2()
{
RandomAccessFile fromFile = null;
RandomAccessFile toFile = null;
try
{
fromFile = new RandomAccessFile("src/fromFile.txt","rw");
FileChannel fromChannel = fromFile.getChannel();
toFile = new RandomAccessFile("src/toFile.txt","rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
System.out.println(count);
fromChannel.transferTo(position, count,toChannel);
}
catch (IOException e)
{
e.printStackTrace();
}
finally{
try{
if(fromFile != null){
fromFile.close();
}
if(toFile != null){
toFile.close();
}
}
catch(IOException e){
e.printStackTrace();
}
}
}
三、pipe
1、示例代码
- Java NIO 管道是2个线程之间的单向数据连接,Pipe有一个source通道和一个sink通道,数据会被写到sink通道,从source通道读取
public static void method1(){
Pipe pipe = null;
ExecutorService exec = Executors.newFixedThreadPool(2);
try{
pipe = Pipe.open();
final Pipe pipeTemp = pipe;
exec.submit(new Callable<Object>(){
@Override
public Object call() throws Exception
{
Pipe.SinkChannel sinkChannel = pipeTemp.sink();//向通道中写数据
while(true){
TimeUnit.SECONDS.sleep(1);
String newData = "Pipe Test At Time "+System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()){
System.out.println(buf);
sinkChannel.write(buf);
}
}
}
});
exec.submit(new Callable<Object>(){
@Override
public Object call() throws Exception
{
Pipe.SourceChannel sourceChannel = pipeTemp.source();//向通道中读数据
while(true){
TimeUnit.SECONDS.sleep(1);
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.clear();
int bytesRead = sourceChannel.read(buf);
System.out.println("bytesRead="+bytesRead);
while(bytesRead >0 ){
buf.flip();
byte b[] = new byte[bytesRead];
int i=0;
while(buf.hasRemaining()){
b[i]=buf.get();
System.out.printf("%X",b[i]);
i++;
}
String s = new String(b);
System.out.println("=================||"+s);
bytesRead = sourceChannel.read(buf);
}
}
}
});
}catch(IOException e){
e.printStackTrace();
}finally{
exec.shutdown();
}
}
四、datagramChannel
1、示例代码
- Java NIO中的DatagramChannel是一个能收发UDP包的通道
- 因为UDP是无连接,所以不能像其它通道那样读取和写入,它发送和接收的是数据包
public static void reveive(){
DatagramChannel channel = null;
try{
channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(8888));
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.clear();
channel.receive(buf);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char)buf.get());
}
System.out.println();
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(channel!=null){
channel.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
public static void send(){
DatagramChannel channel = null;
try{
channel = DatagramChannel.open();
String info = "I'm the Sender!";
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.clear();
buf.put(info.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("10.10.195.115",8888));
System.out.println(bytesSent);
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(channel!=null){
channel.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}