JAVA的NIO
一、概述
NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
JAVA NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的。
Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,该线程可以继续做其它的事情。
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
1.1 JAVA NIO 与 IO 的主要区别
1.2 通道(Channel)与缓冲区(Buffer)——缓冲区中处理数据
- 通道表示打开到 IO 设备(例如:文件、套接字)的连接。
- 若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。
- 然后操作缓冲区,对数据进行处理。
二、缓冲区——一个数组
一个用于特定基本数据类型的容器,一个内存块,底层用数组实现。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
没有boolean:
static XxxBuffer allocate(int capacity) : 创建一个容量为capacity 的 XxxBuffer 对象。
- put():放入数据到 Buffer 中
- get():获取 Buffer 中的数据
2.1 Buffer中的四个核心属性及常用方法
- 容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
- 限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
- 位置 (position):缓冲区中正在操作的数据位置。即下一个要读取或写入的数据的索引,缓冲区的位置不能为负,并且不能大于其限制。
- 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position。在flip()被调用后,mark就作废了。
Buffer使用示例:
public class BasicBuffer {
public static void main(String[] args) {
// 创建一个 Buffer,大小为 5,即可以存放 5 个 int
IntBuffer intBuffer = IntBuffer.allocate(5);
// 向 buffer 中存放数据
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i * 2);
}
// 如何从 buffer 中读取数据
// 将 buffer 转换,读写切换
intBuffer.flip();
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
- flip()
public final Buffer flip() {
this.limit = this.position;
this.position = 0;
this.mark = -1;
return this;
}
- public abstract ByteBuffer putXxx(Xxx value [, int index]);
- 从position当前位置插入元素,Xxx表示基本数据类型。
- 此方法时类型化的 put 和 get,put放入的是什么数据类型,get就应该按顺序使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。
2.2 直接与非直接缓冲区——节省中间的copy步骤
- 字节缓冲区要么是直接的,要么是非直接的。
- 如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前或之后, 虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
- 直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。
- 直接字节缓冲区还可以通过FileChannel 的 map() 方法将文件区域直接映射到内存中来创建,该方法返回 MappedByteBuffer。
- 字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。
2.3 MappedByteBuffer
可以让文件直接在内存(堆外内存)中进行修改,而如何同步到文件由NIO来完成,操作系统不需要拷贝一次。
- map() 方法获取MappedByteBuffer
- 参数1: FileChannel.MapMode.READ_WRITE,使用的读写模式;
- 参数2: 0,可以直接修改的起始位置;
- 参数3: 5,是映射到内存的大小(不是文件中字母的索引位置),即将 1.txt 的多少个字节映射到内存,也就是可以直接修改的范围就是 [0, 5);
- 实际的实例化类型:DirectByteBuffer。
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception{
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
// 获取对应的文件通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数1: FileChannel.MapMode.READ_WRITE,使用的读写模式
* 参数2: 0,可以直接修改的起始位置
* 参数3: 5,是映射到内存的大小(不是文件中字母的索引位置),即将 1.txt 的多少个字节映射到内存,也就是可以直接修改的范围就是 [0, 5)
* 实际的实例化类型:DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0,(byte)'N');
mappedByteBuffer.put(3, (byte)'M');
// 越界,会抛出 IndexOutOfBoundsException
mappedByteBuffer.put(5, (byte)'Y');
randomAccessFile.close();
System.out.println("修改成功~");
}
}
三、通道
3.1 概述
传统的IO流通过DMA总线进行读写。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”,只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互,Channel 本身不存储任何数据。
- 每个客户端连接都会建立一个Channel通道。
- 通道可以同时进行读写,而流只能读或者只能写。
- 通道可以实现异步读写数据。
- 通道可以从缓存读数据,也可以写数据到缓存。
3.2 常用实现类与方法
Channel 在 NIO 中是一个接口:public interface Channel extends Closeable{}。常用的Channel类有:
-
FileChannel:用于读取、写入、映射和操作文件的通道。
-
DatagramChannel:通过 UDP 读写网络中的数据通道。
-
SocketChannel:类似Socket,通过 TCP 读写网络中的数据。
-
ServerSocketChannel:类似ServerSocket,可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
FileChannel 用于文件数据的读写,DatagramChannel用于UDP数据的读写,ServerSocketChannel和SocketChannel用于TCP数据读写。
获取通道:
-
获取通道的一种方式是对支持通道的对象调用getChannel() 方法,支持通道的类如下:
本地IO:FileInputStream、FileOutputStream、RandomAccessFile;
网络IO:DatagramSocket、Socket、ServerSocket。 -
使用 Files 类的静态方法 newByteChannel() 获取字节通道。
-
通过通道的静态方法 open() 打开并返回指定通道。
常用方法:
- public int read(ByteBuffer dst)
- 从通道读取数据并放到缓冲区中。
- 此操作也会移动 Buffer 中的position指针,不断往position中放数据,read完成后position指向limit。
- public int write(ByteBuffer src)
- 把缓冲区的数据写到通道中。
- 此操作也会不断移动Buffer中的position位置直到limit,读取到的数据就是position到limit这两个指针之间的数据。
- public long transferFrom(ReadableByteChannel src, long position, long count)
- 从目标通道中复制数据到当前通道。
- public long transferTo(long position, long count, WritableByteChannel target)
- 把数据从当前通道复制给目标通道。
- 该方法拷贝数据使用了零拷贝,通常用来在网络IO传输中,将FileChannel里面的文件数据直接拷贝到与客户端或者服务端连接的Channel里面,从而达到文件传输。
3.3 FileChannel应用实例
3.3.1 将数据写入到本地文件
FileOutputStream中包含一个FileChannel:
基本流程:
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception{
String str = "hello,尚硅谷";
// 创建一个输出流 -> Channel
FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
// 通过 FileOutputStream 获取对应的 FileChannel
// 这个 FileChannel 真实类型是 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
// 创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将 str 放入 ByteBuffer
byteBuffer.put(str.getBytes());
// 对 ByteBuffer 进行反转,开始读取
byteBuffer.flip();
// 将 ByteBuffer 数据写入到 FileChannel
// 此操作会不断移动 Buffer 中的 position 到 limit 的位置
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
3.3.2 本地文件读数据
基本流程:
public class NIOFileChannel02 {
public static void main(String[] args) throws Exception{
// 创建文件的输入流
File file = new File("d:\\file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
// 通过 fileInputStream 获取对应的 FileChannel
// 实际类型 FileChannelImpl
FileChannel fileChannel = fileInputStream.getChannel();
// 创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
// 将通道的数据读入到 buffer
fileChannel.read(byteBuffer);
//将 ByteBuffer 的字节数据转成 String
// array() 方法
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
3.3.3 使用一个Buffer完成文件的读取
基本流程:
注意——缓冲区的clear():
- 清空 buffer,由于循环的最后执行了 write 操作,会将 position 移动到 limit 的位置。
- 清空 Buffer 的操作才为上一次的循环重置 position的位置到0。
- 如果没有重置 position,那么上次读取后,position 和 limit 位置一样,读取后 read 的值永远为 0。
public class NIOFileChannel03 {
public static void main(String[] args) throws Exception{
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel fileChannel1 = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel fileChannel2 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true){
// 清空 buffer,由于循环的最后执行了 write 操作,会将 position 移动到 limit 的位置
// 清空 Buffer 的操作才为上一次的循环重置 position 的位置
// 如果没有重置 position,那么上次读取后,position 和 limit 位置一样,读取后 read 的值永远为 0
byteBuffer.clear();
// 将数据存入 ByteBuffer,它会基于 Buffer 此刻的 position 和 limit 的值
// 将数据放入 position 的位置,然后不断移动 position 直到其与 limit 相等
int read = fileChannel1.read(byteBuffer);
System.out.println("read=" + read);
if (read == -1) { // 表示读完
break;
}
// 将 buffer 中的数据写入到 FileChannel2 ---- 2.txt
byteBuffer.flip();
fileChannel2.write(byteBuffer);
}
// 关闭相关的流
fileInputStream.close();
fileOutputStream.close();
}
}
3.3.4 拷贝文件—— transferFrom()方法
public class NIOFileChannel04 {
public static void main(String[] args) throws Exception{
// 创建相关流
FileInputStream fileInputStream = new FileInputStream("d:\\a.gif");
FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.gif");
// 获取各个流对应的 FileChannel
FileChannel source = fileInputStream.getChannel();
FileChannel dest = fileOutputStream.getChannel();
// 使用 transferForm 完成拷贝
dest.transferFrom(source, 0, source.size());
// 关闭相关的通道和流
source.close();
dest.close();
fileInputStream.close();
fileOutputStream.close();
}
}
3.4 ServerSocketChannel 和 SocketChannel
ServerSocketChannel:主要用于在服务器监听新的客户端Socket连接。
SocketChannel:网络IO通道,具体负责进行读写操作,NIO把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
3.5 分散(Scatter)与聚集(Gather)
分散读取(Scattering Reads):是指从 Channel 中读取的数据“分散”到多个 Buffer(Buffer数组) 中,按照缓冲区的顺序。
聚集写入(Gathering Writes):是指将多个 Buffer 中的数据“聚集”到 Channel,按照缓冲区的顺序。
使用示例:
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws Exception{
// 使用 ServerSocketChannel 和 SocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
// 绑定端口到 socket,并启动
serverSocketChannel.socket().bind(inetSocketAddress);
// 创建一个 Buffer 数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
// 等待客户端的连接(Telnet)
SocketChannel socketChannel = serverSocketChannel.accept();
// 假定从客户端接受 8 个字节
int msgLength = 8;
// 循环读取
while (true) {
int byteRead = 0;
while (byteRead < msgLength) {
// 直接操作 Buffer 数组
long l = socketChannel.read(byteBuffers);
byteRead += l; //累计读取的字节数
System.out.println("byteRead= " + byteRead);
// 使用流打印,看看当前 buffer 的 position 和 limit
Arrays.stream(byteBuffers)
.map(buffer -> "position=" + buffer.position() + ", limit = " + buffer.limit())
.forEach(System.out::println);
}
// 读数据后需要将所有的 buffer 进行 flip
Arrays.asList(byteBuffers).forEach(Buffer::flip);
// 将数据读出显示
long byteWrite = 0;
while (byteWrite < msgLength) {
long l = socketChannel.write(byteBuffers);
byteWrite += l;
}
// 将所有的 buffer 进行 clear 操作
Arrays.asList(byteBuffers).forEach(Buffer::clear);
System.out.println("byteRead=" + byteRead + ", byteWrite=" + byteWrite
+ ", msgLength=" + msgLength);
}
}
}
四、Selector 选择器
4.1 NIO的非阻塞式网络通信
传统的 IO 流都是阻塞式的,也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其它任务。
因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
JAVA NIO 是非阻塞模式的,当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其它任务。线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
4.2 选择器Selector
选择器(Selector):
是 SelectableChannel 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况。利用 Selector 可使一个单独的线程管理多个Channel,Selector是非阻塞 IO 的核心。
- SelectableChannel
Selector能够检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求,避免了多线程之间的上下文切换导致的开销。
4.2.1 Selector常见方法
- public static Selector open()——Channel的方法
得到一个选择器对象,实例化出 WindowsSelectorImpl对象。
- public int select(long timeout)
- 监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,返回的结果为Channel响应的事件总和,当结果为0时,表示本Selector监听的所有Channel中没有Channel产生事件。
- 如果不传入timeout值,就会阻塞线程,传入值则为阻塞多少毫秒,通过它设置超时时间。
- 之所以需要传入时间,是为了让它等待几秒钟再看有没有Channel会产生事件,从而获取一段时间内产生事件的Channel的总集合再一起处理。
- selector.selectNow()
不会阻塞,立马返回冒泡的事件数
- public Set selectedKeys()
从内部集合中得到所有的SelectionKey
4.2.2 SelectionKey——Channel + Channel的事件
4.2.2.1 概述
在创建与客户端连接的Channel时,会通过ServerSocketChannel得到SocketChannel,然后调用 SelectableChannel.register() 方法,将SocketChannel注册到一个Selector上面。调用该方法后,会返回一个SelectionKey对象,该对象与Channel是一一对应的。
而Selector则调用select() 方法进行监听,通过管理SelectionKey的集合间接地去管理各个Channel。通过SelectionKey调用channel() 方法反向获取SocketChannel。
SelectionKey表示的是Selector和网络通道的注册关系,所以FileChannel是没有办法通过SelectionKey注册到Selector上去的。
常用方法:
- attachment()
获取到该 Channel 关联的 Buffer,即获取共享数据。
4.2.2.2 事件
事件:
当将Channel绑定到Selector上面时,必须同时为该Channel声明一个监听该Channel的事件(Channel和该Channel的事件一起组成了SelectionKey),并将SelectionKey加入到Selector的Set集合中去。
事件类型:
- public static final int OP_READ = 1
- 值为1,表示读操作。
- 代表本Channel已经接收到其它客户端传过来的消息,需要将Channel中的数据读取到Buffer中去。
- public static final int OP_WRITE = 4
- 值为4,表示写操作。
- 代表已经可以向通道中写数据了。一般临时将Channel的事件修改,在处理完后又修改回去。
- public static final int OP_CONNECT = 8
- 值为8,代表建立连接。
- 一般在ServerSocketChannel上绑定该事件,结合 channel.finishConnect()在连接建立异常时进行异常处理。
- public static final int OP_ACCEPT = 16
- 值为16,表示有新的网络连接可以accept。
- 与ServerSocketChannel进行绑定,用于创建新的SocketChannel,并把它注册到Selector上去。
流程:
- 当有客户端建立连接或者进行通信,会在对应的各个Channel中产生不同的事件。
- Selector会一直监听所有的事件,当监听到某个SelectionKey中有事件产生时,会将所有产生事件的SelectionKey统一加入到一个Set集合中去。
- 然后获取到这个集合,对集合中的各个SelectionKey进行判断,判断它产生的是什么事件,再根据不同的事件进行不同的处理。
- 在操作SelectionKey集合的时候,其实就是在一个线程里面对几个不同客户端的连接进行操作。
4.3 示例
4.3.1 实现服务器和客户端的简单通讯
- 服务端
public class NIOServer {
public static void main(String[] args) throws Exception{
// 创建 ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 得到一个 Selector 对象
Selector selector = Selector.open();
// 绑定一个端口 6666
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 设置非阻塞
serverSocketChannel.configureBlocking(false);
// 把 serverSocketChannel 注册到 Selector
// 关心事件为 OP_ACCEPT,有新的客户端连接
SelectionKey register = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println();
// 循环等待客户端连接
while (true) {
// 等待 1 秒,如果没有事件发生,就返回
if (selector.select(1000) == 0) {
System.out.println("服务器等待了1秒,无连接");
continue;
}
// 如果返回的值 > 0,表示已经获取到关注的事件
// 获取到相关的 selectionKey 集合,然后反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 如果是 OP_ACCEPT,有新的客户端连接
if (key.isAcceptable()) {
// 该客户端生成一个 SocketChannel
// 已经是连接了,accept() 不会阻塞
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功,生成了一个SocketChannel:" + socketChannel.hashCode());
// 将 SocketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
// 将 socketChannel 注册到 selector,关注事件为 OP_READ
// 同时给 SocketChannel 关联一个 Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) {
// 通过 key,反向获取到对应的 Channel
SocketChannel channel = (SocketChannel) key.channel();
// 获取到该 Channel 关联的 Buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("from 客户端:" + new String(buffer.array()));
}
// 手动从集合中移除当前的 selectionKey,防止重复操作
keyIterator.remove();
}
}
}
}
- 客户端
public class NIOClient {
public static void main(String[] args) throws Exception{
// 得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
// 设置非阻塞
socketChannel.configureBlocking(false);
// 提供服务器端的 IP 和端口
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 6666);
// 连接服务器
if (!socketChannel.connect(socketAddress)){ //如果不成功
while (!socketChannel.finishConnect()){
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作。。。");
}
}
// 如果连接成功,就发送数据
String str = "hello, NIO";
// wrap()产生一个字节数组
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
// 发送数据,实际上就是将 buffer 数据写入到 Channel
socketChannel.write(byteBuffer);
System.in.read();
}
}
五、NIO群聊系统
实现功能:
5.1 服务器端功能实现
public class GroupChatServer {
//定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6669;
public GroupChatServer() {
try {
selector = Selector.open();
listenChannel = ServerSocketChannel.open();
// 绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
// 设置非阻塞模式
listenChannel.configureBlocking(false);
// 将 listenChannel 注册到 selector,绑定监听事件
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
// 监听逻辑
public void listen() {
try {
//循环处理
while (true) {
int count = selector.select();
if (count > 0) { //有事件处理
// 遍历得到 SelectionKey 集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
// 取出 SelectionKey
SelectionKey key = iterator.next();
// 监听到 accept,连接事件
if (key.isAcceptable()) {
SocketChannel socketChannel = listenChannel.accept();
// 将该 channel 设置非阻塞并注册到 selector
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress() + " 上线...");
}
// 通道可以读取数据,即 server 端收到客户端的消息
if (key.isReadable()) {
//处理读(专门写方法)
readData(key);
}
iterator.remove(); // 删除防止重复操作
}
} else {
System.out.println("等待。。。");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 读取客户端消息
private void readData(SelectionKey key) {
// 定义一个 SocketChannel
SocketChannel channel = null;
try {
// 取到关联的 Channel
channel = (SocketChannel) key.channel();
// 创建缓冲 buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
// 根据count 值判断是否读取到数据
if (count > 0) {
// 把缓冲区的数据转成字符串
String msg = new String(buffer.array());
// 输出该消息
System.out.println("from 客户端:" + msg);
// 向其它的客户端转发消息(去掉自己),专门写一个方法处理
sendInfoToOtherClients(msg, channel);
}
} catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + "离线了...");
// 取消注册
key.cancel();
// 关闭通道
channel.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
// 转发消息给其它客户端(通道)
private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
System.out.println("服务器转发消息中。。。");
// 遍历所有注册到 selector 上的 SocketChannel,并排除 self
for (SelectionKey key : selector.keys()) {
// 通过 key 取出对应的 SocketChannel
Channel targetChannel = key.channel();
// 排除自己
if (targetChannel instanceof SocketChannel && targetChannel != self) {
SocketChannel dest = (SocketChannel) targetChannel;
// 将 msg 存储到 buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
// 将 buffer 的数据写入通道
dest.write(buffer);
}
}
}
public static void main(String[] args) {
// 创建服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
5.2 客户端实现
public class GroupChatClient {
// 定义相关的属性
private static final String HOST = "127.0.0.1";
private static final int PORT = 6669;
private Selector selector;
private SocketChannel socketChannel;
private String username;
public GroupChatClient() throws IOException {
selector = Selector.open();
// 连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
// 设置非阻塞
socketChannel.configureBlocking(false);
// 将 channel 注册到 selector
socketChannel.register(selector, SelectionKey.OP_READ);
// 得到 username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " is ok...");
}
// 向服务器发送消息
public void sendInfo(String info){
info = username + " 说:" + info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
}catch (IOException e){
e.printStackTrace();
}
}
// 读取从服务器端回复的消息
public void readInfo(){
try {
int readChannels = selector.select();
if (readChannels > 0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if (key.isReadable()){
// 得到相关的通道
SocketChannel sc = (SocketChannel)key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
sc.read(buf);
String msg = new String(buf.array());
System.out.println(msg.trim());
}
iterator.remove();
}
}else {
System.out.println("没有可以用的通道...");
}
}catch (Exception e){
}
}
public static void main(String[] args) throws IOException{
GroupChatClient chatClient = new GroupChatClient();
// 启动一个线程用于读取服务器的消息
new Thread(() -> {
while (true){
chatClient.readInfo();
try {
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}).start();
// 主线程用于发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}
5.3 read()方法的细节
使用int read = channel.read(buffer)读取数据时,读取的结果情况:
read=-1:
- 说明客户端的数据发送完毕,并且会主动关闭socket。
- 服务器程序需要关闭socketSocket,并且取消key的注册。这时继续使用SocketChannel进行读操作的话,就会抛出远程主机强迫关闭一个现有连接的IO异常。
read=0:
- 某一时刻SocketChannel中没有数据可读。
- 客户端的数据发送完毕。
- ByteBuffer的position等于limit时也会返回0。