目录:
NIO结构
NIO与传统IO异同
NIO使用步骤
NIO代码
ByteBuffer难点解析
1:NIO结构:
Channel:通道,连接客户端和服务端的一个管道,管道内可以双向传输数据。
Selector:选择器,可以想象成一个环状传送带,上面可以接入很多管道,selector还可以对每个管道设置感兴趣的颜色(连接(红色),读(黄色),写(蓝色),接收数据)。当Selector开始轮询的时候Selector这个传送带就一直转动,当某个管道被传送到感兴趣事件检查点的时候,selector会检查改管道当前颜色(即事件)之前是否被注册成了感兴趣颜色(事件),如果感兴趣,那么Selector就可以对这个管道做处理了,比如把管道传给别的线程,让别的线程完成读写操作。
ByteBuffer:字节缓冲区,本质上是一个连续的字节数组,Selector感兴趣的事件发生后对管道的读操作所读到的数据都存储在ByteBuffer中,而对管道的写操作也是以ByteBuffer为源头,值得注意的是ByteBuffer可以有多个。想想看,我们使用的所有基本类型都可以转换成byte字节,比如Integer类型占4字节,那么传递数字 1 ByteBuffer底层数组被占用了4个格子。
2:与传统IO比较:
1:传统IO一般是一个线程等待连接,连接过来之后分配给processor线程,processor线程与通道连接后如果通道没有数据过来就会阻塞(线程被动挂起)不能做别的事情。NIO则不同,首先:在Selector线程轮询的过程中就已经过滤掉了不感兴趣的事件,其次:在processor处理感兴趣事件的read和write都是非阻塞操作即直接返回的,线程没有被挂起。
2:传统IO的管道是单向的,NIO的管道是双向的
3:两者都是同步的,也就是Java程序亲力亲为的去读写数据,不管传统IO还是NIo都需要read和write方法,这些都是Java程序调用的而不是系统帮我们调用的。NIO2.0里这点得到了改观,即使用异步非阻塞AsynchronousXXX四个类来处理。
3:使用NIO步骤:(服务端)
首先:创建一个传送带
然后:创建一个管道,设置管道为非阻塞,绑定端口
然后:把管道放到传送带上
再然后:启动传送带
其次:传送带感兴趣事件检查点查获一个感兴趣管道,转给其他线程对管道进行非阻塞读写
最后:全使用完,关闭管道
过程很清晰,跟我们现实世界中的传送带效果一样。
4:代码体现:
注意:只写服务器端关键步骤,客户端可以参考这些代码
public class Server implements Runnable {
private Selector selector;
private ByteBuffer buffer = ByteBuffer.allocate(1024);
public Server(int port) {
try {
//1 创建一个传送带
selector = Selector.open();
//2 创建一个管道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3 设置服务器通道为非阻塞方式
ssc.configureBlocking(false);
//4 绑定TCP地址
ssc.bind(new InetSocketAddress(port));
//5 把管道放到传送带上,并在传送带上注册一个感兴趣事件,此处传送带感兴趣事件为连接事件
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port:" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
public void run() {
while (true) {
try {
//1 启动传送带,开始轮询
selector.select();
//2 所有感兴趣事件的keys
SelectionKey Iterator keys = selector.selectedKeys().iterator();
//3 遍历所有感兴趣事件集合
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if(key.isValid()) { //如果key的状态是有效的
if(key.isAcceptable()) { //如果key是阻塞状态,则调用accept()方法
accept(key);
}
if(key.isReadable()) { //如果key是可读状态,则调用read()方法
read(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void accept(SelectionKey key) {
try {
//1 获取服务器通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//2 执行阻塞方法
SocketChannel sc = ssc.accept();
//3 设置阻塞模式为非阻塞
sc.configureBlocking(false);
//4 注册到多路复用选择器上,并设置读取标识
sc.register(selector, SelectionKey.OP_READ);
} catch (Exception e) {
e.printStackTrace();
}
}
private void read(SelectionKey key) {
try {
//1 清空缓冲区中的旧数据
buffer.clear();
//2 获取之前注册的SocketChannel通道
SocketChannel sc = (SocketChannel) key.channel();
//3 将sc中的数据放入buffer中
int count = sc.read(buffer);
if(count == -1) { // == -1表示通道中没有数据
key.channel().close();
key.cancel();
return;
}
//读取到了数据,将buffer的position复位到0
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
//将buffer中的数据写入byte[]中
buffer.get(bytes);
String body = new String(bytes).trim();
System.out.println("Server:" + body);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new Server(8379)).start();
}
}
其他:
涉及到ByteBuffer分类及ByteBuffer的读写这里就不过多介绍了,就是一些指针和模式的变动,主要是flip方法,调用flip方法之后的变化从写模式变成读模式