Java NIO 与 IO的区别:
一.缓冲区(Buffer)
在JavaNIO中负责数据的存取。缓冲区就是数组。用于存储不同数据类型的数据。根据数据类型不同(boolean除外),提供了相应类型的缓冲区。
1⃣️ByteBuffer 2⃣️CharBuffer 3⃣️ShortBuffer 4⃣️IntBuffer 5⃣️LongBuffer 6⃣️FloatBuffer 7⃣️DoubleBuffer
上述缓冲区的管理方式几乎一致,通过allocate()或得缓冲区。
//分配一个指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
缓冲区存取数据的两个核心方法:
1⃣️put() :存入数据到缓冲区中。
2⃣️get() :获取缓冲区中的数据。
二.缓冲区中的四个核心属性
capacity:容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变。
limit:界限,表示缓冲区中可以操作数据的大小。limit后面的数据不能进行读写。
position:位置,表示缓冲区中正在操作数据的位置。
mark:标记,表示记录当前position的位置。可以通过reset()方法将position恢复到mark位置。
@Test
public void test1() {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("------------allocate----------");
System.out.println("capacity : " + byteBuffer.capacity());
System.out.println("limit : " + byteBuffer.limit());
System.out.println("position : " + byteBuffer.position());
byteBuffer.put("abcde".getBytes());
System.out.println("------------put----------");
System.out.println("capacity : " + byteBuffer.capacity());
System.out.println("limit : " + byteBuffer.limit());
System.out.println("position : " + byteBuffer.position());
byteBuffer.flip();
System.out.println("------------flip----------");
System.out.println("capacity : " + byteBuffer.capacity());
System.out.println("limit : " + byteBuffer.limit());
System.out.println("position : " + byteBuffer.position());
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println("------------get----------");
System.out.println("byteBuffer中的数据为 : " + new String(bytes, 0, bytes.length));
System.out.println("capacity : " + byteBuffer.capacity());
System.out.println("limit : " + byteBuffer.limit());
System.out.println("position : " + byteBuffer.position());
byteBuffer.rewind();
System.out.println("------------rewind----------");
System.out.println("capacity : " + byteBuffer.capacity());
System.out.println("limit : " + byteBuffer.limit());
System.out.println("position : " + byteBuffer.position());
byteBuffer.clear();
System.out.println("------------clear----------");
System.out.println("capacity : " + byteBuffer.capacity());
System.out.println("limit : " + byteBuffer.limit());
System.out.println("position : " + byteBuffer.position());
System.out.println((char) byteBuffer.get());
}
得到结果:
当调用allocate()方法之后,会开辟一个数组类型,它的position指向0索引位置,limit和capacity为设置的总长度。当调用put方法之后,position会指向下一个没被占用的索引位置。当调用flip()方法后,开启读模式,limit发挥作用,limit可以理解为存放有用数据的边界,而position再次指向开头索引,这样一来,position和limit之间的数据就是可以被读取的数据了。在调用get()方法之后,开始真正的读数据,以limit为边界,position为开始进行读取数据,得到两者之间的数据。rewind()方法为重读操作,将position重新指回开头,重新读取数据。clear()方法为“清空”缓存的方法,但实际上只是把position和limit归为起始状态,之前写入的数据依然存在,如上述代码中在clear之后,依然可以取出第一个字母a,当重新写入数据时,会覆盖掉之前的数据。
mark的用法:标记当前position,在position移动过后,可以通过reset()方法使position回到之前的一次操作状态。
@Test
public void test2(){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("abcde".getBytes());
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes, 0, 2);
System.out.println(new String(bytes, 0, 2));
System.out.println("capacity : " + byteBuffer.capacity());
System.out.println("limit : " + byteBuffer.limit());
System.out.println("position : " + byteBuffer.position());
byteBuffer.mark();
byteBuffer.get(bytes, 2, 2);
System.out.println(new String(bytes, 2, 2));
System.out.println("capacity : " + byteBuffer.capacity());
System.out.println("limit : " + byteBuffer.limit());
System.out.println("position : " + byteBuffer.position());
byteBuffer.reset();
System.out.println("capacity : " + byteBuffer.capacity());
System.out.println("limit : " + byteBuffer.limit());
System.out.println("position : " + byteBuffer.position());
}
三.直接缓冲区 与 非直接缓冲区
1⃣️直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率。
2⃣️非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立于JVM的内存中。
//分配直接缓冲区第一种方法:
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
//分配直接缓冲区第二种方法:
byteBuffer.isDirect(); //true 判断是否为直接缓冲区
四.通道(Channel)
由java.nio.channels包定义的。Channel表示IO源与目标打开的连接。Channel类似于传统的“流”。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。
通道的主要实现类:
1⃣️FileChannel 2⃣️SocketChannel 3⃣️ServerSocketChannel 4⃣️DatagramChannel
获取通道的主要方法:
-
Java针对支持通道的类提供了getChannel()方法:
本地IO:
FileInputStream / FileOutputStream
RandomAccessFile网络IO: Socket ServerSocket DatagramSocket
2.在JDK1.7中的NIO.2, 针对各个通道提供了静态方法open();
3.在JDK1.7中的NIO.2的Files工具类的newByteChannel()
代码实践:1⃣️利用通道完成文件的复制
@Test
public void test3() throws IOException {
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
FileChannel inChannel = null;
FileChannel outChannel = null;
inputStream = new FileInputStream("1.txt");
outputStream = new FileOutputStream("2.txt");
//获取通道
inChannel = inputStream.getChannel();
outChannel = outputStream.getChannel();
//分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将管道中的数据存入缓冲区
while (inChannel.read(byteBuffer) != -1){
byteBuffer.flip();
outChannel.write(byteBuffer);
byteBuffer.clear();
}
//释放资源
inputStream.close();
outputStream.close();
inChannel.close();
outChannel.close();
}
2⃣️使用直接缓冲区完成文件的复制(内存映射文件)
@Test
public void test4() throws IOException {
FileChannel in = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);
FileChannel out = FileChannel.open(Paths.get("2.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
//内存映射文件
MappedByteBuffer inMappedBuf = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
MappedByteBuffer outMappedBuf = out.map(FileChannel.MapMode.READ_WRITE, 0, in.size());
//直接对缓冲区进行对数据的读写
byte[] bytes = new byte[inMappedBuf.limit()];
inMappedBuf.get(bytes);
outMappedBuf.put(bytes);
//释放资源
in.close();
out.close();
}
五.分散(Scatter) 与 聚集(Gather)
分散读取:将通道中的数据分散到多个缓冲区中。
聚集写入:将多个缓冲区中的数据聚集到通道中。
实例代码:
@Test
public void test5() throws IOException {
RandomAccessFile file = new RandomAccessFile("1.txt", "rw");
//获取通道
FileChannel channel = file.getChannel();
//分配指定大小的缓冲区
ByteBuffer byteBuffer1 = ByteBuffer.allocate(2);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
//分散读取
ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2};
channel.read(byteBuffers);
for (ByteBuffer byteBuffer : byteBuffers){
byteBuffer.flip();
}
System.out.println(new String(byteBuffer1.array(), 0, byteBuffer1.limit()));
System.out.println("-----------------------------");
System.out.println(new String(byteBuffer2.array(), 0, byteBuffer2.limit()));
RandomAccessFile file1 = new RandomAccessFile("2.txt", "rw");
FileChannel channel1 = file1.getChannel();
channel1.write(byteBuffers);
}
六.字符集(Charset)
编码:字符串 --> 字节数组
解码:字节数组 --> 字符串
实例代码:
@Test
public void test6() throws IOException {
Charset gbk = Charset.forName("GBK");
//获取编码器
CharsetEncoder charsetEncoder = gbk.newEncoder();
//获取解码器
CharsetDecoder charsetDecoder = gbk.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(1024);
charBuffer.put("我是大好人");
charBuffer.flip();
//编码
ByteBuffer encode = charsetEncoder.encode(charBuffer);
for (int i = 0; i < encode.limit(); i++) {
System.out.println(encode.get());
}
System.out.println("---------------------------");
//解码
encode.flip();
CharBuffer decode = charsetDecoder.decode(encode);
System.out.println(decode); //我是大好人
System.out.println("---------------------------");
//使用gbk编码,用utf8去解码
encode.flip();
Charset charset = Charset.forName("UTF-8");
CharBuffer decode1 = charset.decode(encode);
System.out.println(decode1); //���Ǵ����
}
七.NIO的 非阻塞式网络编程 与 传统IO阻塞式网络编程 区别
传统IO阻塞式网络编程:
当客户端向服务端发送一个请求之后,如果服务端不能确定客户端的数据真实有效时,这时,该线程会一直处于一个阻塞状态,在此期间,服务端的这个线程不能做任何事情。服务端是如何判断客户端的数据是不是有效数据呢?客户端发送的数据将会先到服务端的内核地址空间,服务端线程会去查看内核地址空间中有没有数据,如果没有数据,那么服务端就会等待。当内核空间中有数据了,服务端将数据从内核地址空间读到用户地址空间,再读入程序中。也就是说,当客户端向服务端传入大量此类数据,将会出现排队现象,没能更好的利用CPU资源。 早期传统IO中利用多线程技术来改善,为每一个发送向服务端的请求都单独开设一个线程,这样当一个线程处于阻塞状态时,不会影响其他线程。但这样的解决方式依然没能完全的利用资源,当一个线程阻塞时,这个线程依然还是等待的。
NIO非阻塞式网络编程:
NIO中在客户端和服务端之间加入了选择器,而选择器的任务就是把客户端的每一个用于传输数据的通道都注册到选择器上,然后选择器会监控这些通道的IO状况(读、写、连接),当某个任务完全准备完成时,选择器才会把这个任务分配到服务端的一个或多个的线程上再去运行。
选择器(Selector):
- 当调用register(Selector sel, int ops)将通道注册到选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。
- 可以监听的事件类型(可以使用SelectionKey的四个常量表示)
1.读:SelectionKey.OP_READ (1)
2.写:SelectionKey.OP_WRITE(4)
3.连接:SelectionKey.OP_CONNECT (8)
4.接收:SelectionKey.OP_ACCEPT(16) - 若注册时不止监听一个事件,则可以使用“位或”操作符连接。例如:
ssChannel.register(open, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
阻塞式代码实践:(不实用Selector)
客户端:
@Test
public void client1() throws IOException {
//获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8989));
//分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//读取本地文件,并发送到服务端去
FileChannel inChannel = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);
while (inChannel.read(byteBuffer) != -1){
byteBuffer.flip();
sChannel.write(byteBuffer);
byteBuffer.clear();
}
//关闭通道
inChannel.close();
sChannel.close();
}
服务端:
@Test
public void server1() throws IOException {
//获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//绑定连接
ssChannel.bind(new InetSocketAddress(8989));
//获取客户端连接的通道
SocketChannel sChannel = ssChannel.accept();
//接收客户端的数据,并保存到本地
FileChannel outChannel = FileChannel.open(Paths.get("2.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (sChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
outChannel.write(byteBuffer);
byteBuffer.clear();
}
sChannel.close();
outChannel.close();
ssChannel.close();
}
非阻塞式实践代码:(使用Selector)
客户端:
@Test
public void client3() throws IOException{
//或得通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8989));
//切换成非阻塞模式*****
sChannel.configureBlocking(false);
//分配一个指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//发送数据给客户端
byteBuffer.put(LocalDateTime.now().toString().getBytes());
byteBuffer.flip();
sChannel.write(byteBuffer);
byteBuffer.clear();
//释放资源
sChannel.close();
}
服务端:
@Test
public void server3() throws IOException{
//获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//切换成非阻塞模式
ssChannel.configureBlocking(false);
//绑定端口号
ssChannel.bind(new InetSocketAddress(8989));
//******************
//获取选择器
Selector open = Selector.open();
//将通道注册到选择器上,并且指定监听事件
ssChannel.register(open, SelectionKey.OP_ACCEPT);
//轮询式的获取选择器上已经就绪的事件
while (open.select() > 0){
//获取当前选择器中所有注册的选择键(已就绪的监听事件)
Iterator<SelectionKey> iterator = open.selectedKeys().iterator();
//迭代获取
while (iterator.hasNext()){
//获取就绪的事件
SelectionKey next = iterator.next();
//判断具体是什么事件准备就绪
if (next.isAcceptable()){
//如果接收就绪,就获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//将客户端管道切换成非阻塞模式
sChannel.configureBlocking(false);
//将该通道注册到选择器上
sChannel.register(open, SelectionKey.OP_READ);
}else if (next.isReadable()){
//获取当前选择器上读就绪状态的通道
SocketChannel sChannel = (SocketChannel) next.channel();
//读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(byteBuffer)) > 0){
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, len));
byteBuffer.clear();
}
}
//取消选择键
iterator.remove();
}
}
//******************
}
[服务端代码解释 :使用选择器的本意是希望所有有可能发生阻塞的线程都交给选择器去判断是否准备就绪,要时刻铭记着这一点才能理解接下来的操作。首先获得ServerSocketChannel通道,这个通道的作用是监听新进来的TCP连接的通道,也就是说它也是一个监听事件,我们需要把它交给选择器去管理;接下来切换到非阻塞模式,绑定端口号。然后通过Selector.open()获得选择器,然后首先将前面说到的ServerSocketChanel注册到选择器上,当有客户端发送TCP请求并且请求就绪后,由选择器告诉我们。接下来选择器会轮询式的获取选择器上已经就绪的事件,通过迭代器获得所有已经就绪的监听事件,并用while循环一个一个的处理,首先判断是什么类型的事件,跟上面的SelectionKey的四种类型一一对应,实践代码中的是Accept事件,于是可以通过ServerSocketChannel管道获取客户端的SocketChannel管道,客户端与服务端的连接打通了,接下来需要先将新拿到的管道进行非阻塞模式切换,然后去想,我们得到的SocketChannel管道的目的是为了将ByteBuffer从客户端发送到服务端,也就是说ByteBuffer是否准备好了或者说是否是有效的数据会导致事件的阻塞与否,所以这个通道我们也需要交给选择器去抉择,因此这里并没有直接对得到的管道进行读数据,而是把管道又注册到选择器中,当while循环中SocketChannel事件准备就绪了,被选择器发现,这里我们需要一个 if (next.isReadable()) 来操作准备就绪的事件, 这次我们可以对得到的管道进行读数据操作了。在操作完管道之后,要将完成的管道remove出去,通过将迭代器中的SelectionKey进行remove即可。]
八.DatagramChannel
Java NIO中的Datagramchannel是一个能收发UDP包的通道。与Socket基本一样。
实践代码:
发送端:
@Test
public void send() throws IOException{
//获取通道
DatagramChannel dChannel = DatagramChannel.open();
//切换到非阻塞模式
dChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Scanner scan = new Scanner(System.in);
while (scan.hasNext()){
String str = scan.next();
byteBuffer.put((LocalDateTime.now() + "\n" + str).getBytes());
byteBuffer.flip();
dChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 8989));
byteBuffer.clear();
}
dChannel.close();
}
接收端:
@Test
public void receive() throws IOException{
//打开通道
DatagramChannel dc = DatagramChannel.open();
dc.bind(new InetSocketAddress(8989));
//切换到非阻塞模式
dc.configureBlocking(false);
//获取选择器
Selector selector = Selector.open();
dc.register(selector, SelectionKey.OP_READ);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (selector.select() > 0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey next = iterator.next();
if (next.isReadable()){
dc.receive(byteBuffer);
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
byteBuffer.clear();
}
iterator.remove();
}
}
dc.close();
}
九.管道(Pipe)
Java NIO管道是两个线程之间的单向数据连接。Pipe有一个source通道和一个skin通道。数据会被写到skin通道,从source通道被读取。
实践代码:
@Test
public void test7() throws IOException{
//获取管道
Pipe pipe = Pipe.open();
//将缓冲区中的数据写入管道
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Pipe.SinkChannel sinkChannel = pipe.sink();
byteBuffer.put("abcde".getBytes());
byteBuffer.flip();
sinkChannel.write(byteBuffer);
byteBuffer.clear();
//读取缓冲区中的数据
Pipe.SourceChannel sourceChannel = pipe.source();
sourceChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
sinkChannel.close();
sourceChannel.close();
}