网络编程:BIO,NIO,AIO和netty的发展和示例demo

博主最近去了解了下网络编程的知识,这里做一个笔记进行记录。

首先给出一个很基础的客户端代码用于测试服务端逻辑执行情况:

	public static void main(String[] args) throws IOException {
		Socket socket = new Socket("127.0.0.1", 8888);
		Scanner scanner = new Scanner(System.in);
		String datas = scanner.nextLine();
		socket.getOutputStream().write(datas.getBytes(StandardCharsets.UTF_8));
		scanner.close();
		socket.close();
	}

BIO(同步阻塞IO)

在jdk1.4之前,对于网络编程,java只有BIO(Blocking -IO)可用,但由于BIO是同步阻塞的,在服务器需要去接收客户端连接,及对客户端的I/O操作都会被阻塞,造成的结果就是当一个线程在等待客户端写入数据时,就无法去进行其他操作,相当于瘫痪在这,性能十分低下;
这种情况的服务器代码示例如下:

public static void main(String[] args) throws IOException {
		//创建基于BIO的服务器,端口号为8888
		ServerSocket serverSocket = new ServerSocket(8888);
		//循环处理连接
		while (true){
			System.out.println("等待连接。。。");
			//接收客户端连接,accept方法会阻塞,等待不到连接,线程会放弃cpu一直等在这
			Socket socket = serverSocket.accept();
			byte[] datas = new byte[1024];
			System.out.println("等待读取数据。。。");
			//读取消息,read方法同样会阻塞,读取不到数据,线程会放弃cpu一直等在这
			socket.getInputStream().read(datas);
			//打印消息
			System.out.println("客户端数据:" + new String(datas, StandardCharsets.UTF_8));
		}
	}

以上模型就是一个最简单的BIO单线程服务器,他的阻塞特性,导致他在同一时刻只能为一个客户端进行服务,不支持并发,如果某个客户端连线上服务器后,一直不写入数据,那服务器对别的客户端来说就跟挂了一样,这是不可取的,所以在JDK1.4以前服务器的编写都是采用多线程的模式去编写,示例代码如下:

public static void main(String[] args) throws IOException {
		//创建一个缓存线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		//创建基于BIO的服务器,绑定8888端口
		ServerSocket serverSocket = new ServerSocket(8888);
		//循环处理连接
		while (true) {
			//accept方法会阻塞,等待不到连接,线程会放弃cpu一直等在这
			Socket socket = serverSocket.accept();
			//获取到客户端连接时,随后的I/O等操作都交给线程池去处理
			threadPool.execute(() -> {
				byte[] datas = new byte[1024];
				try {
					//读取消息
					socket.getInputStream().read(datas);
					//打印
					System.out.println("客户端数据:" + new String(datas, StandardCharsets.UTF_8));
				} catch (IOException e) {
					e.printStackTrace();
				}
			});
		}
	}

以上便是最基本的多线程BIO服务器模型,其中main线程创建完服务器后,就一直循环接收客户端连接,接收到连接后,后续的工作就交由线程池里面的工作线程去处理,由此便可实现BIO服务器对并发的支持;

以上多线程的BIO服务器虽然实现了对并发的支持,但诟病也显而易见,性能实在太差了,这样的处理方式,导致的结果就是一个客户端需要一个线程去处理,那么如果同时有1万个客户端连入,而同时只有1000个线程在于服务器进行交互,那么就相当于服务器为客户端创建的9000个线程就白白的阻塞在这。

NIO(同步非阻塞IO)

为了解决上述问题,在jdk1.4的时候引入了NIO(NON-BLOCKING-IO),是一种同步非阻塞IO,他可以在接收客户端连接,与客户端进行I/O互动的时候都不阻塞,这个特性是让人兴奋的,如此的话我们照着上述的单线程BIO服务器的代码进行简单修改就可以支持并发了!代码如下所示:

public static void main(String[] args) throws IOException {
		//开启一个Nio服务
		ServerSocketChannel nioServer = ServerSocketChannel.open();
		//绑定8888端口
		nioServer.bind(new InetSocketAddress(8888));
		//设置非阻塞模式
		nioServer.configureBlocking(Boolean.FALSE);
		//创建一个集合,用于是收集连接上来的客户端
		List<SocketChannel> socketChannelList = new ArrayList<>();
		//一直循环监听客户连接并处理
		while(true){
			//查看是否有客户端来连接
			SocketChannel socket = nioServer.accept();
			//有的话添加到socketChannel集合
			if (socket != null){
				socketChannelList.add(socket);
			}
			ByteBuffer buffer = ByteBuffer.allocate(1024);
			Iterator<SocketChannel> iterator = socketChannelList.iterator();
			//轮询整个集合,尝试是否可以读取到数据
			while (iterator.hasNext()) {
				SocketChannel socketChannel = iterator.next();
				//设置read非阻塞
				socketChannel.configureBlocking(Boolean.FALSE);
				//读取,readLenth代表读取到的数据长度,等于0代表未读取到数据,小于0代表客户端已经断开
				int readLenth = socketChannel.read(buffer);
				if (readLenth > 0) {
					//todo  执行业务逻辑
					System.out.println("数据:" + new String(buffer.array(), StandardCharsets.UTF_8));
					buffer.flip();
				} else if (readLenth < 0) {
					//客户端断开则从List中移除对应对象,等待gc回收
					iterator.remove();
					socketChannel.close();
				}
			}
		}
	}

