NIO
什么是NIO
IO回顾
- IO:Input Output(输入 输出)
- IO技术的作用:解决设备和设备之间的数据传输问题
- IO的应用场景:图片上传、下载、打印机打印信息表、解析XML…
概念
- 即Java New IO
- 是1个全新的、JDK1.4后提供的IO API
- Java API中提供了两套NIO,一套是针对标准输入输出NIO(即文件的读写),另一套就是网络编程NIO
作用
- NIO和IO有相同的作用和目的,但实现方式不同
- 可替代标准Java IO的IO API
- IO是以流的方式处理数据,而NIO是以块的方式处理数据
流与块的比较
- NIO和IO最大的区别是数据打包和传输方式
- IO是以流的方式处理数据,而NIO是以块的方式处理数据
面向流的IO一次一个字节地处理数据,一个输入流产生一个字节,一个输出流就消费一个字节
面向块的IO系统以块的形式处理数据,每一个操作都在一步中产生或消费一个数据块。按块要比按流快得多
(举例:拿水龙头来比喻,流就像水龙头滴水,每次只有一滴;块就像水龙头往水壶里放水,放满之后对一整个水壶的水进行操作)
新特性
对比于Java IO,NIO具备的新特性如下:
阻塞与非阻塞:当进程执行时,需要的数据还未就绪时,是否处在一个等待状态?等待–>阻塞,非等待–>非阻塞例:(1)打电话给书店老板,询问有没有NIO这本书,书店老板去查询图书(2)刷会抖音–>非阻塞;干等着–>阻塞
- 可简单地认为:IO是面向流地处理,NIO是面向块(缓冲区)的处理
- 面向流的I/O系统一次一个字节地处理数据
- 一个面向块(缓冲区)的IO系统以块的形式处理数据
核心组件
Java NIO的核心组件包括:
- 通道(Channel)
- 缓冲区(Buffer)
- 选择器(Selector)
在NIO中并不是以流的方式来处理数据的,而是以buffer缓冲区和Channel管道配合使用来处理数据
Selector是因为NIO可以使用异步的非阻塞模式才加入的东西
简单理解一下:
- Channel管道比作铁路,buffer缓冲区比作铁路上的火车(运载着货物,即数据)
而我们的NIO就是通过Channel管道运输着存储数据的Buffer缓冲区来实现数据的处理! - 要时刻记住:Channel不与数据打交道,它只负责运输数据,与数据打交道的是Buffer缓冲区
- Channel–>运输
- Buffer–>数据
相对于传统IO而言,流是单向的。对于NIO而言,有了Channel管道这个概念,我们的读写都是双向的(铁路Channel上的火车Buffer能运送货物Data从广州去北京,自然就能通过同一条铁路Channel从北京返还到广州)
Buffer缓冲区
Buffer缓冲区概述
作用:缓冲区,用来存放具体要被传输的数据,比如文件、scoket 等。这里将数据装入 Buffer 再通过通道进行传输。
Buffer 就是一个数组,用来保存不同数据类型的数据
在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是 ByteBuffer,对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应,它们之间的继承关系如下图所示
- ByteBuffer:存储字节数据到缓冲区
- ShortBuffer:存储字符串数据到缓冲区
- CharBuffer:存储字符数据到缓冲区
- IntBuffer:存储整数数据到缓冲区
- LongBuffer:存储长整型数据到缓冲区
- DoubleBuffer:存储小数到缓冲区
- FloatBuffer:存储小数到缓冲区
对于Java中的基本数据类型,都有一个Buffer类型与之相对应,最常用的自然是ByteBuffer类(二进制数据)
ByteBuffer的创建方式
三种创建方式
- 在堆中创建缓冲区:
static allocate(int capacity)
<–重点使用此方式
capacity表示缓冲区容量 - 在系统内存中创建缓冲区:
static allocateDirect(int capacity)
- 通过普通数组创建缓冲区:
static wrap(byte[] arr)
代码演示
package com.lagou.nio.buffer;
import java.nio.ByteBuffer;
/**
* 演示ByteBuffer创建的三种方式
*/
public class BufferDemo01 {
public static void main(String[] args) {
//第一种创建方式-->在堆中创建缓冲区:allocate(int capacity)-->一般使用此方式
ByteBuffer buffer = ByteBuffer.allocate(10);
//第二种创建方式-->在系统内存中创建缓冲区:allocateDirect(int capacity)
ByteBuffer buffer1 = ByteBuffer.allocateDirect(10);
//第三种创建方式-->通过普通数组创建缓冲区:wrap(byte[] arr)
byte[] arr = {97, 98, 99};
ByteBuffer buffer2 = ByteBuffer.wrap(arr);
}
}
常用方法
拿到一个缓冲区我们往往会做什么?很简单,就是读取缓冲区的数据/写数据到缓冲区中。
所以,缓冲区的核心方法就是:
- put(byte b) : 给数组(即缓冲区)添加元素
- get() :获取一个元素
代码演示
package com.lagou.nio.buffer;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class BufferDemo02 {
public static void main(String[] args) {
//1、创建对象
ByteBuffer buffer = ByteBuffer.allocate(10);
//2、使用put()方法给缓冲区添加元素
buffer.put((byte) 10); //方法中要byte类型实参,因此要强转类型
buffer.put((byte) 20);
buffer.put((byte) 30);
//3、打印
byte[] array = buffer.array(); //Arrays.toString()函数中参数需要是数组,因此转换一下
System.out.println(Arrays.toString(array));
//4、使用get()方法获取缓冲区指定索引位置的元素,从0开始
byte b = buffer.get(1);
System.out.println("索引位置1的元素是:" + b); //20
}
}
Buffer类维护了4个核心变量属性来提供关于其所包含的数组的信息。它们是:
-
容量Capacity
-
- 缓冲区能够容纳的数据元素的最大数量。容量在缓冲区创建时被设定,并且永远不能被改变。(不能被改变的原因也很简单,底层是数组嘛)
-
界限Limit
-
- 缓冲区中可以操作数据的大小,代表了当前缓冲区中一共有多少数据(从limit开始后面的位置不能操作)。
-
位置Position
-
-
下一个要被读或写的元素的位置。Position会自动由相应的
get( )
和put( )
函数更新。 -
以上三个属性值之间有一些相对大小的关系:0 <= position <= limit <= capacity 例:- 如果我们创建一个新的容量大小为20 的 ByteBuffer 对象,在初始化的时候,position 设置为 0, limit 和 capacity 被设置为 10,在以后使用 ByteBuffer对象过程中,capacity 的值不会再发生变化,而其它两个个将会随着使用而变化。
-
-
标记Mark
- 一个备忘位置。用于记录上一次读写的位置(即position的值)
- 使用mark()方法可以将mark值标记为上一次读写位置(即position位置)
- 使用reset()方法可以将position定位到mark位置,即上一次读写的位置
ByteBuffer写模式和读模式下4个核心变量的变化
首先展示一下是创建缓冲区后,核心变量的值是怎么变化的
public static void main(String[] args) {
// 创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 看一下初始时4个核心变量的值
System.out.println("初始时-->limit--->"+byteBuffer.limit());
System.out.println("初始时-->position--->"+byteBuffer.position());
System.out.println("初始时-->capacity--->"+byteBuffer.capacity());
System.out.println("初始时-->mark--->" + byteBuffer.mark());
System.out.println("--------------------------------------");
// 添加一些数据到缓冲区中
String s = "JavaEE";
byteBuffer.put(s.getBytes());
// 看一下初始时4个核心变量的值
System.out.println("put完之后-->limit--->"+byteBuffer.limit());
System.out.println("put完之后-->position--->"+byteBuffer.position());
System.out.println("put完之后-->capacity--->"+byteBuffer.capacity());
System.out.println("put完之后-->mark--->" + byteBuffer.mark());
}
运行结果如下:
现在我想要从缓存区拿数据,怎么拿呀??NIO给了我们一个flip()
方法。这个方法可以改动position和limit的位置!
还是上面的代码,我们执行byteBuffer.flip()
语句后,再看看4个核心属性的值会发生什么变化:
很明显的是:
- limit变成了position的位置了
- 而position变成了0
看到这里的同学可能就会想到了:当调用完filp()
时:limit是限制读到哪里,而position是从哪里读,此时我们就实现从头读到目前有数据地方的所有内容了
因此一般我们称filp()
为 “切换成读模式”
- 每当要从缓存区的时候读取数据时,就调用
filp()
“切换成读模式”
切换成读模式之后,我们就可以读取缓冲区的数据了:
// 创建一个limit()大小的字节数组(因为就只有limit这么多个数据可读)
byte[] bytes = new byte[byteBuffer.limit()];
// 将读取的数据装进我们的字节数组中
byteBuffer.get(bytes);
// 输出数据
System.out.println(new String(bytes, 0, bytes.length)); //输出读取到的"JavaEE"
随后输出一下核心变量的值看看:
读完我们还想写数据到缓冲区,那就使用clear()
函数,这个函数会“清空”缓冲区:
- 注意:数据没有真正被清空,只是被遗忘掉了(即又变回到写模式了,要读的话读不出来,只能读position位置即首位置上的元素)
全过程如图所示:
Channel通道
Channel通道概述
Channel 表示 IO 源与目标打开的连接(是双向的)。 Channel 类似于传统的“流”。
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中(白话: 就是数据传输用的通道,作用是打开到IO设备的连接,文件、套接字都行)
例:相当于一根管子,buffer中的数据可以通过管子写入被操作的资源当中,也可以将资源通过管子写入到buffer中去
Channel API
通道(Channel):由 java.nio.channels 包定义的。
Java 为 Channel 接口提供的最主要实现类如下:
- FileChannel:用于读取、写入、映射和操作文件的通道(用于本地资源处理)
- DatagramChannel:通过 UDP 读写网络中的数据通道(用于网络资源处理,UDP)
- SocketChannel:通过 TCP 读写网络中的数据(用于网络资源处理,TCP)
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来 的连接都会创建一个 SocketChannel(用于网络资源处理服务器端)
FileChannel基本使用
-
需求:使用FileChannel结合buffer缓冲区完成文件的复制
package com.lagou.nio.buffer; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class FileChannelCopy { /** * 使用FileChannel和ByteBuffer完成文件的复制 * @param args */ public static void main(String[] args) { FileInputStream fileInputStream = null; FileOutputStream fileOutputStream = null; try { //1、创建输入输出流(依赖于IO流获取channel) fileInputStream = new FileInputStream("E:\\FilesForTest\\LaGou_Java\\NIO\\a.jpg"); fileOutputStream = new FileOutputStream("E:\\Code\\LaGou\\javase\\src\\com\\lagou\\nio\\复制.jpg"); //2、通过IO流获取Channel通道 FileChannel channelIn = fileInputStream.getChannel(); FileChannel channelOut = fileOutputStream.getChannel(); //3、创建缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //4、循环读写 while (channelIn.read(buffer) != -1) { //4-1、读完之后要filp()切成读模式,让之后的channelOut.write()可以从buffer头部读取到所有数据并“卸货” buffer.flip(); //4-2、到达目的地,“火车”buffer先读数据后把读到的数据“卸货” channelOut.write(buffer); //4-3、卸货完后“火车”要”清空“,以便回去接着read()”装货“ buffer.clear(); } } catch (IOException e) { e.printStackTrace(); } finally { try { //5、关流 fileInputStream.close(); fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
-
一定要注意理解循环中先filp()方便目的地”从头卸货“,之后到达目的地channelOut.write(buffer)从buffer上卸货,最后再clear()清空buffer以便下一次”装货“
网路编程收发信息
BIO:同步阻塞式IO(之前学习的网络编程即是BIO的),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
-
客户端
package com.lagou.nio.channel; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class NIOClient { public static void main(String[] args) { /** * 以下为BIO的客户端代码以作对比: * Socket clientSocket = new Socket("127.0.0.1", 9999); * OutputStream outputStream = clientSocket.getOutputStream(); * outputStream.write("你好呀".getBytes()); * outputStream.close(); * clientSocket.close(); */ //NIO模式下网络编程 SocketChannel socketChannel = null; try { //1、创建对象:使用静态的SocketChannel.open()方法创建SocketChannel对象后调用connect()方法连接到服务器端 socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999)); //2、创建缓冲区数组并往缓冲区上“装货” ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("你好呀".getBytes()); //注意:在向服务器端write()“运货卸货”之前,要先将position和limit变量切换为读模式,否则服务器端会接收到数据之后的内容--什么也没有 byteBuffer.flip(); //3、输出数据:在创建的channel上“运货卸货” socketChannel.write(byteBuffer); } catch (IOException e) { e.printStackTrace(); } finally { //4、关闭通道 try { socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } } }
-
服务器端
package com.lagou.nio.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 NIOServer { public static void main(String[] args) { /** * 以下为BIO的服务器端代码以作对比: * Socket serverSocket = new Socket(9999); * Socket socket = serverSocket.accept(); * InputStream inputStream = socket.getInputStream(); * byte[] bytes = new byte[1024]; * int len = inputStream.read(bytes); * System.out.println(new String(bytes, 0, len)); * socket.close(); */ ServerSocketChannel serverSocketChannel = null; try { //1、使用静态方法ServerSocketChannel.open()方法创建服务器对象,监听对应的端口 serverSocketChannel = ServerSocketChannel.open(); //绑定要监听的端口 serverSocketChannel.bind(new InetSocketAddress(9999)); //2、连接客户端 SocketChannel socketChannel = serverSocketChannel.accept(); //注意如果只有此行代码,则此时还是阻塞的 //3、读取数据 //准备缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //4、读取到的字节长度-->通道上“卸货”,装入自家的“运货叉车”byteBuffer int len = socketChannel.read(byteBuffer); //5、打印-->取下自家缓冲区内的数据打印出来 System.out.println(new String(byteBuffer.array(), 0, len)); } catch (IOException e) { e.printStackTrace(); } finally { } } }
accept()阻塞问题
-
服务器端
package com.lagou.nio.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 NIOServer { public static void main(String[] args) { /** * 以下为BIO的服务器端代码以作对比: * Socket serverSocket = new Socket(9999); * Socket socket = serverSocket.accept(); * InputStream inputStream = socket.getInputStream(); * byte[] bytes = new byte[1024]; * int len = inputStream.read(bytes); * System.out.println(new String(bytes, 0, len)); * socket.close(); */ ServerSocketChannel serverSocketChannel = null; try { //1、使用静态方法ServerSocketChannel.open()方法创建服务器对象,监听对应的端口 serverSocketChannel = ServerSocketChannel.open(); //绑定要监听的端口 serverSocketChannel.bind(new InetSocketAddress(9999)); //⭐设置为非阻塞连接 serverSocketChannel.configureBlocking(false); while (true) { //2、连接客户端 SocketChannel socketChannel = serverSocketChannel.accept(); //注意如果只有此行代码,则此时还是阻塞的 //若连接上了客户端-->socketChannel != null时,与客户端通信 if (socketChannel != null) { //3、读取数据 //准备缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //4、读取到的字节长度-->通道上“卸货”,装入自家的“运货叉车”byteBuffer int len = socketChannel.read(byteBuffer); //5、打印-->取下自家缓冲区内的数据打印出来 System.out.println(new String(byteBuffer.array(), 0, len)); break; } else { //没有连接到服务器的客户端,做其他的事 System.out.println("做一些别的事"); Thread.sleep(2000); } } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } finally { } } }
Selector选择器(多路复用器)
多路复用的概念
一个选择器可以同时监听多个服务器端口, 帮多个服务器端口同时等待客户端的访问
Selector和Channel的关系
Channel和Buffer比较好理解 ,联系也比较密切,他们的关系简单来说就是:数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中
选择器和他们之间的关系又是什么?
选择器(Selector) 是 Channel(通道)的多路复用器,Selector 可以同时监控多个通道的 IO(输入输出) 状况(可以将多个channel注册到selector上)
Selector的作用是什么?
Selector的作用总的来说是:负责监听事件和选择事件的对应通道
选择器提供选择执行已经就绪的任务的能力。从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道,减少开销。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。
可选择通道(SelectableChannel)
注意:并不是所有的Channel,都是可以被Selector 复用的。比方说,FileChannel就不能被选择器复用。为什么呢?
判断一个Channel 能被Selector 复用,有一个前提:判断他是否继承了一个抽象类SelectableChannel。如果继承了SelectableChannel,则可以被复用,否则不能
SelectableChannel 的结构如下图:
SelectableChannel类提供了实现通道的可选择性所需要的公共方法
通道和选择器注册之后,他们是绑定的关系吗?
答:不是。不是一对一的关系。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
通道和选择器之间的关系,使用注册的方式完成。SelectableChannel可以被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的。
Channel注册到Selector
使用Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器时。
第一个参数:指定通道要注册的选择器是谁
第二个参数:指定选择器需要查询的通道操作
可以供选择器查询的通道操作,从类型来分,包括以下四种:
(1)可读 : SelectionKey.OP_READ
(2)可写 : SelectionKey.OP_WRITE
(3)连接 : SelectionKey.OP_CONNECT
(4)接收 : SelectionKey.OP_ACCEPT
如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
选择键(SelectionKey)
Channel和Selector的关系确定好后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。这个工作,使用选择器Selector的select()方法完成。select方法的作用,对感兴趣的通道操作,进行就绪状态的查询。
Selector可以不断的查询Channel中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是Selector感兴趣的操作,就会被Selector选中,放入选择键集合中。
select() :选择器等待客户端连接的方法
阻塞问题:
1.在开始没有客户访问的时候是阻塞的
2.在有客户来访问的时候方法会变成非阻塞的
3.如果客户的访问被处理结束之后,又会恢复成阻塞的
selectedKeys() :选择器会把被连接的服务端对象放在Set集合中,这个方法就是返回一个Set集合
Selector的使用流程
step1、创建Selector
Selector对象是通过调用静态工厂方法open()来实例化的,如下:
// 1、获取Selector选择器
Selector selector = Selector.open();
step2、将Channel注册到Selector
要实现Selector管理Channel,需要将Channel注册到相应的Selector上,如下:
// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));
// 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
上面通过调用通道的register()方法会将它注册到一个选择器上。
首先需要注意的是:
与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException
step3、轮询查询就绪操作
万事俱备,下一步是查询就绪的操作。
通过Selector的 select() 方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是SelectionKey对象的Set集合中。
select()方法返回的int值,表示有多少通道已经就绪
而一旦调用select()方法,并且返回值不为0时,下一步干啥?
通过调用Selector的selectedKeys()方法来访问已选择键集合,然后迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作
具体使用举例
-
客户端:同上《网络编程收发信息》客户端代码举例
-
服务器端
package com.lagou.nio.selector; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set; public class SelectorServer { /** * 以下实现NIO中服务器端多路复用代码-->能使用一个线程与套接字为9999\8888\7777的客户端进行通信 * @param args * @throws IOException */ public static void main(String[] args) throws IOException { //小目标:实现通道注册到选择器上 //1、获取selector选择器 Selector selector = Selector.open(); //2、获取通道 ServerSocketChannel serverSocketChannel1 = ServerSocketChannel.open(); ServerSocketChannel serverSocketChannel2 = ServerSocketChannel.open(); ServerSocketChannel serverSocketChannel3 = ServerSocketChannel.open(); serverSocketChannel1.bind(new InetSocketAddress(9999)); //绑定端口号 serverSocketChannel2.bind(new InetSocketAddress(8888)); //绑定端口号 serverSocketChannel3.bind(new InetSocketAddress(7777)); //绑定端口号 //3、将通道设置为非阻塞(重要:与selector一起使用时,channel必须要处在非阻塞模式下,如果是阻塞的,会抛出异常) serverSocketChannel1.configureBlocking(false); serverSocketChannel2.configureBlocking(false); serverSocketChannel3.configureBlocking(false); //4、将通道注册到选择器上。指定监听时间为”接收“事件 serverSocketChannel1.register(selector, SelectionKey.OP_ACCEPT); serverSocketChannel2.register(selector, SelectionKey.OP_ACCEPT); serverSocketChannel3.register(selector, SelectionKey.OP_ACCEPT); //select():查询已经准备就绪的通道操作 返回值:表示有多少通道已经就绪 //阻塞:阻塞到至少有一个通道上的事件就绪了 //5、采用轮询的方式,查询准备就绪的事件 while (selector.select() > 0) { //6、集合中就是所有准备就绪的操作集合 Set<SelectionKey> keySet = selector.selectedKeys(); Iterator<SelectionKey> selectionKeys = keySet.iterator(); while (selectionKeys.hasNext()) { //7、获取已经”准备就绪“的事件 SelectionKey selectionKey = selectionKeys.next(); /*//8、判断事件的类型后可以根据类型做出对应操作---这里是Accept类型 if (selectionKey.isAcceptable()) { } else if (selectionKey.isConnectable()) { }*/ //8、获取ServerSocketChannel ServerSocketChannel severSocketChannel = (ServerSocketChannel) selectionKey.channel(); //9、接收客户端发来的数据 SocketChannel socketChannel = severSocketChannel.accept(); //10、读取数据 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int len = 0; while ((len = socketChannel.read(byteBuffer)) != -1) { byteBuffer.flip(); //切换成读模式 System.out.println(new String(byteBuffer.array(), 0, len)); byteBuffer.clear(); //清空,接着装下一批数据 } //11、关闭资源 socketChannel.close(); } //12、移除选择键 selectionKeys.remove(); } //13、关闭连接 serverSocketChannel1.close(); serverSocketChannel2.close(); serverSocketChannel3.close(); } }