Java共支持3种网络编程IO模式:BIO,NIO,AIO
NIO它是Java1.4引入的一个新的IO API,可以替代标准的Java IO API。NIO支持面向缓冲区的,基于通道的ID操作,NIO将以更加高效的方式进行文件的读写操作
BIO就是你教小孩写作业,他遇到一个不会的就卡住就来问你一次,因为要辅导作业导致你无法再做其他事情。
NIO就是你教小孩写作业,他遇到一个不会的先空着慢慢做,然后继续做下一题,最后做完了再等你去检查作业。
AIO就是你教小孩写作业,他遇到一个不会的先空着慢慢想,然后继续做下一题,最后做完了还会自己检查对错再告诉你结果。
1.1 同步阻塞IO(BIO)
同步:发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
传统服务器会为每一个客户端请求建立一个线程,由于线程单独负责一个请求,这种模式会带来线程数量剧增,消耗服务器的资源,为了规避这个问题,都采用线程池模型,并设置最大线程池数量,但是也会有新的问题,比如超过线程池设置的最大数量,会导致多的请求分配不到线程无法处理。
1.2 异步非阻塞IO(AIO)
异步:发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发
发起请求操作之后不用管了,这个任务会交给操作系统完成,还会自动告诉操作结果
1.3 同步非阻塞IO(NIO)
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
它是采用了基于Reactor模式的工作方式,IO调用不会被阻塞,是注册感兴趣的特定I/O事件,在发生特定的事件的时候再通知我们去处理,本质就是延迟IO的操作,直到真正发生IO的时候才执行。
Java NIO由以及几个核心部分组成:Channels
、Buffers
、Selectors
Channels(通道)
和IO中的stream流差不多是一个等级的,stream是单向的,要么读要么写,主要实现有inputstream、outputstream等,而Channel是双向的能读能写,主要实现有:FileChannle、DatagramChannel、SocketChannle和ServerSocketChannel
NIO中封装了对数据源的操作,通过Channle可以操作数据源,不用关心数据源的具体结构。
Channel是一个对象可以用来读取写入数据,每个 channel 对应一个 buffer缓冲区,但它不是直接读写,所有数据都通过Buffer对象来处理,写数据先写入Buffer、读数据先读Buffer。从通道读取数据到缓冲流,从缓冲区写数据到通道
Buffers(缓冲区)
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的。
Selectors(选择器)
通过一个Selectors去监听多个Channels,当通道中有感兴趣的事情发生就会告诉我们,我们再进行执行。从Selectors中获得响应的key,然后在key里面找到事件具体的SelectorChannel以获得客户端发来的数据
NIO-Channel
NIO的通道类似于流,既可以写入数据到通道又可以从通道中读取数据,但流的读写是单向的,通道可以异步读写。但总是要先读到一个buffer中,或者从buffer中写入
Channel实现
- FileChannel:文件通道,用于文件的读和写
- DatagramChannel:用于 UDP 连接的接收和发送
- SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端
- ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求
FileChannel 介绍
FileChannel 类可以实现常用的 read,write 以及 scatter/gather 操作,同时它也提供了很多专用于文件的新方法
从 FileChannel 读取数据
@Test public void test() throws IOException { //通过RandomAccessFile来获取一个FileChannel 实例 RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw"); FileChannel inChannel = aFile.getChannel(); //分配48字节的ByteBuffer ByteBuffer buf = ByteBuffer.allocate(48); //从 FileChannel 中读取数据 int bytesRead = inChannel.read(buf); //关闭FileChannel inChannel.close(); System.out.println(bytesRead); }
从 FileChannel 写入数据
@Test public void test() throws IOException { RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw"); FileChannel inChannel = aFile.getChannel(); String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf1 = ByteBuffer.allocate(48); //往ByteBuffer里面写入数据 buf1.put(newData.getBytes()); buf1.flip(); //直到Buffer 中已经没有尚未写入通道的字节 while(buf1.hasRemaining()) { inChannel.write(buf1); } inChannel.close(); }
其他方法
- position:在 FileChannel 的某个特定位置进行数据的读/写操作
- size:将返回FileChannel 实例所关联文件的大小
- truncate:截取一个文件指定长度后面的部分将被删除
- force:操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的数据一定会即时写到磁盘上。要保证这一点,需要调用 该方法
- transferFrom:从源通道传输到 FileChannel 中
- transferTo():从 FileChannel 传输到其他的 channel 中
- Scatter(分散):将从 Channel 中读取的数据分散到多个 Buffer 中
- Gather (聚集):将多个 Buffer 中的数据聚集后发送到 Channel
SocketChannel 介绍
SocketChannel 就是 NIO 对于非阻塞 socket 操作的支持的组件,其在 socket 上封装了一层,主要是支持了非阻塞的读写。同时改进了传统的单向流 API,,Channel同时支持读写。socket 通道类主要分为 DatagramChannel、SocketChannel 和 ServerSocketChannel,
ServerSocketChannel
ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉ServerSocket 执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行
public static final String GREETING = "Hello java nio.\r\n"; public static void main(String[] argv) throws Exception { int port = 1234; // default if (argv.length > 0) { port = Integer.parseInt(argv[0]); } ByteBuffer buffer = ByteBuffer.wrap(GREETING.getBytes()); //打开 ServerSocketChannel ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress(port)); ssc.configureBlocking(false); //监听新的连接 while (true) { System.out.println("Waiting for connections"); //监听新的连接 accept方法会一直阻塞直到有新连接到达 SocketChannel sc = ssc.accept(); //判断为null直接返回,可以将阻塞变为非阻塞模式 if (sc == null) { System.out.println("null"); Thread.sleep(2000); } else { System.out.println("Incoming connection from: " + sc.socket().getRemoteSocketAddress()); buffer.rewind(); sc.write(buffer); sc.close(); } } }
由于 ServerSocketChannel 没有 bind()方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。使用对等 ServerSocket 的 API来根据需要设置其他的 socket 选项
accept()方法会一直阻塞直到有新连接到达,可以设置成非阻塞模式。在非阻塞模式下accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null
SocketChannel
SocketChannel 是一个连接到 TCP 网络套接字的通道
- SocketChannel 是用来连接 Socket 套接字
- SocketChannel 主要用途用来处理网络 I/O 的通道
- SocketChannel 是基于 TCP 连接传输
- SocketChannel 实现了可选择通道,可以被多路复用的
@Test public void test4() throws IOException { SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80)); socketChannel.isOpen(); // 测试 ocketChannel是否为 open 状态 socketChannel.isConnected(); //测试SocketChannel是否已经被连接 socketChannel.isConnectionPending(); //测试SocketChannel是否正在进行连接 socketChannel.finishConnect(); //校验正在进行套接字连接的SocketChannel是否已经完成连接 socketChannel.configureBlocking(false);//false 表示非阻塞,true 表示阻塞 //设置socket套接字的相关参数 socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE); socketChannel.getOption(StandardSocketOptions.SO_RCVBUF); ByteBuffer byteBuffer = ByteBuffer.allocate(16); socketChannel.read(byteBuffer); socketChannel.close(); System.out.println("read over"); }
DatagramChannel 介绍
每一个 DatagramChannel 对象有一个关联的 DatagramSocket 对象,SocketChannel 模拟连接导向的流协议(如 TCP/IP),DatagramChannel 则模拟包导向的无连接协议(如 UDP/IP)。DatagramChannel 可以发送单独的数据报给不同的目的地址,DatagramChannel 对象也可以接收来自任意地址的数据包。 每个到达的数据报都含有关于它来自何处的信息(源地址)
接收数据
DatagramChannel server = DatagramChannel.open(); server.socket().bind(new InetSocketAddress(10086)); ByteBuffer receiveBuffer = ByteBuffer.allocate(64); receiveBuffer.clear(); SocketAddress receiveAddr = server.receive(receiveBuffer);
发送数据
DatagramChannel server = DatagramChannel.open(); ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes()); server.send(sendBuffer, new InetSocketAddress("127.0.0.1",10086));
示例
客户端发送,服务端接收的例子
/*DatagramChannel发包*/ @Test public void sendDatagram() throws IOException, InterruptedException { DatagramChannel sendChannel = DatagramChannel.open(); InetSocketAddress sendAddress = new InetSocketAddress("127.0.0.1", 9999); while (true) { sendChannel.send(ByteBuffer.wrap("发包".getBytes("UTF-8")), sendAddress); System.out.println("发包端发包"); Thread.sleep(1000); } } /** * DatagramChannel接收包 * */ @Test public void receive() throws IOException { DatagramChannel receiveChannel = DatagramChannel.open(); InetSocketAddress receiveAddress = new InetSocketAddress(9999); receiveChannel.bind(receiveAddress); ByteBuffer receiveBuffer = ByteBuffer.allocate(512); while (true) { receiveBuffer.clear(); SocketAddress sendAddress = receiveChannel.receive(receiveBuffer); receiveBuffer.flip(); System.out.print(sendAddress.toString() + " "); System.out.println(Charset.forName("UTF-8").decode(receiveBuffer)); } }
NIO-Buffer
Buffer实现
Buffer有三个属性:capacity,position 和 limit
- capacity:作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”。只能往里写capacity 个 byte、long,char 等类型,常写48等等,表示48个ByteBuffer块,满了需要清空才能写入
- position :写数据到 Buffer 中时,position 表示写入数据的当前位置,position 的初始值为0。当一个 byte、long 等数据写到 Buffer 后, position 会向下移动到下一个可插入数据的 Buffer 单元。position 最大可为 capacity – 1。当调用ByteBuffer.flip()position会被清为0
- limit:
- 写数据时,limit 表示可对 Buffer 最多写入多少个数据。写模式下,limit 等于Buffer 的 capacity
- 读数据时,limit 表示 Buffer 里有多少可读数据(not null 的数据),因此能读到之前写入的所有数据
Buffer关键实现有:ByteBuffer、Charbuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer,分贝对应byte、char、double、float、int、long、short
Buffer写入数据
分配
要想获得一个 Buffer 对象首先要进行分配,一个分配 48 字节 capacity 的 ByteBuffer 的例子
ByteBuffer buf = ByteBuffer.allocate(48);
向 Buffer 中写数据
1、从 Channel 写到 Buffer
int bytesRead = inChannel.read(buf);
2、通过 Buffer 的 put()方法写到Buffer 里
buf.put(127);
flip
flip 方法将 Buffer 从写模式切换到读模式。调用 flip()方法会将 position 设回 0,并将 limit 设置成之前 position 的值,就是标记当前读到的位置
最后效果
RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw"); FileChannel inChannel = aFile.getChannel(); ByteBuffer buf = ByteBuffer.allocate(48); //从Channel 写到 Buffer int bytesRead = inChannel.read(buf); inChannel.close(); System.out.println(bytesRead);
Buffer读数据
RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw"); FileChannel inChannel = aFile.getChannel(); String newData = "New String to write to file..." + System.currentTimeMillis(); ByteBuffer buf1 = ByteBuffer.allocate(48); //往ByteBuffer里面写入数据 buf1.put(newData.getBytes()); buf1.flip(); //直到Buffer 中已经没有尚未写入通道的字节 while (buf1.hasRemaining()) { inChannel.write(buf1); } inChannel.close();
NIO-Selector
Selector介绍
Selector 一般称 为选择器 ,也可以翻译为 多路复用器 。它是 Java NIO 核心组件中的一个,用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个 channels,也就是可以管理多个网络链接。使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销
SelectableChannel 介绍
- 不是所有的 Channel 都可以被 Selector 复用的。比方说,FileChannel 就不能被选择器复用。判断一个 Channel 能被 Selector 复用,有一个前提:判断他是否继承了一个抽象类 SelectableChannel。如果继承了 SelectableChannel,则可以被复用,否则不能
- SelectableChannel 类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。所有 socket 通道,都继承了 SelectableChannel 类都是可选择的,包括从管道(Pipe)对象的中获得的通道。而 FileChannel 类,没有继承 SelectableChannel,因此是不是可选通道
- 一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。通道和选择器之间的关系,使用注册的方式完成。SelectableChannel 可以被注册到Selector 对象上,在注册的时候,需要指定通道的哪些操作,是 Selector 感兴趣的
Selector使用
@Test public void testSelector() throws IOException { // 1、获取 Selector 选择器 Selector selector = Selector.open(); //2、获取通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 3.设置为非阻塞 serverSocketChannel.configureBlocking(false); // 4、绑定连接 serverSocketChannel.bind(new InetSocketAddress(9999)); // 5、将通道注册到选择器上,并制定监听事件为:“接收”事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); }
与 Selector 一起使用时,Channel 必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException。这意味着,FileChannel 不能与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式,而套接字相关的所有的通道都可以
一个通道,并没有一定要支持所有的四种操作。比如服务器通道ServerSocketChannel 支持 Accept 接受操作,而 SocketChannel 客户端通道则不支持。可以通过通道上的 validOps()方法,来获取特定通道下所有支持的操作集合
查询已经就绪的通道
Set selectionKeys = selector.selectedKeys(); Iterator keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()){ SelectionKey key = (SelectionKey) keyIterator.next(); if(key.isAcceptable()){ }else if (key.isConnectable()){ }else if(key.isReadable()){ }else if(key.isWritable()){ } keyIterator.remove(); }
总结NIO步骤
- 创建 Selector 选择器
- 创建 ServerSocketChannel 通道,并绑定监听端口
- 设置 Channel 通道是非阻塞模式
- 把 Channel 注册到 Socketor 选择器上,监听连接事件
- 调用 Selector 的 select 方法(循环调用),监测通道的就绪状况
- 调用 selectKeys 方法获取就绪 channel 集合
- 遍历就绪 channel 集合,判断就绪事件类型,实现具体的业务操作
- 根据业务,决定是否需要再次注册监听事件,重复执行第3步操作
NIO-Pipe
Java NIO 管道是 2 个线程之间的单向数据连接。Pipe 有一个 source 通道和一个 sink通道。数据会被写到 sink 通道,从 source 通道读
示例
@Test public void testPipe() throws IOException { //1、获取通道 Pipe pipe = Pipe.open(); //2、获取 sink 管道,用来传送数据 Pipe.SinkChannel sinkChannel = pipe.sink(); //3、申请一定大小的缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("atguigu".getBytes()); byteBuffer.flip(); //4、sink 发送数据 sinkChannel.write(byteBuffer); //5、创建接收 pipe 数据的 source 管道 Pipe.SourceChannel sourceChannel = pipe.source(); //6、接收数据,并保存到缓冲区中 ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024); int length = sourceChannel.read(byteBuffer2); System.out.println(new String(byteBuffer2.array(), 0, length)); sourceChannel.close(); sinkChannel.close(); }
NIO-FileLock
文件锁是在多个程序同时访问、修改同一个文件,很容易因为文件数据不同步而出现问题。给文件加一个锁,同一时间,只能有一个程序修改此文件, 或者程序都只能读此文件,这就解决了同步问题
文件锁是进程级别的,不是线程级别的。例如两个应用程序不能去修改同一个文件,但是同一个进程中的多个线程可以修改
文件锁又分为:排他锁和共享锁
NIO-其他
Path
Java Path 接口是 Java NIO 更新的一部分,Java Path 接口是在 Java7 中添加到 Java NIO 的,Java Path 实例表示文件系统中的路径。一个路径可以指向一个文件或一个目录。路径可以是绝对路径,也可以是相对路径。java.nio.file.Path 接口类似于 java.io.File 类,但是有一些差别。可以使用 Path 接口来替换 File 类的使用
Path path = Paths.get("d:\\atguigu\\001.txt");
Files
Files 类(java.nio.file.Files)提供了几种操作文件系统中的文件的方法
1、Files.createDirectory()方法,用于根据 Path 实例创建一个新目录
2、Files.copy(),从一个路径拷贝一个文件到另外一个目录
3、Files.move()用于将文件从一个路径移动到另一个路径
4、Files.delete()方法可以删除一个文件或者目录
5、Files.walkFileTree()方法包含递归遍历目录树功能,将 Path 实例和 FileVisitor作为参数。Path 实例指向要遍历的目录,FileVisitor 在遍历期间被调用
AsynchronousFileChannel
在 Java 7 中,Java NIO 中添加了 AsynchronousFileChannel,也就是是异步地将数据写入文件