java BIO与NIO入门

java BIO

java BIO 就是传统java io 编程,为同步阻塞式编程,即一个请求一个线程。

java BIO工作机制
在这里插入图片描述
代码示例:
服务端

package com.demo.bio;

import com.demo.IoUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @authors:静静
 * @description:服务端代码
 */
public class Server {
    public static void main(String[] args) {
        try {
            //1.定义一个serverSocket对象进行服务端的端口注册
            ServerSocket serverSocket = new ServerSocket(8848);
            //2.监听客户端的socket
            Socket socket = serverSocket.accept();
            //获取socket中的字节输入流
            InputStream is = socket.getInputStream();
            BufferedReader bufferedReader = IoUtils.buildBufferedReader(is);
            String msg;
            while ((msg = bufferedReader.readLine()) != null) {
                System.out.println("服务端接受到的消息:" + msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端`

package com.demo.bio;

import com.demo.IoUtils;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @authors:静静
 * @description:客户端代码
 */
public class Client {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1", 8848);
            OutputStream os = socket.getOutputStream();
            BufferedWriter bufferedWriter = IoUtils.buildBufferedWriter(os);
            bufferedWriter.write("hello BIO\n");
            bufferedWriter.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

需要注意的是这里服务端与客户端的socket是一种相持机制,即一端宕机另一端也会随之宕机
同时BIO也有多发和多收模式请看下面的代码示例
服务端代码

package com.demo.multibio;

import com.demo.IoUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @authors:静静
 * @description:服务端代码
 */
public class Server {
    public static void main(String[] args) {
        try {
            //1.定义一个serverSocket对象进行服务端的端口注册
            ServerSocket serverSocket = new ServerSocket(8848);
            ThreadPoolExecutor thread = IoUtils.buildThreadPoolExecutor();
            while (true) {
                Socket socket = serverSocket.accept();
                thread.execute(() -> {
                    //获取socket中的字节输入流
                    InputStream is = null;
                    try {
                        is = socket.getInputStream();
                        BufferedReader bufferedReader = IoUtils.buildBufferedReader(is);
                        String msg;
                        while ((msg = bufferedReader.readLine()) != null) {
                            System.out.println("服务端接受到的消息:" + msg);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端代码

package com.demo.multibio;

import com.demo.IoUtils;

import java.io.BufferedWriter;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
 * @authors:静静
 * @description:客户端代码
 */
public class Client {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1", 8848);
            OutputStream os = socket.getOutputStream();
            BufferedWriter bufferedWriter = IoUtils.buildBufferedWriter(os);
            Scanner scanner = new Scanner(System.in);
            while (true) {
                System.out.print("type in:");
                String msg = scanner.nextLine();
                bufferedWriter.write(msg+'\n');
                bufferedWriter.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

NIO三大核心原理
Buffer缓冲区,Channel信道,Selector选择器

Buffer缓冲区

本质上是一块可以读写的内存,这块内存被包装成NIO Buffer对象,并提供了一组Api,便于操作和管理这块内存。

Channel通道

可以理解成是一个可以双向读写的流,通道可以非阻塞式读取和写入数据到缓冲区,也支持异步读写

Selector选择器

Selector是一个java NIO组件,可以检查一个或多个NIO通道,并确定哪些通道已经准备好进行读写,实现了一个线程管理多个通道(Channel),从而管理多个网络连接,提高效率。

原理图
在这里插入图片描述

  • 每个Channel都对应一个Buffer
  • 一个线程对应一个Selector选择器,而一个Selector对应多个Channel
  • 程序切换到哪一个Channel是由事件决定的,Selector会根据不同的事件,在各个通道切换
  • Buffer本质上是一块内存,底层是一个数组
  • 在NIO中,数据的读取和写入都是通过Buffer完成的,不同于BIO中要么是输入流,要么是输出流,不能双向操作,NIO中的Buffer既可以读也可以写。
  • java NIO系统的核心在于:通过Channel(通道)和Buffer(缓冲区),通道表示打开IO设备的连接,如果需要使用NIO系统,则需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区,然后通过对缓冲区的操作对数据进行处理。总的来说就是Channel负责传输,Buffer负责存取数据

Buffer缓冲区
常见子类

ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer

常用方法

Buffer常见方法
Buffer clear() 清空缓冲区并返回对缓冲区的引用
Buffer flip() 为 将缓冲区的界限设置为当前位置,
并将当前位置充值为 0
int capacity() 返回 Buffer 的 capacity 大小
boolean hasRemaining() 判断缓冲区中是否还有元素
int limit() 返回 Buffer 的界限(limit) 的位置
Buffer limit(int n) 将设置缓冲区界限为 n,
并返回一个具有新 limit 的缓冲区对象
Buffer mark() 对缓冲区设置标记
int position() 返回缓冲区的当前位置 position
Buffer position(int n) 将设置缓冲区的当前位置为 n,
并返回修改后的 Buffer 对象
int remaining() 返回 position 和 limit 之间的元素个数
Buffer reset() 将位置 position 转到以前设置的mark 所在的位置
Buffer rewind() 将位置设为为 0, 取消设置的 mark
get() :读取单个字节
get(byte[] dst):批量读取多个字节到 dst 中
get(int index):读取指定索引位置的字节(不会移动 position)
放 入数据到 Buffer 中 中
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将 src 中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)

使用Buffer读写数据的基本操作流程为

1 写入数据到Buffer
2 调用flip()方法,转换为读取模式
3 从Buffer中读取数据
4 调用buffer.clear()方法或者buffer.compact()方法清除缓冲区

示例代码如下

 ByteBuffer buffer = ByteBuffer.allocate(1024);
 buffer.put("aaa".getBytes());
 buffer.flip();
 byte[] bytes = new byte[buffer.remaining()];
 buffer.get(bytes);
 System.out.println(new String(bytes));//aaa
 buffer.clear();

直接内存与非直接内存

什么是直接内存与非直接内存

根据官方文档的描述:

byte byffer可以是两种类型,一种是基于直接内存(也就是
非堆内存);另一种是非直接内存(也就是堆内存)。对于直
接内存来说,JVM将会在IO操作上具有更高的性能,因为它
直接作用于本地系统的IO操作。而非直接内存,也就是堆内
存中的数据,如果要作IO操作,会先从本进程内存复制到直接
内存,再利用本地IO处理。

从数据流的角度,非直接内存是下面这样的作用链:
本地IO-->直接内存-->非直接内存-->直接内存-->本地IO

而直接内存是:
本地IO-->直接内存-->本地IO

很明显,在做IO处理时,比如网络发送大量数据时,直接内
存会具有更高的效率。直接内存使用allocateDirect创建,但
是它比申请普通的堆内存需要耗费更高的性能。不过,这
部分的数据是在JVM之外的,因此它不会占用应用的内
存。所以呢,当你有很大的数据要缓存,并且它的生命
周期又很长,那么就比较适合使用直接内存。只是一般
来说,如果不是能带来很明显的性能提升,还是推荐直接
使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲
区可通过调用其 isDirect()  方法来确定。

直接内存使用场景

1 有很大的数据需要存储,它的生命周期又很长
2 适合频繁的IO操作,比如网络并发场景

示例代码

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);//创建基于直接内存的buffer对象
System.out.println(byteBuffer.isDirect());

Channel通道

通道Channe概述

通道(Channel):由 java.nio.channels 包定义 的。
Channel 表示 IO 源与目标打开的连接。 
Channel 类似于传统的“流”。只不过 Channel 本身不
能直接访问数据,Channel 只能与 Buffer 进行交互。

1 NIO 的通道类似于流,但有些区别如下:

通道可以同时进行读写,而流只能读或者只能写

通道可以实现异步读写数据

通道可以从缓冲读数据,也可以写数据到缓冲:

2 BIO 中的 stream 是单向的,例如 FileInputStream 
对象只能进行读取数据的操作,而 NIO 中的通道(Channel)
  是双向的,可以读操作,也可以写操作。

3 Channel 在 NIO 中是一个接口

public interface Channel extends Closeable{}

常见Channel实现类

FileChannel:用于读取、写入、映射和操作文件的通道。
DatagramChannel:通过 UDP 读写网络中的数据通道。
SocketChannel:通过 TCP 读写网络中的数据。
ServerSocketChannel:可以监听新进来的 TCP 连接,
对每一个新进来的连接都会创建一个 SocketChannel。 
【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】

获取通道的方法

获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket
获取通道的其他方式是使用 Files 类的静态方法 
newByteChannel() 获取字节通道。或者通过通道的静态
方法 open() 打开并返回指定通道

FileChannel常见方法

int read(ByteBuffer dst) 从Channel到中读取数据到ByteBuffer
long  read(ByteBuffer[] dsts) 将Channel到中的数据“分散”到ByteBuffer[]
int  write(ByteBuffer src)将ByteBuffer 到中的数据写入到  Channel
long write(ByteBuffer[] srcs)将ByteBuffer[] 到中的数据“聚集”到  Channel
long position() 返回此通道的文件位置
FileChannel position(long p) 设置此通道的文件位置
long size() 返回此通道的文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为给定大小
void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中

案例一:将本地数据写到文件中去

public void ChannelTest() throws IOException {
        //创建一个文件输出流,从中获取chaneel
        FileOutputStream os = new FileOutputStream("target.txt");

        FileChannel channel = os.getChannel();

        //创建buffer缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        //将数据写入到缓冲区中
        buffer.put("111111111111111111111111\r\n111111111111111111".getBytes());

        //将缓冲区切换为可写模式(将position置为0)
        buffer.flip();

        //将数据通过channel写出到指定文件中
        channel.write(buffer);

        channel.close();
    }

案例二:读取本地文件中的数据

public void ChannelTest1() throws IOException {
        //创建一个文件输入流,从中获取chaneel
        FileInputStream is = new FileInputStream("target.txt");
        //获取通道
        FileChannel channel = is.getChannel();
        //分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(8);
        StringBuilder builder = new StringBuilder();
        while (true){
            //先清空缓冲区,放在循环最后和一开始效果一样
            buffer.clear();
            int flag = channel.read(buffer);
            if (flag == -1){
                break;
            }
            buffer.flip();
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);
            builder.append(new String(bytes));
        }
        System.out.println(builder);
        channel.close();
    }

案例三:复制文件

 public void copyTest() throws IOException {
        //创建一个文件输入流,并从中获取chaneel
        FileChannel isChannel = new FileInputStream("target.txt").getChannel();
        //创建一个相应的文件输出流,并从中获取channel
        FileChannel osChannel = new FileOutputStream("copyTarger.txt").getChannel();
        //分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(8);
        while (true) {
            //先清空缓冲区,放在循环最后和一开始效果一样
            buffer.clear();
            int flag = isChannel.read(buffer);
            if (flag == -1) {
                break;
            }
            buffer.flip();
            osChannel.write(buffer);
        }
        osChannel.close();
        isChannel.close();
    }

分散和聚集:将通道中的数据分散到各个缓冲区中,将多个缓冲区中的数据聚集到通道中,不过经过测试好像第一种更快一些,也可能是我这边代码有些问题

 public void copyTest2() throws IOException {
        //创建一个文件输入流,并从中获取chaneel
        FileChannel isChannel = new FileInputStream("target.txt").getChannel();
        //创建一个相应的文件输出流,并从中获取channel
        FileChannel osChannel = new FileOutputStream("copyTarget.txt").getChannel();
        //分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(8);
        ByteBuffer buffer1 = ByteBuffer.allocate(16);
        ByteBuffer[] buffers = {buffer, buffer1};
        while (true){
            for (ByteBuffer byteBuffer : buffers) {
                byteBuffer.clear();
            }
            long flag = isChannel.read(buffers);
            if (flag == -1) {
                break;
            }
            for (ByteBuffer byteBuffer : buffers) {
                byteBuffer.flip();
            }
            osChannel.write(buffers);
        }
        osChannel.close();
        isChannel.close();
   }

transferFrom 从目标通道中去复制原通道数据

@Test
    public void transferFromTest() throws Exception {
        // 创建字节输入管道
        FileInputStream is = new FileInputStream("target.txt");
        FileChannel isChannel = is.getChannel();
        // 创建字节输出流管道
        FileOutputStream fos = new FileOutputStream("target1.txt");
        FileChannel osChannel = fos.getChannel();
        // 复制
        osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size());
        isChannel.close();
        osChannel.close();
    }

transferTo 从目标通道中去复制原通道数据

@Test
public void test02() throws Exception {
    // 创建字节输入管道
    FileInputStream is = new FileInputStream("target.txt");
    FileChannel isChannel = is.getChannel();
    // 创建字节输出流管道
    FileOutputStream fos = new FileOutputStream("target1.txt");
    FileChannel osChannel = fos.getChannel();
    // 复制
    isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel);
    isChannel.close();
    osChannel.close();
}

选择器Selector

选择器(Selector) 是 SelectableChannle 对象的多路复用器,
Selector 可以同时监控多个 SelectableChannel 的 IO 状况,
也就是说,利用 Selector可使一个单独的线程管理多
个 Channel。Selector 是非阻塞 IO 的核心

Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,
处理多个的客户端连接,就会使用到 Selector(选择器)
Selector 能够检测多个注册的通道上是否有事件发生(注
意:多个 Channel 以事件的方式可以注册到同一个
Selector),如果有事件发生,便获取事件然后针对每个事
件进行相应的处理。这样就可以只用一个单线程去管理多
个通道,也就是管理多个连接和请求。只有在 连接/通道 
真正有读写事件发生时,才会进行读写,就大大地减少
了系统开销,并且不必为每个连接都创建一个线程,
不用去维护多个线程
避免了多线程之间的上下文切换导致的开销

使用selector开发通信服务端代码

package com.demo.nio;

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;

/**
 * @authors:静静
 * @description:null
 */
public class Server {
    public static void main(String[] args) throws Exception {
        //获取socket通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //将通道设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //绑定端口
        serverSocketChannel.bind(new InetSocketAddress(8848));
        //获取选择器
        Selector selector = Selector.open();
        //将通道注册到选择器中,并指定绑定的事件为监听接收事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //轮询监听器中的事件,如果没有事件则会阻塞在这里
        while (selector.select() > 0) {
            //获取监听器中的已经就绪的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            //遍历已经就绪的事件
            while (iterator.hasNext()) {
                //拿到相应的事件对象
                SelectionKey selectionKey = iterator.next();
                //判断当前事件的的类型
                if (selectionKey.isAcceptable()) {
                    //获取当前的客户端通道
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //将通道切换为非阻塞模式
                    socketChannel.configureBlocking(false);
                    //将当前通道注册到选择器中,并指定为读模式以获取通道中的信息
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    //获取当前选择器上的读就绪事件
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    //将数据读出
                    ByteBuffer buffer = ByteBuffer.allocate(512);
                    while (true) {
                        buffer.clear();
                        if (channel.read(buffer) == 0) {
                            break;
                        }
                        buffer.flip();
                        System.out.println(new String(buffer.array(),0,buffer.remaining()));
                    }
                }
                //处理完毕后移除事件
                iterator.remove();
            }
        }
    }
}

客户端代码开发

package com.demo.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

/**
 * @authors:静静
 * @description:null
 */
public class Client {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8848));
        socketChannel.configureBlocking(false);
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        Scanner scanner = new Scanner(System.in);
        while (true){
            byteBuffer.clear();
            String str = scanner.nextLine();
            byteBuffer.put(str.getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
        }
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值