NIO的基本介绍
java NIO 全称 java non-blocking IO,是同步非阻塞的。
NIO有三大核心部分
- Channel(通道):传统IO流中内置了通道,通道是双向的,可读可写,Channel不存储数据,主要配合缓冲区进行数据的传输。
- Buffer(缓冲区):客户端与通道之间的缓冲区,两者间的数据读写交互都会经过这个缓冲区
- Selector(选择器):会监听选择器下的所有通道,如果某个通道发生读写事件,这个选择器就会去处理这个通道的读写事件,通道没有发生事件,也不会阻塞。
三大核心组件的简单关系图如下所示:
NIO非阻塞模式:如果客户端向通道中写入了数据,这时线程就可以从通道中读取这个数据;如果通道中没有数据可用,线程也不会阻塞在这里,直至通道变的可以读取之前,该线程可以做其他事情。而BIO中建立连接后线程读完数据后会阻塞,直到新的数据过来。
NIO是基于事件驱动的,如果通道中没有事件,便不会处理。
NIO与BIO的区别
- BIO阻塞的,NIO非阻塞的
- BIO通过流处理数据,NIO通过缓冲区处理数据,缓冲区IO效率比流IO效率高很多
- BIO基于字节流和字符流进行操作,而NIO基于通道和缓冲区进行操作,数据总是从通道读取到缓冲区,或是从缓冲区写入到通道,选择器用于监听多个通道的事件,因此一个线程就可以监听多个客户端通道。
NIO的三大核心组件
Buffer缓冲区
缓冲区的基本使用
缓冲区:本质上是一个可以读写的内存块,可以看成一个容器对象(数组),通道与客户端的数据传输都需要经过这个缓冲区。
数据的读写交互都是通过Buffer缓冲区进行的,例举一个Buffer的基本使用如下所示:
public class BufferTest {
public static void main(String[] args) {
//创建一个int型的缓冲区,分配空间为5,可以存放5个int
IntBuffer intBuffer = IntBuffer.allocate(5);
for (int i = 1; i <= 5; i++) {
intBuffer.put(i);
}
//读取缓冲区数据,调用flip方法,读写切换
intBuffer.flip();
//有剩余数据就读取,内部有一个指针移动,直到返回false表示读完
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
Buffer类的属性
Buffer类中定义了所有缓冲区都具有的四个属性,这些属性可以体现当对缓冲区写数据或读数据时,缓冲区内部状态的变化。
- mark:标记
- position:缓冲区中当前的位置
- limit:缓冲区的终点(索引最后的位置)
- capacity:缓冲区可以容纳的最大数据量
举例如下:
缓冲区分配内部容量为5,内部数组hb元素都为0,position当前位置为0,limit缓冲区界限5
每添加一个元素,position的位置都加1,直到limit界限,如果循环次数超过容量大小,会报BufferOverflowException异常
调用flip()方法切换状态,从写状态转为读状态,position指向的位置从尾部移动到首部
每读到缓冲区一个数据,position位置往后移动一位,值+1
Buffer的分散和合并
NIO支持通过多个Buffer完成读写操作
- Scattering:将数据写入到Buffer时,可以采用buffer数组,依次写入
- Gathering:从buffer读取数据时,可以采用buffer数组,依次读
服务端代码,监听端口8000
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws IOException {
//创建ServerSocketChannel绑定端口8000
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(8000);
serverSocketChannel.socket().bind(inetSocketAddress);
//创建Buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等待客户端的连接
SocketChannel socketChannel = serverSocketChannel.accept();
long byteRead = socketChannel.read(byteBuffers);
System.out.println("从客户端读取到的字节数:"+byteRead);
//输出当前Buffer里的position和limit
Arrays.asList(byteBuffers).stream().map(
buffer -> "position=" + buffer.position() + ",limit=" + buffer.limit()
).forEach(System.out::println);
//将所有buffer切换状态
Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
//将byteBuffers数组的数据写出
long byteWrite = socketChannel.write(byteBuffers);
System.out.println("从buffer数组中写出的字节数:"+byteWrite);
}
}
客户端测试
连接服务端:telnet 127.0.0.1 8000
客户端发送数据:send hello123
服务端打印如下,buffer数组中,第一个buffer元素大小为5存了3字节,第二个buffer存了3字节
客户端发送的数据分散到了buffer数组中的各个元素里存储。
获取的时候从buffer数组里获取,会将buffer数组里各个元素的数据合并。
从客户端读取到的字节数:8
position=5,limit=5
position=3,limit=3
从buffer数组中写出的字节数:8
Channel通道
channel基本介绍
负责配合缓冲区进行数据的传递
channel通道与流的区别
- 通道可以同时进行读写,而流只能读或只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲区读数据,也可以向缓冲区写数据
Channel是一个接口,主要实现类有:FileChannel,SocketChannel,ServerSocketChannel
FileChannel类
FileChannel类主要对本地文件进行IO操作,常见方法有:
-
public int read(ByteBuffer dst):从通道读取数据放到缓冲区中
-
public int write(ByteBuffer src):把缓冲区中数据写入到通道中
-
public long transferTo(long position, long count, WritableByteChannel target):把数据从当前通道复制给目标通道
-
public long transferFrom(ReadableByteChannel src, long position, long count):从目标通道复制数据到当前通道
举例1:将字符串数据通过FileChannel通道写入到D盘test.txt文件中
public class FileChannelTest {
public static void main(String[] args) throws Exception {
String str = "hello FileChannel";
//创建输出流FileOutputStream,获取FileChannel
FileOutputStream fileOutputStream = new FileOutputStream("d:\\test.txt");
FileChannel fileChannel = fileOutputStream.getChannel();
//创建一个字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将字符串放入到缓冲区
buffer.put(str.getBytes(StandardCharsets.UTF_8));
//切换缓冲区状态为读
buffer.flip();
//从缓冲区中读数据,然后将读取到的数据写入到通道中
fileChannel.write(buffer);
fileOutputStream.close();
}
}
举例2:读取文件数据,并在控制台输出
public class FileChannelTest {
public static void main(String[] args) throws Exception {
//创建输入流FileInputStream,获取FileChannel
FileInputStream fileInputStream = new FileInputStream("d:\\test.txt");
FileChannel fileChannel = fileInputStream.getChannel();
//创建一个字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(fileInputStream.available());
//将通道数据读入到缓冲区
fileChannel.read(buffer);
System.out.println(new String(buffer.array()));
fileInputStream.close();
}
}
举例3:通过Buffer完成文件拷贝
public class FileChannelTest {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("d:\\1.txt");
FileChannel inputChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("d:\\2.txt");
FileChannel outputChannel = fileOutputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
//这里需要调用clear方法,重置position位置
buffer.clear();
int read = inputChannel.read(buffer);
/*
假设1.txt文件内容字节长度为20,字节缓冲区大小为10,
需要循环两遍读取才能读完。第一次读取然后写入到文件
2.txt结束时,position=limit=10
第二次循环如果没有调用clear()方法,重置position=0,
那么读到的长度read=0,if判断为false,会导致死循环无法退出
*/
if (read == -1) {
break;
}
//需要将写状态切换为读状态
buffer.flip();
//将buffer中的数据写入到outputChannel中,相当于写到了文件2.txt中
outputChannel.write(buffer);
}
fileInputStream.close();
fileOutputStream.close();
}
}
举例4:通过transferFrom完成文件拷贝
public class FileChannelTest {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("d:\\1.jpg");
FileChannel sourceChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("d:\\2.jpg");
FileChannel destChannel = fileOutputStream.getChannel();
destChannel.transferFrom(sourceChannel,0,sourceChannel.size());
sourceChannel.close();
destChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
}
Selector选择器
Selector基本介绍
NIO是非阻塞的IO方式,可以用一个线程处理多个客户端的连接,这里就会使用到选择器Selector(也叫多路复用器)。多个Channel可以以事件的方式注册到同一个Selector,就是通过一个数据结构将多个Channel和Selector的关系关联起来,这样Selector就能够检测到这些注册的通道上是否有事件发生。如果有事件发生,便获取事件然后针对每个事件做对应的处理,这样就完成了一个线程处理多个请求/连接的功能,不用像BIO那样一个线程处理一个连接。
Selector选择器通过属性selectedKeys关联Channel通道
NIO网络编程相关的(Selector,SelectionKey,ServerSocketChannel,SocketChannel)流程
- 服务端ServerSocketChannel监听端口,等待客户端连接
- 每个客户端连接时,都会通过ServerSocketChannel得到SocketChannel
- 将SocketChannel通过方法register(Selector sel, int ops, Object att),注册到Selector上,一个Selector可以注册多个的SocketChannel
- 注册后,register方法的返回值是SelectionKey
- Selector中有个集合存放所有返回的SelectionKey
- Selector通过方法select()监听所有通道,返回值是有事件发生的通道个数。
- 如果通道有事件发生了,获得有事件发生的SelectionKey
- 根据SelectionKey的channel()方法得到SocketChannel处理业务
SelectionKey API
SelectionKey表示Selector和网络通道的注册关系,共四种:
- int OP_READ = 1 << 0; 代表读操作
- int OP_WRITE = 1 << 2; 代表写操作
- int OP_CONNECT = 1 << 3; 代表连接已经建立
- int OP_ACCEPT = 1 << 4; 代表有新的网络连接可以accept
其它方法
- public abstract SelectableChannel channel() 得到与之关联的通道
- public abstract Selector selector(); 得到选择器Selector对象
- public final Object attachment(); 得到与之关联的共享数据
- public final boolean isReadable() 是否可读
- public final boolean isWritable() 是否可写
- public final boolean isAcceptable() 是否可accept
NIO编程案例
NIO网络编程案例
- 服务端启动
- 客户端启动,连接到服务端,然后发送数据
- 服务端接收到客户端发送过来的数据,打印
服务端代码
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建一个ServerSocketChannel,监听端口8000
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//创建Selector对象
Selector selector = Selector.open();
//将serverSocketChannel注册到Selector中,关心事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待连接
while (true) {
if (selector.select(1000) == 0) {
System.out.println("服务器等待1s,无连接");
continue;
}
//如果返回大于0,表示有连接,select方法返回的值就是有事件发生的通道数目
//Set<SelectionKey>:存储所有发生事件的通道对应的SelectionKey
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey selectionKey = keyIterator.next();
//根据key对应通道发生的事件做相应的处理
if (selectionKey.isAcceptable()) {
//事件是OP_ACCEPT,有新的客户端连接,分配一个SocketChannel
//这里accept方法不会阻塞,因为本身就是一个连接的事件到这里的
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
//将socketChannel注册到Selector中,关注事件为OP_READ
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (selectionKey.isReadable()) {
//获取selectionKey对应的通道
SocketChannel channel = (SocketChannel)selectionKey.channel();
//获取通道对应的缓冲区Buffer
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
channel.read(buffer);
System.out.println("接收来自客户端的数据为:" + new String(buffer.array(),0,buffer.position()));
}
//移除当前key
keyIterator.remove();
}
}
}
}
客户端代码
public class NIOClient {
public static void main(String[] args) throws Exception {
//创建一个SocketChannel用于与服务端通讯
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8000);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("服务器连接失败,请重试!");
TimeUnit.MILLISECONDS.sleep(10);
}
}
//连接成功,发送数据到服务端
String data = "hello,server";
ByteBuffer buffer = ByteBuffer.wrap(data.getBytes());
socketChannel.write(buffer);
System.in.read();
}
}
NIO群聊功能案例
案例要求
- 编写一个NIO群聊系统,实现服务端和客户端的简单通讯(非阻塞)
- 实现多人群聊
- 服务器端:可以监测用户上线,离线,并实现消息转发功能
- 客户端:通过channel可以无阻塞的发送消息给其它所有用户,同时可以接收其它用户发送的消息(由服务器转发得到)
服务端代码
@Slf4j
public class ChatServer {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
public static final int PORT = 8080;
public ChatServer() {
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
//监听通道事件
private void listen() {
try {
//循环判断是否有连接等事件发生
while (true) {
int count = selector.select();
if (count > 0) {
//有事件发生
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
//连接事件
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
log.info("{} ===> 上线了",socketChannel.getRemoteAddress());
}
//读事件
if (key.isReadable()) {
//读取通道中的数据
readData(key);
}
keyIterator.remove();
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
//读取通道中的数据
private void readData(SelectionKey key) {
SocketChannel channel = null;
try {
channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
String message = new String(buffer.array(),0,buffer.position());
if (count > 0) {
log.info("接收到来自客户端的消息:{}",message);
}
//向其它客户端转发消息
sendInfoToOtherClients(message, channel);
} catch (IOException e) {
try {
log.info("{} ===> 离线了",channel.getRemoteAddress());
//取消注册
key.cancel();
//关闭通道
channel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
//将消息转发到其它客户端
private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
log.info("服务器开始转发消息.....");
//将消息发送到所有的SocketChannel中,排除掉自己self
for (SelectionKey key: selector.keys()) {
Channel channel = key.channel();
if (channel instanceof SocketChannel && channel != self) {
SocketChannel socketChannel = (SocketChannel) channel;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
socketChannel.write(buffer);
}
}
}
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
chatServer.listen();
}
}
客户端代码
@Slf4j
public class ChatClient {
private Selector selector;
private SocketChannel socketChannel;
public static final String HOST = "127.0.0.1";
public static final int PORT = 8080;
private String username;
public ChatClient() throws IOException {
selector = Selector.open();
socketChannel = socketChannel.open(new InetSocketAddress(HOST, PORT));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
username = socketChannel.getLocalAddress().toString().substring(1);
log.info("客户端【{}】登录成功",username);
}
//向服务器发送消息
public void sendInfo(String info) {
info = username + " 说:" + info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
//读取从服务端回复的消息
public void readInfo() {
try {
int readChannels = selector.select();
if (readChannels > 0) {
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
String msg = new String(buffer.array(),0,buffer.position());
log.info("服务端发送过来的数据为:{}",msg);
}
}
keyIterator.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
ChatClient chatClient = new ChatClient();
//启动一个线程,每隔3秒,读取从服务器发送的数据
new Thread(() -> {
while (true) {
chatClient.readInfo();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//发送数据给服务端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
chatClient.sendInfo(msg);
}
}
}
演示
启动服务端,然后启动3个客户端
服务端效果
20:34:33.978 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5573 ===> 上线了
20:34:36.127 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5581 ===> 上线了
20:34:37.858 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5589 ===> 上线了
20:34:48.956 [main] INFO com.dingmb.nio.ChatServer - 接收到来自客户端的消息:127.0.0.1:5573 说:我是5573
20:34:48.957 [main] INFO com.dingmb.nio.ChatServer - 服务器开始转发消息.....
20:34:56.464 [main] INFO com.dingmb.nio.ChatServer - 接收到来自客户端的消息:127.0.0.1:5581 说:我是5581
20:34:56.465 [main] INFO com.dingmb.nio.ChatServer - 服务器开始转发消息.....
20:35:04.682 [main] INFO com.dingmb.nio.ChatServer - 接收到来自客户端的消息:127.0.0.1:5589 说:我是5589
20:35:04.682 [main] INFO com.dingmb.nio.ChatServer - 服务器开始转发消息.....
20:35:14.314 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5573 ===> 离线了
20:35:16.590 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5581 ===> 离线了
20:35:18.728 [main] INFO com.dingmb.nio.ChatServer - /127.0.0.1:5589 ===> 离线了
客户端1效果
20:34:33.978 [main] INFO com.dingmb.nio.ChatClient - 客户端【127.0.0.1:5573】登录成功
我是5573
20:34:56.466 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5581 说:我是5581
20:35:04.682 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5589 说:我是5589
客户端2效果
20:34:36.133 [main] INFO com.dingmb.nio.ChatClient - 客户端【127.0.0.1:5581】登录成功
20:34:48.958 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5573 说:我是5573
我是5581
20:35:04.683 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5589 说:我是5589
客户端3效果
20:34:37.864 [main] INFO com.dingmb.nio.ChatClient - 客户端【127.0.0.1:5589】登录成功
20:34:48.958 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5573 说:我是5573
20:34:56.465 [Thread-0] INFO com.dingmb.nio.ChatClient - 服务端发送过来的数据为:127.0.0.1:5581 说:我是5581
我是5589