网络编程 之 从BIO到 NIO加多线程高性能网络编程实战

网络编程 之 从BIO到 NIO加多线程高性能网络编程实战

阻塞非阻塞/同步异步

概念

阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞

同步和异步是指:发送方和接收方是否协调步调一致

  • 阻塞(blocking) IO:资源不可用时,IO请求一直阻塞,直到反馈结果(有数据或超时)。
  • 非阻塞(non-blocking)IO: 资源不可用时,IO请求离开返回,返回数据标识资源不可用。
  • 同步(synchronous) IO:应用阻塞在发送或接收数据的状态,直到数据传输成功或返回结果。
  • 异步(asynchronous) IO:应用发送或接收数据后立刻返回,处理结果通过回调来通知。

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候。

阻塞:往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里。

非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。

同步和异步都是基于应用程序和操作系统处理 IO 事件所采用的方式。比如同步:是应用程序要直接参与 IO 读写的操作。异步:所有的 IO 读写交给操作系统去处理,应用程序只需要等待通知。

同步方式在处理 IO 事件的时候,必须阻塞在某个方法上面等待我们的 IO 事件完成(阻塞 IO 事件或者通过轮询 IO事件的方式),对于异步来说,所有的 IO 读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完成真正的 IO 操作,当操作完成 IO 后,会给我们的应用程序一个通知。

同步 : 阻塞到 IO 事件,阻塞到 read 或则 write。这个时候我们就完全不能做自己的事情。让读写方法加入到线程里面,然后阻塞线程来实现,对线程的性能开销比较大。

代码中使用的API:ServerSocket#accept、InputStream#read 都是阻塞的API。操作系统底层API中,默认Socket操作都是Blocking型,send/recv等接口都是阻塞的。

带来的问题: 阻塞导致在处理网络I/O时,一个线程只能处理一个网络连接。

组合

同步阻塞

同步非阻塞

异步阻塞

异步非阻塞

IO模型

5种IO模型

  • 阻塞IO模型:Socket Read 面向字节流InputStream、OutPutStream
  • 非阻塞IO模型:不再面向字节流,面向缓冲 API,Java NIO 面向缓冲实现
  • IO复用模型
  • 信号驱动IO模型
  • 异步IO模型:AIO Windows的IOCP支持比较好,目前linux没有好的支持

linux底层IO多路复用事件模型

select、poll,返回后遍历文件描述符来获取已经就绪的socket,描述符越多效率越低

epoll、kqueue,不需要遍历了,注册了一个事件函数,效率比poll要高

windows aio 事件模型:IOCP

JAVA NIO

始于Java 1.4,提供了新的JAVA IO操作非阻塞API。用意是替代Java IO 和 Java Networking相关的API。

NIO中的三个核心组件

  • Buffer:缓冲区
  • Channel:通道
  • Selector:选择器

Buffer 缓冲区

Buffer 缓冲区介绍

缓冲区本质上是一个可以写入数据的内存块(类似数组),然后可以再次读取。此内存块包含在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用内存块。

相比较直接对数组的操作,Buffer API更加容易操作和管理。

使用Buffer进行数据写入与读取,需要进行如下四个步骤:

  1. 将数据写入缓冲区

  2. 调用buffer.flip(),转换为读取模式

  3. 缓冲区读取数据

  4. 调用buffer.clear()或buffer.compact()转为写模式

Buffer 缓冲区工作原理

Buffer三个重要属性:

capacity 容量: 作为一个内存块,Buffer具有一定的固定大小,也称为"容量" 。

position 位置: 写入模式时代表写数据的位置。读取模式时代表读取数据的位置。

limit 限制: 写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。

ByteBuffer内存类型

介绍

ByteBuffer为性能关键型代码提供了直接内存(direct堆外)和非直接内存(heap堆)两种实现。

堆内内存获取的方式:

java 复制代码
ByteBuffer directByteBuffer = ByteBuffer.allocate(noBytes);

