1、Socket网络编程回顾

Socket概述
Socket,套接字就是两台主机之间逻辑连接的端点。
1、TCP/IP协议是传输层协议(三次握手,四次挥手),主要解决数据如何在网络中传输,
2、HTTP是应用层协议,主要解决如何包装数据。
3、Socket是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,
数据要在网络中传输,必修包含进行网络通信必须的五种信 息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口

Socket整体流程

Socket编程主要涉及到客户端和服务端两个方面,
1、首先是在服务器端创建一个服务器套接字 (ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到 65536,但是0到1024是为特权服务保留的端口号,可以选择任意一个当前没有被其他进程使用的端 口。
2、客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。

image.png

代码实现

服务端

  1. public class ServerDemo {
  2. public static void main(String[] args) throws Exception {
  3. //1.创建一个线程池,如果有客户端连接就创建一个线程, 与之通信
  4. ExecutorService executorService = Executors.newCachedThreadPool();
  5. //2.创建 ServerSocket 对象,并绑定端口
  6. ServerSocket serverSocket = new ServerSocket(9999);
  7. System.out.println("服务器已启动");
  8. while (true) {
  9. //3.监听客户端 的连接请求
  10. final Socket socket = serverSocket.accept();
  11. System.out.println("有客户端连接");
  12. //4.开启新的线程池处理(处理新的客户端请求)
  13. executorService.execute(new Runnable() {
  14. @Override
  15. public void run() {
  16. handle(socket);
  17. }
  18. });
  19. }
  20. }
  21. /**socket处理发送过来的数据,并处理响应*/
  22. public static void handle(Socket socket) {
  23. try {
  24. /**先打印当前线程的id,及线程的名称*/
  25. System.out.println("线程ID:" + Thread.currentThread().getId()
  26. + " 线程名称:" + Thread.currentThread().getName());
  27. //从连接中取出输入流来接收消息(通过输入流,接收客户端所发过来的数据)
  28. InputStream is = socket.getInputStream();
  29. byte[] b = new byte[1024];
  30. int read = is.read(b);
  31. // 打印客户端发送过来的信息
  32. System.out.println("客户端:" + new String(b, 0, read));
  33. //连接中取出输出流并回话 (获取数据流进行回应)
  34. OutputStream os = socket.getOutputStream();
  35. os.write("没钱".getBytes());
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. } finally {
  39. try {
  40. //最后关闭连接
  41. socket.close();
  42. } catch (IOException e) {
  43. e.printStackTrace();
  44. }
  45. }
  46. }
  47. }

客户端

public class ClientDemo {
    public static void main(String[] args) throws Exception {
        while (true) {
            //1.创建 Socket 对象(并且指定ip及端口号)
            Socket s = new Socket("127.0.0.1", 9999);

            //2.从连接中取出输出流并发消息(因为需要向服务端发送数据)
            OutputStream os = s.getOutputStream();
            System.out.println("请输入:");
            /** 通过控制台进行输入*/
            Scanner sc = new Scanner(System.in);
            String msg = sc.nextLine();
            os.write(msg.getBytes());

            //3.从连接中取出输入流并接收回话 (客户端还需要获取服务端的数据)
            InputStream is = s.getInputStream();
            byte[] b = new byte[1024];

            // 读取服务端返回的数据,并打印
            int read = is.read(b);
            System.out.println("老板说:" + new String(b, 0, read).trim());
            //4.关闭
            s.close();
        }
    }
}

2、I/O模型

  1. I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的 性能
    2. Java 共支持 3 种网络编程模型/IO 模式:BIO(同步并阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)

阻塞与非阻塞
主要指的是访问IO的线程是否会阻塞(或处于等待)
线程访问资源,该资源是否准备就绪的一种处理方式 。
阻塞:线程会等待资源做好就绪准备,再执行其他的操作
非阻塞:线程不会等待资源做好就绪准备,也会去执行其他的操作

同步和异步:
主要是指的数据的请求方式
同步和异步是指访问数据的一种机制
同步:线程会等待资源就绪后,再获取结果
异步:请求资源后不会处于等待状态,通过回调来获取资源返回结果

BIO(同步并阻塞)

Java BIO就是传统的 socket编程. BIO(blocking I/O) :
同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器 端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程 池机制改善(实现多个客户连接服务器)。

工作机制

image.png image.png

BIO问题分析
1. 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write
2. 并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
3. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费

NIO(同步非阻塞)

同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),
客户端发送的连接请求都会注册到多路复用器上(Selector),多路复用器轮询到连接有 I/O 请求就交给服务端线程进行处理。
image.png image.png

