文章目录
java实践11之网络IO BIO和NIO(下)
2、NIO
NIO为什么NIO是非阻塞的,阻塞体现在哪
大家是否有这样的疑问,处理对应的事件和select都是阻塞的,为什么说NIO是非阻塞的?我认为NIO非阻塞优化主要是在底层,我们看不到。
NIO的非阻塞主要提现在:操作系统侧,其中的channel和程序中handler处理器,不是绑定的,中间增加了监视器,来轮训通道并通知channel来处理,通知完后立马结束,继续轮下个channel就绪的事件,不会等待server中的handler执行完毕。
BIO的socket和handler处理器是绑定的,必须等待handler完成,所以BIO是阻塞的。
如果请求过多,使用这种非阻塞的方式,会极大的提高操作系统侧的处理效率。
例子:拿刚才餐厅的例子
在BIO的基础上,NIO优化了处理流程,当请求到达时,先记录请求表,再弄一个服务员(这个就是selector)循环每桌用户,来点菜时,点菜后直接交给后厨,然后厨来处理,然后服务员继续下一个用户。 服务人员不受,用户点菜和后厨做菜绑定限制,不阻塞,完成的才进行处理,未完成的就处理一个。
NIO为什么使用单线程就能处理大量的请求?从上面的例子中,我们就知道NIO为什么按单个或者很少的线程就能处理大量的请求
我认为,主要体现在:
监视器是单线程的,他的工作主要为轮询channel中是否有就绪是事件,有就绪的事件,则通知server来处理。也可以使用2个线程由于它做的事也不会很多,所以不会使用很多的线程来进行处理。
这里的单线程,是指请求处理器/监视器一般单线程即可。
拿上一篇的例子来说,服务员只是监视点好菜客户,然后把菜单给后厨就完毕。他做的工作非常少。所以他只需要单个或者很少的线程即可。
NIO中的Buffer
通过上面的图中可以看到,NIO核心组件:监视器(即selector)、channel。
、Buffer,Buffer它本质上是一个内存块,既可以写入数据,也可以从中读取数据,一个Channel对应一个Buffer。
BIO面向字节流读取的,NIO是面向块(缓冲区)读取的。NIO中的Buffer缓冲区内部其实也是一块缓存byte[]数组,BIO也是byte[],既然都是byte[],那么为什么说NIO比较好呢?
我理解NIO中的面向块(buffer),比较好主要是因为3点:
1)、NIO的buffer相当于另外开辟的一块内存空间,想当于中转站。当有数据是可以先保存在缓冲区(中转站),进行预处理。可以理解为我们开发中的产品。当有需求时 尤其是在研发忙的情况下,需求可以先提给产品 (缓冲区)。当研发有空时,可以再找产品处理需求。
2)、操作更简单,他对byte[]进行了包装,使我们可以更加方便的操作byte数组。
3)、速度会更快,在BIO中,使用的byte[]数组为堆内存,而在NIO中Buffer是可以使用非堆内存。也就是说使用Buffer可以使用Direct方式直接操作系统内存,减少数据拷贝次数。并且也可以使用unsafe来提高处理速度。
可以参考我的这篇文章文件IO 的ByteBuffer https://blog.csdn.net/m252131895/article/details/126729414
NIO中的channel
channel也是是NIO技术中的核心组件,我认为 它和IO中的Stream流是相似的, 但是Stream是单向的,而Channel则是双向的。
在客户端<->服务端,交互程中,读和写都是互斥,没有两端同时读或同时写,都是一个读,一个写。基于此底层通过channel封装了对数据的操作,使其上层可以即能读又能写。
NIO中的selector和IO多路复用技术
selector选择器我认为这个才是NIO中最重要的组件,也叫多路复用器。从上面NIO的模型图中的”监视器”,这个就是selector。
通过1个线程,来获取多个channel中的时间状态,这个就叫IO多路复用。
select 多路复用器
最早的多路复用器,使用的方式为,一个监视器,来循环查看 请求存放列表。有就绪的IO事件则通知我们的程序来处理。
由此我们可以发现select多路复用器其中的问题。
1)如果客户端有100个连接,实际数据正在交互的只有5个,那么每次都挨个查看100个是否有就绪的IO,其中95个是无效的,会造成资源浪费。
2)select底层监控的请求存放列表,有1024的限制。我理解为 请求存放列表,使用的是数组结构。无论长度设置为1024或2048都会出现越界的情况。
poll多路复用器
poll多路复用器和select工作原理是一样的。都是循环查看请求存放列表,是否有就绪的IO事件。它只是基于select,修改了底层 “请求存放列表”的数据结构,更改为由和数组改为“链表”数据结构。解除了1024或2048的限制 具体大小可以取决于机器的性能配置。但是还是没有解决,无效遍历的情况。
epoll多路复用器
基于select和poll的问题,又出现了epoll多路复用器。在”请求存放列表”和 ” 监视器”中间增加了 就绪列表,并且请求存放表 改为了树的存储结构。
1、监视器监视请求列表改为 监视就绪的id列表。有就绪的则通知java程序处理。没就绪的则等待。
2、当有列表中有就绪事件时,会把就绪的id存放到”就绪的id列表中”。
3、监视器会根据id,再次查询”请求存放表”。来通知java程序处理。
在这种模式下,监视器处理的都为有效的请求。不需要像select/poll那样扫描,整个集合,请求存放列表中请求很多时,由于使用了树的数据结构,也能快速查找定位。
3、简单介绍Netty框架的使用
大家是否发现,上面的NIO,使用起来比较繁琐,而且很多功能,例如心跳检测、粘包、拆包、多监视器等问题,都需要自己从头设计开发。针对于这些问题,出现了Netty框架。
Netty,是一套网络编程开发框架,底层封装了NIO。并对其进行了一些优化。提供了一套易于使用的API工具类。使我们能更加方便的进行网络开发。
使用demo:
服务端:
public class NettyService {
private int port;
public NettyService(int port) {
this.port = port;
}
private Channel channel;
// private Map<String, Channel> channels;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private ServerBootstrap bootstrap;
public void run() throws Exception {
/***
* NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器,
* Netty提供了许多不同的EventLoopGroup的实现用来处理不同传输协议。 实现了一个服务端的应用,
* 因此会有2个NioEventLoopGroup会被使用。 第一个经常被叫做‘boss’,用来接收进来的连接。
* 第二个经常被叫做‘worker’,用来处理已经被接收的连接, 一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。
* 如何知道多少个线程已经被使用,如何映射到已经创建的Channels上都需要依赖于EventLoopGroup的实现,
* 并且可以通过构造函数来配置他们的关系。
*/
bossGroup = new NioEventLoopGroup();
workerGroup = new NioEventLoopGroup();
System.out.println("准备运行端口:" + port);
/**
* ServerBootstrap 是一个启动NIO服务的辅助启动类 你可以在这个服务中直接使用Channel
*/
ServerBootstrap bootstrap = new ServerBootstrap();
/**
* 这一步是必须的,如果没有设置group将会报java.lang.IllegalStateException: group not
* set异常
*/
bootstrap.group(bossGroup, workerGroup);
/***
* ServerSocketChannel以NIO的selector为基础进行实现的,用来接收新的连接
* 这里告诉Channel如何获取新的连接.
*/
bootstrap.channel(NioServerSocketChannel.class);
NettyServiceHandler sahandler = new NettyServiceHandler();
/***
* 这里的事件处理类经常会被用来处理一个最近的已经接收的Channel。 ChannelInitializer是一个特殊的处理类,
* 他的目的是帮助使用者配置一个新的Channel。
* 也许你想通过增加一些处理类比如NettyServerHandler来配置一个新的Channel
* 或者其对应的ChannelPipeline来实现你的网络程序。 当你的程序变的复杂时,可能你会增加更多的处理类到pipline上,
* 然后提取这些匿名类到最顶层的类上。
*/
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
// ch.pipeline().addLast("decoder", new StringDecoder());
// ch.pipeline().addLast("encoder", new StringEncoder());
// ch.pipeline().addLast(new LineBasedFrameDecoder(2048));
ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(2048, 0, 4, 0, 4));
ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(4));
//进行超时重连、断开、空闲回收处理
// ch.pipeline().addLast("server-idle-handler", new IdleStateHandler(3000,3000, 0, TimeUnit.MILLISECONDS));
ch.pipeline().addLast(sahandler);// demo1.discard
}
});
/***
* 设置指定的通道实现的配置参数。 例如正在写一个TCP/IP的服务端,
* 因此要设置socket的参数选项比如tcpNoDelay和keepAlive。
*/
//初始化服务端可连接队列
bootstrap.option(ChannelOption.SO_BACKLOG, 128);
/***
* option()是提供给NioServerSocketChannel用来接收进来的连接。
* childOption()是提供给由父管道ServerChannel接收到的连接,
* 在这个例子中也是NioServerSocketChannel。
*/
//检测死连接,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
/***
* 绑定端口并启动去接收进来的连接
*/
ChannelFuture channelFuture = bootstrap.bind(port).sync();
同步不可呗中断//而sync是可被中断
channelFuture.syncUninterruptibly();
this.channel = channelFuture.channel();
/**
* 这里会一直等待,直到socket被关闭 ,此处不用调用下面的方法, 因为rpc是运行在web服务,本身程序一直在运行
*/
// chanel.closeFuture().sync();
//Thread.sleep(10000000l);
}
public Channel getChannel(){
return this.channel;
}
protected void close() throws Throwable {
try {
if (channel != null) {
// unbind.
channel.close();
}
if (bootstrap != null) {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
} catch (Exception e) {
// TODO: handle exception
}
}
// 将程序跑起来
public static void main(String[] args) throws Exception {
new NettyService(8080).run();
System.out.println("server:run()");
}
}
服务端事件处理类:
@Sharable
public class NettyServiceHandler extends ChannelInboundHandlerAdapter {
/**
* 这里我们覆盖了chanelRead()事件处理方法。 每当从客户端收到新的数据时, 这个方法会在收到消息时被调用,
* 这个例子中,收到的消息的类型是ByteBuf
*
* @param ctx
* 通道处理的上下文信息
* @param msg
* 接收的消息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
String ip =this.getIp(ctx.channel().remoteAddress());;
System.out.println("server:收到客户端"+ip+"的请求");
//Thread.sleep(5000);
ByteBuf buf = (ByteBuf) msg;
byte[] b=new byte[buf.readableBytes()];
buf.getBytes(buf.readerIndex(), b);
String str = "你好啊 "+new String(b);
ByteBuf resp = Unpooled.buffer(str.length());
resp.writeBytes(str.getBytes());
ctx.writeAndFlush(resp);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 抛弃收到的数据
ReferenceCountUtil.release(msg);
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
}
public String getIp(SocketAddress insocket){
return ((InetSocketAddress)insocket).getAddress().getHostAddress();
}
/***
* 这个方法会在发生异常时触发
*
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
/**
* exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,即当 Netty 由于 IO
* 错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来 并且把关联的 channel
* 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不 同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
*/
System.out.println(ctx.channel().remoteAddress());
// 出现异常就关闭
cause.printStackTrace();
ctx.close();
}
//空闲处理
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("server 空闲回收");
super.userEventTriggered(ctx, evt);
}
}
客户端:
public class NettyClient {
// static final int SIZE = Integer.parseInt(System.getProperty("size",
// "256"));
Channel chanel;
String ip;
int port;
public NettyClient(String ip, int port) {
initBootstrap(ip, port);
}
public static void main(String[] args) {
try {
NettyClient nc = new NettyClient("127.0.0.1", 8080);
byte[] req = "张三".getBytes();
ByteBuf message = Unpooled.buffer(req.length);
message.writeBytes(req);
nc.getChanel().writeAndFlush(message);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void initBootstrap(String ip, int port) {
// Configure the client.
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
// 设置超时
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
// ch.pipeline().addLast(new
// LineBasedFrameDecoder(2048));
// ch.pipeline().addLast(new ObjectEncoder());
// ch.pipeline().addLast(new
// ObjectDecoder(ClassResolvers.cacheDisabled(null)));
ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(2048, 0, 4, 0, 4));
ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(4));
//进行超时重连、断开、空闲回收处理
ch.pipeline().addLast("client-idle-handler",
new IdleStateHandler(3000, 3000, 0, MILLISECONDS));
ch.pipeline().addLast("handler", new NettyClientHandler());
}
});
ChannelFuture future = bootstrap.connect(ip, port);
future.awaitUninterruptibly(3000, TimeUnit.MILLISECONDS);
chanel = future.channel();
// future.channel().writeAndFlush(text);
// future.channel().writeAndFlush(text + 2);
// future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
group.shutdownGracefully();
}
// finally {
// group.shutdownGracefully();
// }
}
public Channel getChanel() {
return chanel;
}
}
客户端处理类:
@Sharable
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 解析Ip地址
// ((InetSocketAddress)ctx.channel().remoteAddress()).getAddress().getHostAddress()
ByteBuf buf = (ByteBuf) msg;
byte[] b=new byte[buf.readableBytes()];
buf.getBytes(buf.readerIndex(), b);
String str=new String(b);
System.out.println("客户端:收到服务端返回:"+str);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress());
super.channelActive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
/**
* //进行超时重连、断开、空闲回收处理
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
}
}
Netty的东西还有很多,这里只是分享个简单的demo,关于netty其他的东西先不说了,需要大家自己去查资料了。
总结
1、NIO和BIO的使用方法。
2、理解buffer selector channel在NIO中的作用。
3、理解BIO的阻塞点和使用场景。在一般小并发的场景中,可以使用BIO+线程池的方式,他的特点是使用简单,便于理解。但他是阻塞的在底层,底层把handler和socket绑定在一起,如果没有数据传输,只连接,那么handler不完成则会一直等待。由于是在底层,所以我们无法修改。
4、理解NIO的非阻塞点和使用场景,他针对BIO的问题,使用了多路复用技术,增加了监视器。把socket和handler进行了解绑。来进行非阻塞处理。
5、理解了BIO和NIO阻塞点后,我们也应该知道,如果我们的handler处理程序过慢,那么无论是NIO或BIO或是Netty,都不能解决问题。
最后其实我认为最重要的是,无论NIO和BIO,最主要的是要学习的是他们的处理思路。比如Epoll采用回调的方式来节省资源,解决了无效轮训问题。采用树来加快查询速度,poll中采用链表的方式来解决数组长度的限制。NIO的拆分思路,单独拆出来一个线程,处理请求,只做简单的存表操作,这样1个线程就可以处理大量的请求。学习buffer,增加缓冲区去处理数据等等。 我认为这些思路才是最重要的。
关于文章中的一些概念和描述名词,可能有错误,但大概的思路应该是正确的。我认为很多概念名词其实也是模糊的难以界定的,我不想从网上摘抄一些八股文来讲解,只能根据我在项目中的体会,给大家分享下我的理解,希望会对大家有帮助。
好了这篇分享完了,希望对大家会有帮助,文章中有哪些错误,或者理解的不对,欢迎大家多多和我交流、批评和指正。