架构底层的NIO入门,如何保证微服务高效通信的

Java BIO

  1. Java BIO 就是传统的 java io编程,其相关的类和接口在 java.io包下
  2. BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善
  3. BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解。

BIO 代码示例

package nio;

import java.io.InputStream;
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 Exception {
        //线程池机制
        //思路
        //1. 创建一个线程池
        //2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
        //ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        ExecutorService newCachedThreadPool = Executors.newSingleThreadExecutor();
        //创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器启动了");
        while (true) {
            System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName()+"等待连接....");
            //监听,等待客户端连接,阻塞
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");

            //就创建一个线程,与之通讯(单独写一个方法)
            newCachedThreadPool.execute(new Runnable() {
                public void run() { //我们重写
                    //可以和客户端通讯
                    handler(socket);
                }
            });
        }
    }

    //编写一个handler方法,和客户端通讯
    public static void handler(Socket socket) {
        try {
            byte[] bytes = new byte[1024];
            //通过socket 获取输入流
            InputStream inputStream = socket.getInputStream();
            //循环的读取客户端发送的数据
            while (true) {
                //read也是阻塞的
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName()+"读到:"+
                    new String(bytes, 0, read)); //输出客户端发送的数据
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("关闭和client的连接");
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

java BIO的缺点

  1. 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
    file
  2. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。其他请求不能使用当前被挂起的线程。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IIFgERKi-1587207564828)(http://www.txjava.cn/wordpress/wp-content/uploads/2020/04/image-1586499972233.png)]

我们可以通过telnet来测试

我们能看到,每一个请求都需要一个一个线程来处理
file
此时我们如果采用单例线程池来做多个请求,我们发现程序无法同时处理多个请求,只有关闭正在阻塞的请求才能去处理其他的请求。显然不合理。

此种方式符合 同步阻塞IO模型
file

NIO

  1. Java NIO 全称 java non-blocking IO,是指 JDK提供的新 API。从JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为NIO(即New IO),是同步非阻塞的
  2. NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
  3. NIO有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
  4. NIO 是面向缓冲区编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
  5. Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
  6. 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。
  7. HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP1.1 大了好几个数量级
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ef2wNLD3-1587207564837)(http://www.txjava.cn/wordpress/wp-content/uploads/2020/04/image-1586502329862.png)]

简单NIO示例

服务端代码:

package nio.demo1;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class NIOServer {

    /**
     * 创建一个缓存区
     * @param args
     * @throws Exception
     */

    static ByteBuffer buffer = ByteBuffer.allocate(1024);

    public static void main(String[] args) throws Exception {
        //获得服务器断点的服务管道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //绑定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置成异步
        serverSocketChannel.configureBlocking(false);

        while (true){
            //获得客户端连接过来的管道
            SocketChannel socketChannel = serverSocketChannel.accept();
            if(socketChannel != null){
                socketChannel.configureBlocking(false);
                int length = socketChannel.read(buffer);
                if(length != -1){
                    System.out.println("接收到的数据是:"+new String(buffer.array(), 0, length));
                }
            }
        }
    }
}

客户端代码

public class NIOClient {

    public static void main(String[] args) throws Exception {
        InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLocalHost(), 6666);
        Socket socket = new Socket();
        socket.connect(socketAddress);
        socket.getOutputStream().write("hello".getBytes());
    }
}

在服务端由于是非阻塞IO,那么我们无法监控到客户端的连接,所以我们采用不断轮询的方式来监控(显然这是一种性能的巨大消耗),此种方法是同步非阻塞IO模型的一种方式
file

NIO多路复用解决方案

我们可以看到,我们刚刚手动的方式来做了一个很不合理的方案。那么到底什么样的方案是完美的方案呢?

选择器

  1. Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)
  2. Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
  3. 只有在连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程, 避免了多线程之间的上下文切换导致的开销
    file

NIO多路复用实现代码

服务端:

package nio.demo2;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NIOServer1 {

    public static void main(String[] args) throws Exception {
        //获得服务器断点的服务管道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //绑定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置成异步
        serverSocketChannel.configureBlocking(false);
        //创建Selector
        Selector selector = Selector.open();
        //注册
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            while(selector.select(2000) == 0){
                System.out.println("等待连接");
                continue;
            }
            //获得准备就绪的key
            Set<SelectionKey> keys = selector.selectedKeys();
            //遍历keys
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()){
                //获得key
                SelectionKey key = iterator.next();
                if(key.isAcceptable()){
                    //获得通道
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    if(socketChannel != null){
                        System.out.println(Thread.currentThread().getName()+" 接收到一个连接:"+socketChannel.getRemoteAddress());
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    }
                }
                if(key.isReadable()){
                    //获得通道
                    SocketChannel channel = (SocketChannel) key.channel();
                    //获得缓冲区
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    int length = channel.read(buffer);
                    if(length != -1){
                        System.out.println(Thread.currentThread().getName()+"  发送的数据是:"+new String(buffer.array(), 0, length)+"   客户端的地址:"+channel.getRemoteAddress());
                    }
                    buffer.clear();
                }
                iterator.remove();
            }
        }
    }
}

程序流程:

file

  1. 服务端启动时候获得Selector,注册给ServerSocketChannel
  2. Selector 调用select方法, 监听请求和读写事件,返回有事件发生的通道的个数.
  3. 通过selector获得就绪状态的键集合,然后遍历这个集合判断这个键的类型
  4. 当客户端连接时key类型是OP_ACCEPT,会通过 ServerSocketChannel 得到 SocketChannel, 把该通道连同buffer注册到selector选择器上。一个 selector 上可以注册多个 SocketChannel
  5. 注册后返回一个 SelectionKey, 会和该 Selector 关联(集合)
  6. 进一步得到各个 SelectionKey (有事件发生)
  7. 如果key的类型是OP_READ或者OP_WRITE(读写),通过 SelectionKey反向获取 SocketChannel , 方法 channel()
  8. 可以通过得到的channel, 完成业务处理
    此刻我会发现我们的NIO使用的就是IO多路复用的模型。
    file
    更详细的Spring源码解析请关注:java架构师免费课程
    每晚20:00直播分享高级java架构技术
    扫描加入QQ交流群264572737
    在这里插入图片描述
发布了87 篇原创文章 · 获赞 63 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/renlianggee/article/details/105603346