NIO学习
ByteBuffer
ByteBuffer使用
- 向buffer写入数据,调用channel.read(buffer)
- 调用flip()切换至读模式
- 从buffer读取数据,调用buffer.get()
- 调用clear()或compact()切换成写模式
public static void main(String[] args) {
// FileChannel
// 1.输入输出流 2.RandomAccessFile
try (FileChannel channel = new FileInputStream("data.txt").getChannel()) {
// 准备缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
// 从channel读取数据,写入缓冲区
int len = channel.read(buffer);
log.debug("读取到的字节数 {}", len);
if (len == -1) {
break;
}
// 打印缓冲区内容
buffer.flip();//切换读模式
while (buffer.hasRemaining()) { //是否还有剩余未读数据
byte b = buffer.get();
log.debug("实际字节 {}", (char)b);
}
// 切换为写模式
buffer.clear();
}
} catch (IOException e) {
}
}
ByteBuffer结构
属性
- capacity
- position
- limit(读写限制)
读模式下:limit为当前最大写入数量
写模式下:capacity=limit
网络编程
阻塞与非阻塞
阻塞线程(当没有获取到连接或者没有读取到数据时阻塞)
public static void main(String[] args) throws IOException {
// 使用nio阻塞模式
// 构建buffer
ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel open = ServerSocketChannel.open();
//绑定端口
open.bind(new InetSocketAddress(8080));
// 建立连接的集合
List<SocketChannel> channels = new ArrayList<>();
while (true) {
// accept 建立客户端连接,与客户端通信
log.debug("connect...");
SocketChannel src = open.accept();//阻塞方法,停止运行
log.debug("connectted... {}", src);
channels.add(src);
for (SocketChannel channel : channels) {
// 接收客户端信息
log.debug("before read.. {}", channel);
channel.read(buffer); // 阻塞方法,停止运行
log.debug("after read.. {}", channel);
buffer.flip();
System.out.println(buffer);
buffer.clear();
}
}
}
非阻塞模式
public static void main(String[] args) throws IOException {
// 使用nio阻塞模式
// 构建buffer
ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel open = ServerSocketChannel.open();
// 设置成非阻塞
open.configureBlocking(false);
//绑定端口
open.bind(new InetSocketAddress(8080));
// 建立连接的集合
List<SocketChannel> channels = new ArrayList<>();
while (true) {
// accept 建立客户端连接,与客户端通信
SocketChannel src = open.accept();//非阻塞,返回null
if (src != null) {
log.debug("connectted... {}", src);
src.configureBlocking(false);//设置成非阻塞模式
channels.add(src);
for (SocketChannel channel : channels) {
// 接收客户端信息
int read = channel.read(buffer);// 非阻塞,线程不停止,未读到数据返回0
if (read > 0) {
log.debug("after read.. {}", channel);
buffer.flip();
System.out.println(buffer);
buffer.clear();
}
}
}
}
}
问题:CPU空转
Selector
public static void main(String[] args) throws IOException {
// 创建selector,管理多个channel
Selector selector = Selector.open();
ServerSocketChannel open = ServerSocketChannel.open();
open.configureBlocking(false);
// 建立selector和channel之间的联系
// selectionKey事件发生后,通过它得到事件和channel信息
SelectionKey openKey = open.register(selector, 0, null);
openKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key : {}", openKey);
open.bind(new InetSocketAddress(8080));
while (true) {
// selector方法,没有事件就阻塞,有事件继续运行
// 事件未处理,不会阻塞,必须处理或者取消
selector.select();
// 处理事件,selectKey内部包含了所有发生的事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 需要删除selectionKey中的原色,防止下次处理会出现问题
iterator.remove();
log.debug("register key : {}", key);
// 区分事件类型
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, 0, null);
scKey.interestOps(SelectionKey.OP_READ);
log.debug("{}", sc);
} else if (key.isReadable()) {
// 触发事件的channel
try {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
int read = channel.read(buffer);// 正常断开的返回值是-1
if (read == -1) {
key.cancel();
} else {
buffer.flip();
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();
}
}
// key.cancel();
}
}
}
处理消息边界
- 客户端和服务端约定固定长度
- 使用两种不同长度接收
使用attach关联buffer(从而可以进行扩容操作)
public static void main(String[] args) throws IOException {
// 创建selector,管理多个channel
Selector selector = Selector.open();
ServerSocketChannel open = ServerSocketChannel.open();
open.configureBlocking(false);
// 建立selector和channel之间的联系
// selectionKey事件发生后,通过它得到事件和channel信息
SelectionKey openKey = open.register(selector, 0, null);
openKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key : {}", openKey);
open.bind(new InetSocketAddress(8080));
while (true) {
// selector方法,没有事件就阻塞,有事件继续运行
// 事件未处理,不会阻塞,必须处理或者取消
selector.select();
// 处理事件,selectKey内部包含了所有发生的事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 需要删除selectionKey中的原色,防止下次处理会出现问题
iterator.remove();
log.debug("register key : {}", key);
// 区分事件类型
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16); //attachment
// 将bytebuffer作为附件关联
SelectionKey scKey = sc.register(selector, 0, buffer);
scKey.interestOps(SelectionKey.OP_READ);
log.debug("{}", sc);
} else if (key.isReadable()) {
// 触发事件的channel
try {
SocketChannel channel = (SocketChannel) key.channel();
// 获取关联的附件
ByteBuffer buffer = (ByteBuffer) key.attachment();
int read = channel.read(buffer);// 正常断开的返回值是-1
if (read == -1) {
key.cancel();
} else {
split(buffer);
if (buffer.position() == buffer.limit()) {
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
buffer.flip();
newBuffer.put(buffer);
key.attach(newBuffer);
}
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();
}
}
}
}
}
ByteBuffer大小分配
- ByteBuffer不是线程安全,需要每个channel维护一个独立的buffer
- Bytebuffer不能太大,需要设计可变的buffer
- 第一种方式:首先分配一个较小的buffer,每次数据不够进行扩容,优点保证消息连续,缺点是拷贝耗费性能
- 第二种方式,多个数组组成buffer,多出来的内容写入数组,优点避免拷贝的性能损耗,缺点消息不连续,解析复杂
多线程优化
主线程建立连接,子线程处理读写事件
public class MultiThreadServer {
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("boss");
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector boss = Selector.open();
SelectionKey bossKey = ssc.register(boss, 0, null);
bossKey.interestOps(SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while (true) {
boss.select();
Iterator<SelectionKey> iterator = boss.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
}
}
}
}
class Worker implements Runnable{
private Thread thread;
private Selector worker;
private String name;
private volatile boolean start = false;
public Worker(String name) {
this.name = name;
}
// 初始化线程selector
public void register() throws IOException {
if (!start) {
thread = new Thread(this, name);
thread.start();
worker = Selector.open();
start = true;
}
}
@Override
public void run() {
while (true) {
try {
worker.select();
Iterator<SelectionKey> iterator = worker.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(16);
SocketChannel channel = (SocketChannel) key.channel();
channel.read(buffer);
buffer.flip();
}
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
启动之后,因为register和selector先后顺序导致阻塞
解决方法:
- 使用线程安全队列传递数据,其他线程进行唤醒操作
- 先唤醒,然后register
public class MultiThreadServer {
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("boss");
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector boss = Selector.open();
SelectionKey bossKey = ssc.register(boss, 0, null);
bossKey.interestOps(SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
// 创建worker,并进行注册
Worker worker = new Worker("work-0");
while (true) {
boss.select();
Iterator<SelectionKey> iterator = boss.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
log.debug("connected... {}", sc.getRemoteAddress());
log.debug("before register... {}", sc.getRemoteAddress());
worker.register(sc);// boss线程调用
// 关联selector
log.debug("after register... {}", sc.getRemoteAddress());
}
}
}
}
static class Worker implements Runnable{
private Thread thread;
private Selector worker;
private String name;
private volatile boolean start = false;
// 线程传递数据
private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
public Worker(String name) {
this.name = name;
}
// 初始化线程selector
public void register(SocketChannel sc) throws IOException {
if (!start) {
worker = Selector.open();
thread = new Thread(this, name);
thread.start();
start = true;
}
// 队列添加任务,并没有执行
1.
queue.add(() -> {
try {
sc.register(worker, SelectionKey.OP_READ, null); // boss
} catch (ClosedChannelException e) {
e.printStackTrace();
}
});
worker.wakeup();
2.
worker.wakeup();
sc.register(worker, SelectionKey.OP_READ, null); // boss
}
@Override
public void run() {
while (true) {
try {
worker.select(); // 当前阻塞,wakeup进行唤醒
Runnable task = queue.poll();
if (task != null) {
task.run();
}
Iterator<SelectionKey> iterator = worker.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(16);
SocketChannel channel = (SocketChannel) key.channel();
log.debug("read.. {}", channel.getRemoteAddress());
channel.read(buffer);
buffer.flip();
}
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
问题
Runtime.getRuntime().availableProcessors()在docker中获取到的是物理机的核心数,JDK10解决(使用jvm中的UseContainerSupport配置)
NIO和BIO
stream和channel
- stream不会自动缓冲数据,channel会利用系统提供的发送缓冲区,接收缓冲区
- stream只支持阻塞API,channel同时支持阻塞,非阻塞,配合selector实现多路复用
- 都支持读写
IO模型
用户空间和内核空间的切换
阻塞IO:用户线程被阻塞,等待返回
非阻塞IO:用户线程没有被阻塞,有数据的情况下还是阻塞(等待数据非阻塞,复制数据阻塞)
多路复用:使用selector,等待数据阻塞(select),复制数据阻塞(read)
异步IO:线程自己不去获取结果,由其他线程送结果
同步IO:线程自己获取结果
零拷贝
读写操作:用户态和内核态切换3次,数据拷贝4次
NIO优化
使用DireByteBuffer
- ByteBuffer.allocate(10) HeapByteBuffer使用java内存
- ByteBuffer.allocateDirect(10) DireByteBuffer使用操作系统内存
切换2次
sendFile()方法—transferTo/transferFrom拷贝数据(无需切换到用户态)
切换1次
后续优化直接使用DMA刷进去,减少一次切换
AIO
解决数据复制阶段的阻塞问题