bio nio netty的演变

本文详细介绍了Java中的同步堵塞BIO、同步非堵塞NIO、同步非堵塞NIO采用多路复用器(selector)以及异步非堵塞AIO(NIO2)四种网络编程模型,分析了各自的优缺点和应用场景。此外,还探讨了Netty基于NIO的reactor事件触发模型,以及如何在Netty中实现服务器和客户端的通信。
摘要由CSDN通过智能技术生成

1、同步堵塞BIO

 

 缺点:

1、IO代码里read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源
2、如果线程很多,会导致服务器线程太多,压力太大,比如C10K问题
应用场景:
BIO 方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高, 但程序简单易理解。

 

package yc.fen.dou.nettyStu.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8000);
        while (true) {
            System.out.println("等待连接。。");
            Socket clientSocket = serverSocket.accept();  //堵塞位置1
            System.out.println("有客户端连接了。。");
            handler(clientSocket);
        }
    }
    private static void handler(Socket clientSocket) throws IOException {
        byte[] bytes = new byte[1024];
        System.out.println("准备read。。");
        //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
        int read = clientSocket.getInputStream().read(bytes);
        System.out.println("read完毕。。");
       // if (read !=  ‐1) {
            System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
     ///   }
        clientSocket.getOutputStream().write("HelloClient".getBytes());
        clientSocket.getOutputStream().flush();
    }
}

 

以上是传统的bio模型实现代码,

缺点:如果这个请求handler(clientSocket)没有处理完,serverSocket.accept()会一直处于堵塞状态

package yc.fen.dou.nettyStu.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8000);
        while (true) {
            System.out.println("等待连接。。");
            Socket clientSocket = serverSocket.accept();  //堵塞位置1
            System.out.println("有客户端连接了。。");
        //    handler(clientSocket);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(clientSocket); //堵塞位置2
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
    private static void handler(Socket clientSocket) throws IOException {
        byte[] bytes = new byte[1024];
        System.out.println("准备read。。");
        //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
        int read = clientSocket.getInputStream().read(bytes);
        System.out.println("read完毕。。");
       // if (read !=  ‐1) {
            System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
     ///   }
        clientSocket.getOutputStream().write("HelloClient".getBytes());
        clientSocket.getOutputStream().flush();
    }
}

 优点:handler(clientSocket)采用异步处理,serverSocket.accept()不会进行堵塞

 缺点:当请求量很大的时候,频繁创建线程会消耗过多内存资源

2、同步非堵塞NIO 

 同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用

器轮询到连接有IO请求就进行处理,JDK1.4开始引入。
应用场景:
NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯,编程比较复杂
package yc.fen.dou.nettyStu.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class NioServer {
    // 保存客户端连接
    static List<SocketChannel> channelList = new ArrayList<>();
    public static void main(String[] args) throws IOException, InterruptedException {
        // 创建NIO ServerSocketChannel,与BIO的serverSocket类似
         ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false); //false为接受客户请求不堵塞
        System.out.println("服务启动成功");
        while (true) {
            // 非阻塞模式accept方法不会阻塞,否则会阻塞
            // NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
            SocketChannel socketChannel = serverSocket.accept();
            if (socketChannel != null) {
                // 如果有客户端进行连接
                 System.out.println("连接成功");
                 // 设置SocketChannel为非阻塞
                 socketChannel.configureBlocking(false);//false为处理客户请求不堵塞
                 // 保存客户端连接在List中
                  channelList.add(socketChannel);
            }
            // 遍历连接进行数据读取
            Iterator<SocketChannel> iterator = channelList.iterator();
            while (iterator.hasNext()) {
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                // 非阻塞模式read方法不会阻塞,否则会阻塞
                 int len = sc.read(byteBuffer);
                 // 如果有数据,把数据打印出来
                  if (len > 0) {
                      System.out.println("接收到消息:" + new String(byteBuffer.array()));
                  } else if (len == Integer.parseInt("-1")) {
                      // 如果客户端断开,把socket从集合中去掉
                      iterator.remove();
                      System.out.println("客户端断开连接");
                  }
            }
        }
    }
}

优点:1、在接受客户端请求的时候,不会堵塞,在处理任务的时候也不会堵塞

 缺点:在处理任务的时候也不会堵塞,但是因为是单线程,只能一个一个进行处理,比如有一万个请求, Iterator<SocketChannel> iterator = channelList.iterator(); channelList有1w个记录, 其中只有10个请求数据准备好了,那10个已经准备的请求可能被其他没有准备好的请求堵塞了,因为要一个一个执行

 

