通常来说NIO中的所有IO都是从 Channel(通道) 开始的。
从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。
数据读取和写入操作图示:
通道可以读也可以写,流一般来说是单向的(只能读或者写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流)。
通道可以异步读写。
通道总是基于缓冲区Buffer来读写。
FileChannel: 用于文件的数据读写
DatagramChannel: 用于UDP的数据读写
SocketChannel: 用于TCP的数据读写,一般是客户端实现
ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现
下面的UML图使用Idea生成的。
使用FileChannel读取数据到Buffer(缓冲区)以及利用Buffer(缓冲区)写入数据到FileChannel:
package com.gupaoedu.vip.netty.io.nio.channel; import java.io.*; import java.nio.*; import java.nio.channels.*; public class FileInputDemo { public static void main( String args[] ) throws Exception { //创建随机读取文件对象,或者用 RandomAccessFile fin = new RandomAccessFile("E://test.txt", "rw"); // 获取通道 FileChannel fc = fin.getChannel(); // 创建缓冲区 ByteBuffer buffer = ByteBuffer.allocate(100); // 读取数据到缓冲区 fc.read(buffer); buffer.flip(); while (buffer.remaining() > 0) { byte b = buffer.get(); System.out.print(((char)b)); } buffer.clear(); fin.close(); } }
##############################################################################
package com.gupaoedu.vip.netty.io.nio.channel; import java.io.*; import java.nio.*; import java.nio.channels.*; public class FileOutputDemo { public static void main( String args[] ) throws Exception { FileOutputStream fout = new FileOutputStream( "E://test1.txt" ); FileChannel fc = fout.getChannel(); ByteBuffer buffer = ByteBuffer.allocate( 1024 ); buffer.put( "Hello, Ginny".getBytes() ); buffer.flip(); fc.write(buffer); fout.close(); } }
运行效果:
1. 开启FileChannel
使用之前,FileChannel必须被打开 ,但是你无法直接打开FileChannel(FileChannel是抽象类)。需要通过 InputStream , OutputStream 或 RandomAccessFile 获取FileChannel。
2. 从FileChannel读取数据/写入数据
从FileChannel中读取数据/写入数据之前首先要创建一个Buffer(缓冲区)对象,Buffer(缓冲区)对象的使用我们在上一篇文章中已经详细说明了,如果不了解的话可以看我的上一篇关于Buffer的文章。
使用FileChannel的read()方法读取数据:
使用FileChannel的write()方法写入数据:
3. 关闭FileChannel
完成使用后,FileChannel您必须关闭它。
利用SocketChannel和ServerSocketChannel实现客户端与服务器端简单通信:
SocketChannel 用于创建基于tcp协议的客户端对象,因为SocketChannel中不存在accept()方法,所以,它不能成为一个服务端程序。通过 connect()方法 ,SocketChannel对象可以连接到其他tcp服务器程序。
客户端:
public NIOChatClient() throws IOException{ selector = Selector.open(); //连接远程主机的IP和端口 client = SocketChannel.open(serverAdrress); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } public void session(){ //开辟一个新线程从服务器端读数据 new Reader().start(); //开辟一个新线程往服务器端写数据 new Writer().start(); }
ServerSocketChannel 允许我们监听TCP链接请求,通过ServerSocketChannelImpl的 accept()方法 可以创建一个SocketChannel对象用户从客户端读/写数据。
服务端:
package netty.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set; /** * @author Harry * @Description //TODO * @Date 2019/9/1117:14 */ public class NIODemo { private ServerSocketChannel serverSocket; private int port = 8083; private ByteBuffer byteBuffer = ByteBuffer.allocate(1024); private Selector selector; //初始化ServerSocket public NIODemo(int port) { try { this.port = port; serverSocket = ServerSocketChannel.open(); System.out.println("NIO项目启动监听:" + this.port); serverSocket.bind(new InetSocketAddress(this.port)); //BIO 升级版本 NIO,为了兼容BIO,NIO模型默认是采用阻塞式 serverSocket.configureBlocking(false); //大堂经理,准备干活 selector = Selector.open(); //登记并改变状态 serverSocket.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } //轮询监听 private void listen(){ try { while (true){ selector.select(); //每次都拿到所有的号子 SetselectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); //不断地迭代,就叫轮询 //同步体现在这里,因为每次只能拿一个key,每次只能处理一种状态 while (iterator.hasNext()){ SelectionKey key = iterator.next(); iterator.remove(); //每一个key代表一种状态 //没一个号对应一个业务 //数据就绪、数据可读、数据可写 等等等等 process(key); } } } catch (IOException e) { e.printStackTrace(); } } //处理读写数据 private void process(SelectionKey key){ try { //针对于每一种状态给一个反应 if(key.isAcceptable()){ ServerSocketChannel server = (ServerSocketChannel) key.channel(); //这个方法体现非阻塞,不管你数据有没有准备好 //你给我一个状态和反馈 SocketChannel channel = server.accept(); //一定一定要记得设置为非阻塞 channel.configureBlocking(false); //当数据准备就绪的时候,将状态改为可读 key = channel.register(selector, SelectionKey.OP_READ); }else if(key.isReadable()){ SocketChannel channel = (SocketChannel)key.channel(); int read = channel.read(byteBuffer); if(read>0){ byteBuffer.flip(); String content = new String(byteBuffer.array(), 0, read); key = channel.register(selector, SelectionKey.OP_WRITE); //在key上携带一个附件,一会再写出去 key.attach(content); System.out.println("读取的内容是:" + content); } }else if(key.isWritable()){ SocketChannel channel = (SocketChannel)key.channel(); String content = (String)key.attachment(); channel.write(ByteBuffer.wrap(("输出:" + content).getBytes())); channel.close(); } }catch (Exception e){ e.printStackTrace(); } } public static void main(String[] args) { new NIODemo(8082).listen(); } }
运行效果:
考虑到篇幅问题,下面只给出大致步骤,不贴代码,可以结合上述实例理解。
1.通过SocketChannel连接到远程服务器
2.创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据
3.关闭SocketChannel
1.通过ServerSocketChannel 绑定ip地址和端口号
2.通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据
3.创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据
4. 关闭SocketChannel和ServerSocketChannel
DataGramChannel,类似于java 网络编程的DatagramSocket类;使用UDP进行网络传输, UDP是无连接,面向数据报文段的协议,对传输的数据不保证安全与完整 ;和上面介绍的SocketChannel和ServerSocketChannel的使用方法类似,所以这里就简单介绍一下如何使用。
1.获取DataGramChannel
上面代码表示程序可以在1234端口接收数据报。
2.接收/发送消息
接收消息:
先创建一个缓存区对象,然后通过receive方法接收消息,这个方法返回一个SocketAddress对象,表示发送消息方的地址:
发送消息:
由于UDP下,服务端和客户端通信并不需要建立连接,只需要知道对方地址即可发出消息,但是是否发送成功或者成功被接收到是没有保证的;发送消息通过send方法发出,改方法返回一个int值,表示成功发送的字节数:
这个例子发送一串字符:“datagramchannel”到主机名为”localhost”服务器的端口8080上。
Channel 提供了一种被称为 Scatter/Gather 的新功能,也称为本地矢量 I/O。Scatter/Gather 是指在多个缓冲区上实现一个简单的 I/O 操作。正确使用 Scatter / Gather可以明显提高性能。
大多数现代操作系统都支持本地矢量I/O(native vectored I/O)操作。当您在一个通道上请求一个Scatter/Gather操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区,减少或避免了缓冲区拷贝和系统调用;
Scatter/Gather应该使用直接的ByteBuffers以从本地I/O获取最大性能优势。
Scatter/Gather功能是通道(Channel)提供的 并不是Buffer。
Scatter: 从一个Channel读取的信息分散到N个缓冲区中(Buufer).
Gather: 将N个Buffer里面内容按照顺序发送到一个Channel.
"scattering read"是把数据从单个Channel写入到多个buffer,如下图所示:
示例代码:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
read()方法内部会负责把数据按顺序写进传入的buffer数组内。一个buffer写满后,接着写到下一个buffer中。
举个例子,假如通道中有200个字节数据,那么header会被写入128个字节数据,body会被写入72个字节数据;
注意:
无论是scatter还是gather操作,都是按照buffer在数组中的顺序来依次读取或写入的;
"gathering write"把多个buffer的数据写入到同一个channel中:
示例代码:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
write()方法内部会负责把数据按顺序写入到channel中。
注意:
并不是所有数据都写入到通道,写入的数据要根据position和limit的值来判断,只有position和limit之间的数据才会被写入;
举个例子,假如以上header缓冲区中有128个字节数据,但此时position=0,limit=58;那么只有下标索引为0-57的数据才会被写入到通道中。
在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。
transferFrom() :transferFrom方法把数据从通道源传输到FileChannel
transferTo() :transferTo方法把FileChannel数据传输到另一个channel\
参考自:
https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484951&idx=1&sn=0cef67df4b883b198da467c927533316&source=41#wechat_redirect