堆外内存获取的方式:

java 复制代码
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(noBytes);

好处:

1、 进行网络IO或者文件IO时比heapBuffer少一次拷贝。(file/socket ---- OS memory ---- jvm heap)

GC会移动对象内存,在写file或socket的过程中, JVM的实现中,会先把数据复制到堆外,再进行写入。

2、 GC范围之外,降低GC压力,但实现了自动管理。DirectByteBuffer中有一个Cleaner对象(PhantomReference),

Cleaner被GC前会执行clean方法,触发DirectByteBuffer中定义的Deallocator

建议:

1、 性能确实可观的时候才去使用;分配给大型、长寿命; (网络传输、文件读写场景)

2、 通过虚拟机参数MaxDirectMemorySize限制大小,防止耗尽整个机器的内存;

使用
堆内
java 复制代码
public class BufferDemo {
    public static void main(String[] args) {
        // 构建一个byte字节缓冲区,容量是4
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        // 默认写入模式,查看三个重要的指标
        System.out.println(String.format("初始化:capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));
        // 写入2字节的数据
        byteBuffer.put((byte) 1);
        byteBuffer.put((byte) 2);
        byteBuffer.put((byte) 3);
        // 再看数据
        System.out.println(String.format("写入3字节后,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));

        // 转换为读取模式(不调用flip方法,也是可以读取数据的,但是position记录读取的位置不对)
        System.out.println("#######开始读取");
        byteBuffer.flip();
        byte a = byteBuffer.get();
        System.out.println(a);
        byte b = byteBuffer.get();
        System.out.println(b);
        System.out.println(String.format("读取2字节数据后,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));

        // 继续写入3字节,此时读模式下,limit=3,position=2.继续写入只能覆盖写入一条数据
        // clear()方法清除整个缓冲区。compact()方法仅清除已阅读的数据。转为写入模式
        byteBuffer.compact(); // buffer : 1 , 3
        byteBuffer.put((byte) 3);
        byteBuffer.put((byte) 4);
        byteBuffer.put((byte) 5);
        System.out.println(String.format("最终的情况,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
                byteBuffer.position(), byteBuffer.limit()));

        // rewind() 重置position为0
        // mark() 标记position的位置
        // reset() 重置position为上次mark()标记的位置

    }
}
堆外
java 复制代码
public class DirectBufferDemo {
   public static void main(String[] args) {
      // 构建一个byte字节缓冲区,容量是4
      ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4);
      // 默认写入模式,查看三个重要的指标
      System.out.println(String.format("初始化:capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
            byteBuffer.position(), byteBuffer.limit()));
      // 写入2字节的数据
      byteBuffer.put((byte) 1);
      byteBuffer.put((byte) 2);
      byteBuffer.put((byte) 3);
      // 再看数据
      System.out.println(String.format("写入3字节后,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
            byteBuffer.position(), byteBuffer.limit()));

      // 转换为读取模式(不调用flip方法,也是可以读取数据的,但是position记录读取的位置不对)
      System.out.println("#######开始读取");
      byteBuffer.flip();
      byte a = byteBuffer.get();
      System.out.println(a);
      byte b = byteBuffer.get();
      System.out.println(b);
      System.out.println(String.format("读取2字节数据后,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
            byteBuffer.position(), byteBuffer.limit()));

      // 继续写入3字节,此时读模式下,limit=3,position=2.继续写入只能覆盖写入一条数据
      // clear()方法清除整个缓冲区。compact()方法仅清除已阅读的数据。转为写入模式
      byteBuffer.compact();
      byteBuffer.put((byte) 3);
      byteBuffer.put((byte) 4);
      byteBuffer.put((byte) 5);
      System.out.println(String.format("最终的情况,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(),
            byteBuffer.position(), byteBuffer.limit()));
      byteBuffer.array();
      // rewind() 重置position为0
      // mark() 标记position的位置
      // reset() 重置position为上次mark()标记的位置
   }
}

Channel通道

详解

Channel的API涵盖了UDP/TCP网络和文件IO

FileChannel

DatagramChannel

SocketChannel

ServerSocketChannel

和标准IO Stream操作的区别

在一个通道内进行读取和写入,可以非阻塞读取和写入通道,通道始终读取或写入缓冲区,而stream通常是单向的(input或output)

SocketChannel 客户端

SocketChannel用于建立TCP网络连接,类似java.net.Socket。有两种创建socketChannel形式:

  1. 客户端主动发起和服务器的连接。

  2. 服务端获取的新连接

java 复制代码
// 客户端主动发起连接的方式
SocketChannel socketChannel = SocketChannel.open(); 
socketChannel .configureBlocking( false ); // 设置为非阻塞模式
socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));

socketChannel.write(byteBuffer); // 发送请求数据 -- 向通道写入数据
int bytesRead = socketChannel.read( byteBuffer ); // 读取服务端返回 -- 读取缓冲区的数据
socketChannel.close(); // 关闭连接

write写: write()在尚未写入任何内容时就可能返回了。需要在循环中调用write()。

read读: read()方法可能直接返回而根本不读取任何数据,根据返回的int值判断读取了多少字节。

ServerSocketChannel 服务端

ServerSocketChannel可以监听新建的TCP连接通道,类似ServerSocket。

java 复制代码
// 创建网络服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 绑定端口
while(true){
    SocketChannel socketChannel = serverSocketChannel.accept(); // 获取新tcp连接通道
    if(socketChannel != null){ 
    // tcp请求 读取/响应
    }
}

**serverSocketChannel.accept():**如果该通道处于非阻塞模式,那么如果没有挂起的连接,该方法将立即返回null。 必须检查返回的 SocketChannel是否为null

使用Channel写一个服务器

模拟服务端NIOServer
java 复制代码
/**
 * 直接基于非阻塞的写法
 */
public class NIOServer {

    public static void main(String[] args) throws Exception {
        // 创建网络服务端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
        serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 绑定端口
        System.out.println("启动成功");
        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept(); // 获取新tcp连接通道
            // tcp请求 读取/响应
            if (socketChannel != null) {
                System.out.println("收到新连接 : " + socketChannel.getRemoteAddress());
                socketChannel.configureBlocking(false); // 默认是阻塞的,一定要设置为非阻塞
                try {
                    ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
                    while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
                        // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
                        if (requestBuffer.position() > 0) break;
                    }
                    if(requestBuffer.position() == 0) continue; // 如果没数据了, 则不继续后面的处理
                    requestBuffer.flip();
                    byte[] content = new byte[requestBuffer.limit()];
                    requestBuffer.get(content);
                    System.out.println(new String(content));
                    System.out.println("收到数据,来自:"+ socketChannel.getRemoteAddress());

                    // 响应结果 200
                    String response = "HTTP/1.1 200 OK\r\n" +
                            "Content-Length: 11\r\n\r\n" +
                            "Hello World";
                    ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                    while (buffer.hasRemaining()) {
                        socketChannel.write(buffer);// 非阻塞
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        // 用到了非阻塞的API, 在设计上,和BIO可以有很大的不同.继续改进
    }
}
模拟客户端NIOClient
java 复制代码
public class NIOClient {

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        while (!socketChannel.finishConnect()) {
            // 没连接上,则一直等待
            Thread.yield();
        }
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入:");
        // 发送内容
        String msg = scanner.nextLine();
        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
        while (buffer.hasRemaining()) {
            socketChannel.write(buffer);
        }
        // 读取响应
        System.out.println("收到服务端响应:");
        ByteBuffer requestBuffer = ByteBuffer.allocate(1024);

        while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
            // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
            if (requestBuffer.position() > 0) break;
        }
        requestBuffer.flip();
        byte[] content = new byte[requestBuffer.limit()];
        requestBuffer.get(content);
        System.out.println(new String(content));
        scanner.close();
        socketChannel.close();
    }

}
模拟服务端NIOServer的升级版1

升级前的版本是一直轮询通道的方式,接收到一个客户端channel就处理一个

缺点:只能一个挨着一个执行,轮询通道的方式,低效,浪费CPU

升级后的版本也是一直轮询通道的方式,接收一个客户端channel,先缓存到一个列表中,当没有客户端channel的时候再去遍历列表处理

缺点:虽然能在少量的请求中,解决轮询浪费CPU,但是请求量大了导致要么一直在接受channel,添加到列表中,无法处理请求,要么一直在处理请求无法接收请求

java 复制代码
**
 * 直接基于非阻塞的写法,一个线程处理轮询所有请求
 */
public class NIOServer1 {
    /**
     * 已经建立连接的集合
     */
    private static ArrayList<SocketChannel> channels = new ArrayList<>();

    public static void main(String[] args) throws Exception {
        // 创建网络服务端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
        serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 绑定端口
        System.out.println("启动成功");
        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept(); // 获取新tcp连接通道
            // tcp请求 读取/响应
            if (socketChannel != null) {
                System.out.println("收到新连接 : " + socketChannel.getRemoteAddress());
                socketChannel.configureBlocking(false); // 默认是阻塞的,一定要设置为非阻塞
                channels.add(socketChannel);
            } else {
                // 没有新连接的情况下,就去处理现有连接的数据,处理完的就删除掉
                Iterator<SocketChannel> iterator = channels.iterator();
                while (iterator.hasNext()) {
                    SocketChannel ch = iterator.next();
                    try {
                        ByteBuffer requestBuffer = ByteBuffer.allocate(1024);

                        if (ch.read(requestBuffer) == 0) {
                            // 等于0,代表这个通道没有数据需要处理,那就待会再处理
                            continue;
                        }
                        while (ch.isOpen() && ch.read(requestBuffer) != -1) {
                            // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
                            if (requestBuffer.position() > 0) break;
                        }
                        if (requestBuffer.position() == 0) continue; // 如果没数据了, 则不继续后面的处理
                        requestBuffer.flip();
                        byte[] content = new byte[requestBuffer.limit()];
                        requestBuffer.get(content);
                        System.out.println(new String(content));
                        System.out.println("收到数据,来自:" + ch.getRemoteAddress());

                        // 响应结果 200
                        String response = "HTTP/1.1 200 OK\r\n" +
                                "Content-Length: 11\r\n\r\n" +
                                "Hello World";
                        ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                        while (buffer.hasRemaining()) {
                            ch.write(buffer);
                        }
                        iterator.remove();
                    } catch (IOException e) {
                        e.printStackTrace();
                        iterator.remove();
                    }
                }
            }
        }
        // 用到了非阻塞的API, 再设计上,和BIO可以有很大的不同
        // 问题: 轮询通道的方式,低效,浪费CPU
    }
}
模拟服务器NIOServer的升级版2
java 复制代码
/**
 * 结合Selector实现非阻塞服务器
 */
public class NIOServerV2 {

    public static void main(String[] args) throws Exception {
        // 1. 创建服务端的channel对象
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式

        // 2. 创建Selector
        Selector selector = Selector.open();

        // 3. 把服务端的channel注册到selector,注册accept事件
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0);
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        // 4. 绑定端口,启动服务
        serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 绑定端口
        System.out.println("启动成功");

        while (true) {
            // 5. 启动selector(管家)
            selector.select();// 阻塞,直到事件通知才会返回

            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                if (key.isAcceptable()) {
                    SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("收到新连接:" + socketChannel);
                } else if (key.isReadable()) {// 客户端连接有数据可以读时触发
                    try {
                        SocketChannel socketChannel = (SocketChannel) key.channel();

                        ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
                        while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
                            // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
                            if (requestBuffer.position() > 0) break;
                        }
                        if (requestBuffer.position() == 0) continue; // 如果没数据了, 则不继续后面的处理
                        requestBuffer.flip();
                        byte[] content = new byte[requestBuffer.remaining()];
                        requestBuffer.get(content);
                        System.out.println(new String(content));
                        System.out.println("收到数据,来自:" + socketChannel.getRemoteAddress());
                        // TODO 业务操作 数据库 接口调用等等

                        // 响应结果 200
                        String response = "HTTP/1.1 200 OK\r\n" +
                                "Content-Length: 11\r\n\r\n" +
                                "Hello World";
                        ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                        while (buffer.hasRemaining()) {
                            socketChannel.write(buffer);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        key.cancel();
                    }
                }
            }
        }
    }
}
模拟客户端NIOClient的升级版2
java 复制代码
public class NIOClientV2 {

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        Selector selector = Selector.open();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        socketChannel.connect(new InetSocketAddress("localhost", 8080));

