部分内容参考:https://blog.csdn.net/yjp198713/column/info/18912
I/O
1. 概述
1.1 I/O 是什么
输入/输出(I/O
)是在主存和外部设备(例如磁盘驱动器、終端和网络)之间复制数据的过程。
输入操作是从I/O
设备复制数据到主存,而输出操作是从主存复制数据到I/O
设备。
1.2 I/O 对性能的影响
在我们的程序中,通常涉及到大量的I/O
操作,如一个Http
请求、数据库的CRUD
、文件的上传与下载等。
但是用户进程并不能直接控制主存将数据在外部设备之间的复制,如用户程序想要对磁盘中操作系统的内容进行修改,这种情况显然是不被允许的,所以这就需要操作系统的介入了。
首先,我们可以看一下用户进行一次I\O
操作的流程。应用进程发出读请求,由用户态切入内核态进行系统调用,接着等待磁盘的数据拷贝到内核态,然后由内核态复制到用户态空间,然后再切换回用户态。当用户对用户态空间的数据查看并修改后,发出写请求,由用户态切入内核态进行系统调用,将用户态的空间拷贝到内核态,最后在拷贝到磁盘,结束后切换回用户态。
可以看到,完成一次读写操作,涉及到四次用户态与内核态的转换,同时需要经历四次的拷贝。这一切操作都非常影响性能,所以I\O
操作对性能的影响非常严重,由此产生了多种I\O
模型来提升性能。
1.3 同步与异步
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。
1.4 阻塞与非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
还是上面的例子你:打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。
2. BIO (Blocking I/O)
BIO
即为我们传统意义上的I/O
,阻塞式I/O
。
BIO
模型示意图:
通过上图可以看到,应用进程发出一个系统调用后,便一直等待数据到达,其间一直没有做任何事。
2.1 传统 BIO 示例
这里,我们通过代码,体验一下这种情况:
package BIO;
import java.util.Scanner;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author wangzhao
* @date 2020/6/19 16:34
*/
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8888);
while (true){
System.out.println("等待客户端连接!!!");
Socket socket = ss.accept();
System.out.println(socket.getRemoteSocketAddress() + "连接了服务器!!!");
Scanner scanner = new Scanner(socket.getInputStream());
System.out.println("等待客户端输入!!!");
String msg = scanner.nextLine();
System.out.println(msg);
}
}
}
package BIO;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @author wangzhao
* @date 2020/6/19 16:51
*/
public class BIOClient {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动了!");
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream outputStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
outputStream.write(new String(s).getBytes());
socket.close();
}
}
1. 启动服务器端
2. 启动一个客户端程序
3. 在启动一个客户端程序(此时有两个客户端程序在运行)
4. 第一个客户端进行输入
观察上述的代码,可以发现,服务端在两处被阻塞,在阻塞其间无法处理其他客户端的连接,这样的话就再阻塞期间无法处理其他的事件,效率非常低下。
2.2 伪异步 BIO 示例
我们可以为每一个Socket
开启一个线程,这样阻塞的话,也只会让其中的一个线程被阻塞,不会影响其他线程的Socket
通信。
package BIO;
import java.util.Scanner;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author wangzhao
* @date 2020/6/19 16:34
*/
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8888);
while (true){
System.out.println("等待客户端连接!!!");
Socket socket = ss.accept();
System.out.println(socket.getRemoteSocketAddress() + "连接了服务器!!!");
new Thread(() -> {
Scanner scanner = null;
try {
scanner = new Scanner(socket.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("等待客户端输入!!!");
String msg = scanner.nextLine();
System.out.println(msg);
}).start();
}
}
}
为每一个Socket
都创建一个线程,线程的频繁创建和销毁非常浪费资源,所以可以使用线程池替换线程的创建。
不过因为它的底层任然是同步阻塞的BIO
模型,因此无法从根本上解决问题。
2.3 阻塞的问题根源
为什么accept()
、read()
方法会被阻塞。API
文档中对于 serverSocket.accept()
方法的使用描述:
Listens for a connection to be made to this socket and accepts it.
The method blocks until a connection is made.
服务器线程发起一个accept
动作,询问操作系统 是否有新的socket
套接字信息从端口xx发送过来。
注意,是询问操作系统。也就是说socket
套接字的IO
模式支持是基于操作系统的,那么自然同步IO
/异步IO
的支持就是需要操作系统级别的了。如下图:
如果操作系统没有发现有套接字从指定的端口xx
来,那么操作系统就会等待。这样serverSocket.accept()
方法就会一直等待。这就是为什么accept()
方法为什么会阻塞:它内部的实现是使用的操作系统级别的同步IO
。
3. NIO (No Blocking I/O)
NIO
非阻塞式I/O
。
NIO
模型示意图:
通过模型示意图可以看到,NIO
的应用进程每隔一段时间询问内核数据有无被准备好,没有的话就可以执行自己的事情。虽然这段时间可能在我们看来非常短暂,但是CPU
的运行速度非常快,完全可以执行大量的运算。
3.1 Java NIO 简介
Java NIO
模型图如下:
Channel
通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。一个通道会有一个专属的文件状态描述符。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据。
JDK API
中的Channel
的描述是:
通道表示到外部设备(如硬件设备、文件、网络套接字或程序组件)的开放连接,该实体能够执行一个或多个不同的I/O操作,例如读取或写入。
道通道是打开的还是关闭的?一个通道在创建时是打开的,一旦关闭它就保持关闭状态。
一旦通道关闭,任何对其调用I/O操作的尝试都将导致引发ClosedChannelException。
通道是否打开可以通过调用其 isOpen 方法进行测试。
JAVA NIO
框架中,Channel
通道的类型有:
- 所有被
Selector
(选择器)注册的通道,只能是继承SelectableChannel
类的子类。 ServerSocketChannel
:应用服务器程序的监听通道。只有通过这个通道,应用程序才能向操作系统注册支持“多路复用IO”的端口监听。同时支持UDP协议和TCP协议。ScoketChannel
:TCP Socket套接字的监听通道,一个Socket套接字对应了一个客户端IP:端口
到服务器IP:端口
的通信连接。DatagramChannel
:UDP 数据报文的监听通道。
Buffer
数据缓存区:在JAVA NIO
框架中,为了保证每个通道的数据读写速度。JAVA NIO
框架为每一种需要支持数据读写的通道集成了Buffer
的支持。
这句话怎么理解呢?例如ServerSocketChannel
通道它只支持对OP_ACCEPT
事件的监听,所以它是不能直接进行网络数据内容的读写的。所以ServerSocketChannel
是没有集成Buffer
的。
Buffer
有两种工作模式:写模式和读模式。在读模式下,应用程序只能从Buffer
中读取数据,不能进行写操作。但是在写模式下,应用程序是可以进行读操作的,这就表示可能会出现脏读的情况。所以一旦您决定要从Buffer
中读取数据,一定要将Buffer
的状态改为读模式。
Buffer
中的参数项:
在进行读写时,变化如下:
- 我们通过
ByteBuffer.allocate(11)
方法创建一个11
个byte
的数组缓冲区,初始状态如图所示,position
的位置为0
,capacity
和limit
默认都是数组长度。
- 当我们写入
5
个字节时位置变化如下图所示:
- 这时我们需要将缓冲区的
5
个字节数据写入Channel
通信信道,所以我们需要调用byteBuffer.flip()
方法,数组的状态又发生如下变化:
这时底层操作系统就可以从缓冲区中正确读取这 5
个字节数据发送出去了。在下一次写数据之前我们在调一下 clear()
方法。缓冲区的索引状态又回到初始位置。
这里还要说明一下 mark
,当我们调用 mark()
时,它将记录当前 position
的前一个位置,当我们调用 reset
时,position
将恢复 mark
记录下来的值。
还有一点需要说明,通过 Channel
获取的 I/O
数据首先要经过操作系统的 Socket
缓冲区再将数据复制到 Buffer
中,这个的操作系统缓冲区就是底层的 TCP
协议关联的 RecvQ
或者 SendQ
队列,从操作系统缓冲区到用户缓冲区复制数据比较耗性能,Buffer
提供了另外一种直接操作操作系统缓冲区的的方式即 ByteBuffer.allocateDirector(size)
,这个方法返回的 byteBuffer
就是与底层存储空间关联的缓冲区,它的操作方式与 linux2.4
内核的 sendfile
操作方式类似。
Selector
Selector
的英文含义是“选择器”,也可以把它称之为“轮询代理器”、“事件订阅器”、“Channel
容器管理机”都行。
事件订阅和Channel
管理:
应用程序将向Selector
对象注册需要它关注的Channel
,以及具体的某一个Channel
会对哪些IO
事件感兴趣。Selector
中也会维护一个“已经注册的Channel
”的容器。
以下代码来自WindowsSelectorImpl
实现类中,对已经注册的Channel
的管理容器:
// Initial capacity of the poll array
private final int INIT_CAP = 8;
// Maximum number of sockets for select().
// Should be INIT_CAP times a power of 2
private final static int MAX_SELECTABLE_FDS = 1024;
// The list of SelectableChannels serviced by this Selector. Every mod
// MAX_SELECTABLE_FDS entry is bogus, to align this array with the poll
// array, where the corresponding entry is occupied by the wakeupSocket
private SelectionKeyImpl[] channelArray = new SelectionKeyImpl[INIT_CAP];
轮询代理: 应用层不再通过阻塞模式或者非阻塞模式直接询问操作系统“事件有没有发生”,而是由Selector
代其询问。
3.2 NIO 示例
package NIO;
import java.nio.channels.*;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* @author wangzhao
* @date 2020/6/19 19:54
*/
public class NIOServer {
public static void main(String arg[]) {
System.out.println("服务器端启动");
try {
//1、创建服务器端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2、切换异步非阻塞
serverSocketChannel.configureBlocking(false);
//3、切换读取模式
serverSocketChannel.bind(new InetSocketAddress(8888));
//4、获取选择器
Selector selector = Selector.open();
//5、将通道注册到选择器中去,并且监听已经收到的事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//6、轮询获取“已经准备就绪的”的事件
while (selector.select() > 0) {
//7、获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
//8、获取准备“就绪”的是事件
SelectionKey sk = iterator.next();
//9、 判断具体是什么事件准备就绪
if (sk.isAcceptable()) {
//10、 若“接收就绪”,获取客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println(socketChannel.getRemoteAddress() + "连接了!!!");
//11、 切换非阻塞模式
socketChannel.configureBlocking(false);
//12、 将该通道注册到选择器上
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()){
//13、 获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14、 指定缓冲区大小,读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buf)) > 0) { //sChannel 读取数据到ByteBuffer
buf.flip(); //切换到读取模式
String str = new String(buf.array(), 0, len); //读取
System.out.println("msg = "+str);
buf.clear();
}
}
//15. 取消选择键 SelectionKey
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
package NIO;
import java.io.IOException;
import java.util.Scanner;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @author wangzhao
* @date 2020/6/19 20:16
*/
public class NIOClient {
public static void main(String[] args) {
System.out.println("客户端已经启动.............");
try {
//1、创建socker通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
//2、切换异步非阻塞
socketChannel.configureBlocking(false);
//3、指定缓冲区大小
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
byteBuffer.put(scanner.nextLine().getBytes());
//4、切换到读取模式
byteBuffer.flip();
//5、写入到缓冲区
socketChannel.write(byteBuffer);
byteBuffer.clear();
//6、关闭通道
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 首先我们启动服务端
- 接着运行一个客户端
- 再运行一个客户端
4. 客户端进行输入
通过对比NIO Server
和伪异步BIO
的代码,我们实现了单线程便与之等同的功能。
3.3 I/O 多路复用模型
NIO
中单线程的环境下并不会发生阻塞,这是因为其采用了I/O
多路复用的模型。
在多路复用IO
模型中,会有一个线程(Java
中的Selector
)不断去轮询多个socket
的状态,只有当socket
真正有读写事件时,才真正调用实际的IO
读写操作。因为在多路复用IO
模型中,只需要使用一个线程就可以管理多个socket
,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket
读写事件进行时,才会使用IO
资源,所以它大大减少了资源占用。
4. AIO (Async IO)
异步IO
则采用“订阅-通知”模式:即应用程序向操作系统注册IO
监听,然后继续做自己的事情。当操作系统发生IO
事件,并且准备好数据后,在主动通知应用程序,触发相应的函数。
- 和同步
IO
一样,异步IO
也是由操作系统进行支持的。微软的windows系统提供了一种异步IO
技术:IOCP
(I/O CompletionPort
,I/O
完成端口); - Linux下由于没有这种异步
IO
技术,所以使用的是epoll
对异步IO
进行模拟。
4.1 Java AIO 简介
4.2 AIO 示例
package demo.com.test.io.aio;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AioSocketServer {
private static final Object waitObject = new Object();
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
/*
* 对于使用的线程池技术,我一定要多说几句
* 1、Executors是线程池生成工具,通过这个工具我们可以很轻松的生成“固定大小的线程池”、“调度池”、“可伸缩线程数量的池”。具体请看API Doc
* 2、当然您也可以通过ThreadPoolExecutor直接生成池。
* 3、这个线程池是用来得到操作系统的“IO事件通知”的,不是用来进行“得到IO数据后的业务处理的”。要进行后者的操作,您可以再使用一个池(最好不要混用)
* 4、您也可以不使用线程池(不推荐),如果决定不使用线程池,直接AsynchronousServerSocketChannel.open()就行了。
* */
ExecutorService threadPool = Executors.newFixedThreadPool(20);
AsynchronousChannelGroup group = AsynchronousChannelGroup.withThreadPool(threadPool);
final AsynchronousServerSocketChannel serverSocket = AsynchronousServerSocketChannel.open(group);
//设置要监听的端口“0.0.0.0”代表本机所有IP设备
serverSocket.bind(new InetSocketAddress("0.0.0.0", 8083));
//为AsynchronousServerSocketChannel注册监听,注意只是为AsynchronousServerSocketChannel通道注册监听
//并不包括为 随后客户端和服务器 socketchannel通道注册的监听
serverSocket.accept(null, new ServerSocketChannelHandle(serverSocket));
//等待,以便观察现象(这个和要讲解的原理本身没有任何关系,只是为了保证守护线程不会退出)
synchronized(waitObject) {
waitObject.wait();
}
}
}
package demo.com.test.io.aio;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* 这个处理器类,专门用来响应 ServerSocketChannel 的事件。
* ServerSocketChannel只有一种事件:接受客户端的连接
* @author keep_trying
*/
public class ServerSocketChannelHandle implements CompletionHandler<AsynchronousSocketChannel, Void> {
/**
* 日志
*/
private static final Log LOGGER = LogFactory.getLog(ServerSocketChannelHandle.class);
private AsynchronousServerSocketChannel serverSocketChannel;
/**
* @param serverSocketChannel
*/
public ServerSocketChannelHandle(AsynchronousServerSocketChannel serverSocketChannel) {
this.serverSocketChannel = serverSocketChannel;
}
/**
* 注意,我们分别观察 this、socketChannel、attachment三个对象的id。
* 来观察不同客户端连接到达时,这三个对象的变化,以说明ServerSocketChannelHandle的监听模式
*/
@Override
public void completed(AsynchronousSocketChannel socketChannel, Void attachment) {
ServerSocketChannelHandle.LOGGER.info("completed(AsynchronousSocketChannel result, ByteBuffer attachment)");
//每次都要重新注册监听(一次注册,一次响应),但是由于“文件状态标示符”是独享的,所以不需要担心有“漏掉的”事件
this.serverSocketChannel.accept(attachment, this);
//为这个新的socketChannel注册“read”事件,以便操作系统在收到数据并准备好后,主动通知应用程序
//在这里,由于我们要将这个客户端多次传输的数据累加起来一起处理,所以我们将一个stringbuffer对象作为一个“附件”依附在这个channel上
//
ByteBuffer readBuffer = ByteBuffer.allocate(2550);
socketChannel.read(readBuffer, new StringBuffer(), new SocketChannelReadHandle(socketChannel , readBuffer));
}
/* (non-Javadoc)
* @see java.nio.channels.CompletionHandler#failed(java.lang.Throwable, java.lang.Object)
*/
@Override
public void failed(Throwable exc, Void attachment) {
ServerSocketChannelHandle.LOGGER.info("failed(Throwable exc, ByteBuffer attachment)");
}
}
package demo.com.test.io.aio;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* 负责对每一个socketChannel的数据获取事件进行监听。<p>
*
* 重要的说明:一个socketchannel都会有一个独立工作的SocketChannelReadHandle对象(CompletionHandler接口的实现),
* 其中又都将独享一个“文件状态标示”对象FileDescriptor、
* 一个独立的由程序员定义的Buffer缓存(这里我们使用的是ByteBuffer)、
* 所以不用担心在服务器端会出现“窜对象”这种情况,因为JAVA AIO框架已经帮您组织好了。<p>
*
* 但是最重要的,用于生成channel的对象:AsynchronousChannelProvider是单例模式,无论在哪组socketchannel,
* 对是一个对象引用(但这没关系,因为您不会直接操作这个AsynchronousChannelProvider对象)。
* @author keep_trying
*/
public class SocketChannelReadHandle implements CompletionHandler<Integer, StringBuffer> {
/**
* 日志
*/
private static final Log LOGGER = LogFactory.getLog(SocketChannelReadHandle.class);
private AsynchronousSocketChannel socketChannel;
/**
* 专门用于进行这个通道数据缓存操作的ByteBuffer<br>
* 当然,您也可以作为CompletionHandler的attachment形式传入。<br>
* 这是,在这段示例代码中,attachment被我们用来记录所有传送过来的Stringbuffer了。
*/
private ByteBuffer byteBuffer;
public SocketChannelReadHandle(AsynchronousSocketChannel socketChannel , ByteBuffer byteBuffer) {
this.socketChannel = socketChannel;
this.byteBuffer = byteBuffer;
}
/* (non-Javadoc)
* @see java.nio.channels.CompletionHandler#completed(java.lang.Object, java.lang.Object)
*/
@Override
public void completed(Integer result, StringBuffer historyContext) {
//如果条件成立,说明客户端主动终止了TCP套接字,这时服务端终止就可以了
if(result == -1) {
try {
this.socketChannel.close();
} catch (IOException e) {
SocketChannelReadHandle.LOGGER.error(e);
}
return;
}
SocketChannelReadHandle.LOGGER.info("completed(Integer result, Void attachment) : 然后我们来取出通道中准备好的值");
/*
* 实际上,由于我们从Integer result知道了本次channel从操作系统获取数据总长度
* 所以实际上,我们不需要切换成“读模式”的,但是为了保证编码的规范性,还是建议进行切换。
*
* 另外,无论是JAVA AIO框架还是JAVA NIO框架,都会出现“buffer的总容量”小于“当前从操作系统获取到的总数据量”,
* 但区别是,JAVA AIO框架中,我们不需要专门考虑处理这样的情况,因为JAVA AIO框架已经帮我们做了处理(做成了多次通知)
* */
this.byteBuffer.flip();
byte[] contexts = new byte[1024];
this.byteBuffer.get(contexts, 0, result);
this.byteBuffer.clear();
try {
String nowContent = new String(contexts , 0 , result , "UTF-8");
historyContext.append(nowContent);
SocketChannelReadHandle.LOGGER.info("================目前的传输结果:" + historyContext);
} catch (UnsupportedEncodingException e) {
SocketChannelReadHandle.LOGGER.error(e);
}
//如果条件成立,说明还没有接收到“结束标记”
if(historyContext.indexOf("over") == -1) {
return;
}else{
//清空已经读取的缓存,并从新切换为写状态(这里要注意clear()和capacity()两个方法的区别)
this.byteBuffer.clear();
SocketChannelReadHandle.LOGGER.info("客户端发来的信息======message : " + historyContext);
//======================================================
// 当然接受完成后,可以在这里正式处理业务了
//======================================================
//回发数据,并关闭channel
ByteBuffer sendBuffer = null;
try {
sendBuffer = ByteBuffer.wrap(URLEncoder.encode("你好客户端,这是服务器的返回数据", "UTF-8").getBytes());
socketChannel.write(sendBuffer);
socketChannel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//=========================================================================
// 和上篇文章的代码相同,我们以“over”符号作为客户端完整信息的标记
//=========================================================================
SocketChannelReadHandle.LOGGER.info("=======收到完整信息,开始处理业务=========");
historyContext = new StringBuffer();
//还要继续监听(一次监听一次通知)
this.socketChannel.read(this.byteBuffer, historyContext, this);
}
/* (non-Javadoc)
* @see java.nio.channels.CompletionHandler#failed(java.lang.Throwable, java.lang.Object)
*/
@Override
public void failed(Throwable exc, StringBuffer historyContext) {
SocketChannelReadHandle.LOGGER.info("=====发现客户端异常关闭,服务器将关闭TCP通道");
try {
this.socketChannel.close();
} catch (IOException e) {
SocketChannelReadHandle.LOGGER.error(e);
}
}
}
在JAVA NIO
框架中,我们说到了一个重要概念“selector
”(选择器)。它负责代替应用查询所有已注册的通道到操作系统中进行IO
事件轮询、管理当前注册的通道集合,定位发生事件的通道等操作;
但是在JAVA AIO
框架中,由于应用程序不是“轮询”方式,而是订阅-通知方式,所以不再需要“selector
”(选择器)了,改由channel
通道直接到操作系统注册监听。