浅谈Java IO模型BIO、NIO、Netty

225 阅读6分钟

简介

  • BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
  • NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
  • Netty是一个提供异步事件驱动的网络应用框架,用以快速开发高性能、高可靠的网络服务器和客户端程序。Netty简化了网络程序的开发,是很多框架和公司都在使用的技术。Netty并非横空出世,它是在BIO,NIO,AIO演变中的产物,是一种NIO框架。

BIO

  • 网络编程的基本模型是C/S模型,即两个进程间的通信。
  • 服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。
  • 传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。

在这里插入图片描述

该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就挂了。这种通信模型在高并发的场景是没法应用的。

**同步阻塞式BIO创建的Server源码**:
package com.scoket.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {
    public static void main(String[] args) throws IOException {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        RequestHandler requestHandler = new RequestHandler();
        try(ServerSocket serverSocket = new ServerSocket(8888)){
            System.out.println("BIOServer has started,listenning on port: "+serverSocket.getLocalSocketAddress());
            //多个客户端的请求连接
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("Connection from" + clientSocket.getRemoteSocketAddress());
                //为了给线程池可以去进行任务的执行
                executor.submit(new ClientHandler(clientSocket,requestHandler));
            }
        }
    }
}

同步阻塞式BIO创建的Client源码:

package com.scoket.bio;

import java.io.IOException;

import java.net.Socket;
import java.util.Scanner;

public class ClientHandler implements Runnable {
    private final Socket clientSocket;
    private final RequestHandler requestHandler;

    public ClientHandler(Socket clientSocket, RequestHandler requestHandler) {
        this.clientSocket = clientSocket;
        this.requestHandler = requestHandler;
    }
    @Override
    public void run() {
        try (Scanner input = new Scanner(clientSocket.getInputStream())) {
            //针对每个请求能够进行无限的交互操作
            while (true) {
                //真正阻塞的原因在这边,一直要等待客户端的输入信息,线程被耽搁的点
                String request = input.nextLine();
                if ("quit".equals(request)) {
                    break;
                }
                System.out.println(request);
                String response = requestHandler.handler(request);
                clientSocket.getOutputStream().write(response.getBytes());
            }
        }catch (IOException e){
            throw new RuntimeException(e);
        }
    }

}

同步阻塞式BIO创建的RequestHandler源码:

package com.scoket.bio;

public class RequestHandler {
    public String handler(String request){
        return "From BIOServer Hello" + request + "\n";
    }
}

NIO

  • NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现。
  • 新增的着两种通道都支持阻塞和非阻塞两种模式。
  • 阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。
  • 对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。
  • NIO编程是面向通道的(BIO是面向流的),流分为写入/写出,是单向的,意味着通道是可以进行双向读写的。NIO所有基于channel的API对数据的操作都是间接通过操作缓冲区ByteBuffer
  • In : 磁盘 --通道–> ByteBuffer(内存)–>数据(内存)
  • Out: 数据(内存)–>ByteBuffer(内存) --> 通道 --> 磁盘

NIO编程涉及ServerSocketChannel、SocketChannel、ByteBuffer、Selector(通道选择器)

在这里插入图片描述

同步非阻塞式NIO创建的服务端源码:

package com.scoket.nio;

import com.scoket.bio.RequestHandler;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;


public class NIOServer {
    //主线程  Channel[Server Client], Selector, Buffer
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(9999));
        System.out.println("NIOServer has started,listening on port: "+serverSocketChannel.getLocalAddress());
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        RequestHandler requestHandler = new RequestHandler();
        while (true){
            int select = selector.select();
            if (select == 0){
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                //处理每个客户端的连接操作
                SelectionKey key = iterator.next();
                if (key.isAcceptable()){
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = channel.accept();
                    System.out.println("Connection from: "+clientChannel.getRemoteAddress());
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector,SelectionKey.OP_READ);
                }
                if (key.isReadable()){
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.read(buffer);
                    String request = new String(buffer.array()).trim();
                    buffer.clear();
                    System.out.println(String.format("From %s : %s",channel.getRemoteAddress(),request));
                    String response = requestHandler.handler(request);
                    channel.write(ByteBuffer.wrap(response.getBytes()));
                }
                iterator.remove();;
            }
        }
    }
}

Netty

  • Netty的通讯管道:通过AOP的编程思想(责任链设计模式),实现消息的编解码。
    在这里插入图片描述

    服务端编程:
    1)创建ServerBootstrap sbt=new ServerBootstrap();
    2)创建EventLoopGroup boss、worker
    3)关联boss和worker> sbt.group(boss,worker);
    4)设置ServerSocketChannel实现类sbt.channel(NioServerSocketChannel);
    5)初始化通道sbt.childHandler(初始化通道);
    6)绑定端口并启动服务ChannelFuture future=sbt.bind(port).sync();
    7)等待服务关闭 future.channel().closeFuture().sync();
    8 ) 释放线程资源boss、worker # shutdownGraceFully();

Netty创建的客户端源码:

package com.cherry.socket.netty.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class ClientBootStrap {
    public static void main(String[] args) throws InterruptedException {
        //1.Create Bootstrap
        Bootstrap bt = new Bootstrap();
        //2.Create thread pool(worker)
        EventLoopGroup worker = new NioEventLoopGroup();
        //3.关联线程池
        bt.group(worker);
        //4.Set up the client
        bt.channel(NioSocketChannel.class);
        //5.Initialize the communication channel (key point)
        bt.handler(new ClientChannelInitializer());
        //6.连接
        ChannelFuture channelFuture = bt.connect("127.0.0.1",9999).sync();
        //7.Wait for the server to close
        channelFuture.channel().closeFuture().sync();
        //8.release the resource
        worker.shutdownGracefully();
    }
}
package com.cherry.socket.netty.client;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.CharsetUtil;
import javax.xml.transform.Source;

public class ClientChannelHander extends ChannelHandlerAdapter {
    /*捕获数据在通道传输过程的异常*/
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //super.exceptionCaught(ctx, cause);
        System.out.println("错误:"+cause.getMessage());
    }
    /*接收数据*/
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //super.channelRead(ctx, msg);
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println(((ByteBuf)msg).toString(CharsetUtil.UTF_8));
    }
    /*链接服务器时发送数据*/
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //super.channelActive(ctx);
        ByteBuf buf = Unpooled.buffer();
        buf.writeBytes("你好,我是客户端!".getBytes());
        ctx.writeAndFlush(buf);
    }
}
package com.cherry.socket.netty.client;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;

public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        //communication channel
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast(new ClientChannelHander());
    }
}

上述BIO NIO Netty所用的pom.xml

<!-- socket 相关  注意版本,不然有些方法不能用-->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>5.0.0.Alpha2</version>
</dependency>

通过对Netty的分析,我们将他的优点总结如下:
1.API使用简单,开发门槛低;
2.功能强大,预设了很多编码功能,支持多种主流协议;
3.定制能力强,可以通过ChannelHandler对通信框架进行灵活扩展;
4.性能高,通过与其他的主流NIO框架对比,Netty的综合性能最优;
5.成熟,稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG烦恼;
6.社区活跃,版本迭代周期短;
7.经过大规模的商业应用考验,质量得到验证。Netty在互联网、大数据、网络游戏、企业应用、电信软件等众多领域已经得到了成功商用,也 证明他已经完全能满足不同行业的商应用了;