高并发基础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();
}
总结:本人总结,仅供大家参考,希望对大家有所帮助,谢谢!