文章目录
IO
阻塞和非阻塞主要指的是访问 IO 的线程是否会阻塞(或者说是等待)线程访问资源,该资源是否准备就绪的一种处理方式
BIO(同步阻塞IO)
BIO是同步阻塞式的IO,以流的方式处理数据(效率低)。
我们熟知的Socket就是BIO,每一个socket套接字需要使用一个线程来处理。当多个socket请求与服务端建立连接时,服务端不能提供相应数量的处理线程,没有分配到处理线程的连接自然就会阻塞或者是被拒绝了。
创建一个服务器端Serve类
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
System.out.println("哈哈");
//接受请求(阻塞)
Socket socket = serverSocket.accept();
System.out.println("阻塞1");
//获取输入流(阻塞)
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("阻塞2");
//获取输出流
System.out.println(reader.readLine());
PrintStream printStream = new PrintStream(socket.getOutputStream());
printStream.println("找我什么事?");
socket.close();
}
}
}
创建一个客户端Clientr类
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 9999);
System.out.println("请输入:");
Scanner scanner = new Scanner(System.in);
String line = scanner.nextLine();
PrintStream printStream = new PrintStream(socket.getOutputStream());
printStream.println(line);
//获取输入流(阻塞)
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println(reader.readLine());
socket.close();
}
}
测试结果:
- 测试1:只启动服务端,控制台只输出了“哈哈”,说明**serverSocket.accept()**是阻塞的
- 测试2:启动客户端,此时服务端输出了“阻塞1”,没有输出“阻塞2‘’
- 测试3:在客户端输入“你在吗”,服务端输出“找我什么事?”说明**socket.getInputStream()**也是阻塞的
NIO(同步非阻塞IO)
NIO是对BIO的改进,它是同步非阻塞的IO,以块的方式处理数据(效率高)
NIO基于通道和缓冲区进行数据操作,数据总是从通道读取到缓冲区中,或者从缓冲区中写到通道中。
Selector(选择器)用于监听多个通道的时间,(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIO的三大核心
NIO的三大核心: Channel(通道)、Buffer(缓冲区)、Selector(选择器)
文件IO(不支持非阻塞的操作)
缓冲区(buffer)
实际就是一个容器,是一个特殊的数组,缓冲区内部内置了一些机制,能够跟踪和记录缓冲区的状态和变化。
Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer
在NIO中Buffer是一个顶层抽象父类,常用的子类有:ByteBuffer,ShortBuffer等。
ByteBuffer·····类中的一些常用方法,这里以ByteBuffer类为例
常用方法 | 说明 |
---|---|
public abstract ByteBuffer put(byte[] b) | 存储字节数据到缓冲区 |
public abstract byte[] get() | 从缓冲区获得字节数据 |
public final byte[] array() | 把缓冲区数据转换成字节数组 |
public static ByteBuffer allocate(int capacity) | 设置缓冲区的初始容量 |
public final Buffer flip() | 翻转缓冲区,重置位置到初始位置 |
通道(Channel)
类似于 BIO 中的 stream,例如 FileInputStream 对象,用来建立到目标(文件,网络套接字,硬件设备等)的一个连接,但是需要注意:BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,既可以用来进行读操作,也可以用来进行写操作
常用的 Channel 类有:
- FileChannel 用于文件的数据读写
- DatagramChannel 用于 UDP 的数据读写
- ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写
常用的方法,以FileChannel类为例:
常用方法 | 说明 |
---|---|
public int read(ByteBuffer dst) | 从通道读取数据并放到缓冲区中 |
public int write(ByteBuffer src) | 把缓冲区的数据写到通道中 |
public long transferFrom(ReadableByteChannel src, long position, long count) | 从目标通道中复制数据到当前通道(特别适合复制大文件) |
public long transferTo(long position, long count, WritableByteChannel target) | 把数据从当前通道复制给目标通道(特别适合复制大文件) |
例子
向文件中写入数据、从文件中读取数据、复制文件
public class TestNIO {
@Test
//向文件中写数据
public void test1() throws Exception {
//创建文件输出流
FileOutputStream fos = new FileOutputStream("basic.txt");
//获取通道
FileChannel channel = fos.getChannel();
//获取缓冲数组
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
//往缓区写入字节数组
String str = "hello nio";
byteBuffer.put(str.getBytes());
//翻转缓冲区
byteBuffer.flip();
//把缓冲区写到通道中
channel.write(byteBuffer);
//关闭流
fos.close();
}
@Test
//从文件中读数据
public void test2() throws Exception {
File file = new File("basic.txt");
//创建文件输入流
FileInputStream fis = new FileInputStream(file);
//获取通道
FileChannel channel = fis.getChannel();
//获取缓区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//从通道中读取数据到缓冲区中
channel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
//关闭流
fis.close();
}
@Test
//复制文件
public void test3() throws Exception {
//创建文件输入流
FileInputStream fis = new FileInputStream("basic.txt");
//创建文件输出流
FileOutputStream fos = new FileOutputStream("I:\\test\\test.txt");
//获取两个通道
FileChannel fisChannel = fis.getChannel();
FileChannel fosChannel = fos.getChannel();
//把fisChannel通道中的数据复制到fosChannel通道中
fosChannel.transferFrom(fisChannel, 0, fisChannel.size());
//关闭流
fis.close();
fos.close();
}
}
注意: 使用文件IO时,把缓冲区中的数据写到Channel通道之前一定要调用Buffer类的 flip() 方法,翻转缓冲区
网络IO
文件IO中的Channel并不支持非阻塞操作,NIO主要就是进行网络IO,java中网络IO是非阻塞IO。基于事件驱动,非常适用于服务器需要大量连接,但数据量不大的情况
java中常用的编写Socket服务器,通用的几种模式
-
一个客户端连接用一个线程
优点:编码简单
缺点:如果连接的客户端比较多,则 分配的线程也很多,就会导致服务器资源耗尽而崩溃 -
每一个客户端连接交给一个拥有固定数量线程的连接池
优点:编码简单,可处理大量连接
缺点:线程开销很大,如果连接比较多,则排队现象比较严重 -
使用java中的NIO,用非阻塞IO的方式处理,这种模式可以用一个线程处理大量的客户端连接
选择器(Selector)
作用: 能够检测多个注册服务上的通道是否有事件发生,如果有事件发生,便可以获取到事件然后针对每个事件进行相应的处理。这样就可以使用单线程管理多个客户端连接,这样使得只有真正的读写事件发生时,才会调用函数进行读写。减少了系统的开销,并且不必为每一个连接都创建一个线程,不用去维护多个线程
常用方法:
常用方法 | 说明 |
---|---|
public static Selector open() | 得到一个选择器对象 |
public int select(long timeout) | 监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间 |
public Set selectedKeys() | 从内部集合中得到所有的 SelectionKey |
SelectionKey
作用: 代表了Selector和网络通道的四种注册关系,一共有四种:
- int OP_ACCEPT:有新的网络连接可以 accept,值为 16
- int OP_CONNECT:代表连接已经建立,值为 8
- int OP_READ 和 int OP_WRITE:代表了读、写操作,值为 1 和 4
常用方法 | 说明 |
---|---|
public abstract Selector selector() | 得到与之关联的 Selector 对象 |
public abstract SelectableChannel channel() | 得到与之关联的通道 |
public final Object attachment() | 得到与之关联的共享数据 |
public final boolean isAcceptable() | 是否可以 accept |
public final boolean isReadable() | 是否可以读 |
public final boolean isWritable() | 是否可以写 |
ServerSocketChannel
作用: 用于在服务器端监听一个新的连接
常用方法 | 说明 |
---|---|
public abstract Selector selector() | 得到一个 ServerSocketChannel 通道 |
public final ServerSocketChannel bind(SocketAddress local) | 设置服务器端端口号 |
public final SelectableChannel configureBlocking(boolean block) | 设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式 |
public SocketChannel accept() | 接受一个连接,返回代表这个连接的通道对象 |
public final SelectionKey register(Selector sel, int ops) | 注册一个选择器并设置监听事件 |
SocketChannel
作用: 网络IO通道,负责读写操作,负责从网络中读取数据到缓冲区中,或者把数据写入到缓冲区中
常用方法 | 说明 |
---|---|
public static SocketChannel open() | 得到一个 SocketChannel 通道 |
public final SelectableChannel configureBlocking(boolean block) | 设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式 |
Pblic boolean connect(SocketAddress remote) | 连接服务器 |
public boolean finishConnect() | 如果上面的方法连接失败,接下来就要通过该方法完成连接操作 |
public int write(ByteBuffer src) | 往通道里写数据 |
public int read(ByteBuffer dst) | 从通道里读数据 |
public final SelectionKey register(Selector sel, int ops, Object att) | 注册一个选择器并设置监听事件,最后一个参数可以设置共享数据 |
public final void close() | 关闭通道 |
例子
创建一个客户端NIOClient类
public class NIOClient {
public static void main(String[] args) throws Exception {
//获取通道
SocketChannel channel = SocketChannel.open();
//设置非阻塞方式
channel.configureBlocking(false);
//提供服务器端IP和端口号
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
//尝试连接服务器
if (!channel.connect(address)) {
while (!channel.finishConnect()) {//体现了nio非阻塞的优势
System.out.println("Client连接客户端的同时,可以做别的事情");
}
}
String str = "你好啊,我是NIO客户端";
//获取缓冲区,并向其中写入数据
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
//发送数据
channel.write(byteBuffer);
System.in.read();
}
}
创建一个服务器端NIOServer类
public class NIOServer {
public static void main(String[] args) throws Exception {
//获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口号
serverSocketChannel.bind(new InetSocketAddress(9999));
//设置非阻塞方式
serverSocketChannel.configureBlocking(false);
//获取选择器
Selector selector = Selector.open();
//将ServerSocketChannel对象注册给Selector对象
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//register()方法的第二个参数是设置的监听事件
//干活
while (true) {
//监控客户端
if (selector.select(2000)==0) {//nio非阻塞的优势
System.out.println("没有客户端请求,我可以做别的");
continue;
}
//得到selectionKey
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历selectionKey,判断通道里的事件
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {//判断是否有连接请求
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {//客户端请求事件
System.out.println("OP_ACCEPT");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (selectionKey.isReadable()) {//读取客户端数据事件
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
channel.read(byteBuffer);
System.out.println("收到客户端数据:"+new String(byteBuffer.array()));
}
//手动从集合中移除当前selectionKey,防止重复处理
iterator.remove();
}
}
}
}
AIO(异步非阻塞)
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
- AsynchronousFileChannel
- AsynchronousDatagramChannel
其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数
总结
-
BIO是同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
-
NIO是同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
-
AIO是异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
BIO、NIO、AIO适用场景分析
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高
- NIO方式适用于连接数目多且连接比较短的架构,可充分利用服务器资源
- AIO方式使用于连接数目多且连接比较长的架构,充分调用OS参与并发操作