AIO(异步非阻塞)

AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长 的应用
Proactor 模式是一个消息异步通知的设计模式,Proactor 通知的不是就绪事件,而是操作完成事 件,这也就是操作系统异步 IO 的主要模型。

image.png

BIO、NIO、AIO 适用场景分析

  1. BIO(同步并阻塞) 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高, 并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解
    2. NIO(同步非阻塞) 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕 系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持
    3. AIO(异步非阻塞) 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分 调用 OS 参与并发操作, 编程比较复杂,JDK7 开始支持。

3、NIO编程

Java NIO 全称java non-blocking IO ,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系 列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的.
1. NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器) 。
2. NIO是 面向缓冲区编程的。数据读取到一个缓冲区中,需要时可在缓冲区中前后移动,这就增加了 处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
3. Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的 数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可 以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某 通道,但不需要等待它完全写入, 这个线程同时可以去做别的事情。通俗理解:NIO 是可以做到 用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配50 或者 100 个 线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。

NIO和 BIO的比较

  1. BIO 以流的方式处理数据,而 NIO 以缓冲区的方式处理数据,缓冲区 I/O 的效率比流 I/O 高很多
    2. BIO 是阻塞的,NIO则是非阻塞的
    3. BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据 总是从通道读取到缓冲区中,或者从缓冲区写入到通道中Selector(选择器)用于监听多个通道的事件(比如:连接请求, 数据到达等),因此使用单个线程就可以监听多个客户端通道

NIO 三大核心原理示意图

一张图描述 NIO 的 Selector 、 Channel 和 Buffer 的关系
image.png

  1. 每个 channel 都会对应一个 Buffer
    2. Selector 对应一个线程, 一个线程对应多个 channel(连接)
    3. 每个 channel 都注册到 Selector选择器上
    4. Selector不断轮询查看Channel上的事件, 事件是通道Channel非常重要的概念
    5. Selector 会根据不同的事件,完成不同的处理操作
    6. Buffer 就是一个内存块 , 底层是有一个数组
    7. 数据的读取写入是通过 Buffer, 这个和 BIO , BIO 中要么是输入流,或者是输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写 , channel 是双向的.

4、缓冲区(Buffer)

缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个数组,该对象 提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的 状态变化情况。Channel 提供从网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer.
image.png

Buffer常用API介绍

Buffer 类及其子类
image.png
在 NIO 中,Buffer是一个顶层父类,它是一个抽象类, 类的层级关系图,常用的缓冲区分别对应 byte,short, int, long,float,double,char 7种. (用的时候用其子类即可)

缓冲区对象创建

方法名 说明
static ByteBuffer allocate(长度) 创建byte类型的指定长度的缓冲区
static ByteBuffer wrap(byte[] array) 创建一个有内容的byte类型缓冲区
public class CreateBufferDemo {
    public static void main(String[] args) {
        //1.创建指定长度为5的缓冲区(空缓冲区)  ByteBuffer为例
        ByteBuffer allocate = ByteBuffer.allocate(5);
        for (int i = 0; i < 5; i++) {
            System.out.println(allocate.get());//从缓冲区当中拿去数据
        }
        //超过缓存区的长度继续拿的时候,会报错. 后续讲解
        //System.out.println(allocate.get());//从缓冲区当中拿去数据

        //2.创建一个有内容的缓冲区 (传入一个字节数组)
        ByteBuffer wrap = ByteBuffer.wrap("lagou".getBytes());
        for (int i = 0; i < 5; i++) {
            System.out.println(wrap.get());
        }
    }
}

