阻塞和非阻塞: 是针对发起的IO请求操作后 是否立刻返回一个标志信息而不让请求线程等待,当数据准备未完成时,请求线程的状态:
阻塞:往往需要等待数据准备好过后才处理其他的事情,否则一直阻塞等待。
非阻塞:无论数据是否准备好,都不会阻塞等待。
同步和异步:同步和异步一般是面向操作系统和应用程序对IO操作的层面上来区别的。
同步时,应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某个方法上,直到数据准备就绪;或者采用轮询的策略,实时检查数据的就绪状态,如果就绪则获取数据。
异步时,则所有的IO操作都交给操作系统处理,与我们的应用程序没有直接关系,当操作系统完成了IO读写操作时,会给我们的应用程序发送通知,我们应用程序直接拿走数据即可。
二、BIO
介绍:BIO(Blocking I/O) 同步阻塞IO,JDK1.4之前唯一选择, 只有这一种IO模型 ,在java.io包中。
同步阻塞:阻塞线程直到获取到结果。
问题:
有多个客户端进行连接时, 如: client1, client2 进行连接, 由于client1先与服务端进行了连接, 此时服务端只能处理client1的请求, 什么时候client1处理完, 什么时候才能处理其他的请求, 服务端的线程一直处理client1的请求, 所以再有其他的请求, 无办法处理。
Socket底层使用的是BIO, 同步阻塞的IO, 必须接收消息之后, 代码才能继续向下执行。
服务端一个线程同时只能处理一个客户端的一个请求。
解决办法:
服务端收到一个客户端,就创建一个线程处理客户端的消息。
缺点:
每个客户端需要对应服务端的一个子线程。 所以有多少个客户端, 服务端就需要创建多少个子线程。如果客户端特别多,几万甚至几百万,服务器端就需要有几万甚至几百万的子线程。 由于每个子线程都有自己的独立线程区,这么多子线程可能就产生内存不足等问题。而且这么多的子线程也需要进行调度, 切换, 销毁这也是非常消耗性能的。
虽然可以解决一个服务端处理多个客户端,但是因为过多的子线程导致系统资源消耗过多,线程切换导致性能下降也是我们不得不需要考虑的问题。可以使用线程池,在一定程度上提升程序性能,但是并没有解决根本问题。
NIO称为New IO(新的IO)又称Non-Blocking IO(非阻塞IO), 同步非阻塞IO模型,Java 从JDK1.4版本开始推出了NIO模型, 在java.nio包中。
同步非阻塞:必须接收到消息程序才能结束,但不阻塞线程。
优点:服务端的一个线程可以处理多个客户端请求, 决了使用BIO服务端创建过多子线程产生内存不足, 这么多的子线程也需要进行调度, 切换, 销毁也是非常消耗性能的问题。
缺点:使用起来比较复杂。
原理:

