java实践12之网络IO BIO和NIO(下)

java实践11之网络IO BIO和NIO(下)

接上集 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,增加缓冲区去处理数据等等。 我认为这些思路才是最重要的。

  关于文章中的一些概念和描述名词,可能有错误,但大概的思路应该是正确的。我认为很多概念名词其实也是模糊的难以界定的,我不想从网上摘抄一些八股文来讲解,只能根据我在项目中的体会,给大家分享下我的理解,希望会对大家有帮助。

好了这篇分享完了,希望对大家会有帮助,文章中有哪些错误,或者理解的不对,欢迎大家多多和我交流、批评和指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值