缓冲区对象添加数据

方法名 说明
int position()/position(int newPosition) 获得当前要操作的索引/修改当前要操作的索引位 置
int limit()/limit(int newLimit) 最多能操作到哪个索引/修改最多能操作的索引位 置
int capacity() 返回缓冲区的总长度
int remaining()/boolean hasRemaining() 还有多少能操作索引个数/是否还有能操作
put(byte b)/put(byte[] src) 添加一个字节/添加字节数组
/**
 * 向缓冲区中添加数据
 */
public class PutBufferDemo {
    public static void main(String[] args) {
        //1.创建一个缓冲区
        ByteBuffer allocate = ByteBuffer.allocate(10);
        System.out.println(allocate.position());//0 获取当前索引所在位置
        System.out.println(allocate.limit());//10 最多能操作到哪个索引位置
        System.out.println(allocate.capacity());//10 返回缓冲区总长度
        System.out.println(allocate.remaining());//10 还有多少个可以操作的个数

        System.out.println("----------------");
        // 修改当前索引所在位置
        //allocate.position(1);
        // 修改最多能操作到哪个索引的位置
        //allocate.limit(9);
        // System.out.println(allocate.position());//1 获取当前索引所在位置
        //System.out.println(allocate.limit());//9 最多能操作到哪个索引位置
        //System.out.println(allocate.capacity());//10 返回缓冲区总长度
        //System.out.println(allocate.remaining());//8 还有多少个可以操作的个数

        // 添加一个字节
        allocate.put((byte) 97);
        System.out.println(allocate.position());//1 获取当前索引所在位置
        System.out.println(allocate.limit());//10 最多能操作到哪个索引位置
        System.out.println(allocate.capacity());//10 返回缓冲区总长度
        System.out.println(allocate.remaining());//9 还有多少个可以操作的个数

        System.out.println("----------------");
        // 添加一个数组(三个字节)
        allocate.put("abc".getBytes());
        System.out.println(allocate.position());//4 获取当前索引所在位置
        System.out.println(allocate.limit());//10 最多能操作到哪个索引位置
        System.out.println(allocate.capacity());//10 返回缓冲区总长度
        System.out.println(allocate.remaining());//6 还有多少个可以操作的个数

        System.out.println("----------------");
        // 添加一个数组(4字节+6字节)
        allocate.put("123456".getBytes());
        System.out.println(allocate.position());//10 获取当前索引所在位置
        System.out.println(allocate.limit());//10 最多能操作到哪个索引位置
        System.out.println(allocate.capacity());//10 返回缓冲区总长度
        System.out.println(allocate.remaining());//0 还有多少个可以操作的个数
        System.out.println(allocate.hasRemaining());//false 是否还能操作

        System.out.println("----------------");
        //如果缓冲区满了. 可以调整position位置, 就可以重复写. 会覆盖之前存入索引位置的值
        allocate.position(0);
        allocate.put("123456".getBytes());
        System.out.println(allocate.position());//6 获取当前索引所在位置
        System.out.println(allocate.limit());//10 最多能操作到哪个索引位置
        System.out.println(allocate.capacity());//10 返回缓冲区总长度
        System.out.println(allocate.remaining());//4 还有多少个可以操作的个数
        System.out.println(allocate.hasRemaining());//true 是否还能操作

    }
}

缓冲区对象读取数据