以上代码逻辑十分简单,在while循环中,先去查看是否有客户端进行连接,有的话则添加到客户端连接集合中,然后轮询整个集合,查看是否有写入操作,有的话则将读取数据进行打印,如果轮询到有断开的客户端则从集合中移除。

以上的NIO示例,不仅在单线程的情况下支持并发,并且在性能方面也是大大的得到了跃升,如果再合理的搭配线程池写出来的服务器性能也是碾压BIO的,但是!!上述代码并不是真正的NIO服务器demo,这一段代码的编写,仅是为了表达NIO服务器的工作机制,他就是通过不断轮询的方式去获取进行客户端的连接及与客户端的I/O操作的。

上述代码中,轮询是通过java语言的for循环去进行,而整个NIO服务器的重心也是在轮询这一部分,那么为了优化轮询的性能,java语言的设计师们把轮询的工作交给了OS去做,通过调用OS的epoll函数或者selector函数去做的,这可以大大的提升轮询的性能。

NIO服务器demo代码如下:

public static void main(String[] args) throws IOException {
		//创建nio服务器
		ServerSocketChannel nioServer = ServerSocketChannel.open();
		//绑定地址
		nioServer.bind(new InetSocketAddress(8888));
		//设置不阻塞
		nioServer.configureBlocking(Boolean.FALSE);
		//获取一个os底层epoll事件的一个实例,此处用于监听某种事件,如nio的客户端接入事件,读事件,写事件
		Selector selector = Selector.open();
		//向nioServer中注册选择器与客户端接入事件
		nioServer.register(selector, SelectionKey.OP_ACCEPT);
		//数据缓存区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		System.out.println("nio服务器启动成功。。。");

		//一直循环处理事件
		while (true) {
			//阻塞获取触发的事件数量,注意!! 此处可能是连接事件或者读事件
			//即有客户端试图进行连接,或已建立连接的客户端试图写入数据
			int count = selector.select();
			//被触发的事件个数是否大于0
			if (count > 0){
				//获取所有事件
				Set<SelectionKey> keys = selector.selectedKeys();

				Iterator<SelectionKey> iterator = keys.iterator();
				//遍历所有事件
				if (iterator.hasNext()) {
					SelectionKey key = iterator.next();
					//一定要将此处取得的key(事件)移除,因为这个key已经获取到并进行处理了
					iterator.remove();
					//对事件进行处理
					//是否可连接,即客户端连接事件
					if (key.isAcceptable()){
						//进行连接,获取客户端
						ServerSocketChannel socketChannel = (ServerSocketChannel) key.channel();
						SocketChannel channel = socketChannel.accept();
						//配置非阻塞
						channel.configureBlocking(Boolean.FALSE);
						//注册 读事件,在selector.selectedKeys()时检查触发
						channel.register(selector, SelectionKey.OP_READ);
					}
					//是否可读取,即读取事件
					else if (key.isReadable()){
						//获取客户端
						SocketChannel channel = (SocketChannel) key.channel();
						//进行数据读取
						buffer.clear();
						int readLenth = channel.read(buffer);
						if (readLenth > 0){
							System.out.println("已接收数据:" + new String(buffer.array(), StandardCharsets.UTF_8));
						}
						//此处还可以注册 写事件,
						//channel.register(selector, SelectionKey.OP_WRITE);
						//由于此处的channel没有进行关闭,所以一直都会是可写的,selector.selectedKeys()会一直被触发
					}
				}
			}
		}
	}

可以看到,这个demo看起来复杂了不少,而且有一个很关键的对象 —> Selector类对象,为了方便理解,你可以把他想象为os系统底层的轮询函数(epoll,selector)对象,那么这段代码的逻辑就是创建好NIO服务后,向NIO服务器中注册选择器,并设置轮询是要捕获的事件(客户端连接事件,读,写事件等),捕获到事件后,在根据事件类型进行处理。

