【网络通信】Java NIO
基本概念和IO入门对于任何程序设计语言而言,输入输出(Input/Output)系统都是非常核心的功能。程序运行需要数据,数据的获取往往需要跟外部系统进行通信,外部系统可能是文件、数据库、其他程序、网络、IO设备等等。外部系统比较复杂多变,那么我们有必要通过某种手段进行抽象、屏蔽外部的差异,从而实现更加便捷的编程。Jvm虚拟机主要打交道的io操作是文件,内存,网络输入(Input)指的是:可以让程
二、NIO
事件驱动+多路复用
写事件代表底层缓冲区是否有空间,有则响应true
2.1 NIO 简介
NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket
和 ServerSocket
相对应的 SocketChannel
和 ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。
对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
- BIO模型中,一个连接来了,会创建一个线程,对应一个while死循环,死循环的目的就是不断监测这条连接上是否有数据可以读,大多数情况下,1w个连接里面同一时刻只有少量的连接有数据可读,因此,很多个while死循环都白白浪费掉了,因为读不出啥数据。
- 而在NIO模型中,他把这么多while死循环变成一个死循环,这个死循环由一个线程控制
- NIO中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接的所有读写都由这个线程来负责
那么NIO如何做到一个线程,一个while死循环就能监测1w个连接是否有数据可读的呢?这就是NIO模型中selector的作用,一条连接来了之后,现在不创建一个while死循环去监听是否有数据可读了,而是直接把这条连接注册到selector上,然后,通过检查这个selector,就可以批量监测出有数据可读的连接,进而读取数据
NIO和BIO之间第一个最大的区别是,
- BIO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
- NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
IO的各种流是阻塞的。这意味着,当一个线程调用read()
或 write()
时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
- Channel中数据的读取是通过Buffer , 一种非阻塞的读取方式。
- 你既可以读取也可以写入到Channel,流只能读取或者写入,inputStream和outputStream。
- Channel可以异步地读和写。
- channel永远都是从一个buffer中读或者写入到一个buffer中去。
- Selector 多路复用器 单线程模型,线程的资源开销相对比较小。一个selector同时检查一组信道的I/O状态。Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
2.2 NIO的组成
NIO是一种同步非阻塞IO,主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(多路复用器)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(多路复用器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
- 通道(铁路)负责传输,通道表示打开到IO设备的连接
- 缓存区(火车)负责存储
1)Channel (通道)
NIO 通过Channel(通道) 进行读写
java.nio 包中实现的以下几个 Channel:
- FileChannel:文件通道,用于文件的读和写
- DatagramChannel:用于 UDP 连接的接收和发送
- SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端
- ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求
Channel 经常翻译为通道,类似 IO 中的流,用于读取和写入。它与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
- Channel表示IO源与目标打开的连接。Channel类似于传统的”流“,只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。
- 通道是双向的,可读也可写,而流的读写是单向的。
- 无论读写,通道只能和Buffer交互。通过 Buffer,通道可以异步地读写。buffer与socket交互
- Channel是一个独立的处理器,专门用于IO操作,附属于CPU。
- 在提出IO请求的时候,CPU不需要进行干预,也就提高了效率。
内存向CPU申请权限,得到DMA权限后就可由内存进行IO,无需占用CPU资源,CPU就可以进行别的计算了。
但DMA总线过多就造成总线冲突的问题,也会影响性能。所以就提出了通道的方式。通道是一个完全独立的处理器,专门进行IO操作。
通道用于源节点与目标节点的连接。在Java NIO中负责缓冲区中数据的传输。Channel本身并不存储数据,因此需要配合Buffer一起使用
获取通道
方式1:通过流获取该流对应的通道。
// 流对象.getChannel() 方法
FileChannel fc = new FileOutputStream("data.txt").getChannel();
方式2:在JDK1.7中的NIO.2 针对各个通道提供了静态方法 FileChannel.open()
;
/*
java.nio.channels.Channel通道接口:
主要实现类
用于本地数据传输:
|-- FileChannel
|-- RandomAccessFile
用于网络数据传输:
|-- SocketChannel
|-- ServerSocketChannel
|-- DatagramChannel
*/
例:
方式3:在JDK1.7中的NIO2 的Files工具类的 newByteChannel()
;
FileChannel
我想文件操作对于大家来说应该是最熟悉的,不过我们在说 NIO 的时候,其实 FileChannel 并不是关注的重点。而且后面我们说非阻塞的时候会看到,FileChannel 是不支持非阻塞的。
这里算是简单介绍下常用的操作吧,感兴趣的读者瞄一眼就是了。
初始化:
FileInputStream inputStream = new FileInputStream(new File("/data.txt"));
FileChannel fileChannel = inputStream.getChannel();
当然了,我们也可以从 RandomAccessFile#getChannel 来得到 FileChannel。
读取文件内容:
ByteBuffer buffer = ByteBuffer.allocate(1024);
int num = fileChannel.read(buffer);
前面我们也说了,所有的 Channel 都是和 Buffer 打交道的。
写入文件内容:
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("随机写入一些内容到 Buffer 中".getBytes());
// Buffer 切换为读模式
buffer.flip();
while(buffer.hasRemaining()) {
// 将 Buffer 中的内容写入文件
fileChannel.write(buffer);
}
SocketChannel
我们前面说了,我们可以将 SocketChannel 理解成一个 TCP 客户端。虽然这么理解有点狭隘,因为我们在介绍 ServerSocketChannel 的时候会看到另一种使用方式。
打开一个 TCP 连接:
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("https://www.javadoop.com", 80));
当然了,上面的这行代码等价于下面的两行:
// 打开一个通道
SocketChannel socketChannel = SocketChannel.open();
// 发起连接
socketChannel.connect(new InetSocketAddress("https://www.javadoop.com", 80));
SocketChannel 的读写和 FileChannel 没什么区别,就是操作缓冲区。
// 读取数据
socketChannel.read(buffer);
// 写入数据到网络连接中
while(buffer.hasRemaining()) {
socketChannel.write(buffer);
}
不要在这里停留太久,先继续往下走。
ServerSocketChannel
之前说 SocketChannel 是 TCP 客户端,这里说的 ServerSocketChannel 就是对应的服务端。
ServerSocketChannel 用于监听机器端口,管理从这个端口进来的 TCP 连接。
// 实例化
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 监听 8080 端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true) {
// 一旦有一个 TCP 连接进来,就对应创建一个 SocketChannel 进行处理
SocketChannel socketChannel = serverSocketChannel.accept();
}
这里我们可以看到 SocketChannel 的第二个实例化方式
到这里,我们应该能理解 SocketChannel 了,它不仅仅是 TCP 客户端,它代表的是一个网络通道,可读可写。
ServerSocketChannel 不和 Buffer 打交道了,因为它并不实际处理数据,它一旦接收到请求后,实例化 SocketChannel,之后在这个连接通道上的数据传递它就不管了,因为它需要继续监听端口,等待下一个连接。
DatagramChannel
UDP 和 TCP 不一样,DatagramChannel 一个类处理了服务端和客户端。
科普一下,UDP 是面向无连接的,不需要和对方握手,不需要通知对方,就可以直接将数据包投出去,至于能不能送达,它是不知道的
监听端口:
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9090));
ByteBuffer buf = ByteBuffer.allocate(48);
channel.receive(buf);
发送数据:
String newData = "New String to write to file..."
+ System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
ServerSocketChannel.java
ServerSocketChannel 在服务器端监听新的客户端 Socket 连接。
NIO 中的 ServerSocketChannel 功能类似 ServerSocket, SocketChannel 功能类似 Socket
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
implements NetworkChannel{
protected ServerSocketChannel(SelectorProvider provider) {
super(provider);
}
// 得到一个ServerSocketChannel通道
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
// 给ServerSocketChannel绑定端口号
public final ServerSocketChannel bind(SocketAddress local)
throws IOException{
return bind(local, 0);
}
public abstract ServerSocket socket();
// 接收一个连接,返回代表这个连接的通道对象
public abstract SocketChannel accept() throws IOException;
// 设置阻塞或非阻塞,取值 false 表示采用非阻塞模式 // AbstractSelectableChannel.java
public final SelectableChannel configureBlocking(boolean block) {
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if (blocking == block)
return this;
if (block && haveValidKeys())
throw new IllegalBlockingModeException();
implConfigureBlocking(block);
blocking = block;
}
return this;
}
//注册一个选择器并设置监听事件
public final SelectionKey register(Selector sel, int ops),
SocketChannel.java
SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
public abstract class SocketChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
{//多了 ByteChannel, ScatteringByteChannel, GatheringByteChannel 分散聚集功能
protected SocketChannel(SelectorProvider provider) {
super(provider);
}
//得到一个 SocketChannel 通道
public static SocketChannel open() throws IOException {
return SelectorProvider.provider().openSocketChannel();
}
//设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public final SelectableChannel configureBlocking(boolean block);
public abstract Socket socket();
//从通道里读数据
public final long read(ByteBuffer[] dsts) throws IOException {
return read(dsts, 0, dsts.length);
}
//往通道里写数据
public int write(ByteBuffer src);
// 连接服务器
public abstract boolean connect(SocketAddress remote) throws IOException;
// 如果上面的方法连接失败,接下来就要通过该方法完成连接操作
public abstract boolean finishConnect() throws IOException;
//注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
public final SelectionKey register(Selector sel, int ops, Object att);
//关闭通道
public final void close();
}
2)Buffer(缓冲区)
IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。
缓冲区的作用的是,用户缓冲区满的时候,才进行一次系统调用
Buffer在Java NIO 中负责数据的存取,缓冲区就是数组,用于存储不同数据类型的数据。
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区类型
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。
Buffer 常用子类:
ByteBuffer // 核心类,后面的类都是它的装饰类
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
//没有BoolBuffewr
MappedByteBuffer // 用于实现内存映射文件
//我们可以通过他们的静态方法获取对应的buffer
static XxxBuffer allocate(int capacity)
java.nio 定义了以下几个 Buffer 的实现
buffer的属性
缓冲区的四个核心属性,用法buf.capacity()
就像数组有数组容量,每次访问元素要指定下标,Buffer 中也有几个重要属性:position、limit、capacity。
capacity
: 容量,表示缓冲区中最大存储数据的容量,一但声明不能改变。(因为底层是数组,数组一但被创建就不能被改变)。比如 capacity 为 1024 的 IntBuffer,代表其一次可以存放 1024 个 int 类型的值。一旦 Buffer 的容量达到 capacity,需要清空 Buffer,才能重新写入值。limit
: 界限,表示缓冲区中可以操作数据的大小。(limit后数据不能进行读写)。- 在写模式下表示最多能写入多少数据,此时limit==Capacity
- 在读模式下表示最多能读多少数据,此时和缓存中的实际数据大小相同,因为 Buffer 不一定被写满了。
- 在写模式下调用flip方法,那么limit就设置为当前position的值(即当前写了多少数据),然后把postion设置置为0,以表示读操作从缓存的头开始读。也就是说调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。
position
: 表示缓冲区中正在操作数据的位置,初始值是 0,每往 Buffer 中写入一个值,position 就自动加 1,代表下一次的写入位置。读操作的时候也是类似的,每读一个值,position 就自动加 1。position <= limit <= capacity。- 从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了。
mark
:标记,表示记录当前position的位置,可以通过reset()恢复到mark的位置。
读和写时候的属性的意义是不一样的
从buffer读和往buffer写时候的属性的含义是不一样的,我们通过flip()函数翻转这些属性,flip的源码
public final Buffer flip(){
limit = position;//把写时候的position赋给读的limit
position=0;//把position置为0 从头读
mark=-1;
return this;
}
初始化 Buffer
每个 Buffer 实现类都提供了一个静态方法 allocate(int capacity)
帮助我们快速实例化一个 Buffer。如:
ByteBuffer byteBuf = ByteBuffer.allocate(1024);
IntBuffer intBuf = IntBuffer.allocate(1024);
LongBuffer longBuf = LongBuffer.allocate(1024);
// ...
另外,我们经常使用 wrap 方法来初始化一个 Buffer。
public static ByteBuffer wrap(byte[] array) {
...
}
buffer读写方法
从channel到buffer:我们要将来自 Channel 的数据填充到 Buffer 中,在系统层面上,这个操作我们称为读操作,因为数据是从外部(文件或网络等)读到内存中。
// 获取 Buffer 中的数据
get();//从buffer中读取单个字节
get(byte[] dst);//批量读取多个字节到 dst数组 中
get(int index);//读取指定索引位置的字节(不会移动 position)
new String(buffer.array()).trim();
当然了,除了将数据从 Buffer 取出来使用,更常见的操作是将我们写入的数据传输到 Channel 中,如通过 FileChannel 将数据写入到文件中,通过 SocketChannel 将数据写入网络发送到远程机器等。对应的,这种操作,我们称之为**写操作**。
```java
int num = channel.write(buf);
```
从buffer到channel
// 放入数据到 Buffer 中
put(byte b);//将给定单个字节写入缓冲区的当前位置
put(byte[] src);//将 src数组 中的字节写入缓冲区的当前位置
put(int index, byte b); //将指定字节写入缓冲区的索引位置(不会移动 position)A;
put(byte[] src, int offset, int length) {...}
上述这些方法需要自己控制 Buffer 大小,不能超过 capacity,超过会抛 java.nio.BufferOverflowException 异常。
flip
前面介绍了写操作,每写入一个值,position 的值都需要加 1,所以 position 最后会指向最后一次写入的位置的后面一个,如果 Buffer 写满了,那么 position 等于 capacity(position 从 0 开始)。
如果要读 Buffer 中的值,需要切换模式,从写入模式切换到读出模式。注意,通常在说 NIO 的读操作的时候,我们说的是从 Channel 中读数据到 Buffer 中,对应的是对 Buffer 的写入操作,初学者需要理清楚这个。
调用 Buffer 的 flip() 方法,可以从写入模式切换到读取模式。其实这个方法也就是设置了一下 position 和 limit 值罢了。
public final Buffer flip() {
limit = position; // 将 limit 设置为实际写入的数据数量
position = 0; // 重置 position 为 0
mark = -1; // mark 之后再说
return this;
}
rewind() & clear() & compact()
rewind():会重置 position 为 0,通常用于重新从头读写 Buffer。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
clear():有点重置 Buffer 的意思,相当于重新实例化了一样。
通常,我们会先填充 Buffer,然后从 Buffer 读取数据,之后我们再重新往里填充新的数据,我们一般在重新填充之前先调用 clear()。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
compact():和 clear() 一样的是,它们都是在准备往 Buffer 填充新的数据之前调用。
前面说的 clear() 方法会重置几个属性,但是我们要看到,clear() 方法并不会将 Buffer 中的数据清空,只不过后续的写入会覆盖掉原来的数据,也就相当于清空了数据了。
而 compact() 方法有点不一样,调用这个方法以后,会先处理还没有读取的数据,也就是 position 到 limit 之间的数据(还没有读过的数据),先将这些数据移到左边,然后在这个基础上再开始写入。很明显,此时 limit 还是等于 capacity,position 指向原来数据的右边。
// 往buffer写数据后,如果要从buffer中读数据,必须调用flip函数,flip函数修改的是buffer的几个属性
// 重新读
rewind();
// 清空读,该为写
byteBuffer.clear();
mark() & reset()
除了 position、limit、capacity 这三个基本的属性外,还有一个常用的属性就是 mark。
mark 用于临时保存 position 的值,每次调用 mark() 方法都会将 mark 设值为当前的 position,便于后续需要的时候使用。
public final Buffer mark() {
mark = position;
return this;
}
那到底什么时候用呢?考虑以下场景,我们在 position 为 5 的时候,先 mark() 一下,然后继续往下读,读到第 10 的时候,我想重新回到 position 为 5 的地方重新来一遍,那只要调一下 reset() 方法,position 就回到 5 了。
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
buffer示例
import java.nio.ByteBuffer;
public class TestNIO {
public static void main(String[] args) {
//分配缓存
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//存入数据到缓冲区中
String str = "abcde";
byteBuffer.put(str.getBytes());//存入 //写
System.out.println("存阶段----");
System.out.println("position:" + byteBuffer.position() + " limit: " + byteBuffer.limit() + " capcacity:" + byteBuffer.capacity());
//position:5 limit: 1024 capcacity:1024
//从写模式切换到读取数据的模式
byteBuffer.flip();
System.out.println("切换阶段--");
System.out.println("position:" + byteBuffer.position() + " limit: " + byteBuffer.limit() + " capcacity:" + byteBuffer.capacity());
//position:0 limit: 5 capcacity:1024
//获取缓冲区中的数据
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println("取阶段----");
System.out.println(new String(bytes, 0, bytes.length));
System.out.println("position:" + byteBuffer.position() + " limit: " + byteBuffer.limit() + " capcacity:" + byteBuffer.capacity());
//position:5 limit: 5 capcacity:1024
//rewind
byteBuffer.rewind();
System.out.println("rewind重新读阶段--");
System.out.println("position:" + byteBuffer.position() + " limit: " + byteBuffer.limit() + " capcacity:" + byteBuffer.capacity());
//position:0 limit: 5 capcacity:1024
//clear清空缓冲区,但是缓冲区中的数据依然存在,只是处于一种“被遗忘“的状态。只是不知道位置界限等,读取会有困难。
byteBuffer.clear();
System.out.println("clear阶段--");
System.out.println("position:" + byteBuffer.position() + " limit: " + byteBuffer.limit() + " capcacity:" + byteBuffer.capacity());
//position:0 limit: 1024 capcacity:1024
System.out.println((char)byteBuffer.get());//取到a
//mark标记。mark会记录当前的position,limit,capacity,可以通过reset()恢复到mark的位置
byteBuffer.mark();
//然后正常get
byteBuffer.get(bytes,0,2);//输入到bytes数组,取到bc
System.out.println("byte数组:"+new String(bytes,0,2));//bc
//reset
byteBuffer.reset();//恢复到b位置
System.out.println((char)byteBuffer.get());
//判断缓冲区中还有个数
if (byteBuffer.hasRemaining())
System.out.println(byteBuffer.remaining());//1022
}
}
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) {
// 远程地址创建
InetSocketAddress remote = new InetSocketAddress("localhost", 9999);
SocketChannel channel = null;
// 定义缓存。
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
// 开启通道
channel = SocketChannel.open();
// 连接远程服务器。
channel.connect(remote);
Scanner reader = new Scanner(System.in);
while(true){
System.out.print("put message for send to server > ");
String line = reader.nextLine();
if(line.equals("exit")){
break;
}
// 将控制台输入的数据写入到缓存。
buffer.put(line.getBytes("UTF-8"));
// 重置缓存游标
buffer.flip();
// 将数据发送给服务器
channel.write(buffer);
// 清空缓存数据。
buffer.clear();
// 读取服务器返回的数据
int readLength = channel.read(buffer);
if(readLength == -1){
break;
}
// 重置缓存游标
buffer.flip();
byte[] datas = new byte[buffer.remaining()];
// 读取数据到字节数组。
buffer.get(datas);
System.out.println("from server : " + new String(datas, "UTF-8"));
// 清空缓存。
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally{
if(null != channel){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
import java.nio.ByteBuffer;
/**
*
* Buffer的应用固定逻辑
* 写操作顺序
* 1. clear()
* 2. put() -> 写操作
* 3. flip() -> 重置游标
* 4. SocketChannel.write(buffer); -> 将缓存数据发送到网络的另一端
* 5. clear()
*
* 读操作顺序
* 1. clear()
* 2. SocketChannel.read(buffer); -> 从网络中读取数据
* 3. buffer.flip() -> 重置游标
* 4. buffer.get() -> 读取数据
* 5. buffer.clear()
*
*/
public class TestBuffer {
public static void main(String[] args) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(8);
byte[] temp = new byte[]{3,2,1};
// 写入数据之前 : java.nio.HeapByteBuffer[pos=0 lim=8 cap=8]
// pos - 游标位置, lim - 限制数量, cap - 最大容量
System.out.println("写入数据之前 : " + buffer);
// 写入字节数组到缓存
buffer.put(temp);
// 写入数据之后 : java.nio.HeapByteBuffer[pos=3 lim=8 cap=8]
// 游标为3, 限制为8, 容量为8
System.out.println("写入数据之后 : " + buffer);
// 重置游标 , lim = pos ; pos = 0;
buffer.flip();
// 重置游标之后 : java.nio.HeapByteBuffer[pos=0 lim=3 cap=8]
// 游标为0, 限制为3, cap为8
System.out.println("重置游标之后 : " + buffer);
// 清空Buffer, pos = 0; lim = cap;
// buffer.clear();
// get() -> 获取当前游标指向的位置的数据。
// System.out.println(buffer.get());
/*for(int i = 0; i < buffer.remaining(); i++){
// get(int index) -> 获取指定位置的数据。
int data = buffer.get(i);
System.out.println(i + " - " + data);
}*/
}
}
直接缓冲区与非直接缓冲区
- 非直接缓冲区:通过
allocate()
方法分配缓冲区,将缓冲区?建立在JVM的内存中。在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JVM内存开销,处理过程中有复制操作。 - 直接缓冲区:通过
allocateDirect()
方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。只有byteBuffer支持 - 字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
- 直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
- 直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。 Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在
访问期间或稍后的某个时间导致抛出不确定的异常。 - 字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。提供此方法是为了能够在
性能关键型代码中执行显式缓冲区管理。
3)Selector
大家经常听到的 多路复用 在 Java 世界中指的就是它,用于实现一个线程管理多个 Channel。
基本介绍:
-
Java 的 NIO, 用非阻塞的 IO 方式。 可以用一个线程, 处理多个的客户端连接, 就会使用到 Selector(选择器)
-
Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个Selector), 如果有事件发生, 便获取事件然后针对每个事件进行相应的处理。 这样就可以只用一个单线程去管理多个通道, 也就是管理多个连接和请求。
-
只有在 连接/通道 真正有读写事件发生时, 才会进行读写, 就大大地减少了系统开销, 并且不必为每个连接都创建一个线程, 不用去维护多个线程
-
避免了多线程之间的上下文切换导致的开销
代码逻辑:
-
- 当客户端连接时, 会通过 ServerSocketChannel 得到 SocketChannel
-
- Selector 进行监听 select 方法, 返回有事件发生的通道的个数.
-
- 将 socketChannel 注册到 Selector 上,register(Selector sel, int ops), 一个 selector 上可以注册多个 SocketChannel
-
- 注册后返回一个 SelectionKey,会和该 Selector 关联(集合)
-
- 进一步得到各个 SelectionKey (有事件发生)
-
- 在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()
-
- 可以通过 得到的 channel , 完成业务处理
创建selector
-
首先,我们开启一个 Selector。你们爱翻译成选择器也好,多路复用器也好。
Selector selector = Selector.open();
注册事件
-
将 Channel 注册到 Selector 上。前面我们说了,Selector 建立在非阻塞模式之上,所以注册到 Selector 的 Channel 必须要支持非阻塞模式,FileChannel 不支持非阻塞,我们这里讨论最常见的 SocketChannel 和 ServerSocketChannel。
// 将通道设置为非阻塞模式,因为默认都是阻塞模式的 channel.configureBlocking(false); // 注册 SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
register 方法的第二个 int 型参数(使用二进制的标记位)用于表明需要监听哪些感兴趣的事件,共以下四种事件:
注册方法返回值是 SelectionKey 实例,它包含了 Channel 和 Selector 信息,也包括了一个叫做 Interest Set 的信息,即我们设置的我们感兴趣的正在监听的事件集合。
-
调用 select() 方法获取通道信息。用于判断是否有我们感兴趣的事件已经发生了。
selector监听的事件
-
SelectionKey.OP_READ
对应 00000001,通道中有数据可以进行读取
-
SelectionKey.OP_WRITE
对应 00000100,可以往通道中写入数据
-
SelectionKey.OP_CONNECT
对应 00001000,成功建立 TCP 连接
-
SelectionKey.OP_ACCEPT
对应 00010000,接受 TCP 连接
我们可以同时监听一个 Channel 中的发生的多个事件,比如我们要监听 ACCEPT 和 READ 事件,那么指定参数为二进制的 00010001 即十进制数值 17 即可。
Selector常用方法
- select()
调用此方法,会将上次 select 之后的准备好的 channel 对应的 SelectionKey 复制到 selected set 中。如果没有任何通道准备好,这个方法会阻塞,直到至少有一个通道准备好。
-
selectNow()
功能和 select 一样,区别在于如果没有准备好的通道,那么此方法会立即返回 0。
-
select(long timeout)
看了前面两个,这个应该很好理解了,如果没有通道准备好,此方法会等待一会
-
wakeup()
这个方法是用来唤醒等待在 select() 和 select(timeout) 上的线程的。如果 wakeup() 先被调用,此时没有线程在 select 上阻塞,那么之后的一个 select() 或 select(timeout) 会立即返回,而不会阻塞,当然,它只会作用一次。
Selector.java
// 抽象类,实现类是SelectorImpl,他还有子类是WindowsSelectorImpl
public abstract class Selector implements Closeable {
protected Selector() { }
// 得到一个选择器对象
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
// 管理在当前selector上注册的SelectionKey
public abstract Set<SelectionKey> keys();
// 发生时间的SelectionKey
public abstract Set<SelectionKey> selectedKeys();
// 监控所有注册的通道,发生事件时,把SelectionKey加入到selectedKeys中
public abstract int select(long timeout) throws IOException; // 阻塞指定毫秒后返回
public abstract int select() throws IOException; // 阻塞
public abstract int selectNow() throws IOException; // 不阻塞,立马返回
public abstract Selector wakeup(); //唤醒selector
SelectionKey.java
// SelectionKey保存了处理当前请求的ChanneI和Selector,并且提供了不同的操作类型。
public abstract class SelectionKey {
protected SelectionKey() { }
// 得到与SelectionKey关联的通道
public abstract SelectableChannel channel();
public abstract Selector selector();//得到与之关联的 Selector 对象
// Channel在注册Selector的时候可以通过register的第二个参数选择特定的操作,这里的操作就是在SelectionKey中定义的,一共有4种:
// 只有在register方法中注册了相应的操作SeIector才会关心相应类型操作的请求。
public static final int OP_READ = 1 << 0; // 读
public static final int OP_WRITE = 1 << 2; // 写
public static final int OP_CONNECT = 1 << 3; // 连接
public static final int OP_ACCEPT = 1 << 4; // 接受请求
// 得到与之关联的共享数据
private volatile Object attachment = null;
public final Object attachment();//得到与之关联的共享数据
// 设置或改变监听事件
public abstract SelectionKey interestOps(int ops);
// 是否可以读
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
public final boolean isWritable();//是否可以写
public final boolean isAcceptable();//是否可以 accept
// 取消注册
public abstract void cancel();
Channel和并没有谁属于谁的关系,就好像一个分拣员可以为多个地区分拣货物而每个地区也可以有多个分拣员来分拣一样,它们就好像数据库里的多对多的关系,不过Selector这个分拣员分拣得更细,它可以按不同的类型来分拣,分拣后的结果保存在SeIectionKey中,可以分别通过SelectionKey的channel方法和selector方法来获取对应的Channel和Seletor,而且还可以通过isAcceptable,isConnecptable,isReadable和isWritable方法来判断是什么类型的操作。
把通道注册到选择器上,选择器就能监听通道发生的【读,写,连接,接收数据】事件了。
- 选择器的创建:
Selector selector = Selector.open();
- 通过
serverSelector.select(1)
查询一次有没有事件发生 - 如果有时间发生用
serverSelector.selectedKeys();
获取事件的key:- key中有事件的类型,可以通过
key.isAcceptable()
判断 - key中有发生事件的通道,
key.channel()
。- 在连接事件的时候,他获得的是服务器端的通道,通过服务器端的网络通道.accept()可以获得要连接的客户端通道。我们要做的是把客户端通道注册到selector上
- 有其他事件的时候,他获得的是客户端的通道,因为我们之前把客户端的通道有注册上selector上了
- key中有事件的类型,可以通过
- 如果事件的类型是accept:
- 把客户端通道注册到选择器上
- 如果事件的类型是read
- 把服务器的内容传到客户端的通道里
NIO通道代码
NIO服务端代码
- 将服务端socket注册到selector
- 轮询selector.select()
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;
/**
* NIO服务端
*/
public class NIOServer {
// 通道管理器
private Selector selector;
/**
* 启动服务端测试
*/
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(8000);
server.listen();
}
/**
* 获得一个ServerSocket通道,并对该通道做一些初始化的工作
*/
public void initServer(int port) throws IOException {
// 获得一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 获得一个通道管理器 //selector的酒店招聘的服务员,我们需要酒店告诉服务员做什么事情,对什么样的网络事件感兴趣
this.selector = Selector.open();
// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
// 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);//观察门口有没有人进来
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*/
public void listen() throws IOException {
System.out.println("服务端启动成功!");
// 轮询访问selector
while (true) {
// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();//int client = selector.select()
// 获得selector中选中的项的迭代器,选中的项为注册的事件 // selector中有keys,key中有事件类型,比如key.isAcceptable()或者key.isReadable()
Iterator<?> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
// 拿到当前事件key
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 分发处理事件 // dispatch()
handler(key);
}
}
}
/**
* 处理请求
*/
public void handler(SelectionKey key) throws IOException {
// 如果是客户端请求连接事件
if (key.isAcceptable()) {
handlerAccept(key);// 把客户端的通道注册到selector上
} else if (key.isReadable()) { // 获得了可读的事件 // 客户端通道已经读了,你读到buffer吧
handelerRead(key);
}
// Runnable r = (Runnable)(key.attachment()); // 去除的是new Acceptor(selector,ssc)
// if(r!=null) r.run();
}
/**
* 处理连接请求,相当于BIO的socket.accept
*/
public void handlerAccept(SelectionKey key) throws IOException {
// key里拿到服务端socket
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);
// 在这里可以给客户端发送信息哦
System.out.println("新的客户端连接");
// 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
}
/**
* 处理读的事件,相当于BIO的socket.read
*/
public void handelerRead(SelectionKey key) throws IOException {
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if(read > 0){
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:" + msg);
//回写数据
ByteBuffer outBuffer = ByteBuffer.wrap("好的".getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}else{
System.out.println("客户端关闭");
key.cancel();//关闭通道,这里不合理,应该放到try catch中
}
}
}
NIO客户端代码
@Test //读取本地文件发送到服务端 //非阻塞模式完成客户端向服务器端传输数据
public void client() throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",7498));
// 切换成非 阻塞模式
socketChannel.configureBlocking(false);
// 创建一个输入通道
FileChannel inputChannel = FileChannel.open(Paths.get("D:\\NIO.md"), StandardOpenOption.READ);
//分配缓存区
ByteBuffer clientBuffer = ByteBuffer.allocate(1024);
//读取本地文件发送到服务端
while (inputChannel.read(clientBuffer) != -1){
clientBuffer.flip();
socketChannel.write(clientBuffer);// 输入通道读到buffer中,buffer再写到网络通道中
clientBuffer.clear();
}
socketChannel.close();
inputChannel.close();
}
服务端代码
@Test // 服务器接收文件
public void server() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 非阻塞
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(7498));
FileChannel outputChannel = FileChannel.open(Paths.get("C:\\Users\\admin\\Desktop\\test.md"),StandardOpenOption.WRITE,StandardOpenOption.CREATE);
// 选择器
Selector selector = Selector.open();
// 将通道注册到选择器上,并制定监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 轮询式获得选择器里的已经准备就绪的事件
while (selector.select() > 0 ){
// 获取已经就绪的监听事件
Iterator<SelectionKey> selectorIterator = selector.selectedKeys().iterator();
// 迭代获取
while (selectorIterator.hasNext()){
// 获取准备就绪的事件
SelectionKey key = selectorIterator.next();
SocketChannel socketChannel = null;
// 判断是什么事件
if (key.isAcceptable()){
// 或接受就绪,,则获取客户端连接
socketChannel = serverSocketChannel.accept();
//切换非阻塞方式
socketChannel.configureBlocking(false);
// 注册到选择器上
socketChannel.register(selector,SelectionKey.OP_READ);
} else if (key.isReadable()){
// 获取读就绪通道
SocketChannel readChannel = (SocketChannel) key.channel();
readChannel.configureBlocking(false);
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int len = 0;
while ( (len = readChannel.read(readBuffer)) != -1){ // 读到buffer里,buffer再写到输出通道
readBuffer.flip();
System.out.println(new String(readBuffer.array(),0,len));
outputChannel.write(readBuffer);
readBuffer.clear();
}
readChannel.close();
outputChannel.close();
}
}
// 取消选择键
selectorIterator.remove();
/**
* serverSocketChannel.close();不用关闭的,这是服务器端”
*/
// serverSocketChannel.close();
}
}
先启动服务器端,再启动客户端。
NIO的常用方法
// 创建一个Server端的通道
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
// 监听端口
listenerChannel.socket().bind(new InetSocketAddress(3333));
// 设置为非阻塞
listenerChannel.configureBlocking(false);
// 创建一个选择器
Selector serverSelector = Selector.open();
// 把通道注册到选择器,selector监听通道上的OP_ACCEPT或/READ等类型的事件
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
为什么大家都不愿意用 JDK 原生 NIO 进行开发呢?从上面的代码中大家都可以看出来,是真的难用!除了编程复杂、编程模型难之外,它还有以下让人诟病的问题:
- JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%
- 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug
Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。
NIO示例
NIOServer
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
public class NIOServer implements Runnable {
// 多路复用器, 选择器。 用于注册通道的。
private Selector selector;
// 定义了两个缓存。分别用于读和写。 初始化空间大小单位为字节。
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
public static void main(String[] args) {
new Thread(new NIOServer(9999)).start();
}
public NIOServer(int port) {
init(port);
}
private void init(int port){
try {
System.out.println("server starting at port " + port + " ...");
// 开启多路复用器
this.selector = Selector.open();
// 开启服务通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 非阻塞, 如果传递参数true,为阻塞模式。
serverChannel.configureBlocking(false);
// 绑定端口
serverChannel.bind(new InetSocketAddress(port));
// 注册,并标记当前服务通道状态
/*
* register(Selector, int)
* int - 状态编码
* OP_ACCEPT : 连接成功的标记位。
* OP_READ : 可以读取数据的标记
* OP_WRITE : 可以写入数据的标记
* OP_CONNECT : 连接建立后的标记
*/
serverChannel.register(this.selector, SelectionKey.OP_ACCEPT);
System.out.println("server started.");
} catch (IOException e) {
e.printStackTrace();
}
}
public void run(){
while(true){
try {
// 阻塞方法,当至少一个通道被选中,此方法返回。
// 通道是否选择,由注册到多路复用器中的通道标记决定。
this.selector.select();//调用select方法
// 返回以选中的通道标记集合, 集合中保存的是通道的标记。相当于是通道的ID。
Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
while(keys.hasNext()){
SelectionKey key = keys.next();
// 将本次要处理的通道从集合中删除,下次循环根据新的通道列表再次执行必要的业务逻辑
keys.remove();
// 通道是否有效
if(key.isValid()){
// 阻塞状态
try{
if(key.isAcceptable()){
accept(key);
}
}catch(CancelledKeyException cke){
// 断开连接。 出现异常。
key.cancel();
}
// 可读状态
try{
if(key.isReadable()){
read(key);
}
}catch(CancelledKeyException cke){
key.cancel();
}
// 可写状态
try{
if(key.isWritable()){
write(key);
}
}catch(CancelledKeyException cke){
key.cancel();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void accept(SelectionKey key){
try {
// 此通道为init方法中注册到Selector上的ServerSocketChannel
ServerSocketChannel serverChannel = (ServerSocketChannel)key.channel();
// 阻塞方法,当客户端发起请求后返回。 此通道和客户端一一对应。
SocketChannel channel = serverChannel.accept();
channel.configureBlocking(false);
// 设置对应客户端的通道标记状态,此通道为读取数据使用的。
channel.register(this.selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
private void write(SelectionKey key){
this.writeBuffer.clear();
SocketChannel channel = (SocketChannel)key.channel();
Scanner reader = new Scanner(System.in);
try {
System.out.print("put message for send to client > ");
String line = reader.nextLine();
// 将控制台输入的字符串写入Buffer中。 写入的数据是一个字节数组。
writeBuffer.put(line.getBytes("UTF-8"));
writeBuffer.flip();
channel.write(writeBuffer);
channel.register(this.selector, SelectionKey.OP_READ);//注册通道为可读
} catch (IOException e) {
e.printStackTrace();
}
}
private void read(SelectionKey key){//读到服务端
try {
// 清空读缓存。
this.readBuffer.clear();
// 获取通道
SocketChannel channel = (SocketChannel)key.channel();
// 将通道中的数据读取到缓存中。通道中的数据,就是客户端发送给服务器的数据。
int readLength = channel.read(readBuffer);
// 检查客户端是否写入数据。
if(readLength == -1){
// 关闭通道
key.channel().close();
// 关闭连接
key.cancel();
return;
}
/*
* flip, NIO中最复杂的操作就是Buffer的控制。
* Buffer中有一个游标。游标信息在操作后不会归零,如果直接访问Buffer的话,数据有不一致的可能。
* flip是重置游标的方法。NIO编程中,flip方法是常用方法。
*/
this.readBuffer.flip();
// 字节数组,保存具体数据的。 Buffer.remaining() -> 是获取Buffer中有效数据长度的方法。
byte[] datas = new byte[readBuffer.remaining()];
// 是将Buffer中的有效数据保存到字节数组中。
readBuffer.get(datas);
System.out.println("from " + channel.getRemoteAddress() + " client : " + new String(datas, "UTF-8"));
// 注册通道, 标记为写操作。
channel.register(this.selector, SelectionKey.OP_WRITE);//注册通道为可写
} catch (IOException e) {
e.printStackTrace();
try {
key.channel().close();
key.cancel();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
更多推荐
所有评论(0)