文章目录
什么是NIO
NIO是一种非阻塞,基于事件响应的IO
NIO和原生IO有什么不同
IO有三种,BIO,NIO和AIO
BIO:
就是jdk原生的IO,是一个同步阻塞的io(等待客户端链接的时候,等待读写请求的时候都是阻塞的);
客户端和服务端进行io时,都会生成一个线程来管理本次io,当客户端请求多的时候,生成大量线程,耗费资源,用线程池优化,可以解决并发问题,但是线程数并没有减少;
适合小型,访问量不大的项目,代码简单好维护;
NIO:
同步非阻塞io(也可以阻塞);
多个客户端访问服务器端的时候,只会有一个线程进行管理,中间会有一个选择器(多路复用器),选择和某个客户端进行连接(管道);
选择器选择的方式,就是有个while循环,自旋判断当前是否有读写操作,没有就选择和别的管道连接;
适合访问量比较大,连接短的项目;
AIO:
异步非阻塞io;
和NIO的区别在于 “while循环,自旋判断” 的步骤没了,如果有读写操作你就来通知我,而不是我一直循环去问你;
NIO的三大组件核心
三大组件的关系
如图所示:
1.一个channel(可以理解为一个链接)对应一个buffer(一个内存快,底层有一个数组);
2.一个selector对应了多个channel,相当一channel注册到了selector;
3.一个线程对应一个selector,selector通过事件决定对哪个客户端进行连接操作数据,切换channel,是非阻塞的核心;
4.每次数据的读写都要先经过buffer,将数据先放进buffer中在进行操作,普通IO是直接输入输出流的形式;
5.在buffer中如果先对buffer进行写入操作了,现在想读数据,需要 flip 方法切换,反之亦然;
buffer(缓冲区)
1.缓冲区为独立的一块内存,底层包含了一个数组,每次的数据存在这个数组中;
2.所有的读写操作都先经过这个缓冲区,读和写的转换需要 flip 方法切换;
3.底层有4个变量 mark,position,limit,capacity,读写数据就是根据这四个数据来读写的;
mark:标记作用,一般用不到
position:数组记录的数组下标变化,判断是否超过了limit,超过能不能在继续操作数据了
limit:最大能操作的范围,设置3就是能操作数组的前三个数据,设置为capacity,则是能操作全部数据
capacity: 相当于数组的length方法
4.Buffer是一个抽象类,它有很多的子类,IntBuffer,ByteBuffer,基本类型里除了boolen类型以外,全部有相应的实现类
5.前面说到的buffer的底层都有一个数组,这个数组就在对应的子类里,变量名叫做hb,数组存储的数据类型就是当前子类的类型
6.Buffer有很多的api方法可以调用,比如buffer的数据长度,切换读写,从某个位置开始读取 等等,就不一一列举了
Buffer的简单使用
import java.nio.*;
public class NioBufferTest {
public static void main(String[] args) {
//初始化容量为512的buffer, 基本数据类型,除了boolean 都有对应的buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
ShortBuffer shortBuffer = ShortBuffer.allocate(512);
IntBuffer intBuffer = IntBuffer.allocate(512);
LongBuffer longBuffer = LongBuffer.allocate(512);
DoubleBuffer doubleBuffer = DoubleBuffer.allocate(512);
FloatBuffer floatBuffer = FloatBuffer.allocate(512);
CharBuffer charBuffer = CharBuffer.allocate(512);
//向buffer中存放数据
intBuffer.put(10);
intBuffer.put(100);
intBuffer.put(1000);
//上面是存放数据,下面如果要读取数据,就要调用flip方面,将4个属性值就行初始化
intBuffer.flip();
//功能和迭代器的hasNext功能类似
while(intBuffer.hasRemaining()){
//打印buffer中的数据
System.out.println(intBuffer.get());
}
//打印buffer中数据个数
System.out.println(intBuffer.limit());
//打印下标是1,也就是第二个元素
System.out.println(intBuffer.get(1));
}
}
Buffer的注意事项
1.Buffer存数据和取的数据必须一直
例如:ByteBuffer 第一个位置放char类型,第二个放int,那取数时,类型必须和放值顺序保持一直
2.Buffer可以设置只读buffer,调用buffer的asReadOnlyBuffer方法
3.操作大文件使用MappedByteBuffer,他是在堆外内存直接进行操作,少了JVM和操作系统间的复制操作,此类继承自ByteBuffer,所以ByteBuffer的功能它都有
4.Buffer可以使用数组的形式进行读写,此形式的数组是按照数组下标以此进行:
如写数据会先将第一个buffer写满,在写第二个buffer,以此类推,读数据也是一个道理
channel(通道)
1.channel可以理解为一个连接,一个流,它主要是和buffer进行连接,然后对buffer中的数据进行读写
2.它可以异步双向传输(又可以写数据到通道又可以读数据到通道),但是必须和buffer配合使用
3.通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入,和第二点相对应
4.常用实现类
FileChannel:用于操作文件
DatagramChannel:通过UDP读写网络中的数据
SocketChannel:通过TCP读写网络中的数据
ServerSocketChannel:监听每一个TCP连接,对每一个新进来的连接创建一个SocketChannel。
FileChannel的简单使用
1.将"Hello word" 写入文件a.txt中
2.读取文件a.txt中的内容
注意:
Channel.write(buffer):是将缓存区数据写入通道中
Channel.read(buffer):是读取通道中的数据写入缓存区
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
public class NioFileChannelTest {
public static void main(String[] args) {
try {
//将 hello word 写入到文件中
writeToFile(" hello word");
//打印读取的数据
System.out.println(readToFile());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 将msg写入到文件中
*/
public static void writeToFile(String msg) throws Exception {
//获取文件输出流
FileOutputStream fileOutputStream = new FileOutputStream(new File("Z:/a.txt"));
//获取fileChannel
FileChannel fileChannel = fileOutputStream.getChannel();
//初始化buffer
ByteBuffer buffer = ByteBuffer.allocate(512);
//将数据先写入buffer中
buffer.put(msg.getBytes(StandardCharsets.UTF_8));
//上面的buffer在写入操作,下面要读取操作,所以要变换一次状态
buffer.flip();
//将buffer中的数据写入通道中
//由于通道和流是绑定的(流的方法点出了通道),流又和文件绑定,所以写入了通道,文件中就有数据了
fileChannel.write(buffer);
//关闭流
fileOutputStream.close();
fileChannel.close();
}
/**
* 打印从文件中读取的数据
*/
public static String readToFile() throws Exception {
//获取文件输入流
FileInputStream fileInputStream = new FileInputStream(new File("Z:/a.txt"));
//获取fileChannel
FileChannel fileChannel = fileInputStream.getChannel();
//初始化buffer
ByteBuffer buffer = ByteBuffer.allocate(512);
//将通道数据写入buffer中
while (fileInputStream.read() != -1) {
fileChannel.read(buffer);
}
//关闭流
fileInputStream.close();
fileChannel.close();
//转换类型
buffer.flip();
//初始化字符串容器
StringBuffer sb = new StringBuffer();
//如果buffer中有未读取数据,就循环
while (buffer.hasRemaining()) {
//添加到字符串容器中
sb.append((char) buffer.get());
}
return sb.toString();
}
}
selector(选择器)
图片是借来的,出处 https://cloud.tencent.com/developer/article/1129674
1.selector 是基于事件驱动唤醒主线程分配资源进行处理,在没有事件时是阻塞状态的
2.事件驱动包括:读、写、连接成功、请求连接四种
3.服务器端会创建一个ServerSocketChannel,注册到 selector 上面,并在注册时告诉 selector 想要监听的事件
4.客户端会创建一个 SocketChannel,注册到 selector 上面(一个 selector 可以注册多个 SocketChannel)
使用 selector 实现简单的读写通讯
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* server 端代码
*/
public class NioServer {
public static void main(String[] args) {
try {
//获取ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定监听端口 ,监听8888
serverSocketChannel.socket().bind(new InetSocketAddress("localhost",8888));
//设置非阻塞
serverSocketChannel.configureBlocking(false);
//创建selector对象
Selector selector = Selector.open();
//将socket注册到 selector 上,并创建监听事件为 “连接成功”
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//selector对象2秒一次轮询有没有事件发生,select执行大于0,说明有监听的事件发生,返回结果等于几,就说明有几个监听的事件发生
while(true){
if(selector.select(2000) == 0){
System.out.println("当前无事件发生,继续循环");
continue;
}
//获取所有事件key,并循环遍历
Iterator<SelectionKey> iteratorKey = selector.selectedKeys().iterator();
while(iteratorKey.hasNext()){
//当前key
SelectionKey key = iteratorKey.next();
//已经连接的事件
if(key.isAcceptable()){
System.out.println("已经连接的事件");
// 创建新的连接,并且把连接注册到selector上,而且,
// 声明这个channel对读、写操作都感兴趣。
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
}
//读请求的事件
if(key.isReadable()){
System.out.println("读请求的事件");
ByteBuffer readBuff = ByteBuffer.allocate(1024);
//获取出关联的通道
SocketChannel socketChannel = (SocketChannel) key.channel();
//将通道中数据读出,写入buffer
socketChannel.read(readBuff);
//转换
readBuff.flip();
System.out.println("readBuff : " + new String(readBuff.array()));
}
//写求情的事件
if(key.isWritable()){
System.out.println("写求情的事件");
//获取出关联的通道
SocketChannel socketChannel = (SocketChannel) key.channel();
//将通道中数据读出,写入buffer
socketChannel.write(ByteBuffer.wrap("收到一个写请求".getBytes()));
}
//有连接请求的事件
if(key.isConnectable()){
System.out.println("有连接请求的事件");
}
//删除当前key
iteratorKey.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* client 端代码
*/
public class NioClient {
public static void main(String[] args) {
try {
//创建SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//绑定连接的地址
socketChannel.connect(new InetSocketAddress("localhost",8888));
while (!socketChannel.finishConnect()){
System.out.println("连接中,请稍后");
}
// //发送写数据请求
socketChannel.write(ByteBuffer.wrap("给服务器发送写请求".getBytes()));
//发送读数据请求
ByteBuffer readBuffer = ByteBuffer.allocate(64);
while(socketChannel.read(readBuffer) == 0){
System.out.println("正在读取中,请稍后~~");
Thread.sleep(1000);
}
readBuffer.flip();
System.out.println("readBuffer : " + new String(readBuffer.array()));
//不让当前服务停止
System.in.read();
} catch (Exception e) {
e.printStackTrace();
}
}
}
NIO实现简单的群聊功能
要求:
1.实现客户端上线,下线通知功能;
2.实现某一个客户端发送消息,其他客户端也能接收;
代码逻辑:
1.编写服务器端代码
1).监听客户端上线,下线
2).接收客户端发送的消息,并进行转发,转发时提出 当前发送消息的 客户端
2.编写客户端代码
1).连接服务器端
2).发送消息
3).接收消息
package com.clamc.group;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* server 端代码
*/
public class NioServer {
//定义全局变量
ServerSocketChannel serverSocketChannel;
Selector selector;
public NioServer(){
try {
//获取ServerSocketChannel
serverSocketChannel = ServerSocketChannel.open();
//绑定监听端口 ,监听8888
serverSocketChannel.socket().bind(new InetSocketAddress("localhost",8888));
//设置非阻塞
serverSocketChannel.configureBlocking(false);
//创建selector对象
selector = Selector.open();
//将socket注册到 selector 上,并创建监听事件为 “连接成功”
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 监听事件
*/
private void listen(){
try {
while (true) {
//有事件发生,selector.select() 这个方法是堵塞的
if (selector.select() > 0) {
//获取所有事件key,并循环遍历
Iterator<SelectionKey> iteratorKey = selector.selectedKeys().iterator();
while (iteratorKey.hasNext()) {
//当前key
SelectionKey key = iteratorKey.next();
//已经连接的事件
if (key.isAcceptable()) {
//创建一个监听 读事件 的socket
SocketChannel accept = serverSocketChannel.accept();
//设置非阻塞
accept.configureBlocking(false);
//监听 读事件
accept.register(selector, SelectionKey.OP_READ);
System.out.println("用户" + accept.getRemoteAddress() + "上线啦");
send("用户" + accept.getRemoteAddress() + "上线啦",accept);
}
//读请求的事件
if (key.isReadable()) {
read(key);
}
//删除当前key
iteratorKey.remove();
}
}
}
}catch (IOException e){
e.printStackTrace();
}
}
/**
* 读事件的操作
*/
private void read(SelectionKey key) throws IOException {
SocketChannel channel = null;
try {
//获取到当前有事件发生的 SocketChannel
channel = (SocketChannel) key.channel();
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将channel中的数据写入buffer
channel.read(buffer);
String msg = new String(buffer.array());
//打印
System.out.println(msg);
//将数据发送到其他客户端
send(msg, channel);
}catch (IOException e){
System.out.println("用户" + ((SocketChannel)key.channel()).getRemoteAddress() + "下线啦");
//发送消息
send("用户" + ((SocketChannel)key.channel()).getRemoteAddress() + "下线啦",(SocketChannel)key.channel());
//取消注册
key.cancel();
channel.close();
}
}
/**
* 将数据发送到其他客户端
* @param self 当前发送消息的客户端,转发消息时,不应包含当前客户端
*/
private void send(String msg,SocketChannel self) throws IOException {
//获取所有注册的 socket 的迭代器
Iterator<SelectionKey> iterator = selector.keys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
//获取 循环的 客户端SocketChannel
Channel channel = key.channel();
//判断循环的数据 不是当前客户端
if(channel instanceof SocketChannel && channel != self){
//转型
SocketChannel target = (SocketChannel)channel;
//创建 buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将buffer中数据写入通道
target.write(buffer);
}
}
}
public static void main(String[] args) {
//初始化服务器
NioServer nioServer = new NioServer();
nioServer.listen();
}
}
package com.clamc.group;
import java.io.IOException;
import java.net.InetSocketAddress;
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;
/**
* client 端代码
*/
public class NioClient {
//定义全局变量
SocketChannel socketChannel;
Selector selector;
String userName;
public NioClient(){
//创建SocketChannel
try {
//绑定连接的地址
socketChannel = socketChannel.open(new InetSocketAddress("localhost",8888));
//设置非阻塞
socketChannel.configureBlocking(false);
//创建selector对象
selector = Selector.open();
//注册到 selector
socketChannel.register(selector, SelectionKey.OP_READ);
//当前路径,方便 打印
userName = socketChannel.getLocalAddress().toString().substring(1);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 接受服务器消息
*/
private void read() throws IOException {
if(selector.select() > 0){
//读取数据
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey next = iterator.next();
if(next.isReadable()){
SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
// 把读到的缓冲区数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
iterator.remove();
}
}
}
/**
* 发送数据
* @param msg
*/
private void send(String msg){
msg = userName + " : " + msg;
try {
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//接受数据
NioClient client = new NioClient();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
//两秒一次读取数据
client.read();
Thread.sleep(2000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
//发送数据
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
client.send(scanner.nextLine());
}
}
}