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),把数据从当前通道复制给目标通道(特别适合复制大文件)
入门demo
向文件中写入数据、从文件中读取数据、复制文件
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 static ServerSocketChannel open(),得到一个 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 表示采用非阻塞模式 - public 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(),关闭通道
入门小demo
-
创建一个客户端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(); } } } }
小案例
使用NIO完成一个简易的聊天案例。要求:客户端能够给服务器端发送消息,服务器接收到消息时候,能够将消息广播给其他所有的客户端。
-
创建一个聊天客户端类ChatClient
public class ChatClient { private final String IP = "127.0.0.1"; private int port = 9999; private SocketChannel socketChannel; //网络通道 private String userName; public ChatClient() throws IOException { //获取网络通道 socketChannel = SocketChannel.open(); //设置非阻塞方式 socketChannel.configureBlocking(false); if (!socketChannel.connect(new InetSocketAddress(IP,port))) { while (!socketChannel.finishConnect()) {//体现了NIO非阻塞的优势 System.out.println("连接服务器的同事还可以做别的事"); } } //得到客户端IP作为用户名 userName = socketChannel.getLocalAddress().toString().substring(1); System.out.println("------------------"+userName+"is ready---------------"); } //向服务端发送数据 public void sendMsg(String message) throws IOException { //如果从键盘录入的为“bye”则关闭socketChannel,退出聊天 if (message.equalsIgnoreCase("bye")) { socketChannel.close(); return; } String msg = userName + "说:" + message; //获取缓冲区 ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes()); socketChannel.write(byteBuffer); } //从服务端接收数据 public void receiveMsg() throws IOException { //获取缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //将接收到的数据写到缓冲区中 int count = socketChannel.read(byteBuffer); if (count > 0) { System.out.println(new String(byteBuffer.array()).trim()); } } }
-
创建一个聊天客户端类ChatServer
public class ChatServer { private ServerSocketChannel serverSocketChannel; private Selector selector; //获取客户端连接 public ChatServer() throws IOException { //获取ServerSocketChannel通道 serverSocketChannel = ServerSocketChannel.open(); //绑定端口号 serverSocketChannel.bind(new InetSocketAddress(9999)); //设置非阻塞方式 serverSocketChannel.configureBlocking(false); //获取Selector selector = Selector.open(); //把serverSockerChannel注册到服务器 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); } //监控客户端连接 public void start() throws IOException { //干活 while (true) { if (selector.select(2000) == 0) {//体现了NIO非阻塞的优势 System.out.println("没有客户端连接,我可以做别的"); continue; } Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); if (selectionKey.isAcceptable()) { //接收请求,得到SocketChannel SocketChannel socketChannel = serverSocketChannel.accept(); //设置非阻塞方式 socketChannel.configureBlocking(false); //将socketChannel注册到selector中 socketChannel.register(selector, SelectionKey.OP_READ); System.out.println(socketChannel.getRemoteAddress().toString().substring(1)+"上线了..."); } if (selectionKey.isReadable()) { //读取客户端发来的数据 readMsg(selectionKey); } //一定要把当前key删掉,防止重复处理 iterator.remove(); } } } //读取客户端发送来的数据并且进行广播 private void readMsg(SelectionKey selectionKey) throws IOException { //获取通道 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); //获取缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //读取客户端发送的数据 int count = socketChannel.read(byteBuffer); if (count >0) { String message = new String(byteBuffer.array()); printMsg(message); //将客户端发来的消息进行广播 broadCast(message,socketChannel); } } private void broadCast(String message,SocketChannel socketChannel) throws IOException { //得到所有已经就绪的Channel for (SelectionKey key : selector.keys()) { Channel channel = key.channel(); if (channel instanceof SocketChannel && channel != socketChannel) { SocketChannel targetChannel = (SocketChannel) channel; //获取缓冲区 ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes()); targetChannel.write(byteBuffer); } } } private void printMsg(String msg) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(dateFormat.format(new Date())+" "+msg); } public static void main(String[] args) throws IOException { new ChatServer().start(); } }
-
创建聊天测试类TestChat
public class TestChat { public static void main(String[] args) throws IOException { final ChatClient client = new ChatClient(); new Thread() { public void run() { while (true) { try { client.receiveMsg(); Thread.sleep(2000); } catch (Exception e) { e.printStackTrace(); } } } }.start(); Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String msg = scanner.nextLine(); client.sendMsg(msg); } } }
AIO(异步非阻塞)
先由操作系统完成客户端的请求,再通知服务器去启动线程进行处理。
IO的对比
对比总结 | BIO | NIO | AIO |
---|---|---|---|
IO方式 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
API使用难度 | 简单 | 复杂 | 复杂 |
可靠性 | 低 | 高 | 高 |
吞吐量 | 低 | 高 | 高 |
BIO
-
适用于连接较少,对服务器资源消耗很大,但是编程简单。是同步阻塞的。
-
举例:你到餐馆点餐,然后在那儿等着,什么也做不了,只要饭还没有好,就要必须等着
NIO
-
使用于连接数量比较多且连接时间比较短的架构,比如聊天服务器,编程比较复杂。是同步非阻塞的
-
举例:你到餐馆点完餐,然后就可以去玩儿了,玩一会儿就回饭馆问一声,饭好了没。
AIO
-
适用于连接数量多而且连接时间长的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。是异步非阻塞的。
-
举例:饭馆打电话给你说,我们知道你的位置,待会儿给您送来,你安心的玩儿就可以了。类似于外卖。