        while (true) {
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                iterator.remove();

                if (selectionKey.isConnectable()) { // 连接到远程服务器
                    try {
                        if (socketChannel.finishConnect()) { // 完成连接
                            // 连接成功
                            System.out.println("连接成功-" + socketChannel);

                            ByteBuffer buffer = ByteBuffer.allocateDirect(20480);

                            // 切换到感兴趣的事件
                            selectionKey.attach(buffer);
                            selectionKey.interestOps(SelectionKey.OP_WRITE);
                        }
                    } catch (IOException e) {
                        // 连接失败
                        e.printStackTrace();
                        return;
                    }
                } else if (selectionKey.isWritable()) {// 可以开始写数据
                    ByteBuffer buf = (ByteBuffer) selectionKey.attachment();
                    buf.clear();
                    Scanner scanner = new Scanner(System.in);
                    System.out.print("请输入:");
                    // 发送内容
                    String msg = scanner.nextLine();
                    scanner.close();

                    buf.put(msg.getBytes());
                    buf.flip();

                    while (buf.hasRemaining()) {
                        socketChannel.write(buf);
                    }

                    // 切换到感兴趣的事件
                    selectionKey.interestOps(SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {// 可以开始读数据
                    // 读取响应
                    System.out.println("收到服务端响应:");
                    ByteBuffer requestBuffer = ByteBuffer.allocate(1024);

                    while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
                        // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
                        if (requestBuffer.position() > 0) break;
                    }
                    requestBuffer.flip();
                    byte[] content = new byte[requestBuffer.remaining()];
                    requestBuffer.get(content);
                    System.out.println(new String(content));
//                    selectionKey.interestOps(SelectionKey.OP_WRITE);

                }
            }
        }
    }

}
模拟服务器NIOServer的升级版2的改动

