Java NIO
Java NIO的NIO是指New IO,代表Java新IO API,与之对应的是Java OIO(Old IO),代表老IO API。
Java NIO和Java OIO对比:
- Java OIO:面向流的,阻塞的
- Java NIO:面向缓冲区(块)的,非阻塞的
Java NIO核心知识点:
- Buffer
- Channel
- Selector
Buffer
Buffer,即缓冲区,也就是一块内存,用于缓存数据。
作用:读的时候从缓冲区读,而不是每次都读Channel,写的时候等缓冲区满了再写到Channel中,用于实现批量读写,使一次IO处理很多数据,减少IO次数(网络IO或磁盘IO),提高IO性能。
使用Channel读数据和写数据都要使用Buffer:
箭头方向是数据流向
Buffer的子类
Buffer类是父类,其子类有:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
可以看到,不同子类缓存不同数据类型的数据。
Buffer的属性介绍
Buffer类的属性:
-
capacity:容量,限制缓冲区能写入多少数据,比如,对于容量为10的IntBuffer,最多能写入10个int值;对于容量为10的DoubleBuffer,最多能写入10个double值。
-
注意:容量一旦初始化就不能改变
-
-
position:表示当前读/写的位置,可以理解成读/写指针。
-
Buffer有读、写两种模式:
写模式时,position表示下一个写入的位置;
读模式时,position表示下一个读取的位置。
新建一个Buffer时,默认是写模式,可以调用特定API切换读写模式,具体见下文。
-
-
limit:表示读/写的上限,position超过limit就不能再读/写了。
-
mark:标记,用于暂存position的值,配合
mark()和reset()方法使用。
常用操作
-
创建Buffer,有如下两个方法:
- allocate方法
- wrap方法
-
向Buffer写数据:使用
put方法 -
从Buffer读数据:使用
get方法 -
Buffer类提供的API:
-
flip:可将缓冲区从写模式转成读模式
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; } -
clear:可将缓冲区从读模式转成写模式,会丢弃未读完的数据
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }Buffer的子类提供了
compact方法,也可用于将缓冲区从读模式转成写模式,但不会丢弃未读完的数据 -
rewind:重置position位置,然后可进行重新读/写
public final Buffer rewind() { position = 0; mark = -1; return this; } -
mark和 reset:暂存和恢复position
public final Buffer mark() { mark = position; return this; } public final Buffer reset() { int m = mark; if (m < 0) throw new InvalidMarkException(); position = m; return this; }
总结:可以看到,Buffer类提供的API主要是操作Buffer类的4个属性的,而数据的读取和写入API由子类提供。
-
总结可切换读写模式的API
可切换读写模式的API:
- flip方法:「写模式」切换到「读模式」
- clear或compact方法:「读模式」切换到「写模式」
其实,本质上就是操作Buffer对象的position、limit和mark属性
使用Buffer的基本步骤
基本步骤:
- 创建Buffer对象(默认是写模式)
- 使用
put方法写入数据 - 调用
flip方法转成读模式 - 使用
get方法读取数据
代码案例
@Slf4j
public class BufferDemo {
/**
* DEMO:写 读 写 读 重复读
* 上溢:一直写,超过上限,会出现java.nio.BufferOverflowException上溢
* 下溢:一直读,超过上限,会出现java.nio.BufferUnderflowException下溢
* 总结:buffer理解成一个数组,capacity是数组容量,position是游标,limit是游标上限。
*/
@Test
public void t1() {
// 创建Buffer
IntBuffer intBuffer = IntBuffer.allocate(20);
printBufferInfo(intBuffer);
// 写入数据
while (intBuffer.position() < intBuffer.limit()) {
intBuffer.put(intBuffer.position());
}
printBufferInfo(intBuffer);
// 读取数据
intBuffer.flip();
printBufferInfo(intBuffer);
log.info("read: {}", intBuffer.get());
printBufferInfo(intBuffer);
for (int i = 0; i < intBuffer.limit() - 1; i++) {
log.info("read(for): {}", intBuffer.get());
}
printBufferInfo(intBuffer);
System.out.println("----测试用clear清空重新写数据----");
// 再写入数据
intBuffer.clear();
printBufferInfo(intBuffer);
int start = 100;
while (intBuffer.position() < intBuffer.limit() - intBuffer.capacity() / 2) {
intBuffer.put(start++);
}
printBufferInfo(intBuffer);
// 再读取数据
intBuffer.flip();
printBufferInfo(intBuffer);
log.info("read: {}", intBuffer.get());
log.info("read: {}", intBuffer.get());
log.info("read: {}", intBuffer.get());
printBufferInfo(intBuffer);
while (intBuffer.position() < intBuffer.limit()) {
log.info("read(while): {}", intBuffer.get());
}
printBufferInfo(intBuffer);
System.out.println("----测试用rewind重新读数据----");
// 重新读取数据
intBuffer.rewind();
printBufferInfo(intBuffer);
while (intBuffer.position() < intBuffer.limit()) {
log.info("read(while): {}", intBuffer.get());
}
printBufferInfo(intBuffer);
System.out.println("----测试用rewind重新写数据----");
intBuffer.clear();
printBufferInfo(intBuffer);
// 写入数据
while (intBuffer.position() < intBuffer.limit()) {
intBuffer.put(intBuffer.position());
}
printBufferInfo(intBuffer);
// 重新写
intBuffer.rewind();
printBufferInfo(intBuffer);
// 重新写数据
while (intBuffer.position() < intBuffer.limit()) {
intBuffer.put(intBuffer.position()*10);
}
printBufferInfo(intBuffer);
intBuffer.flip();
while (intBuffer.position() < intBuffer.limit()) {
log.info("read(while): {}", intBuffer.get());
}
}
private void printBufferInfo(IntBuffer intBuffer) {
log.info("position = {}", intBuffer.position());
log.info("capacity = {}", intBuffer.capacity());
log.info("limit = {}", intBuffer.limit());
}
/**
* 使用compact压缩,可以在没读完的基础上接着写。
* 理解成:将「剩余未读的数据」往左"推"。
*/
@Test
public void t2() {
IntBuffer intBuffer = IntBuffer.allocate(10);
intBuffer.put(1);
intBuffer.put(2);
intBuffer.put(3);
intBuffer.put(4);
intBuffer.put(5);
intBuffer.put(6);
printBufferInfo(intBuffer);
intBuffer.flip();
// get(i)不会移动position
log.info("{}", intBuffer.get(2));
log.info("{}", intBuffer.get(1));
log.info("{}", intBuffer.get(0));
printBufferInfo(intBuffer);
// get方法才会移动position
log.info("{}", intBuffer.get());
log.info("{}", intBuffer.get());
log.info("{}", intBuffer.get());
printBufferInfo(intBuffer);
intBuffer.compact();
intBuffer.put(7);
intBuffer.put(8);
intBuffer.put(9);
intBuffer.put(10);
intBuffer.flip();
while (intBuffer.position() < intBuffer.limit()) {
log.info("read(while): {}", intBuffer.get());
}
}
/**
* mark和reset,前者记录position,后者重置position。
*/
@Test
public void t3() {
IntBuffer intBuffer = IntBuffer.allocate(10);
intBuffer.put(1);
intBuffer.put(2);
intBuffer.put(3);
intBuffer.put(4);
intBuffer.put(5);
intBuffer.put(6);
intBuffer.put(7);
intBuffer.put(8);
intBuffer.put(9);
intBuffer.put(10);
intBuffer.flip();
while (intBuffer.position() < intBuffer.limit()) {
int i = intBuffer.get(); // get会移动position
if (i == 6) {
intBuffer.mark();
}
log.info("{}", i);
}
printBufferInfo(intBuffer);
log.info("reset");
intBuffer.reset();
printBufferInfo(intBuffer);
while (intBuffer.position() < intBuffer.limit()) {
log.info("read: {}", intBuffer.get());
}
}
/**
* buffer.array()
*/
@Test
public void t4() {
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byteBuffer.put("12345".getBytes());
System.out.println(byteBuffer.position());
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.position()));
byteBuffer.put("abc".getBytes());
System.out.println(byteBuffer.position());
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.position()));
}
}
Channel
Channel,即通道,用于进行IO操作,可以用它读数据,也可以用它写数据。
有如下几种Channel:
-
ServerSocketChannel
对标ServerSocket,服务端用它监听连接、接受连接,创建SocketChannel进行网络通信
-
SocketChannel
对标Socket,使用TCP协议进行网络通信,相当于一个连接
-
DatagramChannel
使用UDP协议进行网络通信
-
FileChannel
用于文件的读写
FileChannel
作用: 用于读写文件
常用操作:
- 创建
- 读/写
- 关闭
- 强制刷盘
直接见case学习:
@Slf4j
public class FileChannelDemo {
/**
* 读文件,channel.read(buffer)
*/
@Test
public void t1() throws IOException {
// 会从classpath(类路径)下去读取资源
URL resource = this.getClass().getResource("/file/t1.txt");
log.info("path: {}", resource.getPath());
// 字节流
FileInputStream fileInputStream = new FileInputStream(resource.getPath());
// 将字节流装饰成字符流
InputStreamReader bufferedInputStream = new InputStreamReader(fileInputStream);
// 字符缓冲流
BufferedReader bufferedReader = new BufferedReader(bufferedInputStream);
// 如果读取了流,会影响FileChannel的读取
// log.info("read:{}", bufferedReader.readLine());
// log.info("read:{}", bufferedReader.readLine());
// log.info("read:{}", bufferedReader.readLine());
// log.info("read:{}", bufferedReader.readLine());
// log.info("read:{}", bufferedReader.readLine());
// log.info("read:{}", bufferedReader.readLine());
// log.info("read:{}", bufferedReader.readLine());
// log.info("read:{}", bufferedReader.readLine());
// bufferedReader.close();
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
log.info("first read: {}", fileChannel.read(byteBuffer));
printBufferInfo(byteBuffer);
// 处理读到的数据
byteBuffer.flip();
printBufferInfo(byteBuffer);
byte[] byteArray = new byte[byteBuffer.limit()];
while (byteBuffer.position() < byteBuffer.limit()) {
byteArray[byteBuffer.position()] = byteBuffer.get();
}
// 再次读取
byteBuffer.clear();
// 没有数据可读返回-1
log.info("second read: {}", fileChannel.read(byteBuffer));
System.out.println("FileChannel read:\n" + new String(byteArray));
// // 测试下用输入流获取的Channel进行写操作
// byteBuffer.clear();
// byteBuffer.put("测试一下".getBytes());
// byteBuffer.flip();
// fileChannel.write(byteBuffer); // 会报错:java.nio.channels.NonWritableChannelException
// fileChannel.close();
}
/**
* 写文件,channel.write(buffer)
*/
@Test
public void t2() throws IOException {
String s1 = "How are you?";
String s2 = "你好吗?";
String s3 = "I am fine,thanks.";
String s4 = "我很好,谢谢。";
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(s1.getBytes());
byteBuffer.put(s2.getBytes());
byteBuffer.put(s3.getBytes());
byteBuffer.put(s4.getBytes());
byteBuffer.flip();
URL resource = this.getClass().getResource("/file");
log.info("path: {}", resource.getPath());
FileOutputStream fileOutputStream = new FileOutputStream(resource.getPath() + "/t2.txt");
FileChannel fileChannel = fileOutputStream.getChannel();
int i = fileChannel.write(byteBuffer);
log.info("i={}", i);
fileChannel.close();
fileOutputStream.close();
}
/**
* 复制文件
*/
@Test
public void t3() throws IOException {
URL resourceDir = this.getClass().getResource("/file");
FileInputStream fileInputStream = new FileInputStream(resourceDir.getPath() + "/1-尚硅谷项目课程系列之Elasticsearch.pdf");
FileOutputStream fileOutputStream = new FileOutputStream(resourceDir.getPath() + "/1-尚硅谷项目课程系列之Elasticsearch(copy).pdf");
copyFile(fileInputStream, fileOutputStream);
}
private void copyFile(FileInputStream fileInputStream, FileOutputStream fileOutputStream) throws IOException {
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inChannel = fileInputStream.getChannel();
outChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readCount = 0;
while (inChannel.read(byteBuffer) != -1) {
readCount++;
byteBuffer.flip();
int outLength = outChannel.write(byteBuffer);
if (outLength != byteBuffer.capacity()) {
log.warn("not equals[{}],outLength={}" , readCount, outLength);
}
byteBuffer.clear();
}
log.info("final readCount={}", readCount);
} finally {
outChannel.close();
fileOutputStream.close();
inChannel.close();
fileInputStream.close();
}
}
/**
* 追加文件。
* 结论:
* 1. 写文件时,是生成新文件再写,还是在原来文件的基础上追加,是由FileOutputStream决定的(append构造器参数)。
* 2. 若FileOutputStream是「追加模式」,则fileChannel.write就是追加写。
*/
@Test
public void t4() throws IOException {
URL resource = this.getClass().getResource("/file/append.txt");
// 流设为追加模式
FileOutputStream fileOutputStream = new FileOutputStream(resource.getPath(), true);
FileChannel outChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("追加一第段\n".getBytes());
byteBuffer.put("追加二第段\n".getBytes());
byteBuffer.flip();
printBufferInfo(byteBuffer);
outChannel.write(byteBuffer);
outChannel.close();
fileOutputStream.close();
}
/**
* 测试FileChannel又读又写。
* 结果:
* 追加的内容又可以被读到。
*/
@Test
public void t5() throws IOException {
// 注意:是classpath类路径下的资源文件,不是编译前resource中的文件
URL resource = this.getClass().getResource("/file/readAndWrite.txt");
FileInputStream inputStream = new FileInputStream(resource.getPath());
// 从输入流获取的FileChannel不可写,只能读
FileChannel inChannel = inputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
inChannel.read(byteBuffer);
byteBuffer.flip();
printBufferInfo(byteBuffer);
byte[] bytes = new byte[byteBuffer.limit()];
while (byteBuffer.position() < byteBuffer.limit()) {
bytes[byteBuffer.position()] = byteBuffer.get();
}
System.out.println("first read:\n" + new String(bytes));
// FileOutputStream outputStream = new FileOutputStream(resource.getPath()); // 使用该构造器会先删除已存在的文件,再生成新文件
// FileChannel outChannel = outputStream.getChannel();
// 可追加
FileOutputStream outputStream = new FileOutputStream(resource.getPath(), true);
FileChannel outChannel = outputStream.getChannel();
byteBuffer.clear();
byteBuffer.put("写第一段话\n".getBytes());
byteBuffer.put("写第二段话\n".getBytes());
byteBuffer.put("写第三段话\n".getBytes());
byteBuffer.flip();
outChannel.write(byteBuffer);
outChannel.close();
outputStream.close();
byteBuffer.clear();
inChannel.read(byteBuffer);
byteBuffer.flip();
printBufferInfo(byteBuffer);
bytes = new byte[byteBuffer.limit()];
while (byteBuffer.position() < byteBuffer.limit()) {
bytes[byteBuffer.position()] = byteBuffer.get();
}
System.out.println("second read:\n" + new String(bytes));
}
/**
* 随机读,不顺序读。
* read的时候指定position
*/
@Test
public void t6() throws IOException {
URL resource = this.getClass().getResource("/file/randomRead.txt");
FileInputStream inputStream = new FileInputStream(resource.getPath());
FileChannel inChannel = inputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = inChannel.read(byteBuffer, "I like apple.\n".length());
log.info("read length:{}", read);
byteBuffer.flip();
printBufferInfo(byteBuffer);
byte[] bytes = new byte[byteBuffer.limit()];
while (byteBuffer.position() < byteBuffer.limit()) {
bytes[byteBuffer.position()] = byteBuffer.get();
}
System.out.println("first read:\n" + new String(bytes));
}
/**
* 随机写,不顺序写。
* 使用追加模式,且write的时候指定position。
* 结论:会覆盖旧内容,会将position这个位置以及之后length长度的内容更新。
*/
@Test
public void t7() throws IOException {
URL resource = this.getClass().getResource("/file/randomWrite.txt");
FileOutputStream outputStream = new FileOutputStream(resource.getPath(), true);
FileChannel fileChannel = outputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("abc".getBytes());
byteBuffer.flip();
int write = fileChannel.write(byteBuffer, 8);// 会覆盖写
log.info("write1 length:{}", write);
fileChannel.force(true); //强制刷盘
byteBuffer.clear();
byteBuffer.put("ABCDEF".getBytes());
byteBuffer.flip();
write = fileChannel.write(byteBuffer, 1); // 会覆盖写
log.info("write2 length:{}", write);
fileChannel.force(true);
byteBuffer.clear();
byteBuffer.put("ilikeapple.whataboutyou?yes,metoo.".getBytes());
byteBuffer.flip();
write = fileChannel.write(byteBuffer, 0); // 会覆盖写
log.info("write3 length:{}", write);
fileChannel.force(true);
fileChannel.close();
outputStream.close();
}
private void printBufferInfo(Buffer buffer) {
log.info("position = {}", buffer.position());
log.info("limit = {}", buffer.limit());
}
}
其他API:
- transferFrom
- transferTo
ServerSocketChannel和SocketChannel
作用:
- ServerSocketChannel:仅服务端使用,用于监听连接、接受连接
- SocketChannel:用于服务端和客户端通信,使用TCP协议
常用操作:
ServerSocketChannel:
- 创建
- 监听客户端连接
- 接受客户端连接
SocketChannel:
- 创建
- 请求连接服务端
- 设置是否阻塞
- 读/写
- 关闭
直接见case学习:
服务端程序:
@Slf4j
public class Server1 {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 9001));
// serverSocketChannel.configureBlocking(false);
SocketChannel socketChannel = serverSocketChannel.accept();
// 使用非阻塞模式时,若客户端没发送数据,socketChannel.read会返回0。为什么!?因为channel没关闭,不会返回-1,但又没读到数据,因此只能返回0。
// socketChannel.configureBlocking(false);
log.info("socketChannel: {}", socketChannel);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readLen = 0;
while ((readLen = socketChannel.read(byteBuffer)) != -1) {
log.info("--readLen:{}--", readLen);
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
while (byteBuffer.hasRemaining()) {
bytes[byteBuffer.position()] = byteBuffer.get();
}
log.info("receive: {}", new String(bytes));
byteBuffer.clear();
}
socketChannel.close();
log.info("server end");
}
}
客户端程序:
@Slf4j
public class Client1 {
public static void main(String[] args) throws IOException, InterruptedException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
boolean connect = socketChannel.connect(new InetSocketAddress("127.0.0.1", 9001));
System.out.println(connect);
while (!socketChannel.finishConnect()) {
}
System.out.println("connect success");
System.out.println("localAddress:" + socketChannel.getLocalAddress().toString());
System.out.println("remoteAddress:" + socketChannel.getRemoteAddress().toString());
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("hello world".getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
// socketChannel.shutdownOutput(); // 测试sleep之前shutdown
Thread.sleep(10000);
socketChannel.shutdownOutput();
socketChannel.close();
log.info("client end");
}
}
DatagramChannel
作用:使用UDP协议进行网络通信。
常用操作:
- 创建
- 绑定、监听端口号
- 设置是否阻塞
- 发送/接收数据
- 关闭
直接见case学习:
定义常量:
public interface Constant {
SocketAddress address1 = new InetSocketAddress("127.0.0.1", 9002);
}
服务端程序:
@Slf4j
public class Server1 {
public static void main(String[] args) throws IOException, InterruptedException {
DatagramChannel datagramChannel = DatagramChannel.open();
System.out.println("localAddress:" + datagramChannel.getLocalAddress());
System.out.println("remoteAddress:" + datagramChannel.getRemoteAddress());
// datagramChannel.configureBlocking(false);
DatagramChannel bindDatagramChannel = datagramChannel.bind(Constant.address1);
System.out.println(datagramChannel == bindDatagramChannel);
System.out.println("localAddress:" + datagramChannel.getLocalAddress().toString());
// System.out.println("remoteAddress:" + datagramChannel.getRemoteAddress().toString());// npe
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketAddress client = null;
System.out.println("start receive");
while ((client = datagramChannel.receive(byteBuffer)) != null) {
System.out.println(client.toString());
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
while (byteBuffer.position() < byteBuffer.limit()) {
bytes[byteBuffer.position()] = byteBuffer.get();
}
log.info("receive:{}", new String(bytes));
byteBuffer.clear();
}
log.info("server sleep");
Thread.sleep(10000);
log.info("server end");
}
}
客户端的程序:
@Slf4j
public class Client1 {
public static void main(String[] args) throws IOException, InterruptedException {
DatagramChannel datagramChannel = DatagramChannel.open();
System.out.println("localAddress:" + datagramChannel.getLocalAddress());
System.out.println("remoteAddress:" + datagramChannel.getRemoteAddress());
datagramChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("hello world, DatagramChanel".getBytes());
byteBuffer.flip();
log.info("start send");
int send = datagramChannel.send(byteBuffer, Constant.address1);
log.info("send: {}", send);
Thread.sleep(3000);
byteBuffer.clear();
byteBuffer.put("client end".getBytes());
byteBuffer.flip();
log.info("start send");
send = datagramChannel.send(byteBuffer, Constant.address1);
log.info("send: {}", send);
log.info("client sleep");
Thread.sleep(10000);
log.info("client end");
datagramChannel.close();
}
}
Selector
Selector,即选择器,是IO多路复用的实现,底层使用select、poll或epoll系统调用,具体用哪个取决于操作系统。
一个Selector可以管理多个Channel,监听多个Channel的IO事件。有了Selector,一个线程就可以使用一个Selector来处理多个网络连接了,不必像BIO那样一个线程处理一个连接。
常用操作
-
创建
-
将Channel注册到Selector,并指定感兴趣的IO事件
注册之后Selector就会监控Channel的IO事件。注意:不是所有的Channel都能被Selector监控,必须是继承了SelectableChannel类型的通道才行,如FileChannel就不行。
-
获取发生了指定IO事件的Channel
使用Selector的基本步骤
- 创建Selector
- 注册Channel
- 使用select方法获取发生了IO事件的Channel
- 然后处理发生了IO事件的Channel
代码案例
直接见case学习:
服务端程序:
@Slf4j
public class NioDiscardServer {
/**
* serverSocket注册到selector
* serverSocket接收连接,并连接成功的socket注册到selector
* @param args
*/
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("localhost", 9800));
log.info("serverSocketChannel.validOps(): {}", serverSocketChannel.validOps());
Selector selector = Selector.open();
// 若Channel没有设置成非阻塞,则注册时会报错:java.nio.channels.IllegalBlockingModeException
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 使用selector监听channel的IO事件
while (true) {
/**
* 为什么客户端关闭了select返回值一直大于0,且selectionKey对象和上次select结果相同!!?
* 因为客户端程序的socketChannel关闭了,但服务端这边的socketChannel还没close(期待服务端这边也close),select就会一直认为有IO事件就绪(即已关闭事件,属于读就绪),
* 只要服务端这边socketChannel也close即可!!!
*/
int select = selector.select();
if (select <= 0) {
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
log.info("select:{}, selectionKeys.size:{}, selectionKeys:{}", select, selectionKeys.size(), selectionKeys);
Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
while (selectionKeyIterator.hasNext()) {
SelectionKey selectionKey = selectionKeyIterator.next();
if (selectionKey.isAcceptable()) {
log.info("--isAcceptable--");
log.info("【isAcceptable】serverSocketChannel == selectionKey.channel(): {}", serverSocketChannel == selectionKey.channel());
SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
// 因为是非阻塞模式,accept会立即返回,可能返回null
if (socketChannel == null) {
log.warn("socketChannel == null");
continue;
}
socketChannel.configureBlocking(false);
// 若Channel没有设置成非阻塞,则注册时会报错:java.nio.channels.IllegalBlockingModeException
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
log.info("--isReadable--");
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
int readLen = 0;
while (true) {
readLen = socketChannel.read(byteBuffer);
if (readLen <= 0) {
log.info("readLen: {}", readLen);
if (readLen == -1) {
// 需要关闭channel,否则会一直监听到可读事件
socketChannel.shutdownInput();
socketChannel.close();
}
break;
}
log.info("------read socketChannel:{}----", socketChannel);
log.info(" content:{}", new String(readByteBuffer(byteBuffer)));
}
} else {
log.error("------error-----");
}
// 处理完需要remove,否则下次select后会重复处理这个selectionKey
selectionKeyIterator.remove();
}
}
}
public static byte[] readByteBuffer(ByteBuffer byteBuffer) {
byteBuffer.flip();
byte[] result = new byte[byteBuffer.limit()];
while (byteBuffer.hasRemaining()) {
result[byteBuffer.position()] = byteBuffer.get();
}
byteBuffer.clear();
return result;
}
}
客户端程序:
@Slf4j
public class NioDiscardClient {
public static List<SocketChannel> socketChannelList = new ArrayList<>();
public static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
/**
* 连接server,发送数据
* @param args
*/
public static void main(String[] args) throws IOException, InterruptedException {
// for (int i = 0; i < 2; i++) { // 测试多个客户端
startClient();
// }
while (true){
TimeUnit.SECONDS.sleep(3);
}
}
public static void startClient() throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("localhost", 9800));
while (!socketChannel.finishConnect()) {}
log.info("--connected socketChannel:{}---", socketChannel);
int i = 100;
while (i-- > 0) {
// debug试试每次发送会发生啥
write(socketChannel, "data" + i);
}
socketChannel.shutdownOutput();
socketChannel.close();
log.info("---close socketChannel:{}---", socketChannel);
// socketChannelList.add(socketChannel);
}
public static void write(SocketChannel socketChannel, String msg) throws IOException {
byteBuffer.clear();
byteBuffer.put(msg.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
}
SelectionKey详解
Channel注册到Selector中的时候就会生成一个SelectionKey对象,并添加到Selector的keys集合中(可看源码)。SelectionKey对象封装了Selector、Channel和感兴趣的IO事件,如下:
Selector会对keys集合中的每个SelectionKey进行监听:其实就是监听Channel是否发生了感兴趣的IO事件,若监听到了,则会把该SelectionKey添加到Selector的selectedKeys集合(称为就绪集)中。
疑问
为啥在处理完SelectionKey后需要将其从Selector的selectedKeys集合(即就绪集)中remove删掉? 如下:
while (true) {
int select = selector.select();
if (select <= 0) {
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
while (selectionKeyIterator.hasNext()) {
SelectionKey selectionKey = selectionKeyIterator.next();
if (selectionKey.isAcceptable()) {
......
} else if (selectionKey.isReadable()) {
......
} else {
......
}
// 此处要remove删除已处理的selectionKey
selectionKeyIterator.remove();
}
}
原因:
如果不删除,则下次selector.selectedKeys();获取到的selectedKeys就绪集还会包含该selectionKey,就会被重复处理。
查看源码:
查看源码可发现,Selector每次select的结果都是追加到selectedKeys就绪集中:
所以,如果处理完一个SelectionKey后不删除的话,会一直保留在selectedKeys集合中。
如何理解Java NIO是面向缓冲区的?
带着问题学习
-
使用Selector时为啥Channel必须是非阻塞的?不设置成非阻塞会咋样?
结论:Channel不设置成非阻塞的话,那注册到Selector时会报错:java.nio.channels.IllegalBlockingModeException
-
是否要服务端程序调用accept方法才完成三次握手?还是说客户端程序调用connect方法请求连接时就完成三次握手了?
结论:使用Wireshark抓包发现,客户端程序调用connect方法就会完成三次握手,无需等服务端程序调用accept方法。
三次握手和四次挥手
回顾下三次握手和四次挥手:
使用Wireshark抓包,发现客户端程序调用connect方法请求连接时就会完成3次握手,而不用等服务端程序调用accept方法。
客户端程序调用
connect方法就会完成三次握手、建立连接,那恶意客户端如果疯狂connect岂不是很容易导致服务端连接被占满?答:这就需要服务端自己采取安全措施了,比如:对来自黑名单的IP直接关闭连接。
图中,端口号60580是客户端程序,端口号9001是服务端程序
当客户端程序调用close方法就会开始四次挥手,客户端会发送FIN报文,然后服务端程序调用close方法后也会发送FIN报文,之后四次挥手过程结束。