方法名 说明
flip() 写切换读模式 limit设置最新数据位置+1, position设置0
get() 读一个字节 (每调用一次就往后移动一次,直到读完数据)
get(byte[] dst) 读多个字节
get(int index) 读指定索引的字节
rewind() 将position设置为0,可以重复读
clear() 读切换写模式 position设置为0 , limit 设置为 capacity,之前的数据会被清空
array() 将缓冲区转换成字节数组返回
/**
 * 从缓冲区中读取数据
 */
public class GetBufferDemo {
    public static void main(String[] args) {
        //1.创建一个指定长度的缓冲区
        ByteBuffer allocate = ByteBuffer.allocate(10);
        allocate.put("0123".getBytes());
        System.out.println("position:" + allocate.position());//4
        System.out.println("limit:" + allocate.limit());//10
        System.out.println("capacity:" + allocate.capacity());//10
        System.out.println("remaining:" + allocate.remaining());//6

        //切换读模式
        System.out.println("读取数据--------------");
        allocate.flip();
        System.out.println("position:" + allocate.position());//4
        System.out.println("limit:" + allocate.limit());//10
        System.out.println("capacity:" + allocate.capacity());//10
        System.out.println("remaining:" + allocate.remaining());//6
        for (int i = 0; i < allocate.limit(); i++) {
            System.out.println(allocate.get());
        }
        //读取完毕后.继续读取会报错,超过limit值
        //System.out.println(allocate.get());
        //读取指定索引字节
        System.out.println("读取指定索引字节--------------");
        System.out.println(allocate.get(1));

        System.out.println("读取多个字节--------------");
        // 重复读取
        allocate.rewind();
        byte[] bytes = new byte[4];
        allocate.get(bytes);
        System.out.println(new String(bytes));

        // 将缓冲区转化字节数组返回
        System.out.println("将缓冲区转化字节数组返回--------------");
        byte[] array = allocate.array();
        System.out.println(new String(array));

        // 切换写模式,覆盖之前索引所在位置的值
        System.out.println("写模式--------------");
        allocate.clear();
        allocate.put("abc".getBytes());
        System.out.println(new String(allocate.array()));

    }
}

5、通道(Channel)

通常来说NIO中的所有IO都是从 Channel(通道) 开始的。NIO 的通道类似于流,但有些区别如下:
1. 通道可以读也可以写(双向的),流一般来说是单向的(只能读或者写,所以之前我们用流进行IO操作的时候 需要分别创建一个输入流和一个输出流)
2. 通道可以异步读写
3. 通道总是基于缓冲区Buffer来读写

image.png

Channel常用类介绍

1. Channel接口

使用的时候是要使用具体的实现类,
常用的Channel实现类类 有 :FileChannel , DatagramChannel ,ServerSocketChannel和 SocketChannel 。FileChannel 用于文件的数据读写, DatagramChannel 用于 UDP 的数据读 写, ServerSocketChannel 和SocketChannel 用于 TCP 的数据读写。【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】

2. SocketChannel 与ServerSocketChannel

类似 Socke和ServerSocket,可以完成客户端与服务端数据的通信工作

ServerSocketChannel

服务端实现步骤:

  1. 打开一个服务端通道
    2. 绑定对应的端口号
    3. 通道默认是阻塞的,需要设置为非阻塞
    4. 检查是否有客户端连接,如果有客户端连接会返回对应的通道
    5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
    6. 给客户端回写数据
    7. 释放资源

服务端代码实现