服务器端读事件后,不直接写了,改为关注写事件,然后在写事件中,进行写回复客户端的相应信息

java 复制代码
public class NIOServerV2 {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);

        Selector selector = Selector.open();

        SelectionKey selectionKey = serverSocketChannel.register(selector, 0);
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        System.out.println("服务启动成功");

        while (true) {
            selector.select();//阻塞

            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();


                if (key.isAcceptable()) {
                    SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
                    channel.configureBlocking(false);
                    channel.register(selector, SelectionKey.OP_READ);
                    System.out.println("收到新连接:" + channel.getRemoteAddress());
                } else if (key.isReadable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(2014);
                    while(channel.isOpen() && channel.read(byteBuffer) != -1){
                        if(byteBuffer.position() > 0) break;
                    }
                    if (byteBuffer.position() == 0) continue; // 如果没数据了, 则不继续后面的处理
                    byteBuffer.flip();
                    byte[] content = new byte[byteBuffer.remaining()];
                    byteBuffer.get(content);
                    System.out.println("收到新连接:" + channel.getRemoteAddress() + " 数据:"+new String(content));
                    channel.register(selector,SelectionKey.OP_WRITE);
                } else if (key.isWritable()){
                    System.out.println("开始写");
                    SocketChannel channel = (SocketChannel) key.channel();
                    String response = "HTTP/1.1 200 OK\r\n" +
                            "Content-Length: 11\r\n\r\n" +
                            "Hello World";
                    ByteBuffer byteBuffer = ByteBuffer.wrap(response.getBytes());
                    while(byteBuffer.hasRemaining()){
                        channel.write(byteBuffer);
                    }
                    //写完后,一定取消写的关注,否则轮询中就会一直写
                    key.interestOps(0);
                }

            }
        }
    }
}

