文章预览
前言
1、Java NIO简介
Java NIO(New IO 或 Non Blocking IO)是从 Java 1.4 版本开始引入的一个新的
IO API,可以替代标准的 Java IO API。NIO 支持面向缓冲区的、基于通道的 IO 操
作。NIO 将以更加高效的方式进行文件的读写操作。
Java NIO 由以下几个核心部分组成:
Channels
Buffers
Selectors
虽然 Java NIO 中除此之外还有很多类和组件,但Channel
,Buffer
和Selector
构成
了核心的 API。其它组件,如Pipe
和FileLock
,只不过是与三个核心组件共同使用的
工具类。
这一节详细介绍一下Channel
,文章会持续更新
2、Channel简介
Channel
,可以翻译成“通道”。Channel
和 IO 中的 Stream(流)是差不
多一个等级的。只不过 Stream 是单向的,譬如:InputStream
, OutputStream
.而
Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。
NIO 中的 Channel 的主要实现有:FileChannel
、DatagramChannel
、
SocketChannel
和 ServerSocketChannel
,这里看名字就可以猜出个所以然来:分别可以对应文件 IO
、UDP
和 TCP
(Server 和 Client)。
一、Channel的使用方法
下面的案例用到了Buffer
Buffer
通常的操作
- 将数据写入缓冲区
- 调用
buffer.flip()
反转读写模式 - 从缓冲区读取数据
- 调用
buffer.clear()
或buffer.compact()
清除缓冲区内容
1.1、FileChannel
FileChannel
类可以实现常用的 read
,write
以及 scatter/gather
操作,同时它也提
供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作
1.1.1、FileChannel读取操作
package com.atguigu.channel;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo1 {
//FileChannel读取数据到buffer中
public static void main(String[] args) throws Exception {
//创建FileChannel
RandomAccessFile aFile = new RandomAccessFile("d:\\01.txt","rw");
FileChannel channel = aFile.getChannel();
//创建Buffer
ByteBuffer buf = ByteBuffer.allocate(1024);
//读取数据到buffer中
int bytesRead = channel.read(buf);
while(bytesRead != -1) {
System.out.println("读取了:"+bytesRead);
buf.flip();
while(buf.hasRemaining()) {
System.out.println((char)buf.get());
}
buf.clear();
bytesRead = channel.read(buf);
}
aFile.close();
System.out.println("结束了");
}
}
1.1.2、FileChanne写操作
package com.atguigu.channel;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
//FileChanne写操作
public class FileChannelDemo2 {
public static void main(String[] args) throws Exception {
// 打开FileChannel
RandomAccessFile aFile = new RandomAccessFile("d:\\001.txt","rw");
FileChannel channel = aFile.getChannel();
//创建buffer对象
ByteBuffer buffer = ByteBuffer.allocate(1024);
String newData = "hello,jiucai!";
buffer.clear();
//写入内容
buffer.put(newData.getBytes());
buffer.flip();
//FileChannel完成最终实现
while (buffer.hasRemaining()) {
channel.write(buffer);
}
//关闭
channel.close();
}
}
1.1.3、通道之间数据传输
FileChanne
通道之间数据传输有两方法transferFrom()
、transferTo()
这两个方法就是相反的操作而已
transferFrom()方式实现
package com.atguigu.channel;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
//通道之间数据传输
public class FileChannelDemo3 {
//transferFrom()
public static void main(String[] args) throws Exception {
// 创建两个fileChannel
RandomAccessFile aFile = new RandomAccessFile("d:\\atguigu\\001.txt","rw");
FileChannel fromChannel = aFile.getChannel();
RandomAccessFile bFile = new RandomAccessFile("d:\\atguigu\\02.txt","rw");
FileChannel toChannel = bFile.getChannel();
//fromChannel 传输到 toChannel
long position = 0;
long size = fromChannel.size();
toChannel.transferFrom(fromChannel,position,size);
aFile.close();
bFile.close();
System.out.println("over!");
}
}
transferTo()方法实现
package com.atguigu.channel;
import java.io.RandomAccessFile;
import java.nio.channels.DatagramChannel;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
//通道之间数据传输
public class FileChannelDemo4 {
//transferTo()
public static void main(String[] args) throws Exception {
// 创建两个fileChannel
RandomAccessFile aFile = new RandomAccessFile("d:\\atguigu\\001.txt","rw");
FileChannel fromChannel = aFile.getChannel();
RandomAccessFile bFile = new RandomAccessFile("d:\\atguigu\\03.txt","rw");
FileChannel toChannel = bFile.getChannel();
//fromChannel 传输到 toChannel
long position = 0;
long size = fromChannel.size();
fromChannel.transferTo(0,size,toChannel);
aFile.close();
bFile.close();
System.out.println("over!");
}
}
1.2、SocketChannel
新的socket通道类可以运行非阻塞模式并且是可选择的。这两个性能可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。本节中我们会看到,再也没有为每个socket连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换总开销。借助新的NIO类,一个或几个线程就可以管理成百上千的活动socket连接了并且只有很少甚至可能没有性能损失。所有的socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都继承了位于java.nio.channels.spi包中的AbstractSelectableChannel。这意味着我们可以用一个Selector对象来执行socket通道的就绪选择(readiness selection)。
请注意DatagramChannel和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。
在我们具体讨论每一种socket通道前,您应该了解socket和socket通道之间的关系。之前的章节中有写道,通道是一个连接I/O服务导管并提供与该服务交互的方法。就某个socket而言,它不会再次实现与之对应的socket通道类中的socket协议API,而java.net中已经存在的socket通道都可以被大多数协议操作重复使用。
全部socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对等socket对象。这些是我们所熟悉的来自java.net的类(Socket、ServerSocket和DatagramSocket),它们已经被更新以识别通道。对等socket可以通过调用socket( )方法从一个通道上获取。此外,这三个java.net类现在都有getChannel( )方法。
Socket通道将与通信协议相关的操作委托给相应的socket对象。socket的方法看起来好像在通道类中重复了一遍,但实际上通道类上的方法会有一些新的或者不同的行为。
要把一个socket通道置于非阻塞模式,我们要依靠所有socket通道类的公有超级类:SelectableChannel。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞I/O和可选择性是紧密相连的,那也正是管理阻塞模式的API代码要在SelectableChannel超级类中定义的原因。
设置或重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking( )方法即可,传递参数值为true则设为阻塞模式,参数值为false值设为非阻塞模式。真的,就这么简单!您可以通过调用isBlocking( )方法来判断某个socket通道当前处于哪种模式。
1.2.1、Demo
package com.atguigu.channel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
public class SocketChannelDemo {
public static void main(String[] args) throws Exception {
//创建SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
// SocketChannel socketChanne2 = SocketChannel.open();
// socketChanne2.connect(new InetSocketAddress("www.baidu.com", 80));
//设置阻塞和非阻塞
socketChannel.configureBlocking(false);
//读操作
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over");
}
}
1.3、ServerSocketChannel
ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的java.net.ServerSocket
执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。
由于 ServerSocketChannel
没有 bind()
方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。我们也是使用对等ServerSocket
的 API 来根据需要设置其他的 socket 选项。
ServerSocketChannel
的 accept()
方法会返回 SocketChannel
类型对象,SocketChannel
可以在非阻塞模式下运行。
其它 Socket
的 accept()
方法会阻塞返回一个 Socket
对象。如果ServerSocketChannel
以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept( )
会立即返回 null
。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册 ServerSocketChannel
对象以实现新连接到达时自动通知的功能。
1.3.1、一个非阻塞的 accept( )方法:
package com.atguigu.channel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerSocketChannelDemo {
public static void main(String[] args) throws Exception {
//端口号
int port = 8888;
//buffer
ByteBuffer buffer = ByteBuffer.wrap("hello 韭菜盖饭".getBytes());
//ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
//绑定
ssc.socket().bind(new InetSocketAddress(port));
//设置非阻塞模式
ssc.configureBlocking(false);
//监听有新链接传入
while(true) {
System.out.println("Waiting for connections");
SocketChannel sc = ssc.accept();
if(sc == null) { //没有链接传入
System.out.println("null");
Thread.sleep(2000);
} else {
System.out.println("Incoming connection from: " + sc.socket().getRemoteSocketAddress());
buffer.rewind(); //指针0
sc.write(buffer);
sc.close();
}
}
}
}
无连接时
有连接时
1.4、DatagramChannel
正如 SocketChannel
对应 Socket
,ServerSocketChannel
对应 ServerSocket
,每一个 DatagramChannel
对象也有一个关联的 DatagramSocket
对象。正如SocketChannel
模拟连接导向的流协议(如 TCP/IP),DatagramChannel
则模拟包导向的无连接协议(如 UDP/IP)。DatagramChannel
是无连接的,每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载。与面向流的的 socket
不同,DatagramChannel
可以发送单独的数据报给不同的目的地址。同样,DatagramChannel
对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处的信息(源地址)
1.4.1、demo
package com.atguigu.channel;
import org.junit.Test;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.Charset;
public class DatagramChannelDemo {
//发送的实现
@Test
public void sendDatagram() throws Exception {
//打开 DatagramChannel
DatagramChannel sendChannel = DatagramChannel.open();
InetSocketAddress sendAddress =
new InetSocketAddress("127.0.0.1",9999);
//发送
while(true) {
ByteBuffer buffer = ByteBuffer.wrap("发送消息123456".getBytes("UTF-8"));
sendChannel.send(buffer,sendAddress);
System.out.println("已经完成发送");
Thread.sleep(1000);
}
}
//接收的实现
@Test
public void receiveDatagram() throws Exception {
//打开DatagramChannel
DatagramChannel receiveChannel = DatagramChannel.open();
InetSocketAddress receiveAddress = new InetSocketAddress(9999);
//绑定
receiveChannel.bind(receiveAddress);
//buffer
ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
//接收
while(true) {
receiveBuffer.clear();
SocketAddress socketAddress = receiveChannel.receive(receiveBuffer);
receiveBuffer.flip();
System.out.println(socketAddress.toString());
System.out.println(Charset.forName("UTF-8").decode(receiveBuffer));
}
}
//连接 read 和 write
@Test
public void testConnect() throws Exception {
//打开DatagramChannel
DatagramChannel connChannel = DatagramChannel.open();
//绑定
connChannel.bind(new InetSocketAddress(9999));
//连接
connChannel.connect(new InetSocketAddress("127.0.0.1",9999));
//write方法
connChannel.write(ByteBuffer.wrap("发送消息".getBytes("UTF-8")));
//buffer
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
while(true) {
readBuffer.clear();
connChannel.read(readBuffer);
readBuffer.flip();
System.out.println(Charset.forName("UTF-8").decode(readBuffer));
}
}
}
发送数据
接收数据