上篇带大家了解了IO的概念,同步异步,阻塞非阻塞的区别,没有看过的小伙伴可以去看下哦 本篇是Netty系列的第二篇,带大家来着重解析NIO,作为Netty的核心,它到底有什么特别的地方呢? 跟着狼王往下看....
我们先来想一个问题,为什么Netty使用NIO,而不是AIO呢?
我想各位心中肯定有自己的答案了,让我们带着问题往下看吧
我们先来重温下这两个的区别:
NIO模型 同步非阻塞 NIO有同步阻塞和同步非阻塞两种模式,一般讲的是同步非阻塞,服务器实现模式为一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO模型 异步非阻塞 服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理, 注:AIO又称为NIO2.0,在JDK7才开始支持。
然后看下Netty作者在这个问题上的原话:
Not faster than NIO (epoll) on unix systems (which is true) There is no daragram suppport Unnecessary threading model (too much abstraction without usage)
不比nio快在Unix系统上
不支持数据报
不必要的线程模型(太多没什么用的抽象化)
所以我们可以总结出以下四点:
Java NIO 是 java 1.4 之后新出的一套IO接口,这里的的新是相对于原有标准的Java IO和Java Networking接口。NIO提供了一种完全不同的操作方式。
NIO中的N可以理解为Non-blocking,不单纯是New。
它支持面向缓冲的,基于通道的I/O操作方法。 随着JDK 7的推出,NIO系统得到了扩展,为文件系统功能和文件处理提供了增强的支持。由于NIO文件类支持的这些新的功能,NIO被广泛应用于文件处理。
1 Channels and Buffers(通道和缓冲区)
IO是面向流的,NIO是面向缓冲区的
2 Non-blocking IO(非阻塞IO)
IO流是阻塞的,NIO流是不阻塞的。
3 Selectors(选择器)
NIO有选择器,而IO没有。
NIO有3个实体:Buffer(缓冲区),Channel(通道),Selector(多路复用器)。
Buffer常见子类
ByteBuffer,存储字节数据到缓冲区,进行网络通信使用最频繁
ShortBuffer,存储字符串数据到缓冲区
CharBuffer,存储字符数据到缓冲区
IntBuffer,存储整数数据到缓冲区
LongBuffer,存储长整型数据到缓冲区
DoubleBuffer,存储小数到缓冲区
FloatBuffer,存储小数到缓冲区
Buffer类属性解析
属性 | 描述 |
---|---|
Capacity | 缓冲区容量,在缓冲区创建时被设定并且不能改变 |
Limit | 表示缓冲区的当前读写终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的 |
Position | 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备 |
Mark | 标记 |
常见方法:
public final int capacity( )//返回此缓冲区的容量
public final int position( )//返回此缓冲区的位置
public final Buffer position (int newPositio)//设置此缓冲区的位置
public final int limit( )//返回此缓冲区的限制
public final Buffer limit (int newLimit)//设置此缓冲区的限制
public final Buffer mark( )//在此缓冲区的位置设置标记
public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
public final Buffer flip( )//反转此缓冲区
public final Buffer rewind( )//重绕此缓冲区
public final int remaining( )//返回当前位置与限制之间的元素数
public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
public abstract Object array();//返回此缓冲区的底层实现数组
public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
ByteBuffer常用方法
public abstract class ByteBuffer {
//缓冲区创建相关api
public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区
public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量
public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用
//构造初始化位置offset和上界length的缓冲区
public static ByteBuffer wrap(byte[] array,int offset, int length)
//缓存区存取相关API
public abstract byte get( );//从当前位置position上get,get之后,position会自动+1
public abstract byte get (int index);//从绝对位置get
public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1
public abstract ByteBuffer put (int index, byte b);//从绝对位置上put
}
常见channel类
1.FileChannel //文件io操作
2.DatagramChannel //UDP数据读写
3.ServerSocketChannel和SocketChannel //TCP数据读写
FileChannel 常用方法
public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
代码实践:
通过FileChannel和ByteBuffer读写文件
public void writeToFile() {
try {
//1.创建一个输出流,并通过输出流获取channel
FileOutputStream out = new FileOutputStream("D:\\fileChannelTest.txt");
final FileChannel channel = out.getChannel();
//2.通过byteBuffer读取字符串并写入到channel中
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(("hello,world!").getBytes());
buffer.flip(); //反转buffer的流向
channel.write(buffer);
channel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public void readFromFile() {
try {
//1.获取输入流,并转化成channel
File file = new File("D:\\fileChannelTest.txt");
FileInputStream inputStream = new FileInputStream(file);
final FileChannel channel = inputStream.getChannel();
//2.从通道中读取数据到buffer,并输出到控制台
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(true) { //循环读取直到全部读取到buffer中
buffer.clear(); //清空缓存区,只是把标记初始化,数据不会清楚
int read = channel.read(buffer);
if (read == -1) { //读取完毕,退出循环
break;
}
}
System.out.println("content is " + new String(buffer.array()));
channel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
文件拷贝
public void readFromFile() {
try {
//1.获取输入流,并获取对应的FileChannel
File file = new File("D:\\fileChannelTest.txt");
FileInputStream inputStream = new FileInputStream(file);
final FileChannel channel = inputStream.getChannel();
//2.从通道中读取数据到buffer,并输出到控制台
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(true) { //循环读取直到全部读取到buffer中
buffer.clear(); //清空缓存区,只是把标记初始化,数据不会清楚
int read = channel.read(buffer);
if (read == -1) { //读取完毕,退出循环
break;
}
}
System.out.println("content is " + new String(buffer.array()));
channel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
注意事项:通过ByteBuffer进行对象的传输时,写入的类型和读取的类型必须一致,否则可能会出现BufferUnderFlowException异常
Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,如果没有事件发生时,当前线程可以处理其他事情
常见方法
public abstract class Selector implements Closeable {
public static Selector open();//得到一个选择器对象
public int select(long timeout);//监控所有注册的通道,当其中有 IO 操作可以进行时,将
对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
public Set<SelectionKey> selectedKeys();//从内部集合中得到所有的 SelectionKey
}
实现流程
构建NIO服务端
1.创建ServerSocketChannel,并绑定5555端口
2.创建selector对象,并将ServerSocketChannel注册到seletor中,监听accept事件
3.通过selectKey.isAcceptable判断是否有客户端建立连接,并注册连接的SocketChannel到selector,监听对应的read事件
4.通过selectKey.isReadable判断通道是否发生读事件,并获取对应的socketChannel读到缓冲区中,并输出数据
代码实现:
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel,-->> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(5555);
serverSocketChannel.socket().bind(inetSocketAddress);
serverSocketChannel.configureBlocking(false); //设置成非阻塞
//开启selector,并注册accept事件
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
selector.select(2000); //监听所有通道
//遍历selectionKeys
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isAcceptable()) { //处理连接事件
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); //设置为非阻塞
System.out.println("client:" + socketChannel.getLocalAddress() + " is connect");
socketChannel.register(selector, SelectionKey.OP_READ); //注册客户端读取事件到selector
} else if (key.isReadable()) { //处理读取事件
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel channel = (SocketChannel) key.channel();
channel.read(byteBuffer);
System.out.println("client:" + channel.getLocalAddress() + " send " + new String(byteBuffer.array()));
}
iterator.remove(); //事件处理完毕,要记得清除
}
}
}
1.创建客户端SocketChannel,并绑定ip和端口号
2.通过ByteBuffer和SocketChannel发送消息到服务端
代码实现:
public static void main(String[] args) throws Exception{
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 5555);
if(!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("客户端正在连接中,请耐心等待");
}
}
ByteBuffer byteBuffer = ByteBuffer.wrap("hello,world".getBytes());
socketChannel.write(byteBuffer);
socketChannel.close();
}
本文狼王带你了解了NIO,了解了为什么Netty选择NIO,解析了NIO三大核心组件:Buffer(缓冲区),Channel(通道),Selector(多路复用器)
从代码层面更直观的展示,并提供了相应的代码实现思路
Netty系列的第二篇也结束了,通过这两篇的铺垫,下篇将会正式开始讲Netty,后续我会不断更新该系列文章,由浅至深,从简到难,多方位多角度的带你认识Netty这个网络框架!希望你们是我最好的观众!