Selector 选择器

详解

Selector是一个Java NIO组件,可以检查一个或多个NIO通道,并确定哪些通道已准备好进行读取或写入。实现单个线程可以管理多个通道,从而管理多个网络连接

一个线程使用Selector监听多个channel的不同事件:四个事件分别对应SelectionKey四个常量。

  1. Connect 连接(SelectionKey.OP_CONNECT)
  2. Accept 准备就绪(OP_ACCEPT)
  3. Read 读取(OP_READ)
  4. Write 写入(OP_WRITE)

实现一个线程处理多个通道的核心概念理解:事件驱动机制

非阻塞的网络通道下,开发者通过Selector注册对于通道感兴趣的事件类型,线程通过监听事件来

触发相应的代码执行。(拓展:更底层是操作系统的多路复用机制)

java 复制代码
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ); //注册感兴趣的事件
while(true) { // 由accept轮询,变成了事件通知的方式。
    int readyChannels = selector.select(); // select 收到新的事件,方法才会返回
    if(readyChannels == 0) continue;
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    while(keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        // 判断不同的事件类型,执行对应的逻辑处理
        // key.isAcceptable() / key.isConnectable() / key.isReadable() / key.isWritable()}
        keyIterator.remove();
    }
}

:更底层是操作系统的多路复用机制)

