这几天在看面试的东西,可能是自己比较笨,花了快两天的时间才理清楚。特此记录一下。
首先我们要理解的一个很重要概念是,客户端连接和传送数据是分开的,连接不代表立马会传输数据。
说说BIO,NIO,AIO到底是什么东西
BIO:同步堵塞
NIO:异步堵塞
AIO:异步非堵塞
看到这里你肯定一脸懵逼,这到底是什么意思,别急,慢慢看。
在JAVA里面IO分两块,一块是操作文件的,一块是操作网络的。下面先讲网络
网络:
一个通俗易懂的例子
下面先举一个通俗的例子加深理解,参演人员:你(客户端),酒吧(服务端),服务员(服务端线程)
BIO:你去酒吧喝酒,酒吧立马给你安排一个专门的服务员小姐姐只为你服务。你是不是觉得美滋滋,但是人家酒吧的压力就大了,那么多咱们这种屌丝,哪里有那么多小姐姐?(客户端会一直占用服务端的线程)
NIO:你去酒吧喝酒,酒吧就TM一个小姐姐,你要喝酒是吧,好啊,坐吧。小姐姐会一会儿过来问你,先生,要酒吗?你说要的话她就给你满上。倒完就跑,跑去问别人。你肯定觉得,难受啊兄嘚。但是对人家酒吧来说就舒服了,人家只要一个服务员就搞定了。(客户端只有在需要的时候才占用服务端的线程,但同时服务端会不停的问你是否需要)
AIO:与NIO的不同在于,连问你的小姐姐都木有了(≧ ﹏ ≦),你要酒了,自己喊,小姐姐,小姐姐,来帮我倒个酒呗。(客户端需要的时候会主动通知服务端)
关键代码实现
篇幅有限,此处只列举关键代码,完整代码请下载后面的项目,自己运行一遍,什么都懂了。
BIO:
ServerNormal:
public final class ServerNormal {
public synchronized static void start(int port) throws IOException{
if(server != null) return;
try{
//通过构造函数创建ServerSocket
//如果端口合法且空闲,服务端就监听成功
server = new ServerSocket(port);
System.out.println("服务器已启动,端口号:" + port);
//通过无线循环监听客户端连接
//如果没有客户端接入,将阻塞在accept操作上。
while(true){
Socket socket = server.accept();
//当有新的客户端接入时,会执行下面的代码
//然后创建一个新的线程处理这条Socket链路
new Thread(new ServerHandler(socket)).start();
}
}finally{
//一些必要的清理工作
if(server != null){
System.out.println("服务器已关闭。");
server.close();
server = null;
}
}
}
}
ServerHandler:
public class ServerHandler implements Runnable{
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
String expression;
String result;
while(true){
//主要原因酒在于(expression = in.readLine())==null这句代码是堵塞的,时间都花在了等待上。
if((expression = in.readLine())==null) break;
System.out.println("服务器收到消息:" + expression);
}
}
可以看出来服务端是在客户端每次有新请求时都去启动了一个新线程的去接收客户端发送的数据的,BufferedReader.readLine()会读满缓冲区或者在读到文件末尾(遇到:”/r”、”/n”、”/r/n”)才返回,这样就会导致网络很慢的适合线程可能会卡很久,从而导致服务端积聚大量的线程,大量的线程会严重影响服务器性能,甚至罢工。如何解决这个问题呢?NIO横空出世。
NIO
ServerHandle
public class ServerHandle implements Runnable {
@Override
public void run() {
//循环遍历selector
while (started) {
try {
//无论是否有读写事件发生,selector每隔1s被唤醒一次
selector.select(10000);
//阻塞,只有当至少一个注册的事件发生的时候才会继续.
Set<SelectionKey> keys = selector.selectedKeys();
if (keys.size()>0)
System.out.println("哈哈哈,我接收到客户端的请求了"+keys.size());
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}
注意看selector.select方法,该方法在客户端没有数据传输的时候是堵塞的,有数据传输的时候才会往下走,具体关于API请自行百度。这个selector就类使用我们前面举例中的服务员小姐姐,哪里客人需要倒酒她就过去给人家倒,没有需要倒酒的时候她就在休息(堵塞)。注意:BIO堵塞的是有没有客户端的连接,有客户端连接就启动一个线程去读取客户端传送过来的数据,而NIO堵塞的是客户端有没有数据传输。
列举NIO和BIO适合的场景。
BIO:你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合。(因为这种方式服务端不需要启动太多的线程且设计简单,用NIO设计过于复杂)(就好像你是个酒神,喝酒是真的快,你让小姐姐总就跑来给你倒酒,人家也烦是不是?所有还不如专门给你一个小姐姐呢?)
NIO:如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。(因为这种方式用BIO实现会造出服务端线程启动太多且因为数据较少不能得到有效的利用,而使用NIO则很适合这种场景)
用人可能会说,那你这个什么NIO不也还是堵塞的吗?你只是避免了服务端有过多的线程再等等啊,并没有实现异步啊。是的,本身上来说NIO并不是真正意义上的异步,称之为new io更合适。那要实现真正的异步怎么玩呢?那请看下面的AIO
AIO
注意:aio是JDK1.7里面才新增的。aio即异步通知,也就是你写完数据了会异步调用一个方法,与NIO的区别在于NIO你需要等到服务端轮询到你,而AIO是你写完了你立马通知到客户端处理你的信息。
AsyncServerHandler
public class AsyncServerHandler implements Runnable {
public CountDownLatch latch;
public AsynchronousServerSocketChannel channel;
@Override
public void run() {
latch = new CountDownLatch(1);
//用于接收客户端的连接,有客户端连接后会回调AcceptHandler里面的方法
channel.accept(this,new AcceptHandler());
//此处,让现场在此阻塞,防止服务端执行完成后退出
latch.await();
}
}
AcceptHandler
public class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncServerHandler> {
@Override
public void completed(AsynchronousSocketChannel channel,AsyncServerHandler serverHandler) {
System.out.println("AcceptHandler.completed()");
//继续接受其他客户端的请求
Server.clientCount++;
System.out.println("连接的客户端数:" + Server.clientCount);
serverHandler.channel.accept(serverHandler, this);
//创建新的Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//异步读 第三个参数为接收消息回调的业务Handler
channel.read(buffer, buffer, new ReadHandler(channel));
}
@Override
public void failed(Throwable exc, AsyncServerHandler serverHandler) {
exc.printStackTrace();
serverHandler.latch.countDown();
}
}
ReadHandler
public class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
//用于读取半包消息和发送应答
private AsynchronousSocketChannel channel;
public ReadHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
//读取到消息后的处理
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("ReadHandler.completed()");
//flip操作
attachment.flip();
//根据
byte[] message = new byte[attachment.remaining()];
attachment.get(message);
try {
String expression = new String(message, "UTF-8");
System.out.println("服务器收到消息: " + expression);
String calrResult = "发送给客户端的消息";
//向客户端发送消息
doWrite(calrResult);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//发送消息
private void doWrite(String result) {
byte[] bytes = result.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
//异步写数据 参数与前面的read一样
channel.write(writeBuffer, writeBuffer,new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
//如果没有发送完,就继续发送直到完成
if (buffer.hasRemaining())
channel.write(buffer, buffer, this);
else{
//创建新的Buffer
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
//异步读 第三个参数为接收消息回调的业务Handler
channel.read(readBuffer, readBuffer, new ReadHandler(channel));
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
}
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
回调的类需要实现CompletionHandler接口的两个方法。completed():在成功适合调用,failed():在失败的适合调用.
可以看到所谓AIO关键的两个类是AsynchronousServerSocketChannel和AsynchronousSocketChannel。前者用于服务端,有个accept()方法,该方法用于接受客户端的连接,客户端有连接后会调用参数中的Handler类。注意这个方法是非堵塞的,只是在成功之后会调用。而AsynchronousSocketChannel的read(),write()均是非堵塞的,会在读取完了之后调用参数中的Handler类。基于这两个类就是真正意义上的异步了。
文件
后续再讨论
关于本文的java源码下载:https://gitee.com/nuoqian1/IoTest
参考文章:
https://ifeve.com/java-nio-vs-io/
https://blog.csdn.net/anxpp/article/details/51512200
https://blog.csdn.net/caolipeng_918/article/details/49534113
https://blog.csdn.net/happyzwh/article/details/53437570