本文借鉴网络视频及网络资料汇总如下,如果您有更好的建议和分享,欢迎您在评论区留言与补充!
1.缓冲区(buffer):
在Java负责的数据的存取,缓冲区底层为数组,用于存储不同数据类型的数据,根据数据类型不同(boolean除外),提供相应类型的缓冲区:
ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,DoubleBuffer,FloatBuffer。
上述缓冲区管理方法基本一致,通过allocate()获取缓冲区。
2.缓冲区获取数据核心方法:
put():存入数据到缓冲区
get():获取缓冲区的数据
3.缓冲区四个核心属性:
capacity:容量,表示缓冲区中最大容量,声明之后不能改变。
limit:界限,表示缓冲区可以操作数据的大小(limit后的数据不能进行读写)。
position:位置,表示缓冲区正在操作数据的位置。
mark:标记,表示记录当前position的位置,通过reset()恢复到mark位置。
0<=mark<=position <= limit <= capacity
import org.junit.Test;
import java.nio.ByteBuffer;
public class NIO1 {
@Test
public void newIOTest1() {
String str = "abcde";
// 1.分配指定大小的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 1024
System.out.println(buffer.capacity()); // 1024
// 2.put存入数据到缓冲区
buffer.put(str.getBytes());
System.out.println(buffer.position()); // 5
System.out.println(buffer.limit()); // 1024
System.out.println(buffer.capacity()); // 1024
// 3.切换到读取数据
buffer.flip();
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 5
System.out.println(buffer.capacity()); // 1024
// 4.get从缓冲区读取数据
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
System.out.println(new String(bytes, 0, bytes.length));
System.out.println(buffer.position()); // 5
System.out.println(buffer.limit()); // 5
System.out.println(buffer.capacity()); // 1024
// 5.rewind(可重复读取)
buffer.rewind();
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 5
System.out.println(buffer.capacity()); // 1024
// 6.clear,清空缓存区,缓存区中数据依然存在但是处于"被遗忘"状态
buffer.clear();
System.out.println(buffer.position()); // 0
System.out.println(buffer.limit()); // 1024
System.out.println(buffer.capacity()); // 1024
System.out.println((char) buffer.get()); // a
}
@Test
public void newIOTest2() {
String str = "abcde";
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(str.getBytes());
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes, 0, 2);
System.out.println(new String(bytes, 0, 2)); // ab
System.out.println(buffer.position()); // 2
buffer.mark();
buffer.get(bytes, 2, 2);
System.out.println(new String(bytes, 2, 2)); // cd
System.out.println(buffer.position()); // 4
buffer.reset();
System.out.println(buffer.position()); // 2
// 查看缓冲区中是否还有剩余数据
if (buffer.hasRemaining()) {
// 获取缓冲区可以操作的数量
System.out.println(buffer.remaining());
}
}
}
4.直接缓冲区与非直接缓冲区:
非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM内存中。
直接缓冲区:通过allocateDirect()方法分配缓冲区,将缓冲区建立在操作系统的物理内存中。
@Test
public void newIOTest3() {
// 获取直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 判断是直接缓冲区还是非直接缓冲区,true为直接缓冲区,false反之
System.out.println(buffer.isDirect());
}
5.通道:
演化过程:
通道(Channel):用于源节点与目标节点的连接,在Java NIO中负责缓冲区数据的传输,Channel本身不存储数据,需要配合缓冲区进行传输。
通道主要实现类:
java.nio.channels.Channel下的接口:
FileChannel:用于本地
SocketChannel:用于网络I/O(TCP)
ServerSocketChannel:用于网络I/O(TCP)
DatagramChannel:用于网络I/O(UDP)
获取通道的几种方式:
1.Java针对支持通道的类提供了getChannel()方法:
本地IO:
FileInputstream/FileOutputstream,RandomAccessFile,
网络IO:
Socket,ServerSocket,DatagramSockte
2.在JDK1.7中的NIO.2针对各个通道提供了静态方法open()
3.在JKD1.7中的NIO.2的Files工具类的newByteChannel()
4.通道之间的数据传输:
transferFrom(),transferTo()
5.分散(Scatter)与聚集(Gather):
分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区
聚集写入(Gathering Writes):将多个缓冲区的数据聚集到通道中
6.字符集:Charset
编码:字符串→字符数组
解码:字符数组→字符串
(以下案例的顺序与上述获取通道方式的编号顺序相反)
import org.junit.Test;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.Map.*;
import java.util.Set;
public class ChannelTest {
@Test
public void channelTest6() {
Charset cs = Charset.forName("GBK");
// 获取编码器
CharsetEncoder ce = cs.newEncoder();
// 获取解码器
CharsetDecoder cd = cs.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(1024);
charBuffer.put("文字 + 英文:Hello");
// 编码
ByteBuffer byteBuffer = null;
try {
charBuffer.flip();
byteBuffer = ce.encode(charBuffer);
for (int i = 0; i < byteBuffer.limit(); i++) {
// 查看对应16个字节是否编码成功
System.out.println(byteBuffer.get());
}
System.out.println(byteBuffer.position());
// 解码
byteBuffer.flip();
CharBuffer cb = cd.decode(byteBuffer);
System.out.println(cb.toString());
// byteBuffer.flip();
// System.out.println((cd.decode(byteBuffer)).toString());
// 用GBK编码,UTF-8解码会导致乱码
Charset charset = Charset.forName("UTF-8");
byteBuffer.flip();
System.out.println((charset.decode(byteBuffer)).toString());
} catch (CharacterCodingException e) {
e.printStackTrace();
}
}
// 查看打印支持的字符集
@Test
public void channelTest5() {
Map<String, Charset> map = Charset.availableCharsets();
Set<Entry<String, Charset>> set = map.entrySet();
for (Entry<String, Charset> entry : set) {
System.out.println(entry.getKey() + "==" + entry.getValue());
}
}
// 分散与聚集
@Test
public void channelTest4() {
RandomAccessFile file = null;
try {
file = new RandomAccessFile("1.txt", "r");
// 获取通道
FileChannel channel1 = file.getChannel();
// 分配多个指定大小的缓冲区
ByteBuffer byteBuffer1 = ByteBuffer.allocate(100);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
// 分散读取
ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2};
channel1.read(byteBuffers);
for (ByteBuffer bytes : byteBuffers) {
bytes.flip();
}
System.out.println(new String(byteBuffers[0].array(), 0, byteBuffers[0].limit()));
System.out.println(new String(byteBuffers[1].array(), 0, byteBuffers[1].limit()));
// 聚集写入
RandomAccessFile file1 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = file1.getChannel();
channel2.write(byteBuffers);
} catch (IOException e) {
e.printStackTrace();
}
}
// 通道之间的数据传输(直接缓冲区)
@Test
public void channelTest3() {
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inChannel = FileChannel.open(Paths.get("图片1"), StandardOpenOption.READ);
outChannel = FileChannel.open(Paths.get("图片4"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
inChannel.transferTo(0, inChannel.size(), outChannel);
// outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 使用直接缓冲区完成文件的复制(内存映射文件,直接缓冲区)
// 复制时间短但不一定稳定
@Test
public void channelTest2() {
long start = System.currentTimeMillis();
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inChannel = FileChannel.open(Paths.get("图片1"), StandardOpenOption.READ);
// StandardOpenOption.CREATE,如果文件不存在则创建文件,如果文件已存在则依旧覆盖存在文件
// StandardOpenOption.CREATE_NEW,如果文件不存在则创建文件,如果文件已存在则报错
outChannel = FileChannel.open(Paths.get("图片4"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE_NEW);
// 内存映射文件
MappedByteBuffer inMappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedByteBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
// 直接对缓冲区进行数据的读写操作
byte[] bytes = new byte[inMappedByteBuffer.limit()];
inMappedByteBuffer.get(bytes);
outMappedByteBuffer.put(bytes);
// 关闭通道
inChannel.close();
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 利用通道完成文件复制(非直接缓冲区)
// 复制时间长
@Test
public void channelTest1() {
FileInputStream fis = null;
FileOutputStream fos = null;
// ①获取通道
FileChannel channel1 = null;
FileChannel channel2 = null;
try {
fis = new FileInputStream("图片1");
fos = new FileOutputStream("图片3");
channel1 = fis.getChannel();
channel2 = fos.getChannel();
// ②分配指定大小
ByteBuffer byteBuffer = ByteBuffer.allocate(900000);
// ③将通道数据存入缓冲区
if (channel1.read(byteBuffer) != -1) {
byteBuffer.flip(); // 切换读取模式
// ④将缓冲区中数据写入通道
channel2.write(byteBuffer);
byteBuffer.clear(); // 清空缓冲区
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
channel1.close();
channel2.close();
fos.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
5.阻塞式与非阻塞式:
阻塞式:
传统的 IO 流都是阻塞式的。当客户端发送读写请求给服务端时,若服务端不能确定该线程的数据是否真实有效,该线程会一直处于阻塞状态,服务端会等待客户端,该线程什么时候有数据,才可以进行工作,但是在等待期间,服务端的该线程做不了任何事情。服务端判断内核地址空间中有没有数据,如果没有数据服务端继续等待,该线程依旧处于阻塞状态,什么时候有数据了在把数据复制到用户地址空间,读到我们的程序中。
因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
非阻塞式:
Java NIO 是非阻塞模式的。正因为客户端与服务端之间建立了通道,选择器会把客户端中每一个用于传输数据的通道注册到选择器上,该选择器的作用是用来监控每一个通道的IO状态。当某一条通道上某一个请求的事件完全准备就绪时,那么服务器才会把该任务分配到服务端的一个或多个线程上,再去运行。如果客户端的请求没有完全准备就绪时,服务端的线程依旧正常工作,不受阻碍。
使用NIO完成网络通信的三个核心:
1.通道(Channel):负责连接
java.nio.channels.Channel 接口:
SelectableChannel,
SocketChannel,
ServerSocketChannel,
DatagramChannel,
Pipe.SinkChannel,
Pipe.SourceChannel
2. 缓冲区(Buffer):负责数据的存取
3. 选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 状况
NIO_阻塞式:
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
// 阻塞式
public class BlockingNIOTest1 {
// 客户端
@Test
public void client() {
// 获取通道
SocketChannel schannel = null;
try {
schannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
FileChannel fchannel = FileChannel.open(Paths.get("图片1"), StandardOpenOption.READ);
// 分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 读取本地文件,并发送到服务器
while (fchannel.read(byteBuffer) != -1) {
byteBuffer.flip();
schannel.write(byteBuffer);
byteBuffer.clear();
}
// 单项关闭客户端即可避免一直等待
schannel.shutdownOutput();
// 接收服务端的反馈
int len = 0;
while ((len = schannel.read(byteBuffer)) != -1) {
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, len));
byteBuffer.clear();
}
// 关闭通道
schannel.close();
fchannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 服务端
@Test
public void server() {
// 获取通道
ServerSocketChannel sschannel = null;
try {
sschannel = ServerSocketChannel.open();
FileChannel fchannel = FileChannel.open(Paths.get("图片2"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 绑定连接
sschannel.bind(new InetSocketAddress(6666));
// 获取客户连接的通道
SocketChannel socketChannel = sschannel.accept();
// 分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 接收客户端的数据,并保存到本地
while (socketChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
fchannel.write(byteBuffer);
byteBuffer.clear();
}
// 发送反馈给客户端
byteBuffer.put("服务端已成功接收".getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
// 关闭通道
sschannel.close();
fchannel.close();
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
NIO_非阻塞式:
import org.junit.Test;
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.Scanner;
public class BlockingNIOTest2 {
// 客户端
@Test
public void client() {
// 1.获取通道
SocketChannel sChannel = null;
try {
sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
// 2.切换非阻塞模式
sChannel.configureBlocking(false);
// 3.分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 4.发送数据给服务端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String str = scanner.next();
byteBuffer.put((LocalDateTime.now().toString() + "\n" + str).getBytes());
byteBuffer.flip();
sChannel.write(byteBuffer);
byteBuffer.clear();
}
// 5.关闭通道
sChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 服务端
@Test
public void server() {
try {
// 1.获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
// 2.切换成非阻塞模式
ssChannel.configureBlocking(false);
// 3.绑定连接
ssChannel.bind(new InetSocketAddress(6666));
// 4.获取选择器
Selector selector = Selector.open();
// 5.将通道注册到选择器上,并指定"监听接收事件"
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6.轮询式的获取选择器上已经"准备就绪"的事件
while (selector.select() > 0) {
// 7.获取当前选择器中所有注册的"选择键(已就绪的监听事件)"
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
// 8.获取准备"就绪"的事件
SelectionKey sKey = it.next();
// 9.判断具体是什么事件准备就绪
if (sKey.isAcceptable()) {
// 10.若"接收就绪",获取客户端链接
SocketChannel sChannel = ssChannel.accept();
// 11.切换非阻塞模式
sChannel.configureBlocking(false);
// 12.将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
} else if (sKey.isReadable()) {
// 13.获取当前选择器上"读就绪"的通道
SocketChannel sChannel = (SocketChannel) sKey.channel();
// 14.读取数据
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();
}
}
// 15.取消选择键 SelectionKey
it.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里加入了Scanner,本人用的是IDEA,但是用@Test注解测试的时候导致控制台无法输入,所以以下是解决方案(嫌麻烦的小伙伴可以将@Test里面的东西放入main函数,也可以解决控制台不能输入的问题):
如图:
在最下方加入:
-Deditable.java.test.console=true
之后重启IDEA即可在控制台写入。
6.NIO_DatagramChannel:
Java NIO中的DatagramChannel是一个能收发UDP包的通道。
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.Scanner;
public class BlockingNIOTest3 {
@Test
public void send() {
DatagramChannel dgChannel = null;
try {
dgChannel = DatagramChannel.open();
dgChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String str = scanner.next();
byteBuffer.put((LocalDateTime.now().toString() + "\n" + str).getBytes());
byteBuffer.flip();
dgChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 6666));
byteBuffer.clear();
}
dgChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void receive() {
DatagramChannel dgChannel = null;
try {
dgChannel = DatagramChannel.open();
dgChannel.configureBlocking(false);
dgChannel.bind(new InetSocketAddress(6666));
Selector selector = Selector.open();
dgChannel.register(selector, SelectionKey.OP_READ);
while (selector.select() > 0) {
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey sKey = it.next();
if (sKey.isReadable()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
dgChannel.receive(byteBuffer);
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
byteBuffer.clear();
}
}
it.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
7.NIO_PiPe:
Java NIO管道是两个线程之间的单项数据连接。PiPe有一个source通道和一个sink通道,数据会被写入到sink通道,从source通道读取。
import org.junit.Test;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Pipe;
public class PiPeTest {
@Test
public void test() {
// 1.获取管道
Pipe pipe = null;
try {
pipe = Pipe.open();
// 2.将缓冲区中的数据写入管道
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Pipe.SinkChannel sinkChannel = pipe.sink();
byteBuffer.put("这是一条数据".getBytes());
byteBuffer.flip();
sinkChannel.write(byteBuffer);
// 3.读取缓冲区中的数据
Pipe.SourceChannel sourceChannel = pipe.source();
byteBuffer.flip();
int len = sourceChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(), 0, len));
} catch (IOException e) {
e.printStackTrace();
}
}
}