2020-11-30

高并发基础NIO

NIO同步非阻塞式IO,用于传输数据,那么首先了解一下JDK中IO的分类吧!
JDK中的IO分类
1.BIO(BlockingIO)-同步阻塞式IO
同步:一个对象或者一段逻辑在一个时间段内只能被一个线程占用。
异步:一个对象或一段逻辑在一个时间内允许被多个线程抢占。
阻塞:线程在获取到结果之前会持续等待,既不往下执行代码也不会报错。
非阻塞:线程无论是否获取到结果,都会继续往下执行或者报错。
2.NIO(NonBlockingIO)-同步非阻塞式IO
tomcat1.8之后用的都是NIO
3.AIO(AsynchronousIO)-异步非阻塞式IO
在JDK1.7中出现
AIO在NIO基础上改进(称为NIO.2)
4.开发中,需要进行大量短任务,使用NIO,需要进行长任务,使用BIO。
BIO的缺点:a.阻塞:阻塞导致效率降低
b.一对一的连接方式(大量连接每次新创建易导致服务器卡顿甚至崩溃)
c.无效链接(BIO无法处理无效的空链接,大量空连接会占用服务器,易导致卡顿崩溃)
NIO三大组件:Buffer、Channel、Selector
一.Buffer-缓冲区
1.作用:用于临时存储数据
2.Buffer底层是基于数组存储数据的,只能存储及基本类型数据提供七种实现类,没有Boolean类型。
3.重要位置:
capacity-容量位:用于标记缓冲区的容量,指向缓冲区的最后一位,容量位不可变。
position-操作位:类似于数组中的下标,position指向哪一位就会读到那一位,完成之后会向后挪一位,在缓冲区刚创建的时候默认是0开始。
limit-限制位:用于限定position所能达到的最大下标,当limit和position重合的时候,表示所有的数据都已经读写完成,在缓冲区刚创建的时候默认和capacity重合。
三者关系:Position <= Limit <= Capacity
4.下面从微观角度观察各状态变量在读写操作中的变化:

(1) Iimit

初始状态下,Position 指向第一个元素的位置,Limit 和 Capacity 指向最后一个元素的下一个虚拟元素的位置。
在这里插入图片描述

(2)Channel.read

读取 5 个元素到缓冲区后,Position 指向第六个元素的位置,Limit 不变。

在这里插入图片描述

(3)Buffer.flip

进行 Flip 操作后,Limit 指向当前的 Position 的位置,Position 指回第一个元素的位置,

在这里插入图片描述

(4)Channel.write

从缓冲区读取 5 个元素写入 Channel 后,Position 指向 Limit 所在的位置。

在这里插入图片描述

(5)Buffer.clear

clear 后缓冲区重置到初始状态。
在这里插入图片描述
存取方法(Accessor )
相对方法(Relative Method):在当前 position 进行读写操作,随后 position 自增1。
绝对方法(Absolute Method):在某个索引位置进行读写操作,不影响 position 和 limit。
(1)get 系列方法(包括 array() ),用于读取缓冲区的数据,其中 byte get(int index) 为绝对方法。
(2)put 系列方法,用于写入数据到缓冲区,其中 ByteBuffer put(int index, byte b) 为绝对方法。

二.Channel-通道
1.作用:用于传输数据
2.概述:Channel可以实现双向传输,Channel默认是阻塞的,手动设置为非阻塞。
3.常见的Channel
文件类型:FileChannel
UDP:DatagramChannel,无连接不可靠的
TCP:SocketChannel客户端,ServerSocketChannel服务端
4.Channel面向的都是Buffer进行操作的
5.Channel代码实践

    文件类型测试:读取文件、写入文件

读取文件:

 public class FileChannelDemo1 {
   
    public static void main(String[] args) throws IOException {
        // 读取文件
        // 获取通道 - 通过FileInputStream获取Channel,所以这个Channel只能读不能写
        FileChannel channel = new FileInputStream("D:\\a.txt").getChannel();
        // 构建ByteBuffer用于存储数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 读取数据
        channel.read(buffer);
        // 解析数据
        System.out.println(new String(buffer.array(), 0, buffer.position()));
        // 关流
        channel.close();

    }
}

写入文件:

public class FileChannelDemo2 {

    public static void main(String[] args) throws IOException {

        // 通过FileOutputStream来获取Channel - 只能写入数据
        FileChannel fc = new FileOutputStream("D:\\test.txt").getChannel();
        // 写出数据
        fc.write(ByteBuffer.wrap("hello ".getBytes()));
        // 关流
        fc.close();

    }

}

    TCP代码测试:客户端与服务端

客户端代码测试:

public class Client {