3、同步非堵塞nio采用多路复用器(selector)

 
NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(多路复用器)
1、channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组
2、channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理
3、NIO 的 Buffer 和 channel 都是既可以读也可以写(bio中的read只能读,write只能写)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NioSelectorServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        // 创建NIO ServerSocketChannel
         ServerSocketChannel serverSocket = ServerSocketChannel.open();
         serverSocket.socket().bind(new InetSocketAddress(9000));
         // 设置ServerSocketChannel为非阻塞
         serverSocket.configureBlocking(false);
         // 打开Selector处理Channel,即创建epoll
          Selector selector = Selector.open();
          // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
          serverSocket.register(selector, SelectionKey.OP_ACCEPT);
          System.out.println("服务启动成功");
          while (true) {
              // 阻塞等待需要处理的事件发生
               selector.select();
               // 获取selector中注册的全部事件的 SelectionKey 实例
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                // 遍历SelectionKey对事件进行处理
               while (iterator.hasNext()) {
                   SelectionKey key = iterator.next();
                   // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = server.accept();
                        socketChannel.configureBlocking(false);
                        // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                        socketChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("客户端连接成功");
                    } else if (key.isReadable()) {
                        // 如果是OP_READ事件,则进行读取和打印
                         SocketChannel socketChannel = (SocketChannel) key.channel();
                         ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                         int len = socketChannel.read(byteBuffer);
                         // 如果有数据,把数据打印出来
                         if (len > 0) {
                             System.out.println("接收到消息:" + new String(byteBuffer.array()));
                         } else if (len == Integer.parseInt("-1")) {
                             // 如果客户端断开连接,关闭Socket
                             System.out.println("客户端断开连接");
                             socketChannel.close();
                         }
                    }
                    //从事件集合里删除本次处理的key,防止下次select重复处理
                     iterator.remove();
               }
          }
    }
}

 SelectionKey一共有四个事件:

客户端的SocketChannel支持 OP_CONNECT, OP_READ, OP_WRITE三个操作。服务端ServerSocketChannel只支持OP_ACCEPT操作,在服务端由ServerSocketChannel的accept()方法产生的SocketChannel只支持OP_READ, OP_WRITE操作。

client/ServerSocketChannel/ServerSocketChannelOP_ACCEPTOP_CONNECTOP_WRITEOP_READ
clientSocketChannel YYY
serverServerSocketChannelY   
serverSocketChannel  YY

OP_ACCEPT就绪条件:当收到一个客户端的连接请求时,该操作就绪。这是ServerSocketChannel上唯一有效的操作。
OP_CONNECT就绪条件:只有客户端SocketChannel会注册该操作,当客户端调用SocketChannel.connect()时,该操作会就绪。
OP_READ就绪条件:该操作对客户端和服务端的SocketChannel都有效,当OS的读缓冲区中有数据可读时,该操作就绪。
OP_WRITE就绪条件:该操作对客户端和服务端的SocketChannel都有效,当OS的写缓冲区中有空闲的空间时(大部分时候都有),该操作就绪。

 

 

 nio也是一个线程,但是不会堵塞,根据现在epoll模型会循环就绪的连接,读,取事件

假如现在有一万个socket连接:

package yc.fen.dou.nettyStu.nio.selectorStu;

public class NioSelectorServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        // 创建NIO ServerSocketChannel
         ServerSocketChannel serverSocket = ServerSocketChannel.open();
         serverSocket.socket().bind(new InetSocketAddress(9000));
         // 设置ServerSocketChannel为非阻塞
         serverSocket.configureBlocking(false);
         // 打开Selector处理Channel,即创建epoll
          Selector selector = Selector.open();     
          // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣图灵诸葛老师
          serverSocket.register(selector, SelectionKey.OP_ACCEPT);       
          System.out.println("服务启动成功");
          while (true) {   //轮询去就绪队列中获取所有的就绪事件
              // 阻塞等待需要处理的事件发生
              //轮询就绪的连接,读,取事件,如果有1w个连接,其中有1q个就绪事件,一次select调用会获取这些1q个就绪的事件
               selector.select();         
               // 获取selector中注册的全部事件的 SelectionKey 实例
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                System.out.println("------keySize------"+selectionKeys.size());
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                // 遍历SelectionKey对事件进行处理
               while (iterator.hasNext()) {
                   SelectionKey key = iterator.next();
                    if (key.isAcceptable()) {
                       //handAccptable()
                    } else if (key.isReadable()) {
                       //handRead()
                    }else if (key.isWritable()) {
                       //handWrite()
                    }
               }
          }
    }
}

 现在来了解一下操作系统epoll模型

 理解select,poll,epoll技术的演变:

I/O多路复用底层主要用的Linux 内核函数(select,poll,epoll)来实现,windows不支持epoll实现,windows底层是基于winsock2的
select函数实现的(不开源)
 
select
poll
epoll(jdk 1.5及以上)
操作方式
遍历
遍历
回调
底层实现
数组
链表
哈希表
IO效率
每次调用都进行线 性遍历,时间复杂 度为O(n)
每次调用都进行 线性遍历,时间
复杂度为O(n)
事件通知方式,每当有IO事件 就绪,系统注册的回调函数就
会被调用,时间复杂度O(1)
最大连接
有上限
无上限
无上限
Redis线程模型 图灵诸葛老师
Redis就是典型的基于epoll的NIO线程模型(nginx也是),epoll实例收集所有事件(连接与读写事件),由一个服务端线程连续处理所有事件 命令。
Redis底层关于epoll的源码实现在redis的src源码目录的ae_epoll.c文件里,感兴趣可以自行研究。
 

 4、异步非堵塞AIO(NIO2)

 