public class NIOServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        //1. 打开一个服务端通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //2. 绑定对应的端口号
        serverSocketChannel.bind(new InetSocketAddress(9999));
        //3. 通道默认是阻塞的,需要设置为非阻塞(默认是true,阻塞的)
        serverSocketChannel.configureBlocking(false);
        System.out.println("服务端启动成功....");

        // 做客户端连接的检查工作
        while (true) {
            //4. 检查是否有客户端连接 有客户端连接会返回对应的通道
            //这个就是客户端连接的通道
            SocketChannel socketChannel = serverSocketChannel.accept();
            // 判断通道是否为空
            if (socketChannel == null) {
                System.out.println("没有客户端连接...我去做别的事情");
                Thread.sleep(2000);
                continue;
            }
            // 如果有连接的话
            //5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
            ByteBuffer allocate = ByteBuffer.allocate(1024);

            //因为管道Channel都是基于缓存区Buffer进行处理的,所以传入一个缓冲区
            //返回值
            //正数: 表示本地读到有效字节数
            //0: 表示本次没有读到数据
            //-1: 表示读到末尾
            int read = socketChannel.read(allocate);
            // 将读取到的信息打印出来
            System.out.println("客户端消息:" + new String(allocate.array(), 0,
                    read, StandardCharsets.UTF_8));
            //6. 给客户端回写数据(Channel是双向的,可以读、也可以写,也是通过缓冲区来写)
            socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));
            //7. 释放资源
            socketChannel.close();
        }
    }
}

SocketChannel

实现端实现步骤

  1. 打开通道
    2. 设置连接IP和端口号
    3. 写出数据
    4. 读取服务器写回的数据
    5. 释放资源

客户端代码实现

public class NIOClient {
    public static void main(String[] args) throws IOException {
        //1. 打开通道
        SocketChannel socketChannel = SocketChannel.open();
        //2. 设置连接IP和端口号
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
        //3. 写出数据(通过缓冲区来写的)
        socketChannel.write(ByteBuffer.wrap("老板.还钱吧!".getBytes(StandardCharsets.UTF_8)));
        //4. 读取服务器写回的数据
        ByteBuffer allocate = ByteBuffer.allocate(1024);// 写一个缓冲区
        int read = socketChannel.read(allocate);//通过缓冲区来读取
        System.out.println("服务端消息:" +
                new String(allocate.array(), 0, read, StandardCharsets.UTF_8));
        //5. 释放资源
        socketChannel.close();
    }
}

6、Selector (选择器)

可以用一个线程,处理多个的客户端连接,就会使用到NIO的Selector(选择器). Selector 能够检测 多个注册的服务端通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的 处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求

没有选择器的情况下:
image.png

存在的问题:在这种没有选择器的情况下,对应每个连接对应一个处理线程。但是连接上并不能马上就会发送信息,服务端就会一直处于等待状态,所以还会产生资源浪费

有选择器的情况下:

image.png
先通过监听器对服务端管道进行监听,一旦有客户端连接,就得到客户端的通讯通道,进而监听客户端的读写情况.
只有在通道真正有读写事件发生时,才会进行读写,(选择器就会通知线程),就大大地减少了系统开销,并且不必为每个连接都 创建一个线程,不用去维护多个线程, 避免了多线程之间的上下文切换导致的开销 。

常用API介绍

1. Selector 类是一个抽象类

image.png

常用方法:

Selector.open() 得到一个选择器对象
selector.select() 阻塞 监控所有注册的通道,当有对应的事件操作时, 会将SelectionKey(事件的一个key)放入 集合内部并返回事件数量
selector.select(1000) 阻塞 1000 毫秒,监控所有注册的通道,当有对应的事件操作时, 会将 SelectionKey放入集合内部并返回
selector.selectedKeys() 返回存有SelectionKey的集合 (事件的集合)

2. SelectionKey

image.png

常用方法
SelectionKey.isAcceptable(): 是否是连接继续事件
SelectionKey.isConnectable(): 是否是连接就绪事件
SelectionKey.isReadable(): 是否是读就绪事件
SelectionKey.isWritable(): 是否是写就绪事件

SelectionKey中定义的4种事件:
SelectionKey.OP_ACCEPT —— 接收连接继续事件,表示服务器监听到了客户连接,服务器 可以接收这个连接了 SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户端与服务器的连接已经建立成功
SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操 作了(通道目前有数据,可以进行读操作了)
SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用 于写操作)

