一、概述
从JDK1.4开始,Java提供了一系列改进I/O的新功能,这些新功能统称为NIO。新增的I/O类保存在java.nio包下。
- NIO与传统IO的区别?
传统IO | NIO |
---|---|
面向流 | 面向缓冲 |
阻塞IO | 非阻塞IO |
- | 选择器 |
-
面向流和面向缓冲
Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。面向流就意味着每次只能够从流中读取一个或多个字节,直到读取完所有字节为止,它们并没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。因此,面向流的IO系统效率一般不高。而NIO采用内存映射文件的处理方式将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件。通常这种方式来处理IO操作比传统方式的效率高得多。所有与缓冲相关的类都保存在java.nio包下。 -
阻塞和非阻塞
1)传统IO会阻塞线程的执行。比如InputStream的read方法。当调用read方法的读取用户输入数据的时候,如果没有读取到有效数据,程序将在此处阻塞线程执行。在这期间,线程不能够做其他事情。
2)Java NIO是非阻塞的,当一个线程从通道中发送请求读取数据的时候,如果还没有可读数据,该线程不会保持阻塞状态,可以继续做其他事情。 -
选择器(Selectors)
NIO新增了选择器的概念,它允许一个单独线程来监视多个输入通道(Channel)。用户可以在注册多个通道的时候使用一个选择器,然后再使用一个单独线程来管理通道。所有NIO中有关channel和selector的类都保存在java.nio.channels包下。
Channel和Buffer是NIO中的两个核心类。Channel是对传统IO系统的模拟,NIO中所有数据都是通过通道进行传输。Buffer可以理解为容器,它的本质是一个数组。发送到Channel中的数据都必须先存放在Buffer中。从Channel中读取的数据也必须先放在Buffer中。
二、使用Buffer
Buffer就像一个数组,它的主要作用是装入数据,然后输出数据。Buffer有三个重要概念:
容量(Capacity):表示Buffer最多可以存储多少数据,容量的大小不可以被修改。
界限(limit):第一个不应该被读出或写入Buffer的索引位置。在limit后的数据既不可读,也不可能写。
位置(Position):下一个可以被读出或写入Buffer的位置索引。当新建一个Buffer对象时,position默认为0。如果从Channel中读取n个数据后,position的值为n。也就是说,position的值刚好等于已经读取到了多少数据。
上图显示Buffer读入一些数据后的示意图。其中,mark代表标记,允许将position定位到mark位置。这些值应该满足下面关系:
0 ≤ mark ≤ position ≤ limit ≤ capacity
Buffer是一个抽象类,对应于基本数据类型,都有相应的Buffer类:ByteBuffer、CharBuffer、IntBuffer、LongBuffer等等。Buffer类提供了allocate方法创建Buffer对象。
static XxxBuffer allocate(int capacity)
例如:创建一个容量为8的CharBuffer对象。
CharBuffer buff = CharBuffer.allocate(8);
Buffer类中包含两个重要方法:
- flip():将limit设置为position所在位置,并将position设置为0。也就意味着,调用该方法实际上就是为Buffer输出数据做准备。
- clear():将position设置为0,将limit设置为capacity所在位置。这样为再次向Buffer写入数据做准备。
注意:对Buffer执行clear方法后,该Buffer对象里的数据依然存在。
除此以外,Buffer的所有子类还提供了两个重要方法:
- put():向Buffer中写入数据。
- get():从Buffer中取出数据,可以是一个一个取出数据,也可以是批量取数据。
示例代码:
public class Demo01 {
public static void main(String[] args) {
// 创建一个容量为8的Buffer对象
CharBuffer buff = CharBuffer.allocate(8);
// 向buff写入数据
buff.put('a');
buff.put('b');
buff.put('c');
System.out.println("position = " + buff.position()); // 3
// 准备读数据
buff.flip();
System.out.println("position = " + buff.position()); // 0
System.out.println("limit = " + buff.limit()); // 3
// 取出第一个数据
char data = buff.get();
System.out.println("取出第一个数据:" + data);
System.out.println("position = " + buff.position()); // 1
// 准备重新写数据
buff.clear();
System.out.println("position = " + buff.position()); // 0
System.out.println("limit = " + buff.limit()); // 8
// 取出索引为2的元素
data = buff.get(2);
System.out.println("取出索引为2的数据:" + data);
System.out.println("position = " + buff.position()); // 0
}
}
从面代码最后取出索引为2的元素,因为采用的是根据索引来取值的方式,所以不会改变position的值。
三、使用Channel
Channel:数据通道,用于I/O操作设备之间的连接,不能存放数据。使用Channel读写数据需要结合Buffer一起使用。如下图:
如果要从Channel中读取数据,就先要把数据从Buffer读取到Channel中。从Channel中写数据也一样,也要先把数据写入到Buffer中。
Channel是一个接口,NIO提供了一些Channel的实现,如FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel等。
3.1 使用FileChannel读数据
FileChannel用于读写、映射和操作文件的通道。可以通过调用FileInputStream或FileOutputStream对象的getChannel方法获取该对象。
使用FileChannel读数据的步骤:
第一步:创建文件输入流对象;
第二步:调用getChannel方法获取Channel对象;
第三步:创建Buffer;
第四步:调用Channel对象的read方法,从Channel中读取数据到Buffer;
第五步:调用Buffer对象的get方法读取数据;
第六步:关闭流;
public class Demo01 {
public static void main(String[] args) throws Exception {
// 创建文件输入流对象
FileInputStream fis = new FileInputStream("aa.txt");
// 获取Channel
FileChannel inChannel = fis.getChannel();
// 创建Buffer
ByteBuffer buf = ByteBuffer.allocate(48);
// 从Channel读取数据到Buffer,该方法返回读取到的字节数据
int len = -1;
while ((len = inChannel.read(buf)) != -1) {
// 将limit设置为position所在位置,并将position设置为0
buf.flip();
// 判断position和limit之间是否还有未读数据
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
// 将position设置为0,将limit设置为capacity所在位置
buf.clear();
}
// 关闭资源
fis.close();
}
}
3.2 使用FileChannel写数据
第一步:创建文件输出流对象;
第二步:调用getChannel方法获取Channel对象;
第三步:创建Buffer;
第四步:调用put方法把数据写入Buffer;
第五步:调泳Channel对象的write方法写入Buffer数据;
第六步:关闭流;
public class Demo01 {
public static void main(String[] args) throws Exception {
// 创建文件输入流对象
FileOutputStream fout = new FileOutputStream("aa.txt", true);
// 获取Channel
FileChannel channel = fout.getChannel();
// 创建Buffer
ByteBuffer cbuf = ByteBuffer.allocate(48);
// 从Channel读取数据到Buffer,该方法返回读取到的字节数据
cbuf.put("广州ETC\r\n".getBytes());
// 准备输出Buffer数据
cbuf.flip();
// 把Buffer数据写入Channel
channel.write(cbuf);
// 关闭资源
fout.close();
}
}
3.3 案例:使用Channel实现文件复制
第一步:创建文件输入通道;
第二步:创建文件输出通道;
第三步:把文件输入通道数据映射到Buffer中;
第四步:向文件输出通道写入Buffer数据;
public class Demo01 {
public static void main(String[] args) throws Exception {
// 获取文件输入输出通道
FileChannel inChannel = new FileInputStream("aa.txt").getChannel();
FileChannel outChannel = new FileOutputStream("aa2.txt").getChannel();
// 把文件输入通道数据全部映射到ByteBuffer
MappedByteBuffer buf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
// 向文件输出通道写入buf数据
outChannel.write(buf);
// 准备再次接收数据
buf.clear();
}
}
四、Charset
JDK1.4提供了Charset来处理字节序列(ByteBuff)和字符序列(CharBuff)之间的转换。除此以外,该类还包含了创建解码器和编码器的方法,还提供了获取Charset所支持字符集的方法。
// 获取Java支持的全部字符集
SortMap<String, Charset> charsetMap = Charset.availableCharsets();
Charset对象的常用方法:
方法名 | 作用 |
---|---|
forName(String charset) | 静态方法,指定字符集创建Charset对象 |
newEncoder() | 创建编码器 |
newDecoder() | 创建解码器 |
decode(ByteBuffer bb) | 将ByteBuffer中的字节序列转换成字符序列 |
encode(ByteBuffer bb) | 将CharBuffer中的字符序列转换成字节序列 |
encode(String str) | 将字符串中的字符序列转换成字节序列 |
示例:
public class Demo02 {
public static void main(String[] args) {
// 指定字符集GBK,创建Charset对象
Charset charset = Charset.forName("GBK");
// 创建CharBuffer
CharBuffer cbuff = CharBuffer.allocate(8);
cbuff.put("中");
cbuff.put("国");
cbuff.flip();
// 将cbuff中的字符序列转换成字节序列
ByteBuffer bbuff = charset.encode(cbuff);
// 将字节序列解码成字符序列后打印到控制台上
System.out.println(charset.decode(bbuff));
}
}
五、Path和Files
为了增强File类的功能,Java7增加了Path和Files工具类。Path代表一个与平台无关的路径。Files提供了大量用于文件操作的静态方法来操作文件。
5.1 Path
Paths包含了两个返回Path的静态工厂方法。
下面是关于Path的一些操作:
getFileName():返回路径名;
toAbsolutePath():获取Path对应的绝对路径;
getNameCount():返回Path里面包含的路径数量;
getParent():返回Path所在路径的上级路径;
getRoot():返回Path所在的根路径;
isAbsolute():判断Path路径是否是绝对路径;
toFile():返回Path路径表示的File对象;
例如:
public class Demo02 {
public static void main(String[] args) {
Path path = Paths.get("d://aa.txt");
System.out.println("返回Path代表的路径名:" + path.getFileName()); // aa.txt
System.out.println("返回Path的绝对路径:" + path.toAbsolutePath()); // d:\aa.txt
System.out.println("返回Path的根路径:" + path.getRoot()); // d:\
System.out.println("返回Path包含的路径数量:" + path.getNameCount()); // 1
System.out.println("判断Path是否是绝对路径?" + path.isAbsolute()); // true
System.out.println("返回Path的父路径:" + path.getParent()); // d:\
System.out.println("返回Path代表的File:" + path.toFile()); // d:\aa.txt
}
}
另外,还可以通过Paths.get(String first, String… more)方法来获取Path对象。
Path path = Paths.get("d:", "aa.txt");
5.2 Files
Files是一个文件操作的工具类,它提供了大量便捷的工具方法,简化文件的操作(如文件复制、读写文件等操作)。
下面是Files对象的一些常用方法:
copy(Path source, OutputStream out):把文件的所有字节数据复制到输出流中;
isHidden(Path path):判断文件是否隐藏;
readAllLines(Path path):从文件中读取所有行;
size(Path path):返回文件的大小,以字节为单位;
write(Path path, byte[] bytes, OpenOption… options):向文件中写入字节数据,第三个参数代表文件的打开方式,该参数可选;
public class Demo02 {
public static void main(String[] args) throws FileNotFoundException, IOException {
// 将good.jpg图片内容复制到goods2.jpg文件中
Files.copy(Paths.get("good.jpg"), new FileOutputStream("good2.jpg"));
// 判断文件是否隐藏
System.out.println("文件是否隐藏?" + Files.isHidden(Paths.get("good.jpg")));
// 读取文件所有行
List<String> lines = Files.readAllLines(Paths.get("aa.txt"));
System.out.println(lines);
// 获取文件大小,以字节为单位
System.out.println("文件大小:" + Files.size(Paths.get("good.jpg")));
// 向文件中写入字节数据
Files.write(Paths.get("aa.txt"), "你好吗?".getBytes(), StandardOpenOption.APPEND);
}
}
5.3 FileVisitor
在传统IO操作下,如果要获取指定目录下所有文件和子目录,只能使用递归算法来实现。Files提供了遍历文件和目录的方法:
- walkFileTree(Path path, FileVisitor visitor):遍历指定目录下的所有文件或子目录;
FileVisitor代表一个文件访问器。当调用walkFileTree方法的时候会自动遍历path路径下所有文件或子目录,并触发FileVisitor中的相应方法,如下所示:
- FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs):访问子目录前自动触发该方法;
- FileVisitResult postVisitDirectory(T dir, IOException ex):访问子目录后自动触发该方法;
- FileVisitResult visitFile(T file, BasicFileAttributes attrs):访问file文件时候自动触发该方法;
- FileVisitResult visitFileFailed(T file, IOException ex):访问file文件失败时自动触发该方法;
上面4个方法都返回一个FileVisitResult对象,它是一个枚举类,代表了访问之后的后续行为。FileVisitResult定义了如下几种后续行为:
- CONTINUE:继续后续行为;
- SKIP_SIBLINGS:继续后续行为,但不访问该文件或目录的兄弟文件或目录;
- SKIP_SUBTREE:继续后续行为,但不访问该文件或目录的子目录树;
- TERMINATE:停止访问后续行为;
例如:在Java工程的根路径下按照下面目录结构创建文件夹aa。
然后使用Files工具类的walkFileTree方法遍历aa文件夹。
public class Demo02 {
public static void main(String[] args) throws IOException {
Files.walkFileTree(Paths.get("aa"), new FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir,
BasicFileAttributes attrs) throws IOException {
System.out.println("访问子目录" + dir.getFileName() + "前...");
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs) throws IOException {
System.out.println("访问文件" + file.getFileName() + "时...");
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc)
throws IOException {
System.out.println("访问文件" + file.getFileName() + "失败...");
return FileVisitResult.TERMINATE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
System.out.println("访问子目录" + dir.getFileName() + "后...");
return FileVisitResult.CONTINUE;
}
});
}
}
程序运行效果:
在实际开发中没有必要为FileVisitor中的4个方法同时提供实现。因此,可以使用FileVisitor的实现类SimpleFileVisitor。我们可以根据实际需要选择性地重写指定方法。
例如:删除上面的aa文件夹。
public class Demo02 {
public static void main(String[] args) throws IOException {
Files.walkFileTree(Paths.get("aa"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs) throws IOException {
// 删除文件
file.toFile().delete();
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
// 删除目录
dir.toFile().delete();
return FileVisitResult.CONTINUE;
}
});
}
}
六、Selector
6.1 Selector介绍
从JDK1.4开始,Java提供了NIO API来开发高性能的网络服务器。其中,下面有几个特殊类需要大家了解一下:
1)Selector:Selector是SelectableChannel对象的多路复用器,它是非阻塞IO的核心。所有采用非阻塞方式进行通信的Channel都应该注册到Selector对象中。一个Selector可以同时监控多个SelectableChannel的IO状况。
2)SelectableChannel:它代表了可以支持非阻塞IO操作的Channel对象(包括ServerSocketChannel和SocketChannel),它可以注册到Selector上。SelectableChannel对象支持阻塞和非阻塞两种模式,所有Channel默认都是阻塞模式。如果要使用非阻塞IO,那么就必须要把SelectableChannel设置为非阻塞模式。
3)ServerSocketChannel:对应java.net.ServerSocket类,支持非阻塞和OP_ACCEPT操作;
4)SocketChannel:对应java.net.Socket类,支持非阻塞操作,以及支持OP_CONNECT、OP_READ、OP_WRITE操作;
5)SelectionKey:它代表了SelectableChannel和Selector之间的关系。一个Selector实例有三种不同类型的SelectionKey集合。
名称 | 描述 |
---|---|
所有的SelectionKey集合 | 代表了注册在Selector上的Channel。该集合可以通过Selector的keys方法获得 |
被选择的SelectionKey集合 | 代表了所有可以通过select()方法获取的、需要进行IO处理的Channel。该集合可以通过selector的selectedKeys()方法获得 |
被取消的SelectionKey集合 | 代表了所有被取消注册关系的Channel。在下一次执行select()方法的时候,这些Channel对应的SelectionKey会被彻底删除,程序通常无法直接访问该集合 |
下图是NIO非阻塞服务器示意图:
从上图可以看出,服务器上的所有非阻塞的Channel(包括Socket和ServerSocketChannel)都需要向Selector进行注册。Selector负责监视这些Channel中数据状态的变化。当其中一个或多个Channel具有可用的IO操作时,该Selector的Select()方法将会返回一个大于0的整数,该整数代表该Selector上正在进行IO操作的Channel的数量。当Selector上注册的所有Channel没有需要处理的IO操作,select()方法将被阻塞,调用该方法的线程也会被阻塞。
6.2 Selector使用
如果使用NIO对网络服务器程序进行重新设计,需要借助于Selector来实现。
6.2.1 开发服务端
服务端的Selector仅需要监听两种操作:连接和读数据。处理连接时,系统只需要将连接完成后所产生的SocketChannel注册到指定的Selector上即可。处理读数据操作时,系统先从该Socket中读取数据,再将数据写入到Selector上注册的所有Channel中。
- 使用nio开发服务端的基本步骤:
第一步:创建一个Selector实例;
Selector selector = Selector.open();
第二步:创建ServerSocketChannel实例;
ServerSocketChannel server= ServerSocketChannel.open();
第三步:给server绑定IP和端口号;
server.bind(new InetSocketAddress("localhost", 30000));
上面端口号可以任意指定。
第四步:将server注册到Selector对象中。
// 设置ServerSocket以非阻塞方式工作
server.configureBlocking(false);
// 将ServerSocket对象注册到指定Selector中
server.register(selector, SelectionKey.OP_ACCEPT);
上面OP_ACCEPT参数表明一个SelectionKey包含Accept事件。当selector检测到关联的ServerSocketChannel准备好接受一个连接或者发生错误时,会将该事件加入到该SelectionKey的ready set集合中,并将该key加入到已选择的SelectionKey集合中。
第五步:调用selector对象的select方法,如果该方法的返回值大于0,说明程序需要对相应的Channel执行IO处理。
while (selector.select() > 0) {
....
}
第六步:如果select()方法的返回值大于0,则依次遍历所有被选择的SelectionKey,并且判断每一个SelectionKey对应的Channel是否有客户端连接,如果有则调用ServerSocketChannel对象的accept方法获取代表客户端的SocketChannel,然后再把它注册到selector上。
// 依次处理所有被选择的SelectionKey
for (SelectionKey sk : selector.selectedKeys()) {
// 从SelectionKey集合中删除正在处理的SelectionKey
selector.selectedKeys().remove(sk);
// 如果SelectionKey对应的Channel包含客户端的连接,
// 那么就调用ServerSocketChannel对象的accept方法,
// 该方法会产生客户端的SocketChannel
if (sk.isAcceptable()) {
// 获取客户端的Channel
SocketChannel sc = server.accept();
// 设置SocketChannel的工作方式为非阻塞
sc.configureBlocking(false);
// 将该SocketChannel对象注册到Selector上,并设置
sc.register(selector, SelectionKey.OP_READ);
// 将sk对应的Channel设置成准备接收其他请求
sk.interestOps(SelectionKey.OP_ACCEPT);
}
}
第七步:如果SocketChannel中有可读数据,则从Channel中读取数据出来;
// 如果sk对应的eChannel有数据读取
if (sk.isReadable()) {
// 获取该SelectionKey对应的Channel,该Channel中有可读的数据
SocketChannel sc = (SocketChannel) sk.channel();
// 定义准备执行读取数据的ByteBuffer
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
// 开始读取数据
try {
while(sc.read(buff) > 0) {
buff.flip();
content += charset.decode(buff);
}
System.out.println("读取到的数据:" + content);
} catch (Exception e) {
// 如果捕获到该sk对应的Channel出现了异常,即表明该Channel对应的client出现问题,
// 所以从Selector中取消sk的注册
sk.cancel();
if (sk.channel() != null) {
sk.channel().close();
}
}
}
第八步:向SocketChannel发送响应数据;
// 如果content的长度大于0,即聊天信息不为空
if (content.length() > 0) {
// 遍历该Selector里注册的所有SelectionKey
for (SelectionKey key : selector.keys()) {
// 获取该key对应的channel
Channel targetChannel = key.channel();
// 如果该channel是SocketChannel对象
// 则将读到的内容写入该Channel中
if (targetChannel instanceof SocketChannel) {
SocketChannel dest = (SocketChannel) targetChannel;
dest.write(charset.encode(content));
}
}
}
完整示例代码:
public class NServer {
static final int PORT = 30000;
static Charset charset = Charset.forName("utf-8");
public static void main(String[] args) {
try {
// 创建Selector
Selector selector = Selector.open();
// 创建ServerSocketChannel
ServerSocketChannel server = ServerSocketChannel.open();
// 给ServerSocketChannel绑定IP和端口号
server.bind(new InetSocketAddress("127.0.0.1", PORT));
// 设置ServerSocketChannel的工作方式为非阻塞
server.configureBlocking(false);
// 将ServerSocketChannel注册到Selector对象中
server.register(selector, SelectionKey.OP_ACCEPT);
// 不断调用Selector对象的select方法,如果selector中有被选择的SelectionKey,
// 则该方法返回被选择SelectionKey的数量。
while (selector.select() > 0) {
// 依次处理所有被选择的SelectionKey
for (SelectionKey sk : selector.selectedKeys()) {
// 从SelectionKey集合中删除正在处理的SelectionKey
selector.selectedKeys().remove(sk);
// 如果SelectionKey对应的Channel包含客户端的连接,
// 那么就调用ServerSocketChannel对象的accept方法,
// 该方法会产生客户端的SocketChannel
if (sk.isAcceptable()) {
// 获取客户端的Channel
SocketChannel sc = server.accept();
// 设置SocketChannel的工作方式为非阻塞
sc.configureBlocking(false);
// 将该SocketChannel对象注册到Selector上,并设置
sc.register(selector, SelectionKey.OP_READ);
// 将sk对应的Channel设置成准备接收其他请求
sk.interestOps(SelectionKey.OP_ACCEPT);
}
// 如果sk对应的eChannel有数据读取
if (sk.isReadable()) {
// 获取该SelectionKey对应的Channel,该Channel中有可读的数据
SocketChannel sc = (SocketChannel) sk.channel();
// 定义准备执行读取数据的ByteBuffer
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
// 开始读取数据
try {
while(sc.read(buff) > 0) {
buff.flip();
content += charset.decode(buff);
}
System.out.println("读取到的数据:" + content);
} catch (Exception e) {
// 如果捕获到该sk对应的Channel出现了异常,即表明该Channel对应的client出现问题,
// 所以从Selector中取消sk的注册
sk.cancel();
if (sk.channel() != null) {
sk.channel().close();
}
}
// 如果content的长度大于0,即聊天信息不为空
if (content.length() > 0) {
// 遍历该Selector里注册的所有SelectionKey
for (SelectionKey key : selector.keys()) {
// 获取该key对应的channel
Channel targetChannel = key.channel();
// 如果该channel是SocketChannel对象
// 则将读到的内容写入该Channel中
if (targetChannel instanceof SocketChannel) {
SocketChannel dest = (SocketChannel) targetChannel;
dest.write(charset.encode(content));
}
}
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
6.2.2 开发客户端
相对于服务端而言,客户端只有一个SocketChannel。系统将SocketChannel注册到Selector后,程序将启动另外一个线程来监听该Select即可。如果程序监听到该Selector的select()方法的返回值大于0,就表明该Selector上有需要进行IO处理的Channel。接着程序就要取出该Channel,并使用nio读取该Channel中的数据。
客户端代码:
class ClientThread extends Thread {
private Selector selector;
public ClientThread(Selector selector) {
this.selector = selector;
}
@Override
public void run() {
try {
while (selector.select() > 0) {
for (SelectionKey sk : selector.selectedKeys()) {
// 删除正在处理的sk
selector.selectedKeys().remove(sk);
// 如果sk对应的Channel中有可读的数据,则把数据读取出来
if (sk.isReadable()) {
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
while (sc.read(buff) > 0) {
buff.flip();
content += NClient.charset.decode(buff);
}
System.out.println("读取到的数据:" + content);
// 为下一次读取做准备
sk.interestOps(SelectionKey.OP_READ);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class NClient {
public static final int PORT = 30000;
public static Charset charset = Charset.forName("utf-8");
public static void main(String[] args) throws IOException {
// 创建多路复用器
Selector selector = Selector.open();
// 创建SocketChannel对象,用于连接到指定主机上
SocketChannel sc = SocketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
// 设置为非阻塞工作方式
sc.configureBlocking(false);
// 将sc注册到selector上
sc.register(selector, SelectionKey.OP_READ);
// 启动线程,读取服务端的数据
new ClientThread(selector).start();
// 读取用户输入
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
// 将读到的数据输出到SocketChannel中
sc.write(charset.encode(line));
}
}
}