NIO学习总结 附:聊天室NIO实现demo

1. 前言

IO模型是使用什么通道对数据进行发送接受,如:文件读取写入,网络传输,选取不同模型很大程度决定了通信性能,在 jdk 1.7为止已经支持了BIO 同步阻塞 NIO 同步非阻塞 AIO 异步非阻塞 ,AIO 虽然提出了方案,但是还没有广泛使用,同时现在网络高并发场景应用基本使用NIO ,如tomcat , flink , spark 等 , 同时已经有了上层封装 如:netty , 因此了解并理解使用NIO 十分有必要,本文在BIO 理解基础上通俗讲解NIO,并介绍记录我对于NIO的理解

2. NIO 几个概念

  • java.nio.Buffer
  • java.nio.channels.Channel
  • java.nio.channels.Selector
  • java.nio.channels.Pipe

在NIO中的操作都是从Channel 和 Buffer 开始的 , Channel 就像流 , Buffer 就像接受流的Byte数组 , Buffer与Channel 都是一起配合使用

2.1 Buffer

缓冲区本质上是一块可以写入数据,可以读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

2.1.1 常用实现类

Buffer 是抽象类,常用基本实现类如下(ByteBuffer 是重点 ):

		ByteBuffer byteBuffer;
		CharBuffer charBuffer;
		IntBuffer intBuffer;
		ShortBuffer shortBuffer;
		LongBuffer longBuffer;
		FloatBuffer floatBuffer;
		DoubleBuffer doubleBuffer;

基础类型,除了boolean

2.1.2 三个属性 capacity,position和limit

capacity 代表缓存区容量 , 如ByteBuffer可以存放多少个Byte

Buffer 有两个模式 , 读模式 , 写模式 ,对于position和limit 不同情况下含义代表不同

  • 写数据时

    position 代表当前位置 ,初始值为0,当写入后会自动向前移动到下一个可插入的Buffer单元位置,最大为capacity -1

    limit 代表你能写入多少数据,等于capacity

  • 读数据时
    position 代表从特定地址读,切换读模式时会重置为0,读取后自动移动到下一个可读单元

    limit 代表最多能读多少数据,因此切换到读模式,会设置为写模式下的position值,也就是说你写了多少就能读多少

2.2 Channel

Channel 特点

  • 双向,可以读取数据,又可以写入数据
  • 可以异步,可以同步
  • 数据需要先读到buffer , 或者 从buffer 写入

2.2.1 Channel 基本实现

Channel 是一个接口 , 基本实现有如下:


// 文件io
FileChannel

// udp
DatagramChannel

// tcp 客户端
SocketChannel
// tcp 服务端
ServerSocketChannel

2.3 Selector

Selector 则是一个选择器,允许单线程处理多个 Channel

因此在使用Selector 时应该把Channel 注册到Selector中 ,同时也需要将Channel 处于非阻塞模式下

2.3.1 注册时有四个类型常量

SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

如果多个可以用 SelectionKey.OP_READ | SelectionKey.OP_WRITE;

2.3.2 SelectionKey

SelectionKey 在注册时会返回的一个对象,里面包含了一些重要的属性,如:

  • interest集合
  • ready集合
  • Channel
  • Selector

interest集合

可以判断某个时间是否在监听列表中

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

ready集合

ready 集合是通道已经准备就绪的操作的集合。如上面一样使用,但是还有更方便的api , 直接调用selectionKey的以下方法:

  • isAcceptable
  • isConnectable
  • isReadable
  • isWritable

Channel + Selector

从SelectionKey访问Channel和Selector很简单。如下:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();

附加对象

可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
// 在注册的时候添加也可以
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

通过Selector选择通道

一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。

// 阻塞方法
int select()
// 等待timeout 毫秒数 阻塞
int select(long timeout)
// 不阻塞,没有就返回0
int selectNow()

如果通过上面方法知道有值,再使用selectedKeys 获得集合,然后遍历分事件类别处理,处理完成后需要将其剔除集合 ,再次有事件会再加入集合, 这里有点像生产者 消费者 的意思

2.4 Pipe

最后的Pipe 则是用于两个线程之间的单向数据连接 , Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

3. 如何使用

3.1 Buffer 使用

// 1. 首先分配大小 
// 创建一个2048 的字节缓存区
ByteBuffer buf = ByteBuffer.allocate(2048);
// 1024 的字符缓存区
CharBuffer buf = CharBuffer.allocate(1024);

// 2. 可以写入内容或者从channel 读取到buffer
//  这里的写入后针对的是Buffer是写入
ByteBuffer buffer = ByteBuffer.allocate(2048);
buffer.put((byte) 12);
channel.read(buffer);

// 3. 切换成读模式,然后就可以读了,这里直接转换成字符串,也可以其他方式 buffer.get() 一个一个读
buffer.flip();
System.out.println(new String(buffer.array()));

// 4. 我想再读一遍
// Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素单元。
buffer.rewind();

// 5.通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如:
buffer.mark();
buffer.reset();

// 6.我打算接着复用换成写模式,清空后
// 如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。
buffer.clear();
//  如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。
buffer.compact();