其底层也是用epoll模型

适用:由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用

5、netty 底层使用NIO多路复用模型

 为什么Netty使用NIO而不是AIO?

在Linux系统上,AIO的底层实现仍使用Epoll,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优
化,Linux上AIO还不够成熟。Netty是 异步非阻塞 框架,Netty在NIO上做了很多异步的封装。
 
现在来了解netty基于NIO的reactor事件触发模型:

server端:

package yc.fen.dou.nettyStu.nio.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class NettyServer {

    public static void main(String[] args) throws Exception {

        //创建两个线程组bossGroup和workerGroup, 含有的子线程NioEventLoop的个数默认为cpu核数的两倍
        // bossGroup只是处理连接请求 ,真正的和客户端业务处理,会交给workerGroup完成
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //创建服务器端的启动对象
            ServerBootstrap bootstrap = new ServerBootstrap();
            //使用链式编程来配置参数
            bootstrap.group(bossGroup, workerGroup) //设置两个线程组
                    .channel(NioServerSocketChannel.class) //使用NioServerSocketChannel作为服务器的通道实现
                    // 初始化服务器连接队列大小,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。
                    // 多个客户端同时来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new ChannelInitializer<SocketChannel>() {//创建通道初始化对象,设置初始化参数

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //对workerGroup的SocketChannel设置处理器
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            System.out.println("netty server start。。");
            //绑定一个端口并且同步, 生成了一个ChannelFuture异步对象,通过isDone()等方法可以判断异步事件的执行情况
            //启动服务器(并绑定端口),bind是异步操作,sync方法是等待异步操作执行完毕
            ChannelFuture cf = bootstrap.bind(9000).sync();
            //给cf注册监听器,监听我们关心的事件
            /*  cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                if (cf.isSuccess()) {
                      System.out.println("监听端口9000成功");
                } else {
                      System.out.println("监听端口9000失败");
                }
              }
             });*/
            //对通道关闭进行监听,closeFuture是异步操作,监听通道关闭
            // 通过sync方法同步等待通道关闭处理完毕,这里会阻塞等待通道关闭完成
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}


 

package yc.fen.dou.nettyStu.nio.netty;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

/**
 * 自定义Handler需要继承netty规定好的某个HandlerAdapter(规范)
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 读取客户端发送的数据
     *
     * @param ctx 上下文对象, 含有通道channel,管道pipeline
     * @param msg 就是客户端发送的数据
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("服务器读取线程 " + Thread.currentThread().getName());
        //Channel channel = ctx.channel();
        //ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链接, 出站入站
        //将 msg 转成一个 ByteBuf,类似NIO 的 ByteBuffer
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 数据读取完毕处理方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ByteBuf buf = Unpooled.copiedBuffer("HelloClient", CharsetUtil.UTF_8);
        ctx.writeAndFlush(buf);
    }

    /**
     * 处理异常, 一般是需要关闭通道
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

 client端:

package yc.fen.dou.nettyStu.nio.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyClient {
    public static void main(String[] args) throws Exception {
        //客户端需要一个事件循环组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            //创建客户端启动对象
            //注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            //设置相关参数
            bootstrap.group(group) //设置线程组
                    .channel(NioSocketChannel.class) // 使用 NioSocketChannel 作为客户端的通道实现
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel channel) throws Exception {
                            //加入处理器
                            channel.pipeline().addLast(new NettyClientHandler());
                        }
                    });
            System.out.println("netty client start");
            //启动客户端去连接服务器端
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9000).sync();
            //对关闭通道进行监听
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }

    }
}


package yc.fen.dou.nettyStu.nio.netty;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 当客户端连接服务器完成就会触发该方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf buf = Unpooled.copiedBuffer("HelloServer", CharsetUtil.UTF_8);
        ctx.writeAndFlush(buf);
    }

    //当通道有读取事件时会触发,即服务端发送数据给客户端
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("收到服务端的消息:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("服务端的地址: " + ctx.channel().remoteAddress());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

 

同步跟堵塞的理解

1、同步和异步,在请求io的时候,客户线程是否会被堵塞 ,如果会被堵塞,则为同步,如果不会被堵塞,则为异步

2、堵塞跟非堵塞

            堵塞:就是客户线程在传输数据的时候,服务器端在接收数据的过程是否会被堵塞

           非堵塞:就是客户线程在传输数据的时候,服务器不会进行堵塞

                        1、非堵塞轮询的方式,就是轮询所有的请求,看哪个已经准备好数据了

                        2、非堵塞多路复用selector方式,就是操作系统帮我们准备好 已经准备好数据的事件,然后选择器就会去处理已经准备好的事件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值