NIO 基础
non-blockiong io:非阻塞
阻塞vs非阻塞
三大组件
1.channel & Buffer
channel : 双向流
Buffer:暂存数据 缓冲区
常见channel:
- List item
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
常见的buffer
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
Selector
多线程版服务器设计
- 内存占用过高,来一个人就要开辟一个线程
- 线程上下文切换成本高 ,cpu上面能承载的线程是有限度的
线程池班的设计
阻塞
阻塞操作是指在执行设备操作时,托不能获得资源,则挂起进程直到满足操作所需的条件后再进行操作。被挂起的进程进入休眠状态(不占用cpu资源),从调度器的运行队列转移到等待队列,直到条件满足。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Iw4czSpD-1611297346687)(C:\Users\123\Pictures\Markdown\image-20201228213409948.png)]
非阻塞
非阻塞操作是指在进行设备操作是,若操作条件不满足并不会挂起,而是直接返回或重新查询(一直占用CPU资源)直到操作条件满足为止。
————————————————
版权声明:本文为CSDN博主「几盒猫」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_46216143/article/details/112984526
- 阻塞模式下,一个线程在同一时间只能处理一个socket
- 仅适合短链接场景
Selector 版设计
selector 的作用就是配合一个线程来管理多个 channel(fileChannel因为是阻塞式的,所以无法使用selector),获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,当一个channel中没有执行任务时,可以去执行其他channel中的任务。适合连接数多,但流量较少的场景
线程的利用率得到提升 。
bytebuffer结构
一开始是空的 写完,position的移动类似于栈 先写入数据再 ++ ,一开始posiotion再0号位置
切换写模式直接重置指针 ,limit是限制
channel 和 读写案列
将读取的数据写入channel 然后放入缓冲区 buffer ,在从buffer 取出字节
向buffer写数据:1.调用channel的read (例如下面的例子)
2.调用buffer的put方法 例如buf.put((byte)127)
@Test
void contextLoads() {
//FileChannel 1.输入流 输出流 2.R
try (FileChannel channel = new FileInputStream(("src/main/resources/data.txt")).getChannel()){
//设置缓冲区
ByteBuffer buffer = ByteBuffer.allocate(3);
//读取数据
while (channel.read(buffer)!=-1){
//打印
buffer.flip() ;//切换成读模式
while (buffer.hasRemaining()){ //看是否还有剩余
byte b = buffer.get();
// log.info("读取到的字节:{}",(char)b);
System.out.print((char) b);
}
buffer.clear() ; //切换到写模式
}
} catch (IOException e) {
e.printStackTrace();
}
}
bytebuffer使用
读取会自动转化为十进制
数据前移并不会删除 position所指向的3号位置 :64,会出现两个64
但是下次写还是会从 position3号位置开始写 直接覆盖64
allocate和allocateDirect
- 放入堆内存会收到gc 影响,读写效率低下 收到gc的影响
- 放入直接内存 读写效率高(少一次拷贝) 不受到gc影响
- netty 对此都有提高
mark & reset
@Test
public void test2(){
ByteBuffer buf = ByteBuffer.allocate(10);
buf.put(new byte[]{'a','b','c','d'});
buf.flip(); // 切换成读模式
//position位置跳到 5
buf.get(new byte[4]);
//重复读 position位置:0
buf.rewind();
byte b = buf.get();
System.out.println((char) b); //p:0->1
byte b2 = buf.get();
System.out.println((char) b2);//p:1->2
//mark & reset
//mark 作一个标记 ,记录position的位置 , 然后读 ,然后reset 将position位置重置到标记位
buf.mark(); //p: 2
byte b3 = buf.get();
System.out.println((char) b3); //p:2->3
byte b4 = buf.get();
System.out.println((char) b4); //p:3->4
buf.reset();
byte b5 = buf.get();
System.out.println((char) b5); //p:2->3
byte b6 = buf.get();
System.out.println((char) b6);//p:3->4
}
字符串《 —----互相转化——》byth
@Test
public void test3(){
//1.字符串转化为 byteBuffer
ByteBuffer buf = ByteBuffer.allocate(15);
buf.put("hello".getBytes());
//2. charset
ByteBuffer hello = StandardCharsets.UTF_8.encode("hello");
//3.wrap
ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8));
}
半包 ,黏包
@Test
public void tes4(){
ByteBuffer buffer = ByteBuffer.allocate(330);
//黏包 三条数据在一起发送以\n 发送
buffer.put("hello\n nihao\nchengkuan".getBytes());
split(buffer) ;
//半包: 数据不全
buffer.put(" xihuan ni \n".getBytes());
split(buffer) ;
}
public static void split(ByteBuffer buffer){
buffer.flip() ;
for (int i =0;i< buffer.limit();i++){
if (buffer.get(i)=='\n' ){
int length = i+1-buffer.position();
ByteBuffer source = ByteBuffer.allocate(length+1);
for ( int k = 0;k <length; k++){
byte out = buffer.get(k);
System.out.print((char) out);
}
}
}
}
FileChannel:文件拷贝
# 传输效率高 最高2G
#可以多次传输
@Test
public void tes5(){
try(
FileChannel from = new FileInputStream("src/main/resources/data.txt").getChannel();
FileChannel to = new FileOutputStream("src/main/resources/to.txt").getChannel();
){
from.transferTo(0,from.size(),to);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
改进
@Test
public void tes5(){
try(
FileChannel from = new FileInputStream("src/main/resources/data.txt").getChannel();
FileChannel to = new FileOutputStream("src/main/resources/to.txt").getChannel();
){
long size = from.size();
//left 还剩余多少字节没有传输
for (long left = from.size();left>0;){
//transferTo一次只能写2g 假设4G 第一次:写了2G 第二次:from.transferTo(4-2, 2, to);
// size-left :从上一次位置开始获取值 left:一共要写多少数据
left -= from.transferTo(size-left, left, to);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
遍历多级目录
@Test
public void tes2() throws IOException {
//在匿名内部内种只能用final类型的变量 ,故此处计数器采用AtomicInteger
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger fileCount = new AtomicInteger();
Files.walkFileTree(Paths.get("src/main"),new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("====>"+dir);
dirCount.incrementAndGet() ;
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("文件夹是: "+file);
return super.visitFile(file, attrs);
}
});
System.out.println(dirCount);
}
Files文件拷贝
@Test
public void tes6() throws IOException {
String source = "E:\\yuanmashidai\\project\\nettytest01\\src\\main\\resources";
//D 换成 E 会造成循环递归
String dis = "D:\\yuanmashidai\\project\\nettytest01\\src\\main\\resources\\to";
Files.walk(Paths.get(source)).forEach(path -> {
//扫到了 src/main/resources/data.txt -》src/main/resources/to/data.txt
String tar = path.toString().replace(source, dis);
if (Files.isDirectory(path)){
try {
Files.createDirectories(Paths.get(tar));
} catch (IOException e) {
e.printStackTrace();
}
}
if (Files.isRegularFile(path)){
try {
Files.copy(path,Paths.get(tar));
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
阻塞模式
服务器端
//阻塞 :线程没法读到数据的时候 就会停止运行
public static void main(String[] args) throws IOException {
//用户发的数据
ByteBuffer buff = ByteBuffer.allocate(16);
//nio 理解阻塞
//建立服务器
log.info("connect ....");
ServerSocketChannel ssc = ServerSocketChannel.open();
//建立监听端口
ssc.bind(new InetSocketAddress(8080)) ;
//建立连接集合
ArrayList<SocketChannel> channels = new ArrayList<>();
while (true){
//sc 与客户端进行通信
SocketChannel sc = ssc.accept();
log.info("connect ....{}",sc);//阻塞模式 连接建立以后才会运行
channels.add(sc) ;
for (SocketChannel channel : channels) {
//读取数据 将数据读进buff
channel.read(buff); //
//buff切换到读模式
buff.flip() ; //buf也是阻塞的 ,再没有数据的时候不会执行大于
log.info("readSource ....{}");
System.out.println((char) buff.get());
//再换成写模式 position 置 0
buff.clear() ;
log.info("clear ....{}");
}
}
}
客户端
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
//与服务器建立连接
sc.connect(new InetSocketAddress("localhost",8080));
System.out.println("waiting");
}
将客户端改为非阻塞模式
//阻塞 :线程没法读到数据的时候 就会停止运行
public static void main(String[] args) throws IOException {
//用户发的数据
ByteBuffer buff = ByteBuffer.allocate(16);
//nio 理解阻塞
//建立服务器
log.info("connect ....");
ServerSocketChannel ssc = ServerSocketChannel.open();
//将 客户端改为非阻塞模式
ssc.configureBlocking(false);
//建立监听端口
ssc.bind(new InetSocketAddress(8080)) ;
//建立连接集合
ArrayList<SocketChannel> channels = new ArrayList<>();
while (true){
//sc 与客户端进行通信
SocketChannel sc = ssc.accept();
if (sc!=null){
sc.configureBlocking(false) ; //连接管道设置为非阻塞
log.info("connect ....{}",sc);//阻塞模式 连接建立以后才会运行
channels.add(sc) ;
}
for (SocketChannel channel : channels) {
//读取数据 将数据读进buff
int read = channel.read(buff);// 未读到数据 read = 0,此时sc = channel
// 当 sc开启非阻塞 ,channel不会报空指针,前提sc不为空
//buff切换到读模式
buff.flip() ; //buf也是阻塞的 ,再没有数据的时候不会执行大于
if(read!=0){
log.info("readSource ....{}");
}
// System.out.println((char) buff.get());
//再换成写模式 position 置 0
buff.clear() ;
if(read!=0){
log.info("clear ....{}");
}
}
}
}
selector 模式
- accept: 会在有连接请求的时候触发 serverSocketChannel.accept() 返回一个 SocketChannel
- connect:是客户端 , 连接建立后触发
// selectionkey关注以下的 事件 sscKey.interestOps(SelectionKey.OP_ACCEPT)
- read :可读事件
- write :可写事件
事件发生后要么处理要么取消不能置之不理
selector 就是用来监听事件的
selector 何时不阻塞
- 客户端发起请求触发 accept
2.客户端发送数据,客户端正常 异常关闭 ,都会触发read事件,如果数据大于buffer缓冲区 会触发多次读取事件
3.channel 可写,会触发write事件
4.在linux发生nio bug时也会
调用selector.wakeup
调用seletor.close
selector所在线程interrupt
@Slf4j
public class server02 {
public static void main(String[] args) throws IOException, InterruptedException {
//引入selecter :管理 多个channel !!!
//1. 创建selector ,selector 会监听四种事件
Selector selector = Selector.open();
ByteBuffer buff = ByteBuffer.allocate(16);
//nio 理解阻塞
//建立服务器
log.info("connect ....");
//ssc 可以创建 channel
ServerSocketChannel ssc = ServerSocketChannel.open();
//将 客户端改为非阻塞模式
ssc.configureBlocking(false);
//2. 将 channel 注册进selector sscKey相当于一个管理员
SelectionKey sscKey = ssc.register(selector, 0, null);
log.debug("管理员是 :{}",sscKey);
sscKey.interestOps(SelectionKey.OP_ACCEPT) ;
//建立监听端口
ssc.bind(new InetSocketAddress(8080)) ;
//建立连接集合
ArrayList<SocketChannel> channels = new ArrayList<>();
while (true){
//3. 没有事件线程阻塞 ,有事件发生 才会恢复向下运行
log.info("遍历一下~~~");
selector.select();
//处理事件 ,selectionKeys 包含了所以可发生的事件 !
// Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
Thread.sleep(5000);
while (iterator.hasNext()){
log.info("遍历一下 SelectionKey ~~~");
//SelectionKey :代表了每一个事件
SelectionKey key = iterator.next();
log.debug("key:{}",key);
//4. 拿到 触发事件的channel 假设将下面的代码进行注释 ,则会发生
//log.info("遍历一下~~~");无线循环 ,未处理的事件会重新加入队列
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//建立连接
SocketChannel sc = channel.accept();
log.debug("channel:{}",sc);
//key.cancel:将事件取消
}
}
}
}
让selector处理不同事件
public static void main(String[] args) throws IOException, InterruptedException {
//引入selecter :管理 多个channel !!!
//1. 创建selector ,selector 会监听四种事件
Selector selector = Selector.open();
ByteBuffer buff = ByteBuffer.allocate(16);
//nio 理解阻塞
//建立服务器
log.info("connect ....");
//ssc 可以创建 channel
ServerSocketChannel ssc = ServerSocketChannel.open();
//将 客户端改为非阻塞模式
ssc.configureBlocking(false);
//2. 将 channel 注册进selector sscKey相当于一个管理员
SelectionKey sscKey = ssc.register(selector, 0, null);
log.debug("管理员是 :{}",sscKey);
sscKey.interestOps(SelectionKey.OP_ACCEPT) ;
//建立监听端口
ssc.bind(new InetSocketAddress(8080)) ;
//建立连接集合
ArrayList<SocketChannel> channels = new ArrayList<>();
while (true){
//3. 没有事件线程阻塞 ,有事件发生 才会恢复向下运行
log.info("遍历一下~~~");
selector.select();
//处理事件 ,selectionKeys 包含了所以可发生的事件 !
// Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
Thread.sleep(5000);
while (iterator.hasNext()){
log.info("遍历一下 SelectionKey ~~~");
//SelectionKey :代表了每一个事件
SelectionKey key = iterator.next();
//区分事件类型
if (key.isAcceptable()){
log.debug("is accept things ,key:{}",key);
//4. 拿到 触发事件的channel 假设将下面的代码进行注释 ,则会发生
//log.info("遍历一下~~~");无线循环 ,未处理的事件会重新加入队列
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//建立连接
SocketChannel sc = channel.accept();
sc.configureBlocking(false) ;
//将 socketchannel注册给selector SelectionKey 《--- (sscKey 管理ServerSocketChannel)
// 《 -- (sc 管理Channel)
SelectionKey scKey = sc.register(selector, 0, null);
//设置关注事件类型
scKey.interestOps(SelectionKey.OP_READ) ;
log.debug("channel:{}",sc);
}else if (key.isReadable()){
log.debug("is read things ,key:{}",key); //只有触发accetp 建立一个管道 注册进selecor 才能读取数据
SocketChannel channel = (SocketChannel) key.channel();
//读取事件
ByteBuffer buffer = ByteBuffer.allocate(166);
//读取数据
channel.read(buff) ;
buffer.flip() ;// 切换读模式
}
//key.cancel:将事件取消
}
}
}
为什么处理完一个事件需要删除
避免重复处理 。
添加链接描述
- ssc (serversocketChannel):监听 accept 注册进红框
- sc(socketChannel):监听读写 注册进红框
- 来了一个accept事件: ssc 进入绿框 处理事件 :建立了一个 sc (建立sockerchannel),然后删除事件
- 来了一个 read事件: ssc 进入绿框 处理事件 :建立了一个 sc (监听读写),然后删除事件
public static void main(String[] args) throws IOException, InterruptedException {
//引入selecter :管理 多个channel !!!
//1. 创建selector ,selector 会监听四种事件
Selector selector = Selector.open();
ByteBuffer buff = ByteBuffer.allocate(16);
//nio 理解阻塞
//建立服务器
log.info("connect ....");
//ssc 可以创建 channel
ServerSocketChannel ssc = ServerSocketChannel.open();
//将 客户端改为非阻塞模式
ssc.configureBlocking(false);
//2. 将 channel 注册进selector sscKey相当于一个管理员
SelectionKey sscKey = ssc.register(selector, 0, null);
log.debug("管理员是 :{}",sscKey);
sscKey.interestOps(SelectionKey.OP_ACCEPT) ;
//建立监听端口
ssc.bind(new InetSocketAddress(8080)) ;
//建立连接集合
ArrayList<SocketChannel> channels = new ArrayList<>();
while (true){
//3. 没有事件线程阻塞 ,有事件发生 才会恢复向下运行
log.info("遍历一下~~~");
selector.select();
//处理事件 ,selectionKeys 包含了所以可发生的事件 !
// Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
Thread.sleep(5000);
while (iterator.hasNext()){
log.info("遍历一下 SelectionKey ~~~");
//SelectionKey :代表了每一个事件
SelectionKey key = iterator.next();
/*
* 事件来了之后会加入 一个集合a里面 ,然后用 下面的if分支进行判断并且处理。
* 处理完毕之后加入没有删除该事件 ,下次 while (iterator.hasNext()){}遍历的第一个会是他,
* 例子:第一次 遍历到accept 事件 ,处理: 建立连接
* 第二次 不删除a集合里的第一次的事件 还是会遍历到accept 事件,但是此时没有 accept事件报错
*/
iterator.remove();
//区分事件类型
if (key.isAcceptable()){
log.debug("is accept things ,key:{}",key);
//4. 拿到 触发事件的channel 假设将下面的代码进行注释 ,则会发生
//log.info("遍历一下~~~");无线循环 ,未处理的事件会重新加入队列
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//建立连接
SocketChannel sc = channel.accept();
sc.configureBlocking(false) ;
//将 socketchannel注册给selector SelectionKey 《--- (sscKey 管理ServerSocketChannel)
// 《 -- (sc 管理Channel)
SelectionKey scKey = sc.register(selector, 0, null);
//设置关注事件类型
scKey.interestOps(SelectionKey.OP_READ) ;
log.debug("channel:{}",sc);
}else if (key.isReadable()){
log.debug("is read things ,key:{}",key);
SocketChannel channel = (SocketChannel) key.channel();
//读取事件
ByteBuffer buffer = ByteBuffer.allocate(166);
//读取数据
channel.read(buff) ;
buffer.flip() ;// 切换读模式
}
//key.cancel:将事件取消
}
}
}
}
客户端正常断开,异常断开
在sc 监听读的时候 ,如果客户端异常断开 ,服务端会抛出错误 ,此时 catch 需要抓住错误,并且删除 事件 (防止事件重复消费) ,
当客户端是正常退出的时候 , 不会被抓捕 ,但是 channel.read(buff) 会返回 -1 ,此时 就需要
1.一个客户端正常退出 会无线遍历
2.一个客户端异常退出
解决方案:
}else if (key.isReadable()){
// try{} :防止 客户端断开 ,channel断联会抛错 ,还需要删除key 事件
try{
log.debug("is read things ,key:{}",key);
SocketChannel channel = (SocketChannel) key.channel();
//读取事件
ByteBuffer buffer = ByteBuffer.allocate(166);
//读取数据
int read = channel.read(buff);
if (read == -1){
//客户端正常退出
key.cancel();
}
buffer.flip() ;// 切换读模式
}catch (Exception e){
// key.cancel() ;
e.printStackTrace();
}
}
消息的处理边界
每个socket 都需要自己独有的socket ()
写数据
@Slf4j
public class Server03 {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false) ;
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
serverSocketChannel.bind(new InetSocketAddress(8080));
while (true){
selector.select() ;
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()){
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false) ;
//写数据
StringBuilder stringBuilder = new StringBuilder();
for (int i=0;i< 300000000;i++){
stringBuilder.append("a") ;
}
//Charset.defaultcharset() :指的是jvm输入流、输出流默认使用的编码/解码方式。
ByteBuffer buffer = Charset.defaultCharset().encode(stringBuilder.toString());
//写一次看是否写完 ,没写完让 selectionKey关注一个可写事件
//将 剩余数据挂到 selectionKey 上
log.info("开始写数据咯~~");
int write = socketChannel.write(buffer);
System.out.println(write);
//返回值代表实际写的数据
// 如果有剩余数据
if (buffer.hasRemaining()){
selectionKey.interestOps(selectionKey.interestOps()+SelectionKey.OP_WRITE);
//把未写完的数据挂到sckey ,当socketchannel空了下次就会过来 触发写事件
selectionKey.attach(buffer) ;
}
}else if (selectionKey.isWritable()){
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
SocketChannel channel = (SocketChannel) selectionKey.channel();
int write = channel.write(buffer);
System.out.println(write);
//6 清理工作
if (!buffer.hasRemaining()){
selectionKey.attach(null) ;
//数据写完了 不需要关注可写事件了
selectionKey.interestOps(selectionKey.interestOps()-SelectionKey.OP_WRITE) ;
if (!buffer.hasRemaining()){
//清除上面的 buffer
selectionKey.attach(null);
}
}
}
}
}
}
}
客户端读数据
@Slf4j
public class WriteClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
//与服务器建立连接
socketChannel.connect(new InetSocketAddress("localhost",8080));
ByteBuffer buffer = ByteBuffer.allocate(50000000);
int count = 0 ;
//接收数据
while (true){
log.info("开始打印~~~");
count += socketChannel.read(buffer);
System.out.println(count);
buffer.clear() ;
}
}
}