java 复制代码
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ); //注册感兴趣的事件
while(true) { // 由accept轮询,变成了事件通知的方式。
    int readyChannels = selector.select(); // select 收到新的事件,方法才会返回
    if(readyChannels == 0) continue;
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    while(keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        // 判断不同的事件类型,执行对应的逻辑处理
        // key.isAcceptable() / key.isConnectable() / key.isReadable() / key.isWritable()}
        keyIterator.remove();
    }
}
相关推荐
编程、小哥哥17 分钟前
Java求职面经分享:Spring Boot到微服务,从理论到实践
java·hadoop·spring boot·微服务·kafka
残*影21 分钟前
BIO、NIO、AIO 的区别与实战应用解析
nio
有梦想的攻城狮1 小时前
spring中的BeanFactoryAware接口详解
java·后端·spring·beanfactory
若汝棋茗1 小时前
C#在 .NET 9.0 中启用二进制序列化:配置、风险与替代方案
java·c#·.net·序列化
通达的K1 小时前
Java的常见算法和Lambda表达式
java·数据结构·算法
liubo666_1 小时前
JVM梳理(逻辑清晰)
java·jvm·后端
一刀到底2111 小时前
java 在用redis 的时候,如何合理的处理分页问题? redis应当如何存储性能最佳
java·开发语言·redis
软考真题app1 小时前
软件设计师考试三大核心算法考点深度解析(红黑树 / 拓扑排序 / KMP 算法)真题考点分析——求三连
java·开发语言·算法·软考·软件设计师
暖苏1 小时前
SpringCloud Alibaba微服务-- Sentinel的使用(笔记)
java·spring boot·spring cloud·微服务·sentinel·微服务保护·服务熔断
Johny_Zhao2 小时前
centos8安装部署RADIUS+MySQLPGSQL高可用架构实现
linux·网络·网络安全·信息安全·云计算·shell·cisco·yum源·radius·huawei·系统运维·华三