如上就是一个最基本的单线程NIO服务器了,通过与线程池的结合可以获得良好的性能,但是可以看到NIO的使用还是比较复杂,我们还需要解决nio服务器代码和业务代码的分离,当代码量多的时候,整套项目代码可能就显得非常格外繁重,而且各种可能出现的IO异常,网络异常都需要我们去处理,要把这些都做好并不是一件简单的事情。

netty

基于以上原因netty框架被设计了出来,它是JBOSS公司出品的一个异步的,基于事件驱动的高性能网络编程框架,它基于NIO,并作出了良好的封装,对可能出现的IO异常进行了规避和处理,将我们的业务代码进行了隔离,让我们将开发的重心放在业务上,因其优良的稳定性和良好的设计理念在网络编程中特别火,越来越多的程序员都习惯使用netty去进行网络开发。

首先先上一个netty的代码demo:

public class NettyServerSocket {

	private Integer port;

	public NettyServerSocket(Integer port) {
		this.port = port;
	}

	public void run(){
		//创建一个线程组,用于处理客户端连接
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		//创建一个线程组,用于处理与客户端的I/O操作
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			//引导类,帮助服务启动的辅助类,可以设置 Socket参数
			ServerBootstrap b = new ServerBootstrap();
			//将上述创建的两个线程组设置进来
			b.group(bossGroup, workerGroup)
					//指定所使用的NIO传输
					.channel(NioServerSocketChannel.class)
					//添加channel初始化器
					.childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						public void initChannel(SocketChannel ch) throws Exception {
							//添加一个channel处理器,为我们自定义的类,需要继承ChannelHandler
							ch.pipeline().addLast(new MyNioServerHandler());
						}
					})
					.option(ChannelOption.SO_BACKLOG, 128)
					.childOption(ChannelOption.SO_KEEPALIVE, true);

			// 绑定端口,开始接收进来的连接
			ChannelFuture f = b.bind(port).sync(); // (7)

			// 等待服务器  socket 关闭 。
			// 在这个例子中,这不会发生,但你可以优雅地关闭你的服务器。
			f.channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			workerGroup.shutdownGracefully();
			bossGroup.shutdownGracefully();
		}
	}
	public static void main(String[] args) {
		new NettyServerSocket(8888).run();
	}
}

/**
 * 业务处理器
 * 		父类有很多可供重写的方法
 */
class MyNioServerHandler extends ChannelInboundHandlerAdapter  {
	/**
	 * 当有客户端连入时,此方法会被调用
	 * @param ctx 连接上下文对象
	 * @throws Exception 可能出现的异常
	 */
	@Override
	public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
		super.channelRegistered(ctx);
	}
	
	//当收到客户端消息时,此方法会被调用
	public void channelRead(ChannelHandlerContext ctx,Object msg){
		//强转,netty优化了反人类的ByteBuffer类,变成了较为人性化的ByteBuf
		ByteBuf buf = (ByteBuf) msg;
		//打印消息
		System.out.println(buf.toString(CharsetUtil.UTF_8));
		//回写消息
		ctx.write(buf);
	}

	//在channelRead执行完后,此方法会被调用
	public void channelReadComplete(ChannelHandlerContext ctx){
		//将换区中的数据刷至传输流中,传给客户端
		ctx.flush();
	}

	//出异常时调用,用于异常处理
	public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
		//打印异常堆栈信息
		cause.printStackTrace();
		//关闭客户端连接,释放资源
		ctx.close();
	}
}

上述代码如果对netty框架不了解是不太好看懂的,因为这里不讲netty框架,所以只简单的解释以下上述代码,就是通过netty框架开启了一个nio服务,期间设置了一些初始化的参数,在这些参数中我们要关心的是那个由我们自己编写的handler,上述代码中是MyNioServerHandler,在这个handler类中可以看到,每个方法都是对于一种客户端事件的处理,连接事件,读写事件等等。

由于代码上的注释也比较清晰,这里就不做多的代码讲解了,可以看到netty框架的使用很简单,初始化一个netty服务器,然后在我们设计的handler中去编写我们的业务代码即可,很好的解开了服务器代码和业务代码的代码耦合,并且还帮我们规避了很多业务代码以外的风险,它里面封装的ByteBuf类也比NIO中反人类的ByteBuffer好用很多。

AIO (异步非阻塞IO)

AIO是JDK1.7的时候引入的,它是一个异步非阻塞的IO,它和NIO在概念上的区别就是一个异步,一个同步,这里说的形象一点,他们的差别就是,加入某个线程去read某个客户端的数据,对于NIO而言,是谁去dead,那么后续的处理也是谁干,而AIO则不一样,它会去调一个空闲的线程去干;

AIO的对于客户连接事件,读/写事件的处理跟使用Netty的时候是很像的,也是一种事件触发的方式,即我们提前写好对于这些事件的处理函数,然后当事件发生的时候就自动取调用这个函数,我们看以下代码:

