BIO 与 NIO初探
BIO示例代码
@Test
public void testBIO() throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true){
//Thread block 1
//Listens for a connection to be made to this socket and accepts it.
//The method blocks until a connection is made.
Socket conn = serverSocket.accept();
service.submit((Callable<String>)()->{
byte []request = new byte[1024];
//Thread block 2
// This method blocks until input data is available,
// end of file is detected, or an exception is thrown.
conn.getInputStream().read(request);
System.out.println(new String(request));
return null;
});
}
}
简要分析:
-
当有连接请求时,socketServer通过accept方法获取一个socket
-
accept() 会获取一个连接,如果没有连接该方法会阻塞当前线程
-
取得socket后,将这个socket分给一个线程去处理。此时socket需要等待有效的请求数据到来后,才可以真正开始处理请求。
-
socket交给线程后,这时socketServer才可以接收下一个连接请求。
-
获得连接的顺序是和客户端请求到达服务器的先后顺序相关。
NIO 示例代码
/**
* 此段代码只做示例用,功能并不完整,也没有考虑有所要处理的情况
* @throws IOException
*/
@Test
public void testNIO() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//显示的设置不使用BIO而是使用NIO模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
System.out.println("NIO 服务启动");
//JDK 底层操作系统,多路复用,事件机制,操作系统会告诉JVM(selector和操作系统底层的多路复用机制打交道)
Selector selector = Selector.open();
//主线程循环检测操作系统是否有新的连接
while (true) {
//获取新链接
SocketChannel socketChannel = serverSocketChannel.accept();//非阻塞,不管有没有都要返回,没有返回null
if(socketChannel == null){
continue;
}
//显示的设置不使用BIO而是使用NIO模式
socketChannel.configureBlocking(false);
//通过事件机制,当有数据过来的时候,再去处理,让 selector 帮我们去监控OP_READ事件,或者叫观察者模式(有数据传输)
socketChannel.register(selector, SelectionKey.OP_READ);
//查询事件
selector.select();
Set<SelectionKey> eventKeys = selector.selectedKeys();
//循环遍历事件
Iterator<SelectionKey> iterator = eventKeys.iterator();
while (iterator.hasNext()) {
SelectionKey event = iterator.next();
//SocketChannel channel = (SocketChannel)event.channel();//可以从时间知道是哪个连接
if (event.isReadable()) {//一个连接有数据过来了
service.submit((Callable<String>) () -> {
//读写数据,处理请求,返回响应
//ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(1024);//堆外内存,直接映射
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//封装的数组,对外内存
socketChannel.read(byteBuffer);
//转为读取模式
byteBuffer.flip();
System.out.println(new String(byteBuffer.array()));
// socketChannel.write()
return null;
});
}
}
}
}
- 基于事件驱动,当有连接请求,会将此连接注册到多路复用器上(selector)。
- 在多路复用器上可以注册监听事件,比如监听accept、read
- 通过监听,当真正有请求数据时,才来处理数据。
- 不会阻塞,会不停的轮询是否有就绪的事件,所以处理顺序和连接请求先后顺序无关,与请求数据到来的先后顺序有关
主要对比
-
BIO
一个连接,一个线程,非http
请求,有可能只连接不发请求数据,此时线程是无用浪费的。 -
BIO
处理依赖于连接建立;NIO
处理依赖于请求数据的到来。导致执行顺序不同。- 个线程处理一个请求
- BIO:连接请求来,建立socket,等待请求数据到来(t1),处理时间(t2)
- NIO:连接请求来,注册到selector,设置读监听,等待请求数据(t1),处理时间(t2)
- 此时,两者用时皆为t1+t2,没有区别
- 一个线程处理两个请求
- 第一个请求,等待请求数据(10),处理时间(1)
- 第二个请求,等待请求数据(1),处理时间(2)
- BIO:用时 10+1+1+2=14,第1个执行完用时10+1,等待第一个执行完处理第2个,用时1+2
- NIO:用时 1+2+7+1=11, 第二个数据先到,时间 1+2,此时第一个需要等时为10秒,还没到,还需等待7秒,时间为7+1
- 两个线程处理两个请求
- 第一个请求,等待请求数据(10),处理时间(1)
- 第二个请求,等待请求数据(1),处理时间(2)
- BIO:用时 10+1+2=13,等待第1个请求10,交给工作线程一处理,此时同时接受第2个,等待1秒,处理时间2秒,此间线程一处理时间为一秒,在线程二结束之前就已经结束
- NIO:用时 1+2+7+1=11,第二个数据先到,时间 1+2,此时第一个还没到,还需等待7秒,时间为7+1如果两个请求顺序相反,则bio和nio一样,都是11秒
- 个线程处理一个请求
-
由此可见由于阻塞等待机制的不同,导致效率不同,主要优化点为,不必排队等待,先到先处理,就有可能效率高一点。
-
BIO如果想要处理并发请求,则必须使用多线程,一般后端会用线程池来支持
-
NIO可以使用单线程,可以减少线程切换上下文的消耗。
-
但是虽然单线程减少了线程切换的消耗,但是处理也变为线性的,也就是处理完一个请求,才能处理第二个。 这时,有这么两个场景
- 后端是密集型的计算,没有大量的IO操作,比如读些文件、数据库等
- 后端是有大量的IO操作。
-
当为第一种场景时:
- NIO单线程则比较有优势, 理由是虽然是单线程,但是由于线程的计算是并发计算,不是并行计算,说到底,计算压力还是在CPU上,一个线程计算,没有线程的多余消耗,显然比NIO多线程要高效。BIO则必为多线程,否则将阻塞到天荒地老,但多线程是并发,不是并行,主要还是依靠CPU的线性计算,另外还有处理大量的线程上下文。
- 如果为第二种场景,多线程将有一定优势,多个线程把等待IO的时间能平均开。此时两者区别主要取决于以上分析的处理顺序了,显然NIO要更胜一筹。
NIO 框架
为什么要使用开源框架?
这个问题几乎可以当做废话,框架肯定要比一些原生的API封装了更多地功能,重复造轮子在追求效率的情况并不是明智之举。那么先来说说NIO有什么缺点吧:
- NIO的类库和API还是有点复杂,比如Buffer的使用
- Selector编写复杂,如果对某个事件注册后,业务代码过于耦合
- 需要了解很多多线程的知识,熟悉网络编程
- 面对断连重连、保丢失、粘包等,处理复杂
- NIO存在BUG,根据网上言论说是selector空轮训导致CPU飙升,具体有兴趣的可以看看JDK的官网
那么有了这些问题,就急需一些大牛们开发通用框架来方便劳苦大众了。最受欢迎NIO框架就是MINA和Netty了。
MINA VS Netty
- MINA和Netty的主要贡献者都是同一个人——Trustin lee,韩国Line公司的。
- MINA于2006年开发,到14、15年左右,基本停止维护
- Nety开始于2009年,目前仍由苹果公司的norman maurer在主要维护。
- Norman Maurer是《Netty in Action》一书的作者
讲了一大堆的废话之后,总结来说就是——Netty有前途,学它准没错。
Netty介绍
按照定义来说,Netty是一个异步、事件驱动的用来做高性能、高可靠性的网络应用框架。主要的优点有:
- 框架设计优雅,底层模型随意切换适应不同的网络协议要求
- 提供很多标准的协议、安全、编码解码的支持
- 解决了很多NIO不易用的问题
- 社区更为活跃,在很多开源框架中使用,如Dubbo、RocketMQ、Spark、Spring-data-redis等
主要支持的功能或者特性有:
- 底层核心有:Zero-Copy-Capable Buffer,非常易用的灵拷贝Buffer(这个内容很有意思,稍后专门来说);统一的API;标准可扩展的时间模型
- 传输方面的支持有:管道通信、Http隧道、TCP与UDP
- 协议方面的支持有:基于原始文本和二进制的协议;解压缩;大文件传输;流媒体传输;protobuf编解码;安全认证;http和websocket
Netty服务器小例子
基于Netty的服务器编程可以看做是Reactor模型:
即包含一个接收连接的线程池(也有可能是单个线程,boss线程池)以及一个处理连接的线程池(worker线程池)。boss负责接收连接,并进行IO监听;worker负责后续的处理。为了便于理解Netty,直接看看代码:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
public class NettyNioServer {
public void serve(int port) throws InterruptedException {
final ByteBuf buffer = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi\r\n", Charset.forName("UTF-8")));
// 第一步,创建线程池
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
// 第二步,创建启动类
ServerBootstrap b = new ServerBootstrap();
// 第三步,配置各组件
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buffer.duplicate()).addListener(ChannelFutureListener.CLOSE);
}
});
}
});
// 第四步,开启监听
ChannelFuture f = b.bind().sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully().sync();
workerGroup.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
NettyNioServer server = new NettyNioServer();
server.serve(5555);
}
}
代码非常少,而且想要换成阻塞IO,只需要替换Channel里面的工厂类即可:
public class NettyOioServer {
public void serve(int port) throws InterruptedException {
final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi\r\b", Charset.forName("UTF-8")));
EventLoopGroup bossGroup = new OioEventLoopGroup(1);
EventLoopGroup workerGroup = new OioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)//配置boss和worker
.channel(OioServerSocketChannel.class) // 使用阻塞的SocketChannel
....
总结
NIO在接收请求方式上,无疑是要高效于BIO,原因并非是不阻塞,我认为NIO一样是阻塞的,只是方式不同,先来的有效请求先处理,先阻塞时间短的。此时间可用于等待等待时间长的。
在处理请求上,NIO和BIO并没有什么不同,主要看线程池规划是否和理。NIO相对BIO在密集型计算的模型下,可以用更少的线程,甚至单线程
本文由 zealzhangz 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为:
2020/03/28 11:34