每有一个客户端就会注册到选择器中,选择器轮询的监测每个客户端,当选择器检测到该客户端传输了数据,就会把该客户端连接到服务端。
(1)Buffer(缓冲区)
重要参数:
1. position:当前的位置,表示进行下一个读写操作时的起始位置;
2. limit:结束标记位置,表示进行读写操作时的(最大)结束位置;
3. capacity:该ByteBuffer容量;
4. mark: 自定义的标记位置, -1不自定义定标记位置;
创建方式:
- package com.java.test;
-
- import java.nio.ByteBuffer;
-
- public class Test01 {
- public static void main(String[] args) {
- //(1)创建缓冲区,并设置缓冲区的容量,适用于输入
- ByteBuffer allocate = ByteBuffer.allocate(1024);
- //(2)创建缓冲区,适用于输出
- ByteBuffer wrap = ByteBuffer.wrap("abc".getBytes());
-
- //向缓冲区中添加数据
- allocate.put("aa".getBytes());
- //获取缓冲区的容量
- int capacity = allocate.capacity();
- //获取缓冲区的限制
- int limit = allocate.limit();
- //获取缓冲区添加下一个元素的脚标
- int position = allocate.position();
-
- //获取缓冲区存储数据的数组
- byte[] array = allocate.array();
- String s = new String(array, 0, position);
- System.out.println(s);
-
- }
- }
重置ByteBuffer
clear()
内容不变, postion标志位置变为0, limit结束位置等于capacity容量大小 适用于写之前的操作
-
- public Buffer clear() {
- position = 0;
- limit = capacity;
- mark = -1;
- return this;
- }
flip()
内容不变, limit结束位置等于当前position标志位置, postion标志位置变为0 适用于读之前的操作
-
- public Buffer flip() {
- limit = position;
- position = 0;
- mark = -1;
- return this;
- }
(2)Channel(管道)
通道(Channel),它就像自来水管道一样,网络数据通过channel读取和写入,通道与流的不同之处在于通道是双向的,而流只在一个方向上移动(一个流必须是inputStream或者OutputStream的子类),而通道可以用于读写或者两者同时进行,最关键的是可以与多路复用器结合起来,有多种的状态位,方便多路复用器去识别。事实上通道分为两大类,一类是网络读写的(SelectableChannel),一类是用于文件操作的(FileChannel),我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。
(3)Selector(多路复用器)
简单的说,就是Selector会不断的轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel,从而进行后续的IO操作。
每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。
SelectionKey.OP_ACCEPT: 服务端接收到客户端(服务端接收到了客户端连接), 可以使用isAcceptable()判断(服务端使用该状态)
SelectionKey.OP_CONNECT: 客户端连接服务端(客户端和服务端连接成功), 可以使用isConnectable()判断(客户端使用该状态)
SelectionKey.OP_READ: 通道中有内容的这状态(服务端与客户端都可以使用), 可以使用isReadable()判断(服务端, 客户端使用该状态)
SelectionKey.OP_WRITE: 向通道中写入内容, 可以isWritable()判断(服务端, 客户端使用该状态)
(4)NIO的使用
连接
- package com.java.tcp;
- /*
- * 客户端
- * */
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.SocketChannel;
-
- public class Client {
- public static void main(String[] args) throws IOException {
- //获取客户端的管道
- SocketChannel sc = SocketChannel.open();
- //设置为非阻塞
- sc.configureBlocking(false);
- //连接服务端
- sc.connect(new InetSocketAddress("192.168.141.30",9090));
- //获取多路复用器
- Selector selector = Selector.open();
- //将客户端管道注册到多路复用器中,并设置监听状态
- sc.register(selector, SelectionKey.OP_CONNECT);
- //获取监听到的管道状态
- selector.select();
-
- }
- }
- package com.java.tcp;
- /*
- * 服务端
- * */
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.ServerSocketChannel;
-
- public class Server {
- public static void main(String[] args) throws IOException {
- //获取服务端的管道
- ServerSocketChannel scc = ServerSocketChannel.open();
- //设置为非阻塞
- scc.configureBlocking(false);
- //绑定与监听本机IP及指定端口号
- scc.bind(new InetSocketAddress("192.168.141.30", 9090));
- //获取多路复用器
- Selector selector = Selector.open();
- //将服务端管道注册到多路复用器中,并设置监听状态
- scc.register(selector, SelectionKey.OP_ACCEPT);
- //获取管道的状态,管道没有状态,阻塞
- selector.select();
- System.out.println("客户端已连接");
-
- }
- }
- package com.java.tcp;
- /*
- * 服务端
- * */
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.ByteBuffer;
- import java.nio.channels.*;
- import java.util.Iterator;
- import java.util.Scanner;
- import java.util.Set;
-
- public class Server {
- public static void main(String[] args) throws IOException {
- Scanner scanner = new Scanner(System.in);
- //获取服务端的管道
- ServerSocketChannel scc = ServerSocketChannel.open();
- //设置为非阻塞
- scc.configureBlocking(false);
- //绑定与监听本机IP及指定端口号
- scc.bind(new InetSocketAddress("192.168.141.30", 9090));
- //获取多路复用器
- Selector selector = Selector.open();
- //将服务端管道注册到多路复用器中,并设置监听状态
- scc.register(selector, SelectionKey.OP_ACCEPT);
-
- while (true) {
- //获取管道的状态,管道没有状态,阻塞
- selector.select();
-
- //获取监听到的服务端的所有转态
- Set<SelectionKey> selectionKeys = selector.selectedKeys();
- //获取Set集合的迭代器
- Iterator<SelectionKey> iterator = selectionKeys.iterator();
- //使用迭代器遍历该集合
- while (iterator.hasNext()){
- //获取集合中的每一项
- SelectionKey next = iterator.next();
- //判断状态为接收到了客户端
- if (next.isAcceptable()){
- //获取客户端管道
- SocketChannel sc = scc.accept();
- //设置为非阻塞
- sc.configureBlocking(false);
- //将客户端注册到多路复用器中,并将状态设置为读
- sc.register(selector,SelectionKey.OP_READ);
-
- }else if(next.isReadable()){
- //通过状态获取管道
- SocketChannel sc = (SocketChannel)next.channel();
- //接收客户端的消息
- ByteBuffer allocate = ByteBuffer.allocate(1024);
- sc.read(allocate);
- //获取缓冲区中的数据
- byte[] array = allocate.array();
- String s = new String(array, 0, allocate.position());
- System.out.println(s);
- //将客户端注册到多路复用器中,并将状态设置为写
- sc.register(selector,SelectionKey.OP_WRITE);
- }else if(next.isWritable()){
- //通过状态获取客户端管道
- SocketChannel sc = (SocketChannel) next.channel();
- System.out.println(sc);
- //创建传出数据的缓冲区
- String s = scanner.next();
- s = "服务端: "+s;
- ByteBuffer wrap = ByteBuffer.wrap(s.getBytes());
- //将缓冲区写入客户端管道
- sc.write(wrap);
- //将客户端注册到多路复用器中,并将状态设置为读
- sc.register(selector,SelectionKey.OP_READ);
- }
-
- //将处理过的管道状态删除
- iterator.remove();
- }
- }
-
- }
- }
- package com.java.tcp;
- /*
- * 客户端
- * */
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.ByteBuffer;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.SocketChannel;
- import java.util.Iterator;
- import java.util.Scanner;
- import java.util.Set;
-
- public class Client {
- public static void main(String[] args) throws IOException {
- Scanner scanner = new Scanner(System.in);
- //获取客户端的管道
- SocketChannel sc = SocketChannel.open();
- //设置为非阻塞
- sc.configureBlocking(false);
- //连接服务端
- sc.connect(new InetSocketAddress("192.168.141.30",9090));
- //获取多路复用器
- Selector selector = Selector.open();
- //将客户端管道注册到多路复用器中,并设置监听状态
- sc.register(selector, SelectionKey.OP_CONNECT);
-
- while (true) {
- //获取监听到的管道状态
- selector.select();
-
- //获取监听到的客户端管道的所有状态
- Set<SelectionKey> selectionKeys = selector.selectedKeys();
- Iterator<SelectionKey> iterator = selectionKeys.iterator();
- while (iterator.hasNext()){
- //获取每个状态
- SelectionKey next = iterator.next();
- //判断管道状态为连接到了服务器
- if(next.isConnectable()){
- //判断是否完全连接
- if (sc.isConnectionPending()) {
- //完成客户端和服务端管道的连接
- sc.finishConnect();
- }
- //向服务端发送消息
- ByteBuffer wrap = ByteBuffer.wrap("客户端1连接成功".getBytes());
- sc.write(wrap);
- //将客户端管道注册到多路复用器中,并将状态设置为读
- sc.register(selector,SelectionKey.OP_READ);
- } else if (next.isReadable()) {
- //接收服务端的消息
- ByteBuffer allocate = ByteBuffer.allocate(1024);
- sc.read(allocate);
- //获取缓冲区中的数据
- byte[] array = allocate.array();
- String s = new String(array, 0, allocate.position());
- System.out.println(s);
- //将客户端管道注册到多路复用器中,并将状态设置为写
- sc.register(selector, SelectionKey.OP_WRITE);
- } else if (next.isWritable()) {
- //创建缓冲区,将数据放到缓冲区中
- String s = scanner.next();
- s = "客户端1: " + s;
- ByteBuffer wrap = ByteBuffer.wrap(s.getBytes());
- //将缓冲区放到管道中
- sc.write(wrap);
- //将客户端管道注册到多路复用器中,并将状态设置为读
- sc.register(selector,SelectionKey.OP_READ);
- }
- //将处理过的状态删除
- iterator.remove();
- }
- }
- }
- }
AIO(asynchronous):异步非阻塞IO。
在执行时,当前线程不会阻塞, 调用发起后, 不需要等待返回的结果(结果是以事件驱动的形式返回), 而且也不需要多线程就可以实现多客户端访问。