    public static void main(String[] args) throws IOException {

        // 开启客户端通道
        SocketChannel sc = SocketChannel.open();
        // 手动设置为非阻塞
        sc.configureBlocking(false);
        // 发起连接 - 阻塞
        // 非阻塞:无论是否建立连接,都会继续往下执行
        sc.connect(new InetSocketAddress("localhost", 8096));
        // 判断连接是否建立
        // 如果多次连接没有成功,说明这个连接无法建立
        while (!sc.isConnected()) {
            // 试图重新建立连接
            // 这个方法会自动进行计数,多次计数之后如果仍然无法建立连接,会抛出异常
            sc.finishConnect();
        }
        // 连接建立
        // 写出数据
        sc.write(ByteBuffer.wrap("hello server".getBytes()));
        // 关闭连接
        sc.close();

    }

}

服务端代码测试:

public class Server {

    public static void main(String[] args) throws IOException {

        // 开启服务器端通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 绑定端口
        ssc.bind(new InetSocketAddress(8096));
        // 设置为非阻塞
        ssc.configureBlocking(false);
        // 接收连接
        SocketChannel sc = ssc.accept();
        // 判断是否接收到了连接
        while (sc == null)
            sc = ssc.accept();
        // 连接建立
        // 读取数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        sc.read(buffer);
        // 解析数据
        System.out.println(new String(buffer.array(), 0, buffer.position()));
        // 关流
        ssc.close();

    }

}

三.Selector-多路复用选择器
1.作用:针对事件(客户端和服务端之间能够产生的操作)来进行选择的。
2.实际生产过程中,会考虑将选择器放在服务端来设置,因为客户端建立不可控。
3.Selector是面向Channel操作,要求Channel必须是非阻塞的。
4.Selector代码实践

服务端:Server

public class Server {

    public static void main(String[] args) throws IOException {

        // 开启服务器端的通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 绑定端口
        ssc.bind(new InetSocketAddress(8079));
        // 设置非阻塞
        ssc.configureBlocking(false);
        // 获取选择器
        // 实际过程中,将选择器设置为单例的
        Selector sel = Selector.open();
        // 将通道注册到选择器上,指定要注册的事件
        ssc.register(sel, SelectionKey.OP_ACCEPT);
        // 实际生产过程中,服务器开了应该不会关闭
        // 用while(true)来模拟服务器一直开着
        while (true) {
            // 当服务器一直开着的时候,随着运行时间的延长,服务器就会接收到越来越多的请求
            // 不代表这些请求都是有用请求 - 需要进行选择,选出有用的请求
            sel.select();
            // 确定这一些请求可能会出发的操作:accept/read/write
            Set<SelectionKey> set = sel.selectedKeys();
            // 遍历集合,根据请求的类型不同来分别处理
            Iterator<SelectionKey> it = set.iterator();
            while (it.hasNext()) {
                // 获取到请求类型
                SelectionKey key = it.next();
                // 可接受事件
                if (key.isAcceptable()) {
                    // 先从选择器中获取到通道
                    ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
                    // 接收请求
                    SocketChannel sc = sscx.accept();
                    // 设置非阻塞
                    sc.configureBlocking(false);
                    // 注册可读事件
                    // sc.register(sel, SelectionKey.OP_READ);
                    // 注册可写事件
                    // sc.register(sel, SelectionKey.OP_WRITE);
                    // 在注册事件的时候,后注册的事件会覆盖之前注册的事件
                    // sc.register(sel, SelectionKey.OP_READ + SelectionKey.OP_WRITE);
                    // sc.register(sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                    sc.register(sel, SelectionKey.OP_READ ^ SelectionKey.OP_WRITE);
                }
                // 可读事件
                if (key.isReadable()) {
                    // 获取通道
                    SocketChannel sc = (SocketChannel) key.channel();
                    // 读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    sc.read(buffer);
                    // 解析数据
                    System.out.println(new String(buffer.array(), 0, buffer.position()));
                    // 注销可读事件
                    // key.interestOps()获取到当前通道上的所有事件 --- 再从这些事件中抠掉可读事件
                    // sc.register(sel, key.interestOps() - SelectionKey.OP_READ);
                    sc.register(sel, key.interestOps() ^ SelectionKey.OP_READ);
                }
                // 可写事件
                if (key.isWritable()) {
                    // 获取通道
                    SocketChannel sc = (SocketChannel) key.channel();
                    // 写出数据
                    sc.write(ByteBuffer.wrap("hello client".getBytes()));
                    // 注销可写事件
                    sc.register(sel, key.interestOps() - SelectionKey.OP_WRITE);
                }
                // 移除
                it.remove();
            }
        }

    }

}

客户端:Client

public class Client {

    public static void main(String[] args) throws IOException {

        // 开启客户端通道
        SocketChannel sc = SocketChannel.open();
        // 发起连接
        sc.connect(new InetSocketAddress("localhost", 8079));
        // 写出数据
        sc.write(ByteBuffer.wrap("hi from client".getBytes()));
        // 读取数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        sc.read(buffer);
        System.out.println(new String(buffer.array(), 0, buffer.position()));
        // 关流
        sc.close();

    }

总结:本人总结,仅供大家参考,希望对大家有所帮助,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值