Java NIO 网络IO

其实NIO的主要用途是网络IO,在NIO之前java要使用网络编程就只有用Socket。而Socket是阻塞的,显然对于高并发的场景是不适用的。所以NIO的出现就是解决了这个痛点。

主要思想是把Channel通道注册到Selector中,通过Selector去监听Channel中的事件状态,这样就不需要阻塞等待客户端的连接,从主动等待客户端的连接,变成了通过事件驱动。没有监听的事件,服务器可以做自己的事情。

使用Selector的小例子

接下来趁热打铁,我们来做一个服务器接受客户端消息的例子:

首先服务端代码:

public class NIOServer {
public static void main(String[] args) throws Exception {
//打开一个ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
//绑定地址
serverSocketChannel.bind(address);
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//打开一个选择器
Selector selector = Selector.open();
//serverSocketChannel注册到选择器中,监听连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端的连接
while (true) {
//等待3秒,(返回0相当于没有事件)如果没有事件,则跳过
if (selector.select(3000) == 0) {
System.out.println("服务器等待3秒,没有连接");
continue;
}
//如果有事件selector.select(3000)>0的情况,获取事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//获取迭代器遍历
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()) {
//获取到事件
SelectionKey selectionKey = it.next();
//判断如果是连接事件
if (selectionKey.isAcceptable()) {
//服务器与客户端建立连接,获取socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//设置成非阻塞
socketChannel.configureBlocking(false);
//把socketChannel注册到selector中,监听读事件,并绑定一个缓冲区
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
//如果是读事件
if (selectionKey.isReadable()) {
//获取通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//获取关联的ByteBuffer
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
//打印从客户端获取到的数据
socketChannel.read(buffer);
System.out.println("from 客户端:" + new String(buffer.array()));
}
//从事件集合中删除已处理的事件,防止重复处理
it.remove();
}
}
}
}

客户端代码:

public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
socketChannel.configureBlocking(false);
//连接服务器
boolean connect = socketChannel.connect(address);
//判断是否连接成功
if(!connect){
//等待连接的过程中
while (!socketChannel.finishConnect()){
System.out.println("连接服务器需要时间,期间可以做其他事情...");
}
}
String msg = "hello java技术爱好者!";
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
//把byteBuffer数据写入到通道中
socketChannel.write(byteBuffer);
//让程序卡在这个位置,不关闭连接
System.in.read();
}
}

接下来启动服务端,然后再启动客户端,我们可以看到控制台打印以下信息:

服务器等待3秒,没有连接
服务器等待3秒,没有连接
from 客户端:hello java技术爱好者!
服务器等待3秒,没有连接
服务器等待3秒,没有连接

通过这个例子我们引出以下知识点。

SelectionKey

在SelectionKey类中有四个常量表示四种事件,来看源码:

public abstract class SelectionKey {
//读事件
public static final int OP_READ = 1 << 0; //2^0=1
//写事件
public static final int OP_WRITE = 1 << 2; // 2^2=4
//连接操作,Client端支持的一种操作
public static final int OP_CONNECT = 1 << 3; // 2^3=8
//连接可接受操作,仅ServerSocketChannel支持
public static final int OP_ACCEPT = 1 << 4; // 2^4=16
}

附加的对象(可选),把通道注册到选择器中时可以附加一个对象。

public final SelectionKey register(Selector sel, int ops, Object att)

从selectionKey中获取附件对象可以使用attachment()方法

public final Object attachment() {
return attachment;
}
Author: Tunan
Link: http://yerias.github.io/2021/09/08/java/39/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.