慕课网课程链接:https://www.imooc.com/video/19331
Java 知识目录:https://blog.csdn.net/qq_38009970/article/details/103266870
文章目录
NIO
- 用于应对高性能高并发的场景
BIO 的网络模型缺点
- 阻塞式 IO 模型
- 弹性伸缩能力差
- 多线程耗资源
NIO网络模型
- 主要分为两步:
- 建立连接
- 发送请求
NIO 核心
Channel
- 双向性
- 非阻塞性
- 操作唯一性
实现类
- 文件类:
FileChannel
- UDP类:
DatagramChannel
- TCP类:
ServerSocketChannel/SocketChannel
Channel 使用:ServerSocketChannel && SocketChannel
//服务端通过服务器socket创建channel
ServerSocketChannel serverSocketChannel = new ServerSocketChannel.open();
//服务器端绑定端口
serverSocketChannel.bind(new InetSocketAddress(8000));
//服务器监听客户端连接,建立 socketChannel 连接
SocketChannel socketChannel = serverSocketChanel.accept();
//客户端连接远程主机及端口
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8000));
Buffer
- 读写
Channel
中的数据 - 本质是一块内存区域
- 所有基本类型都有其对应的
Buffer
类
属性
- Capacity:容量
- 表明
byte
数组可以容纳的字节数
- 表明
- Position:位置
- 写数据时,表示当前位置,初始位置为 0。当一个数据写入到
buffer
后,position
会向后移动到可写的位置。最大值为容量 - 1
- 读数据时,
position
会被重置为0。当从中读取数据时,positon
会向后移动到可读的位置
- 写数据时,表示当前位置,初始位置为 0。当一个数据写入到
- Limit:上限
- 写模式下,表示最多能向
buffer
中写入多少数据。此时Limit = Capacity
- 读模式下,表示最多能从
buffer
中读取多少数据。此时Limit = 写模式下的position值
- 写模式下,表示最多能向
- Mark:标记
- 存储一个特定的
position
位置。可以调用Buffered.reset
方法获取这个位置
- 存储一个特定的
API使用 :以 ByteBuffer 为例
Buffer API
Buffer flip() 翻转这个缓冲区。
Buffer mark() 将此缓冲区的标记设置在其位置。
Buffer clear() 清除此缓冲区。
ByteBuffer API:
static ByteBuffer allocate(int capacity) 分配一个新的字节缓冲区。
ByteBuffer put(byte[] src, int offset, int length) 相对大容量 put方法 (可选操作) 。
Selector
- 作用:I/O 的就绪与选择
- 是 NIO 网络编程的基础
SelectonKey 状态
- OP_ACCEPT 操作集位用于插座接受操作。
- OP_CONNECT 用于套接字连接操作的操作集位。
- OP_READ 读操作的操作位。
- OP_WRITE 写操作的操作位。
使用
//创建Selector
Selector selector = new Selector.open();
//将 channel 注册到 selector 上,监听读就绪事件
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_READ);
//阻塞等待 channel 有就绪事件发生
int selectNum = selector.select();
//获取就绪事件的 channel 集合
Set<SelectionKey> selectionkeys = selector.selectedKeys();
NIO编程实现
实现步骤
服务器端
- 创建
Selector
- 创建
ServerSocketChannel
,并绑定监听端口 - 将
Channel
设置为非阻塞模式 - 将
Channel
注册到Selector
上,监听链接事件 - 循环调用
Selector
的select
方法,检测就绪情况 - 调用
selectedKey
方法获取就绪channel
集合 - 判断就绪事件总类,调用业务处理方法
- 根据业务需要决定是否再次注册监听事件,重复执行第 3 步操作
代码示例:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class NioServer {
public void start() throws Exception {
//1. 创建 `Selector`
Selector selector = Selector.open();
//2. 创建 `ServerSocketChannel`,并绑定监听端口
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress(8000));
//3. 将`Channel`设置为非阻塞模式
channel.configureBlocking(false);
//4. 将`Channel` 注册到 `Selector` 上,监听链接事件
channel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动成功");
//5. 循环调用`Selector`的`select`方法,检测就绪情况
for(;;){
//TODO 获取可用连接的数量
int readChannel = selector.select();
//TODO 为什么加这一句
if(readChannel == 0) continue;
//6. 调用 `selectedKey` 方法获取就绪 `channel` 集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
//获取 SelectionKey 实例
SelectionKey selectionKey = iterator.next();
//移除当前的 selectionKey
iterator.remove();
//7. 判断就绪事件总类,调用业务处理方法
//如果是接入事件
if(selectionKey.isAcceptable()){
acceptHandler(channel,selector);
}
//如果是可读事件
if(selectionKey.isReadable()){
readHandler(selectionKey,selector);
}
}
}
}
/**
* 接入事件处理器
*/
public void acceptHandler(ServerSocketChannel serverSocketChannel,Selector selector) throws Exception{
//如果是接入事件,创建socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//将socketChannel设置为非阻塞的工作模式
socketChannel.configureBlocking(false);
//将ServerSocketChannel注册到selector上,监听可读事件
socketChannel.register(selector,SelectionKey.OP_READ);
//回复给客户端信息
socketChannel.write(Charset.forName("UTF-8").encode("你与聊天室里其他人都不是朋友关系,请注意隐私安全"));
}
/**
* 可读事件处理器
*/
public void readHandler(SelectionKey selectionKey,Selector selector)throws Exception{
//从 socketChannel 中获取已就绪的channel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//循环读取客户端的请求信息
String request = "";
while (socketChannel.read(byteBuffer)>0){
//切换buffer模式
byteBuffer.flip();
//读取buffer中的内容
request += Charset.forName("UTF-8").decode(byteBuffer);
}
//将socketChannel再次注册到selector上,监听它的可读事件
socketChannel.register(selector,SelectionKey.OP_READ);
//将客户端发送的请求消息发送给其他客户端
if(request.length()>0){
broadCast(selector,socketChannel,request);
}
}
public void broadCast(Selector selector,
SocketChannel sourceChannel,
String request){
//获取已接入的客户端Channel
Set<SelectionKey> selectionKeys = selector.keys();
selectionKeys.forEach(selectionKey -> {
Channel channel = selectionKey.channel();
if(channel instanceof SocketChannel && channel != sourceChannel){
try {
((SocketChannel)channel).write(Charset.forName("UTF-8").encode(request));
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
public static void main(String[] args) throws Exception {
NioServer nioServer = new NioServer();
nioServer.start();
}
}
客户端
- 连接服务器
- 获取服务器响应数据
- 向服务器发送数据
代码实现
NioClient
public class NioClient {
public void start(String nickname) throws Exception{
//连接服务器
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8000));
System.out.println("客户端启动成功!");
//获取服务端响应数据
Selector selector = Selector.open();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
new Thread(new NioClientHandler(selector)).start();
//向服务器端发送数据
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String request = scanner.nextLine();
if(request != null && request.length()>0){
socketChannel.write(Charset.forName("UTF-8").encode(nickname + " : " + request));
}
}
}
public static void main(String[] args) throws Exception {
NioClient nioClient = new NioClient();
nioClient.start("A");
}
}
ClientHandler
作用:获取服务端响应数据。和 NioClient for循环的代码一致
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class NioClientHandler implements Runnable{
private Selector selector;
public NioClientHandler(Selector selector) {
this.selector = selector;
}
@Override
public void run() {
//循环调用selector接口,检测就绪状态
try {
for(;;){
int select = selector.select();
if(select == 0) continue;
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
if(selectionKey.isReadable()){
readHandler(selectionKey,selector);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void readHandler(SelectionKey selectionKey,Selector selector) throws IOException {
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
StringBuffer response = new StringBuffer(new String(""));
while (channel.read(byteBuffer) > 0){
byteBuffer.flip();
response.append(Charset.forName("UTF-8").decode(byteBuffer));
}
channel.register(selector,SelectionKey.OP_READ);
if(response.length()>0){
System.out.println(response);
}
}
}