一、NIO是什么
在Netty in Action中有这么一段解释
New or non-blocking?
The N in NIO is typically thought to mean non-blocking rather than new.NIO has been
around for so long now that nobody calls it new IO anymore. Most people refer to it as nonblocking IO
新的还是非阻塞的?
NIO中的N更多的意思是非阻塞,而不是新的。NIO已经诞生很久了,已经没有人叫他新的IO了。更多的人更倾向与非阻塞IO。
二、NIO架构模式与优缺点
NIO的诞生,就是为了处理BIO在等待客户端连接以及等待接收数据时的阻塞,并且不用为每一个客户端
去创建线程,而是从线程池中获取线程资源来处理客户端任务,这样可以使用较少的线程来处理业务。
那么NIO时如何去解决这个问题的? 我们从NIO的架构模式来看
客户端可以注册到server中的selector中, selector会循环去判断那些客户端发生了读、写事件,然后从线程池中获取资源去处理这些事件,处理完之后将线程放回线程池,等待下一次的读、写事件。这样就不会造成阻塞,相对于bio有了进一步的系统性能提升。
三、NIO组件
NIO的组件主要包括一下三种:
- channel
每一个客户端都可以认为是一个channel,客户端的读写数据都是通过这个channel进行操作的
- buffer
channel读写数据是基于buffer。也就是说数据是从buffer入到channel中的,同样,buffer也可以接受channel中的数据用于服务端去处理
- selector
监听channel的事件,这些事件主要包括连接以及其他操作。
1、selector
- NIO使用非阻塞Io的方式,使用一个线程来处理多个客户端的连接,其中,selector就是用来处理客户端的事件的
- 当客户端连接时,selector在内部维护一个set集合去管理这些通道,当服务器监听到通道(channel)发生了事件的时候,会将发生事件的通道(channel)收集起来,然后供程序去获取处理。
- selector也是使用一个循环的方式去获取发生事件的通道,不过在获取通道的时候,我们可以指定阻塞时间,这样就可以在没有事件的时候去做其他的事情(不过一般都是循环去获取有事件的通道)。
2、channel
- 一个客户端的连接,也就是一个通道----即channel。
- 传统的io是单项的。比如FileInputStream,只能处理文件的输入,FIleOutPutStream用来输出文件。但是channel是双向的。也就是既可以从channel中读取数据到服务端,也可以写入数据到channel,从而送达客户端。
- 在NIO中,FileChannel用于文件的操作,DatagramChannel用于UDP数据的操作,ServerSocketChannel与SocketChannel用于TCP数据的操作
3、buffer
- buffer是可以存储数据的内存块,表现形式为一个数组
- buffer中有特定的字段用来记录读数据的索引、写数据的索引以及数据的容量等等
- buffer是NIO通讯必须的组件,它与channel进行数据的传输,从而达到客户端与服务端的信息的传递
channel与buffer的使用方式
public static void main(String[] args) throws Exception{
//获取文件的输入流
FileInputStream fileInputStream = new FileInputStream("d:\\1.txt");
//获取channel
FileChannel channel = fileInputStream.getChannel();
//创建一个buffer对象,可存储100字节
ByteBuffer allocate = ByteBuffer.allocate(100);
//将通道中的数据写入到buffer中
channel.read(allocate);
//将buffer进行反转,这样才能去读取数据
allocate.flip();
//将读取到的数据打印到控制台
System.out.println(new String(allocate.array()));
//别忘记关闭资源
channel.close();
fileInputStream.close();
// String str = "hello nio";
// 获取到文件的输出流
// FileOutputStream fileOutputStream = new FileOutputStream("d:\\1.txt");
// 从输出流中获取通道
// FileChannel channel = fileOutputStream.getChannel();
// 将字符串写入到buffer中
// ByteBuffer allocate = ByteBuffer.allocate(100);
// allocate.put(str.getBytes());
// 将buffer进行反转
// allocate.flip();
// 将buffer中的数据写入通道中
// channel.write(allocate);
// channel.close();
// fileOutputStream.close();
}
上面我们在读buffer的时候,会涉及到flip()这个方法,源码是这样去写的
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
由于写数据会造成position的后移,buffer使用position去记录当前读/写的位置。所以,在写完数据之后,需要读数据的时候,需要将position置为0,而能读取到的最大索引位置为上一次写入的索引position----即limit,这样就可以从写状态切换回读状态。
四、NIO使用方法
我们模拟一个客户端以及服务端的聊天系统
服务端代码
public static void main(String[] args) throws Exception{
//创建服务端socketChannel
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
//设置为非阻塞的
listenerChannel.configureBlocking(false);
//绑定端口
listenerChannel.bind(new InetSocketAddress(2222));
//创建selector
Selector selector = Selector.open();
//将通道绑定到selector上
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环去处理发生事件的通道(包括连接、写、读)
while(true){
//获取发生事件的通道的数量
Integer readChannelCount = selector.select();
if(readChannelCount > 0){
//获取发生事件的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//是连接事件,则获取客户端的channel,并且注册到selector上
//如果是连接事件,则这个key里面的通道一定是我们上面创建的ServerSocketChannel
if(key.isAcceptable()){
SocketChannel socketChannel = listenerChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress() + "上线了");
}
//读就绪,则将channel中的数据打印到控制台
if(key.isReadable()){
//获取通道
SocketChannel socketChannel = (SocketChannel) key.channel();
//创建buffer来接收数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将通道中的数据写入buffer
int read = socketChannel.read(byteBuffer);
if(read > 0){
String message = new String(byteBuffer.array());
System.out.println("from 客户端 :" + message);
//转发信息给其他客户端
System.out.println("服务器转发消息...");
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
//获取所有的通道
Iterator<SelectionKey> iterator = selector.keys().iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//排除服务器以及自己
if(selectionKey == key || selectionKey.channel() instanceof ServerSocketChannel)
System.out.println("服务器");
continue;
}
SocketChannel channel =(SocketChannel) selectionKey.channel();
try {
//将信息转发给通道
channel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//删除 防止重复处理
iterator.remove();
}
}else{
//do another things...
}
}
}
客户端代码
public static void main(String[] args) throws Exception {
//创建selector
Selector selector = Selector.open();
SocketChannel = socketChannel = SocketChannel.open(new InetSocketAddress(host, port));
//设置非阻塞的
socketChannel.configureBlocking(false);
//将通道注册到selector中
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端" + socketChannel.getLocalAddress() + " is ok");
//开启一个异步线程,去接受服务端发送的信息
new Thread(() ->{
while (true){
try {
//这里跟server端基本一样
int readCount= selector.select();
if(readCount> 0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
SocketChannel channel = (SocketChannel) selectionKey.channel();
if(selectionKey.isReadable()){
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
System.out.println("接受到消息" + new String(buffer.array()));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
//从控制台输入信息,发送给服务器
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
message = socketChannel.getLocalAddress() + "说:" + message;
socketChannel.write(ByteBuffer.wrap(message.getBytes()));
}
}
测试结果如下
运行服务端,启动两个客户端
服务端结果如下
客户端结果如下
使用客户端52006发送消息
查看服务端与另外一个客户端接收到的结果