3.2 Channel + Buffer 使用

这里 File 举例


RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
// 从文件中获取通道
FileChannel inChannel = aFile.getChannel();
// 分配空间
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {

System.out.println("Read " + bytesRead);
// 单个读取一定要转换模式
buf.flip();

// 循环单个读
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
// 清除再读也可以
buf.clear();
bytesRead = inChannel.read(buf);
}
// 最后关闭
aFile.close();


3.3 Selector 使用

// 实际使用 先打开一个选择器
Selector selector = Selector.open();
// 设置成非阻塞再注册,不然会报错
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
// 循环获取
  int readyChannels = selector.select();
  if(readyChannels == 0) continue;
  // 有事件进来分类处理,这里需要循环剔除元素,使用迭代器遍历
  Set selectedKeys = selector.selectedKeys();
  Iterator keyIterator = selectedKeys.iterator();
  while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    // 分类处理后剔除当前触发事件通道,再有事件会自动添加进来
    keyIterator.remove();
  }
}

3.4 Pipe 使用

// 创建管道
Pipe pipe = Pipe.open();
// 打开sink 写管道
Pipe.SinkChannel sinkChannel = pipe.sink();

// 写入数据到buffer 后
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
// 切换模式
buf.flip();
// 写入管道
while(buf.hasRemaining()) {
    sinkChannel.write(buf);
}

// 读取source管道
Pipe.SourceChannel sourceChannel = pipe.source();

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);


4. 聊天室示例

4.1 读打印工具类ChannelUtil



import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;

public class ChannelUtil {


	public static String println(SelectionKey next) throws IOException {
		if (next.isReadable()) {
			SocketChannel channel = (SocketChannel) next.channel();
			channel.configureBlocking(false);
			ByteBuffer buffer = ByteBuffer.allocate(2048);
			try {
				channel.read(buffer);
				String s = new String(buffer.array());
				System.out.println(s);
				retu.2rn s;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return null;
	}
}


4.2 服务端 ServerTest




import cn.hutool.core.util.StrUtil;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class ServerTest {

	public static InetSocketAddress SERVER = new InetSocketAddress("127.0.0.1", 9292);
	private static Selector selector;

	public static void main(String[] args) throws IOException {
		// 选择器打开
		  selector = Selector.open();
		// 通道打开
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
		// 绑定地址
		serverSocketChannel.bind(SERVER);
		// 非阻塞
		serverSocketChannel.configureBlocking(false);
		// 注册到选择器 并且监听对应时间
		serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

		//循环监听
		while (true) {
			if (selector.select(2000) > 0) {
				Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
				while (keyIterator.hasNext()) {
					SelectionKey key = keyIterator.next();
					if (key.isAcceptable()) {
						SocketChannel client = serverSocketChannel.accept();
						client.configureBlocking(false);
						client.register(selector, SelectionKey.OP_READ);
						System.out.println(client.getRemoteAddress() + " isAcceptable");
					} else if (key.isReadable()) {
						String message = ChannelUtil.println(key);
						sendOther(message,key);
					}
					keyIterator.remove();
				}

			}
		}

	}

	private static void sendOther(String message, SelectionKey key) {
		// 判断为空 即可 , 随意
		// 		return str == null || str.length() == 0;
		if (StrUtil.isEmpty(message)){
			return ;
		}
		Set<SelectionKey> keys = selector.keys();
		for (SelectionKey selectionKey : keys) {
			if (selectionKey!=null&& selectionKey.channel() instanceof SocketChannel &&!selectionKey.equals(key)){
				SocketChannel other = (SocketChannel) selectionKey.channel();
				try {
					other.configureBlocking(false);
					other.write(ByteBuffer.wrap((message).getBytes()));
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

	}
}

4.3 客户端 ClientTest



import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

public class ClientTest {


	private static String name;
	private static SocketChannel socketChannel;
	private static Selector selector;

	public static void main(String[] args) throws IOException, InterruptedException {

		selector = Selector.open();

		socketChannel = SocketChannel.open(ServerTest.SERVER);

		socketChannel.configureBlocking(false);

		socketChannel.register(selector, SelectionKey.OP_READ);

		name = socketChannel.getLocalAddress().toString().substring(1);

		System.out.println(name + " is ok !!!");

		new Thread(() -> {
			while (true) {
				try {
					readMessageInfo();
					TimeUnit.SECONDS.sleep(1);
				} catch (IOException e) {
					e.printStackTrace();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

			}
		}).start();


		Scanner scanner = new Scanner(System.in);
		while (true) {
			if (scanner.hasNext()){
				sendMessageInfo(scanner.next());
			}
			TimeUnit.SECONDS.sleep(1);
		}
	}


	public static void readMessageInfo() throws IOException {
		if (selector.select() > 0) {
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			while (iterator.hasNext()) {
				SelectionKey next = iterator.next();
				ChannelUtil.println(next);
				iterator.remove();
			}
		}
	}

	public static void sendMessageInfo(String message) throws IOException {
		socketChannel.write(ByteBuffer.wrap((name + ":" + message).getBytes()));
	}


}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木秀林

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值