Selector 编码

服务端实现步骤:

  1. 打开一个服务端通道
    2. 绑定对应的端口号
    3. 通道默认是阻塞的,需要设置为非阻塞
    4. 创建选择器
    5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
    6. 检查选择器是否有事件
    7. (如果有事件发生)获取事件集合
    8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
    9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
    10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
    11. 得到客户端通道,读取数据到缓冲区
    12. 给客户端回写数据
    13. 从集合中删除对应的事件, 因为防止二次处理 (如果不删除,下次轮询时候还会有数据的)

服务端代码实现

一般用在服务端,客户端的压力没那么大,所以不用加在客户端也行

public class NIOSelectorServer {
    public static void main(String[] args) throws IOException {
        //1. 打开一个服务端通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //2. 绑定对应的端口号
        serverSocketChannel.bind(new InetSocketAddress(9999));
        //3. 通道默认是阻塞的,需要设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //4. 创建选择器
        Selector selector = Selector.open();
        //5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT(OP_ACCEPT是连接的操作)
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务端启动成功.....");

        // 一直去检查
        while (true) {
            //6. 检查选择器是否有事件
            int select = selector.select(2000);// 写超时时间,阻塞2秒
            if (select == 0) {
                System.out.println("没有事件发生....");
                continue;
            }
            //7. 获取事件集合(select>0 就bison有事件,就需要获取事件的集合)
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 获取事件迭代器
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while (iterator.hasNext()) {
                //8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
                SelectionKey key = iterator.next(); //获取到事件的一个key
                //判断这个key是不是所需要的连接事件,通过isAcceptable方法判断(第一个操作是连接操作)
                if (key.isAcceptable()) {
                    // 如果是连接事件
                    //9. accept得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("有客户端连接.....");
                    //将通道必须设置成非阻塞的状态.因为selector选择器需要轮询监听每个通道的事件
                    // 如果是阻塞,就不能进行轮询查看了
                    socketChannel.configureBlocking(false);
                    //指定监听事件为OP_READ 读就绪事件 (将客户端的通道注册到选择器上,丙设置为读状态)
                    // 站在服务端上而言
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }


                // 客户端第一个过来的动作是连接,连接完了肯定是读啊,所以要判断是不是读事件
                //10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
                if (key.isReadable()) {
                    //11.得到客户端通道,读取数据到缓冲区(根据事件的key来获取)
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    // 创建缓冲区对象,
                    ByteBuffer allocate = ByteBuffer.allocate(1024);
                    // 然后从缓冲区进行读取数据
                    int read = socketChannel.read(allocate);
                    if (read > 0) { // 表示读取到数据了
                        System.out.println("客户端消息:" + new String(allocate.array(), 0, read
                                , StandardCharsets.UTF_8));
                        //12. 给客户端回写数据(通过缓冲区进行写数据)
                        socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));
                        socketChannel.close();
                    }
                }
                //13. 从集合中删除对应的事件, 因为防止二次处理.(最后一步很重要)
                iterator.remove();
            }
        }
    }
}

客户端没有变化:

原生 NIO 存在的问题

  1. NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、 SocketChannel、ByteBuffer等。
    2. 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须 对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
    3. 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥 塞和异常流的处理等等。
    4. JDK NIO 的 Bug:臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到 JDK 1.7版本该问题仍旧存在,没有被根本解决

注意:在NIO中通过Selector的轮询当前是否有IO事件,根据JDK NIO api描述,Selector的select方 法会一直阻塞,直到IO事件达到或超时,但是在Linux平台上这里有时会出现问题,在某些场 景下select方法会直接返回,即使没有超时并且也没有IO事件到达,这就是著名的epoll bug,这是一个比较严重的bug,它会导致线程陷入死循环,会让CPU飙到100%,极大地影 响系统的可靠性,到目前为止,JDK都没有完全解决这个问题。