同步与异步:
同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。
阻塞与非阻塞:
阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。
BIO,一般称之为阻塞(block)IO、基本(basic)IO。就是传统的java.io包,它是基于流模型实现的,交互的方式是同步、阻塞方式,主要应用于文件IO和网络IO。
文件IO就是 InputStream、OutputStream
基于字节操作的 IO、 Writer、Reader
基于字符操作的 IO,网络IO就是 Socket
基于网络操作的 IO。文件IO就是读写操作,就不记录了,这里记录一下网络IO吧。
在JDK1.4之前,建议网络连接的时候只能采用BIO,需要先在服务端启动一个ServerSocket,然后在客户端启动ocket来对服务端进行通信。默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,会先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话客户端线程会等待请求结束后才继续执行,这就是阻塞IO。
服务端代码:
/** * 服务端 */ public class BioServer { public static void main(String[] args) throws Exception{ // 创建ServerSocket对象,绑定端口:9000 ServerSocket serverSocket = new ServerSocket(9000); for (;;){ System.out.println("1"); // 监听客户端程序,看是否有客户端来连接、发消息等(阻塞方式,如果客户端没有连接,会一直阻塞在这里,不会往下执行) Socket accept = serverSocket.accept(); System.out.println("2"); // 从连接中取出输入流来接收消息 InputStream inputStream = accept.getInputStream(); byte[] bytes = new byte[10]; inputStream.read(bytes); // 打印接收到的消息 System.out.println("对方说" + new String(bytes)); // 回答消息 OutputStream outputStream = accept.getOutputStream(); System.out.println("请输入:"); Scanner scanner = new Scanner(System.in); String data = scanner.nextLine(); outputStream.write(data.getBytes()); // 关闭 // serverSocket.close(); } } } 复制代码
客户端代码:
/** * 客户端 */ public class BioClient { public static void main(String[] args) throws Exception{ for (;;){ // 创建Socket对象 Socket socket = new Socket("127.0.0.1", 9000); // 从连接中取出输出流并发送消息 OutputStream outputStream = socket.getOutputStream(); System.out.println("请输入:"); Scanner scanner = new Scanner(System.in); String data = scanner.nextLine(); outputStream.write(data.getBytes()); System.out.println("3"); // 从连接中取出输入流接收并回答(这里也是阻塞,如果没有接收到对方回复的消息,这里会一直阻塞下去) InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[10]; inputStream.read(bytes); System.out.println("4"); System.out.println("对方说:" + new String(bytes).trim()); // 关闭 // socket.close(); } } } 复制代码
1、首先启动服务端,控制台只打印了1,并没有继续往下执行:
2、启动客户端,输入aaa,如下图,服务端控制台继续执行了下去,但是可以看到客户端也阻塞了,控制台只打印了3:
3、服务端回复bbb,客户端打印出4并循环执行:
BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的优点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
NIO,一般称之为新(new)IO、非阻塞(non-blocking)IO。就是java.nio包,是从 JDK1.4开始
提供的新api,Java提供了一系列改进的输入/输出的新特性,统称为NIO。新增了许多用于处理输入输出的类,这些类都被放在java.nio包及子包下,并且对原java.io包中很多类进行改写,新增了满足NIO的功能,交互的方式是同步、非阻塞方式。
NIO和BIO有相同的目的和作用,但是他们的实现方式完全不同,BIO是以 流
的方式处理数据,而NIO是以 块
的方式处理数据,块IO的效率要比流IO高很多。另外, NIO是非阻塞式
的,这一点也和BIO很不相同,使用NIO可以提供非阻塞式的高伸缩性网络。
类型 | 输出方式 | 缓冲区 | 交互方式 |
---|---|---|---|
BIO | 流 | 数组(byte[]) | 同步、阻塞 |
NIO | Channel(通道) | Buffer | 同步、非阻塞(也可以是阻塞的,但没必要) |
NIO主要有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。 传统的BIO是基于字节流和字符流进行操作
, 而NIO基于Channel和Buffer进行操作
,数据总是从通道读取到缓冲区,或者从缓冲区写入通道中。Selector(选择器)用于监听多个通道的事件(比如:连接打开、数据到达)。因此使用单个线程就可以监听多个数据通道。
前面说了NIO和BIO有相同的目的和作用,那么NIO的主要应用也是文件IO和网络IO。下面先解释一下NIO的核心部分概述和API,然后通过代码来看一下。
缓冲区(Buffer):
实际上是一个容器,是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。 在NIO中,Buffer是一个顶层父类,它也是一个抽象类,常用的有以下子类: 对于Java中的基本数据类型,都有一个具体的Buffer类型与之对应,最常用的自然是ByteBuffer类(二进制数据),该类的主要方法有以下几个:
public abstract ByteBuffer put(byte b):存储字节数据到缓冲区。
public abstract byte get():从缓冲区获得字节数据。
public final byte[] array():把缓冲区数据转换成字节数组。
public static ByteBuffer allocate(int capacity):设置缓冲区初始容量。
piblic static ByteBuffer wrap(byte[] array):把一个线程的数组放到缓冲区中使用。
public final Buffer flip():反转缓冲区,充值位置到初始位置。
通道(Channel):
类似于BIO中的stream,用来建立到目标的一个连接, 但是需要注意
:BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道是双向的,既可以用来进行读操作,也可以用来进行写操作。常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel、SocketChannel。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):把数据从当前通道复制给目标通道。
示例:通过NIO往本地文件中写数据:
public class NioWrite { public static void main(String[] args) throws Exception{ // 创建输出流 FileOutputStream fileOutputStream = new FileOutputStream("test.txe"); // 从流中得到一个通道 FileChannel channel = fileOutputStream.getChannel(); // 提供一个缓冲区 ByteBuffer allocate = ByteBuffer.allocate(1024); // 往缓冲区存入数据 String string = "我是要输入的数据"; allocate.put(string.getBytes()); // 翻转缓冲区,将位置设为初始位置 allocate.flip(); // 把缓冲区写到通道中 channel.write(allocate); // 关闭 fileOutputStream.close(); } } 复制代码
可以看到上面代码有个翻转缓冲区的代码,当你把这一行代码去掉之后你会发现,test.txe文件内显示的内容是空的,这是为啥呢?
原因看下图: 缓冲区存数据就是从头然后到尾,而1和2表示的就是指针位置,当我们把 我是要输入的数据
这一段话存到缓冲区中的时候,指针就到了2的位置,然后开始往通道写数据,实际上就是从2的位置开始往下写,而下面内容明显是空的,所以test.txe文件内显示的内容是空的,而缓冲区的flip()方法,就是将指针从2翻转到初始位置,也就是1的位置,再往通道写数据,这时候就有了内容。
示例:通过NIO从本地文件中读取数据:
public class NioRead { public static void main(String[] args) throws Exception{ File file = new File("test.txe"); // 创建输入流 FileInputStream fileInputStream = new FileInputStream(file); // 得到一个通道 FileChannel channel = fileInputStream.getChannel(); // 准备一个缓冲区 ByteBuffer allocate = ByteBuffer.allocate((int) file.length()); // 从通道里读取数据并存到缓冲区 channel.read(allocate); // 打印数据 System.out.println(new String(allocate.array())); // 关闭 fileInputStream.close(); } } 复制代码
选择器(Selector):
选择器,能够检测多个注册的通道上是否有时间发生,如果有时间发生,便获取事件然后针对每个事件进行相应的响应处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接。这样使得只有在连接真正的读写事件发生时,才会调用函数来进行读写,就 大大的减少了系统开销
,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。 常用方法
:
public static Selector open():得到一个选择器对象。
public int select(long timeout):监控所有注册的channel,当其中有注册的IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间。
public Set SelectionKeys():从内部集合中得到所有的SelectionKey。
2、 SelectionKey
:代表了Selector和serverSocketChannel的注册关系,一共四种:
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 abstract SelectionKey interestOps(int ops):设置或改变监听事件。
public final boolean isAccepyable():是否可以accept。
public final boolean isReadable():是否可以读。
public final boolean isWritable():是否可以写。
3、 ServerSocketChannel
:用来在服务器端监听新的客户端Socket连接。
常用方法
:
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):注册一个选择器并设置监听事件。
4、 SocketChannel
:网络IO通道,具体负责进行读写操作。NIO总是把缓冲区的数据写入通道,或者把通道里的数据读出到缓冲区。
常用方法
:
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():关闭通道。
示例:NIO实现服务器端和客户端之间的数据通信(非阻塞、基础版):
客户端代码:
public class NioClient { public static void main(String[] args) throws Exception{ // 得到一个网络通道 SocketChannel socketChannel = SocketChannel.open(); // 设置非阻塞模式 socketChannel.configureBlocking(false); // 提供服务器端的ip地址和端口号 InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9001); // 连接服务器 if (!socketChannel.connect(inetSocketAddress)) { // 循环-直到某一时刻连接到(在连接的时候可以做别的事情,比如下面打印数据,这就是NIO的优势) while (!socketChannel.finishConnect()){ System.out.println("还没有连接到。。。"); } System.out.println("连接到了。。。"); } // 得到一个缓冲区并存入数据 String string = "NIO测试,我是客户端传的数据"; ByteBuffer writeBuffer = ByteBuffer.wrap(string.getBytes()); // 发送数据 socketChannel.write(writeBuffer); // 这里关闭通道,服务器端会报异常,所以这里就先设置阻塞 System.in.read(); } } 复制代码
服务器端代码:
public class NioServer { public static void main(String[] args) throws Exception{ // 得到一个ServerSocketChannel对象 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 得到一个Selector对象 Selector selector = Selector.open(); // 绑定一个端口号 serverSocketChannel.bind(new InetSocketAddress(9001)); // 设置非阻塞式 serverSocketChannel.configureBlocking(false); // 把ServerSocketChannel对象注册给Selector对象 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 做事 for (;;){ // 监控客户端 if (selector.select(2000) == 0){ System.out.println("没有客户端连接我,我可以干别的事情"); continue; } // 得到SelectionKey,判断通道里的事件 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); // 如果是客户端连接事件 if (key.isAcceptable()){ System.out.println("连接事件"); // 接收连接 SocketChannel socketChannel = serverSocketChannel.accept(); // 设置非阻塞式 socketChannel.configureBlocking(false); // 注册 socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } // 读取客户端数据事件 if (key.isReadable()){ SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); channel.read(buffer); System.out.println("客户端发来数据:" + new String(buffer.array())); } // 手动从集合中移除当前key,防止重复处理 iterator.remove(); } } } } 复制代码
1、先启动服务端,看到下图内容,没有客户端连接的时候,NIO可以做别的事情,而不像BIO阻塞在那里,这就是NIO的优势。 2、启动客户端程序,服务端控制台可以看到下图内容,服务端接收到了客户端发送来的数据。( 先连接,才能读写数据,所以最开始发生的事件一定是连接事件,连接成功才会有读写事件
)
先写到这里吧,都是些基础的内容,之后的文章会写比较复杂的NIO案例、Netty框架等!
如果你觉得我的文章对你有帮助话,欢迎关注我的微信公众号:"一个快乐又痛苦的程序员"(无广告,单纯分享原创文章、已pj的实用工具、各种Java学习资源,期待与你共同进步)