public class AioServerSocket {
	//此处要设置aioServer为属性,由于在handler中要使用到这个属性
	public static AsynchronousServerSocketChannel serverSocketChannel;

	public static void main(String[] args) throws IOException, InterruptedException {
		//生成并封装一个处理线程池供nio服务器使用
		AsynchronousChannelGroup threadPoolGroup = AsynchronousChannelGroup.withFixedThreadPool(2,
				Executors.defaultThreadFactory());
		//注册aio的服务器
		serverSocketChannel = AsynchronousServerSocketChannel.open(threadPoolGroup);
		//绑定端口
		serverSocketChannel.bind(new InetSocketAddress(8888));
		//开始接受连接
		//参数一传的对象必须是参数二第二个泛型的子类,然后此对象会作为参数二对象的completed方法的参数
		//参数二是一个处理器,当有客户端连接时会调用该处理器的completed方法去处理
		serverSocketChannel.accept(new AioServerSocket(), new ReceivedHandler());
		System.out.println("aio服务器启动了。。。");
		//因为Aio是异步不阻塞的
		//所以为了防止main线程结束,此处让其一直循环休眠
		while (true){
			Thread.sleep(1000);
		}
	}
}

/**
 *	客户端连接处理器
 *
 *  @author zeng wenbin
 *  @date Created in 2019/11/11
 */
class ReceivedHandler implements CompletionHandler<AsynchronousSocketChannel, AioServerSocket>{
	//用来缓存数据
	private ByteBuffer buffer = ByteBuffer.allocate(1024);

	/**
	 * 当有客户端接入会触发此方法
	 */
	@Override
	public void completed(AsynchronousSocketChannel result, AioServerSocket attachment) {
		try {
			//读取客户端数据,使用读取的处理器去执行
			result.read(buffer, result, new ReadHandler(buffer));
			//此处要循环调用,否则只会处理一次连接请求
			AioServerSocket.serverSocketChannel.accept(attachment, this);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 出异常时会调用此方法
	 * @param exc
	 * @param attachment
	 */
	@Override
	public void failed(Throwable exc, AioServerSocket attachment) {
		try{
			exc.printStackTrace();
		} finally {
			// 监听新的请求,递归调用
			AioServerSocket.serverSocketChannel.accept(attachment, this);
		}
	}
}

/**
 *	客户端数据读取处理器
 *
 *  @author zeng wenbin
 *  @date Created in 2019/11/11
 */
class ReadHandler implements CompletionHandler<Integer, AsynchronousSocketChannel>{

	private ByteBuffer buffer;

	public ReadHandler(ByteBuffer buffer) {
		this.buffer = buffer;
	}

	@Override
	public void completed(Integer result, AsynchronousSocketChannel channel) {
		try {
			if (result < 0) {// 客户端关闭了连接
				channel.close();
			} else if (result == 0) {
				System.out.println("空数据"); // 处理空数据
			} else {
				// 读取请求,处理客户端发送的数据
				buffer.flip();
				channel.read(buffer);
				System.out.println("接收到数据:" + new String(buffer.array(), StandardCharsets.UTF_8));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void failed(Throwable exc, AsynchronousSocketChannel channel) {
		exc.printStackTrace();
		try {
			channel.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

可以看到上述存在3个类,由于代码中的注释也写的比较细了,这里就说以下关键点,第一个类的主线程中初始化了AIO服务器,同时注册了一个客户端连接的处理器,这样在客户端连接进来的时候,他会自动异步调用客户端连接的对应的方法,然后再这个处理器的completed方法中又向客户端连接对象注册了客户数据读取处理器ReadHandler,这样在已连接的客户端进行数据写入操作时,它又会立马异步调用ReadHandler中的complated方法,这就是AIO了。

可以看到AIO也完成了很好的代码解耦,我们也可以在AIO中专心书写我们的业务代码,但总的来说AIO对于功能的划分没有netty细致,而且netty为我们封装了很多很好用的类和方法,所以现在主流是使用netty去进行开发。

而对于性能方面,AIO和NIO一样都充分调用OS参与,需要操作系统的支持,但NIO基本都是使用操作系统的epoll函数,而AIO不太一样,它在window系统中调用了一个事件触发的机制,而epoll函数的原理却依然是轮询,所以在window系统中AIO的性能是比NIO好的,但是在linux系统中是没有事件触发这种机制的,所以不管是AIO还是NIO在linux系统上都是采用epoll函数做轮询,性能可以说是一样的。

而正是由于AIO和NIO两者在linux系统上性能基本一致,所以netty框架一致都基于NIO做开发,没有转向AIO,毕竟服务器大多都是放在linux上的。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值