目录
关于NIO
NIO:非阻塞IO、IO多路复用、信号驱动IO
三个概念:
1.缓冲区buffer:所有数据都是通过缓冲区进行处理
2.通道channel:可以通过它读取和写入数据,通道是双向,而流是单向的
3.选择器selector:用于管理多个通道,监听注册通道的事件
IO的五种模型
一次IO的读取分为两个阶段(写入操作类似):
1.等待内核空间数据准备阶段
2.数据从内核空间拷贝到用户空间
阻塞型IO:两个阶段是连续阻塞的
非阻塞型IO:第一阶段不阻塞的,第二阶段阻塞
IO多路复用:多个IO操作共同使用一个selector(选择器)去询问哪些IO准备好了,selector负责通知哪些数据准备好了的IO,它们再自己去请求内核数据,第一阶段在selector上阻塞,第二阶段在拷贝数据上阻塞
信号驱动IO:用户进程发起读取请求之前先注册一个信号给内核说明自己需要什么数据,这个注册请求立即返回,等内核数据准备好了,主动通知用户进程,用户进程再去请求读取数据,此时,需要等待数据从内核空间拷贝到用户空间再返回,第一阶段不阻塞,第二阶段在拷贝数据上阻塞
异步IO:用户进程发起读取请求后立马返回,当数据完全拷贝到用户空间后通知用户直接使用数据,两阶段都不阻塞
总结:前四种IO均为同步IO,只有最后一种是异步IO
阻塞/非阻塞,更关心的是当前线程是不是被挂起
同步/异步,更关心的是调用结果是不是随着请求结束而返回
为什么不选择异步IO呢?
那是因为异步IO在linux上还不成熟,而我们的服务器通常都是linux,所以现在大部分框架都不是很支持异步IO
BIO、NIO、AIO
BIO,阻塞型 IO,也称为 OIO,Old IO。
NIO,New IO,Java 中使用 IO 多路复用技术实现,放在
java.nio
包下,JDK1.4 引入。AIO,异步 IO,又称为 NIO2,也是放在
java.nio
包下,JDK1.7 引入。
BIO程序(会阻塞当前线程,直到请求结果返回)
public static void main(String[] args) throws Exception {
// 启动服务端,绑定8001端口
ServerSocket serverSocket = new ServerSocket(8001);
System.out.println("server start");
while (true) {
// 开始接受客户端连接
Socket socket = serverSocket.accept();
System.out.println("one client conn" + socket);
// 启动线程处理连接数据
new Thread(()->{
try {
// 读取数据(input和output是相对于客户端的)
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
// 使用readLine读取服务端的每一行数据
while ((msg = reader.readLine()) != null) {
System.out.println("receive msg:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
NIO程序(使用IO多路复用技术实现的,多个IO操作共同使用一个selector选择器,去询问哪些IO做好了准备,selector 负责通知那些数据准备好了的 IO,它们再自己去请求内核数据)
public static void main(String[] args) throws IOException {
// 创建一个selector
Selector selector = Selector.open();
// 创建一个serverSocketChannel
ServerSocketChannel channel = ServerSocketChannel.open();
// 绑定8080端口
channel.bind(new InetSocketAddress(8002));
// 设置为非阻塞状态
channel.configureBlocking(false);
// 将channel注册到selector上,并注册Accept事件
channel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("server start");
while (true) {
// 阻塞在select上(第一阶段阻塞)
selector.select();
// 如果使用的是select(timeout)或selectNow()需要判断返回值是否大于0
// 有就绪的Channel
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历selectKeys
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 如果是accept事件
if (selectionKey.isAcceptable()) {
// 强制转换为ServerSocketChannel
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
// 将SocketChannel注册到Selector上,并注册读事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 如果是读取事件
// 强制转换为SocketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 创建Buffer用于读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据读入到buffer中(第二阶段阻塞)
int length = socketChannel.read(buffer);
if (length > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 将数据读入到byte数组中
buffer.get(bytes);
// 换行符会跟着消息一起传过来
String content = new String(bytes, "UTF-8").replace("\r\n", "");
System.out.println("receive msg: " + content);
}
}
iterator.remove();
}
}
}
AIO程序(AIO,异步 IO,相对于 AIO,其它的 IO 模型本质上都是同步 IO)
public static void main(String[] args) throws IOException {
// 启动服务端
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8003));
System.out.println("server start");
// 监听accept事件,完全异步,不会阻塞
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try {
System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
// 再次监听accept事件
serverSocketChannel.accept(null, this);
// 消息的处理
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据读入到buffer中
Future<Integer> future = socketChannel.read(buffer);
if (future.get() > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 将数据读入到byte数组中
buffer.get(bytes);
String content = new String(bytes, "UTF-8");
// 换行符会当成另一条消息传过来
if (content.equals("\r\n")) {
continue;
}
System.out.println("receive msg: " + content);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("failed");
}
});
// 阻塞住主线程
System.in.read();
}
NIO实现聊天室
package com.example.demo1.chat;
import java.io.IOException;
import java.net.InetAddress;
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;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public class chatServer {
public static void main(String[] args) throws IOException {
// 创建选择器
Selector selector = Selector.open();
// 创建通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("192.168.164.67", 8888));
System.out.println(InetAddress.getLocalHost());
// 设置为非阻塞状态
serverSocketChannel.configureBlocking(false);
// 将accept事件绑定到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞在select上
selector.select();
// 获取选择器中的SelectionKey(1.读事件、2.写事件、3.连接事件、4.接受连接事件)
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历selectKeys
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 如果是accept事件
if (selectionKey.isAcceptable()) {
// 返回创建此键的通道
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
// 接受该通道插座的连接
SocketChannel socketChannel = ssc.accept();
// 输出该通道插座连接到的远程地址
System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
// 设置通道为非阻塞模式
socketChannel.configureBlocking(false);
//将通道注册到选择器上,并注册读事件
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("测试用户是否连接...");
// 将聊天者的通道加入群聊
ChatHolder.join(socketChannel);
}
// 如果是读取事件
else if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据读入到buffer中
int length = socketChannel.read(buffer);
if (length > 0) {
buffer.flip();
// buffer.remaining用于返回缓冲区中字节个数,用于设置字节数组的大小
byte[] bytes = new byte[buffer.remaining()];
// 将数据读入到byte数组中
buffer.get(bytes);
// 换行符会跟着消息一起传过来(使用replace将换行符取代为空格)
String content = new String(bytes, "UTF-8").replace("\r\n", "");
// 如果传输过来的数据字符串为quit则意味着退出群聊
if (content.equalsIgnoreCase("quit")) {
// 退出群聊
ChatHolder.quit(socketChannel);
selectionKey.cancel();
socketChannel.close();
}
// 如果传输过来的数据字符串补位quit则将消息发布到每个加入群聊的用户
else {
// 扩散
ChatHolder.propagate(socketChannel, content);
}
}
}
iterator.remove();
}
}
}
/**
* 聊天者
*/
private static class ChatHolder {
private static final Map<SocketChannel, String> USER_MAP = new ConcurrentHashMap<>();
/**
* 加入群聊
* @param socketChannel
*/
public static void join(SocketChannel socketChannel) {
// 有人加入就给他分配一个id(ThreadLocalRandom:与当前线程隔离的随机数生成器)
String userId = "用户"+ ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
// 调用send方法发送一条消息
send(socketChannel, "您的id为:" + userId + "\n\r");
for (SocketChannel channel : USER_MAP.keySet()) {
send(channel, userId + " 加入了群聊" + "\n\r");
}
// 将当前用户加入到map中
USER_MAP.put(socketChannel, userId);
}
/**
* 退出群聊
* @param socketChannel
*/
public static void quit(SocketChannel socketChannel) {
String userId = USER_MAP.get(socketChannel);
send(socketChannel, "您退出了群聊" + "\n\r");
// 将退出群聊的用户从map中去除
USER_MAP.remove(socketChannel);
for (SocketChannel channel : USER_MAP.keySet()) {
if (channel != socketChannel) {
send(channel, userId + " 退出了群聊" + "\n\r");
}
}
}
/**
* 扩散说话的内容
* @param socketChannel
* @param content
*/
public static void propagate(SocketChannel socketChannel, String content) {
String userId = USER_MAP.get(socketChannel);
for (SocketChannel channel : USER_MAP.keySet()) {
if (channel != socketChannel) {
send(channel, userId + ": " + content + "\n\r");
}
}
}
/**
* 发送消息
* @param socketChannel
* @param msg
*/
private static void send(SocketChannel socketChannel, String msg) {
try {
// 分配一个新的写入字节缓冲区
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
// 将需要传输的数据放入写入缓冲区
writeBuffer.put(msg.getBytes());
// 调换这个buffer的位置,并且设置当前位置为0,将缓存字节数组的下标调增为0,以便从头读取缓存区中的所有数据
writeBuffer.flip();
// 将缓存区中数据写入通道
socketChannel.write(writeBuffer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}