本文会尝试介绍Java中BIO与NIO的范例与原理

使用的模型非常简单:服务器--客户端模型,服务器会将客户端发送的字符串原样发回来。也就是所谓的echo server。

BIO

也就是所谓的Socket通信,直接上代码了

public class BioServer {
public void go(int port) {
try (ServerSocket server = new ServerSocket(port)) {
while (true) {
final Socket socket = server.accept();//接受客户端的连接请求
new Thread(new Runnable() {//创建线程处理这条连接
@Override
public void run() {
try (BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
String line;
while (true) {
if ((line = in.readLine()) == null) {
break;
} out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
} public static void main(String[] args) {
BioServer bioServer = new BioServer();
bioServer.go(9090);
}
}

逻辑不算复杂,先创建一个ServerSocket对象,然后调用accept方法

accept方法会一直阻塞到有客户端发起连接请求为止,accept的返回值为一个Socket对象,在echo server的场景里,这个Socket对象对应于一个TCP连接,我们可以从这个连接上读取/写入数据。

我们需要新建一个线程用于处理这个连接,在这个线程里,我们会尝试从Socket对象里读取数据并作出相应的处理。

但是为什么要把处理线程的逻辑写在一个while循环里并且还要新建一个线程来处理呢?

这是为了并发处理多个客户端连接,如果依然在主线程里操作这个新建的Socket对象,那么在操作的过程中,服务器就无法响应其他客户端的连接请求了(在当前连接断开之前无法再次执行accept方法)

这就是一个线程对应一个连接的模式了。但是大家都知道,线程是一种很昂贵的资源,如果与客户端的连接并不活跃(聊天室场景),为每个连接创建一个线程是非常浪费的。

我在服务器上的测试结果是:最大并发连接数只有1400出头,然后程序就开始抛异常了“java.lang.OutOfMemoryError: unable to create new native thread”。

虽然认真调节JVM和系统参数肯定能有更好的结果,但是并不能在本质上解决BIO的效率问题。

NIO

利用IO多路复用实现的同步非阻塞IO,为用单台服务器处理百万级别的长连接提供了可能。

先上代码:

public class NioServer {
public void go(int port) { try (Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open()) { serverChannel.configureBlocking(false);//配置为非阻塞模式 serverChannel.socket().bind(new InetSocketAddress(port));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);//selector监听channel上的accept事件 while (true) {
selector.select();//阻塞查看是否有事件被触发 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {//如果有事件被触发,则遍历所有事件
SelectionKey selectionKey = iterator.next();
iterator.remove();//Java采用的是水平触发,如果不移除已经处理的事件,下次select会将此事件立即返回 if (selectionKey.isValid()) {//如果事件依然有效
if (selectionKey.isAcceptable()) {//如果是accept事件,说明有新客户端连接进来了,创建一条新的socket连接
ServerSocketChannel serverSocketChannel =
(ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);//将新的socket连接也配置为非阻塞
socketChannel.register(selector,
SelectionKey.OP_READ);//将这个socket上的可读事件也加入到selector的监听事件列表中
} if (selectionKey.isReadable()) {//如果是read事件,说明有客户端发送过来数据,需要将这些数据回写给客户端
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(256); int readBytes = socketChannel.read(byteBuffer);//读取数据
if (readBytes > 0) {
byteBuffer.flip();
socketChannel.write(byteBuffer);//回写
}
}
}
}
} } catch (IOException e) {
e.printStackTrace();
}
} public static void main(String[] args) {
NioServer nioServer = new NioServer();
nioServer.go(9090);
}
}

这个代码就比BIO的版本长很多了,理解起来也不容易。我试着逐步阐释一下

1. 声明一个ServerSocketChannel(注册了一个普通的fd)

2. 声明一个Selector(相当于注册一个epoll里的epfd)

3. 将ServerSocketChannel绑定到Selector,只监听accept事件(相当于调用epoll_ctl方法,把epfd与ServerSocketChannel对应的fd关联起来)

4. 调用Selector的select方法(相当于调用epoll_wait方法,等待所有监听的fd上是否有是事件发生)

5. select方法返回,遍历Selector.selectedKeys()方法的返回值(相当于epoll_wait方法返回时,遍历方法中的events参数)

6. 如果触发的事件是accept类型,创建新的SocketChannel,并将这个SocketChannel也注册到Selector上,关联read事件(相当于新建一个fd,然后再调用epoll_ctl方法将新的fd与epfd关联起来)

7. 如果触发的事件是read类型,则从相关的SocketChannel中读取数据并回写(相当于调用recvfrom方法从fd中读取数据,然后写回到fd中)

可以看出,Java的NIO代码可以完全的对应于epoll的执行逻辑。

在两台物理机上测试了一下,客户端同时发起5w个长连接(单线程处理的极限了,如果想要处理更多连接需要注册多个epfd分担压力)。

每个连接以一秒钟的间隔向服务器发送长度大约为20byte的字符串。

连接全部建立成功,每个请求的平均返回时间小于1ms(偶尔会有2-15ms的波动,由服务器进程的gc操作引起)。服务器的cpu占用是100%(单线程,跑满了一个核)

也就是说,如果配置得当,在单台服务器上处理百万级别的长连接应该是没有问题的。

总结

Java中NIO的效率远高于BIO。

其原因是NIO利用了Linux系统提供的IO多路复用机制(epoll),让在一个线程中监听多个连接上的事件成为可能。

与BIO的一个连接对应一个线程的模型相比,NIO减少了新建线程的操作,也减少了线程切换所带来的开销。让单台服务器处理百万级别的长连接成为可能。

当然,直接使用NIO原生API编程难度较高,幸好市面上已经有许多对NIO做了再次封装的网络库可供使用,这些库不仅降低了学习成本,还修复了NIO中存在的一些bug(比方说JDK1.6以前的Selector.select()方法就存在bug,有可能Selector.select()方法返回,但是selectedKeys中没有事件,这会导致无限循环,直接占满一个core)

例如著名的Netty与Mina

后续我还会写系列文章来介绍Netty的使用与原理。

Java IO 学习(四)BIO/NIO的更多相关文章

  1. Java IO学习--(四)网络

    Java中网络的内容或多或少的超出了Java IO的范畴.关于Java网络更多的是在我的Java网络教程中探讨.但是既然网络是一个常见的数据来源以及数据流目的地,并且因为你使用Java IO的API通 ...

  2. Java IO学习笔记五:BIO到NIO

    作者:Grey 原文地址: Java IO学习笔记五:BIO到NIO 准备环境 准备一个CentOS7的Linux实例: 实例的IP: 192.168.205.138 我们这次实验的目的就是直观感受一 ...

  3. Java IO模型:BIO、NIO、AIO

    Java IO模型:BIO.NIO.AIO 本来是打算直接学习网络框架Netty的,但是先补充了一下自己对Java 几种IO模型的学习和理解.分别是 BIO.NIO.AIO三种IO模型. IO模型的基 ...

  4. Java IO学习笔记四:Socket基础

    作者:Grey 原文地址:Java IO学习笔记四:Socket基础 准备两个Linux实例(安装好jdk1.8),我准备的两个实例的ip地址分别为: io1实例:192.168.205.138 io ...

  5. Java IO学习笔记六:NIO到多路复用

    作者:Grey 原文地址:Java IO学习笔记六:NIO到多路复用 虽然NIO性能上比BIO要好,参考:Java IO学习笔记五:BIO到NIO 但是NIO也有问题,NIO服务端的示例代码中往往会包 ...

  6. IO复用,AIO,BIO,NIO,同步,异步,阻塞和非阻塞 区别参考

    参考https://www.cnblogs.com/aspirant/p/6877350.html?utm_source=itdadao&utm_medium=referral IO复用,AI ...

  7. Java IO学习笔记:概念与原理

    Java IO学习笔记:概念与原理   一.概念   Java中对文件的操作是以流的方式进行的.流是Java内存中的一组有序数据序列.Java将数据从源(文件.内存.键盘.网络)读入到内存 中,形成了 ...

  8. Java IO学习笔记二

    Java IO学习笔记二 流的概念 在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成. 程序中的输入输 ...

  9. Java IO学习笔记二:DirectByteBuffer与HeapByteBuffer

    作者:Grey 原文地址:Java IO学习笔记二:DirectByteBuffer与HeapByteBuffer ByteBuffer.allocate()与ByteBuffer.allocateD ...

  10. Java IO学习笔记三:MMAP与RandomAccessFile

    作者:Grey 原文地址:Java IO学习笔记三:MMAP与RandomAccessFile 关于RandomAccessFile 相较于前面提到的BufferedReader/Writer和Fil ...

随机推荐

  1. Disharmony Trees HDU - 3015

    Disharmony Trees HDU - 3015 One day Sophia finds a very big square. There are n trees in the square. ...

  2. HDU 6228 tree 简单思维树dp

    一.前言 前两天沈阳重现,经过队友提点,得到3题的成绩,但是看到这题下意识觉得题目错了,最后发现实际上是题目读错了....GG 感觉自己前所未有的愚蠢了....不过题目读对了也是一道思维题,但是很好理 ...

  3. 用 Tensorflow 建立 CNN

    稍稍乱入的CNN,本文依然是学习周莫烦视频的笔记. 还有 google 在 udacity 上的 CNN 教程. CNN(Convolutional Neural Networks) 卷积神经网络简单 ...

  4. sql中比较大小

    if object_id('tempdb..#dataOldNew1') is not null drop table #dataOldNew1 select distinct store_cd ,i ...

  5. “帮你”app-NABCD

    1.你的创意解决了用户的什么需求?(N) 本学校已存在的失物招领.表白墙.二手市场等QQ群普遍存在信息冗杂,时效性差等缺点.不能充分发挥信息有效性的,我们的“帮你”APP能够充分发挥信息的有效性,让失 ...

  6. 不同storyboard间跳转

    小项目中用到storyboard,可以按照模块来新建多个sb. 以下是代码实现跳转实现: UIStoryboard *anSb=[UIStoryboard storyboardWithName:@&q ...

  7. [netty4][netty-common]FastThreadLocal及其相关类系列

    FastThreadLocal 概述: ThreadLocal的一个特定变种改善,有更好的存取性能. 内部采用一个数组来代替ThreadLocal内部的hash表来存放变量.虽然这看起来是微不足道的, ...

  8. 连接Oracle 10g时ORA-12514:TNS:监听进程不能解析在连接描述符中给出的SERVICE_NAME错误的解决

    近日服务器断电,导致客户端连接ORACLE服务器时出现ORA-12514错误,在网上查得解决方法如下 解决方法: 1. 打开/network/admin/listener.ora文件,找到: SID_ ...

  9. python-day5-格式化输入

    python格式化输入包含'%'调用,及format方法 使用‘%’进行格式化输出 #最简单的字符串传参 tpl='i am %s '%'alex' >>>i am alex #字符 ...

  10. POJ 3255:Roadblocks(次短路)

    题目大意:求无向图的次短路. 分析: 在起点终点各求一次最短路,枚举边,通过该边的最短路为其权值加上到起点和终点最短路之和,找到最短但又比最短路长的路径. 代码: program block; typ ...