RPC架构设计

本文阐述了socket编程、IO网络模型,netty的原理以及Netty源代码的分析。

RPC架构设计

2022/03/01 校对完成

文章更新历史

2022/03/01 初稿。

socket

socket网络编程

socket概述

socket套接字是两台主机之间逻辑连接的端点。

TCP/IP协议是传输层协议,主要解决数据在网络中的传输

socket是网络通信之间的抽象接口,它包含网络通信的五种基础信息:连接使用的协议、本地主机的ip地址、本地进程协议端口、远程主机ip地址、远程进程的协议端口

socket整体流程

socket编程主要包括客户端和服务端两个方面。

首先在服务端创建一个服务端套接字(ServerSocket),并把它附加到一个端口上,服务端从这个端口监听链接。

端口范围是 0-65536 ,注意 0-1024 是特权服务保留的端口。可选择任意一个不被其他进程使用的端口。

客户端请求与服务端连接时,根据服务端的域名或者ip地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的操作可以像输入输出流一样操作。

image-20220417134215803

代码实现
  1. 服务端代码

    /**
     * 服务端
     *
     * @name: ServerDemo
     * @author: terwer
     * @date: 2022-04-17 14:20
     **/
    public class ServerDemo {
        public static void main(String[] args) throws IOException {
            // 1.创建一个线程池,如果有客户端链接就创建一个线程与之通信
            ExecutorService executorService = Executors.newCachedThreadPool();
            // 2.创建ServerSocket
            ServerSocket serverSocket = new ServerSocket(9999);
            System.out.println("服务器已启动");
            while (true) {
                // 3.监听客户端
                Socket socket = serverSocket.accept();
                System.out.println("有客户端链接");
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        handle(socket);
                    }
                });
            }
        }
    
        private static void handle(Socket socket) {
            try {
                System.out.println("线程ID:" + Thread.currentThread().getId() + ",线程名称:" + Thread.currentThread().getName());
                // 从连接中取出输入流
                InputStream inputStream = socket.getInputStream();
                byte[] b = new byte[1024];
                int read = inputStream.read(b);
                System.out.println("客户端" + new String(b, 0, read));
    
                // 链接中取出输出流并回话
                OutputStream outputStream = socket.getOutputStream();
                outputStream.write("没有".getBytes());
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
  2. 客户端代码

    /**
     * 客户端
     *
     * @name: ClientDemo
     * @author: terwer
     * @date: 2022-04-17 15:30
     **/
    public class ClientDemo {
        public static void main(String[] args) throws IOException {
            while (true) {
                // 1.创建客户端socket
                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();
                // 应该是
                int read = is.read(b);
                System.out.println("老板说:" + new String(b, 0, read).trim());
    
                s.close();
            }
        }
    }
    

IO模型

IO模型说明
  1. 简单理解:用什么样的通道进行数据的发送和接收。在很大程度上决定了程序通信的性能。

  2. Java支持三种网络编程模型I/O模式:BIO(同步阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)

    阻塞与非阻塞

    指的是网络IO的线程是否处于阻塞或者等待状态

    线程访问资源,该资源是否准备就绪的一种处理方式

    image-20220417214737992

    同步和异步

    指的是数据的请求方式,同步和异步是请求数据的一种方式。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iJfy3ctZ-1651642751001)(https://cdn.jsdelivr.net/gh/terwer/upload/img/image-20220417215010935.png)]

BIO(同步阻塞)

Java BIO就是传统的socket编程

BIO(blocking IO):同步阻塞,服务器实现方式为一个链接一个线程,即客户端有一个链接时,服务端就要启动一个线程进行处理,如果这个链接不作任何事情,会造成不必要的线程开销,可以通过线程池改善,实现多个客户端链接服务器。

工作机制
BIO的问题分析
  1. 每个请求都要创建独立的线程,与对应的客户端进行read,业务处理,数据write
  2. 并发量大的时候,需要创建大量的线程来处理链接,系统资源占用较大
  3. 链接建立后,如果当前线程没有数据可读,线程就阻塞在read上,造成线程资源浪费
NIO(同步非阻塞)

同步非阻塞,服务器实现模式为一个线程处理多个请求(链接),客户端发送的链接请求会注册到多路复用器上,多路复用器轮训到链接有IO请求就进行处理。

image-20220418134554952

AIO(异步非阻塞)

AIO引入了异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程。

他的特点是先由操作系统完成后才通知服务端启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

Proactor模式是一个消息异步通知的模式,Proactor通知的不是就绪事件,而是操作完成事件,这也是操作系统异步IO的主要模型。

https://www.zhihu.com/question/26943938

生活中的例子:

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

nio编程

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的关系

  1. 每个Channel都会对应一个Buffer

  2. Selector对应一个线程,一个线程对应多个Channel(连接)

  3. 每个Channel都注册到选择器上

  4. Selector不断轮询查看Channel上的事件,事件是Channel(通道)的重要概念

  5. Selector会根据不同的事件完成不同的操作

  6. Buffer是一个内存块,底层是一个数组

  7. 数据的读取和写入都是通过Buffer。

    跟BIO有区别,BIO中,要么是输入流,要么是输出流,不能是双向的。

    NIO的Buffer可读可写,Channel是双向的。

缓冲区(Buffer)

基本介绍

缓冲区(Buffer):缓冲区本质上是一个可读可写的内存块。

可以理解成一个数组,该对象提供了一组方法,可以轻松的操作内存块。

缓冲区内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。

Channel提供从网络读取数据的通道,但是读取或者写入数据都必须经过Buffer。

Buffer常用API介绍
  1. Buffer类及其子类

    Buffer
    ByteBuffer
    CharBuffer
    DoubleBuffer
    FloatBuffer
    IntBuffer
    LongBuffer
    ShortBuffer

    在NIO中,Buffer是一个顶层父类,他是一个抽象类。常用的缓冲区分别对应byte,char,double,float,int,long,short供7种。

  2. 缓冲区对象创建

    方法名说明
    static ByteBuffer allocate(长度)创建byte类型的指定长度的缓冲区
    static ByteBuffer wrap(byte[] array)创建一个有内容的byte类型的缓冲区

    示例代码

    /**
     * 创建缓冲区
     *
     * @name: CreateBufferDemo
     * @author: terwer
     * @date: 2022-04-18 17:38
     **/
    public class CreateBufferDemo {
        public static void main(String[] args) {
            // 1.创建一个指定长度的缓冲区,ByteBuffer为例
            ByteBuffer byteBuffer = ByteBuffer.allocate(4);
            for (int i = 0; i < 4; i++) {
                System.out.println(byteBuffer.get());
            }
    
            // 在此调用会报错
            // System.out.println(byteBuffer.get());
            System.out.println("==================");
            System.out.println();
    
            // 2.创建一个有内容的缓冲区
            ByteBuffer wrap = ByteBuffer.wrap("test".getBytes(StandardCharsets.UTF_8));
            for (int i = 0; i < 4; i++) {
                System.out.println(wrap.get());
            }
        }
    }
    
  3. 缓冲区对象添加数据

    方法名说明
    Int position()/position(int newPosition)获取当前要操作的索引/修改当前要操作的索引
    int lkimit()/limit(int newLimit)最多能操作到哪个索引/修改最多能操作的索引位置
    int capacity()返回缓冲区的总长度
    int remaining()/boolean hasRemaining()还有多少能操作的索引个数/是否还能操作
    put (byte b)/put(byte[] src)添加一个字节/添加字节数组

    示例代码:

    /**
     * 添加缓冲区
     *
     * @name: PutBufferDemo
     * @author: terwer
     * @date: 2022-04-18 19:27
     **/
    public class PutBufferDemo {
        public static void main(String[] args) {
            // 1.创建一个指定长度的缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(10);
            System.out.println(byteBuffer.position());// 获取当前索引所在的位置
            System.out.println(byteBuffer.limit());// 最多能操作到哪个索引
            System.out.println(byteBuffer.capacity());// 返回缓冲区总长度
            System.out.println(byteBuffer.remaining());// 还有多少个能操作
    
    //        byteBuffer.position(2);
    //        byteBuffer.limit(4);
    //        System.out.println();
    //        System.out.println("============");
    //        System.out.println(byteBuffer.position());// 获取当前索引所在的位置
    //        System.out.println(byteBuffer.limit());// 最多能操作到哪个索引
    //        System.out.println(byteBuffer.capacity());// 返回缓冲区总长度
    //        System.out.println(byteBuffer.remaining());// 还有多少个能操作
    
            // 添加一个字节
            byteBuffer.put((byte) 97);
            System.out.println();
            System.out.println("============");
            System.out.println(byteBuffer.position());// 获取当前索引所在的位置
            System.out.println(byteBuffer.limit());// 最多能操作到哪个索引
            System.out.println(byteBuffer.capacity());// 返回缓冲区总长度
            System.out.println(byteBuffer.remaining());// 还有多少个能操作
    
            // 添加一个字节数组
            byteBuffer.put("test".getBytes(StandardCharsets.UTF_8));
            System.out.println();
            System.out.println("============");
            System.out.println(byteBuffer.position());// 获取当前索引所在的位置
            System.out.println(byteBuffer.limit());// 最多能操作到哪个索引
            System.out.println(byteBuffer.capacity());// 返回缓冲区总长度
            System.out.println(byteBuffer.remaining());// 还有多少个能操作
    
            // 超过缓冲区长度会报错
    //        byteBuffer.put("1234567".getBytes(StandardCharsets.UTF_8));
    //        System.out.println();
    //        System.out.println("============");
    //        System.out.println(byteBuffer.position());// 获取当前索引所在的位置
    //        System.out.println(byteBuffer.limit());// 最多能操作到哪个索引
    //        System.out.println(byteBuffer.capacity());// 返回缓冲区总长度
    //        System.out.println(byteBuffer.remaining());// 还有多少个能操作
    
            // 如果缓冲区满了,可以调整position的位置,会覆盖之前对应索引的值
            byteBuffer.position(0);
            byteBuffer.put("1234567".getBytes(StandardCharsets.UTF_8));
            System.out.println();
            System.out.println("============");
            System.out.println(byteBuffer.position());// 获取当前索引所在的位置
            System.out.println(byteBuffer.limit());// 最多能操作到哪个索引
            System.out.println(byteBuffer.capacity());// 返回缓冲区总长度
            System.out.println(byteBuffer.remaining());// 还有多少个能操作
        }
    }
    
  4. 缓冲区对象读取数据

    方法名介绍
    flip()切换读模式,limit设置position位置,position设置0
    get()读一个字节
    get(byte[] dst)读多个字节
    get(int index)读指定索引的字节
    rewind()将position设置为0,可重复读
    clear()切换写模式,position设置为0,limit设置为capacity
    array()将缓冲区转换成字节数组返回

    flip方法

    clear方法:

    image-20220418195106115

    示例代码:

    /**
     * 从缓冲区读取数据
     *
     * @name: GetBufferDemo
     * @author: terwer
     * @date: 2022-04-18 19:51
     **/
    public class GetBufferDemo {
        public static void main(String[] args) {
            // 1.创建一个指定长度的缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(10);
            byteBuffer.put("0123".getBytes(StandardCharsets.UTF_8));
            System.out.println("position:" + byteBuffer.position());
            System.out.println("limit:" + byteBuffer.limit());
            System.out.println("capacity:" + byteBuffer.capacity());
            System.out.println("remaining:" + byteBuffer.remaining());
    
            // 切换读模式
            System.out.println();
            System.out.println("=================");
            System.out.println("准备读数据:");
            byteBuffer.flip();
            System.out.println("position:" + byteBuffer.position());
            System.out.println("limit:" + byteBuffer.limit());
            System.out.println("capacity:" + byteBuffer.capacity());
            System.out.println("remaining:" + byteBuffer.remaining());
            for (int i = 0; i < byteBuffer.limit(); i++) {
                System.out.println(byteBuffer.get());
            }
            // 读取完毕后,继续读取会报错,超过limit
    //        System.out.println(byteBuffer.get());
            // 读取指定字节
    //        System.out.println("读取指定索引:");
    //        System.out.println(byteBuffer.get(2));
    
            System.out.println("读取多个字节:");
            // 重复读取
            byteBuffer.rewind();
            byte[] dst = new byte[4];
            byteBuffer.get(dst);
            System.out.println(new String(dst));
    
            // 将缓冲区转化为字节数组返回
            System.out.println();
            System.out.println("===========");
            System.out.println("将缓冲区转化为字节数组:");
            byte[] array = byteBuffer.array();
            System.out.println(new String(array));
    
            // 切换写模式,会覆盖之前所有的值
            System.out.println();
            System.out.println("================");
            System.out.println("切换写模式,覆盖之前的值:");
            byteBuffer.clear();
            byteBuffer.put("test".getBytes(StandardCharsets.UTF_8));
            System.out.println(new String(byteBuffer.array()));
        }
    }
    

    注意:

    1. capacity:容量(长度) limit:界限(最多能读/写到哪里) position:位置(读/写哪个索引)
    2. 获取缓冲区的数据之前,要先调用flip()方法,重复读需要调用rewind()方法
    3. 再次写数据之前,需要先调用clear()方法,此时数据还未消失。再次写入数据完成,数据覆盖了才会消失。

通道(Channel)

基本介绍

NIO中所有的IO都是从通道(Channel)开始的。NIO的通道类似于流,但是有区别:

  1. 通道可读可写,流一般是单向的(只能读或者写,所以之前socket的demo里面分别创建一个输入流和输出流)。

  2. 通道可以异步读写。

  3. 通道总是基于缓冲区Buffer来读写

Channel的常用类介绍
  1. Channel接口

    常用的Channel实现类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel

    FileChannel用于文件的数据读写,DatagramChannel用于UDP数据的读写,ServerSocketChannel和SocketChannel用于TCP数据的读写。

    ServerSocketChannel类似于ServerSocket,SocketChannel类似于Socket。

    «Interface»
    Channel
    DatagramChannel
    FileChannel
    ServerSocketChannel
    SocketChannel
  2. SocketChannel和ServerSocketChannel

    类似于Socket和ServerSocket,可用于客户端与服务器的通信。

ServerSocketChannel

服务端实现步骤:

  1. 打开一个服务端通道
  2. 绑定对应的端口号
  3. 通道默认是阻塞的,需要设置为非阻塞
  4. 检查是否有客户端连接,有客户端连接会返回对应的通道
  5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
  6. 给客户端回写数据
  7. 释放资源
/**
 * 服务端
 *
 * @name: NIOServer
 * @author: terwer
 * @date: 2022-04-18 21:59
 **/
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为阻塞,false为非阻塞
        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 byteBuffer = ByteBuffer.allocate(1024);
            // 正数,读到的字节
            // 0,没有读到数据
            // -1,读到了文件末尾
            int read = socketChannel.read(byteBuffer);
            System.out.println("客户端发来的消息:" + new String(byteBuffer.array(), 0, read));

            // 6. 给客户端回写数据
            socketChannel.write(ByteBuffer.wrap("你好,我是服务端".getBytes(StandardCharsets.UTF_8)));

            // 7. 释放资源
            socketChannel.close();
        }

    }
}
SocketChannel

客户端实现步骤:

  1. 打开通道
  2. 设置连接IP和端口号
  3. 写出数据
  4. 读取服务器写回的数据
/**
 * 客户端
 *
 * @name: NIOClient
 * @author: terwer
 * @date: 2022-04-18 22:11
 **/
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 byteBuffer = ByteBuffer.allocate(1024);
        int read = socketChannel.read(byteBuffer);
        System.out.println("服务端回话:" + new String(byteBuffer.array(), 0, read));
        // 5.释放资源
        socketChannel.close();
    }
}

选择器(Selector)

基本介绍

用一个线程,处理多个客户端连接,就会用到NIO的Selector(选择器)。

Selector能够检测多个注册的服务端通道上是否有事件发生。如果有事件发生,便获取事件,然后针对每个事件进行响应的处理。

这样可以用单线程去管理多个通道,也就是管理多个连接和请求。

在没有选择器的情况下,每个连接对应一个请求,但是连接不能马上发送消息,所以会产生资源浪费。

有了选择器之后,只有在通道真正有读写事件发生时,才会进行读写。这样大大减小了系统开销,不必为每个连接创建一个线程,不用去维护多个线程,避免了多线程上下文切换导致的开销。

常用API介绍
  1. Selector是一个抽象类

    KQueueSelectorImpl
    PollSelectorImpl
    Selector
    SelectorImpl

    常用方法:

    Selector.open();// 得到一个选择器对象

    Selector.select();// 阻塞,监控所有注册的通道,当有对应的事件时,会将SelectionKey放入集合内部并返回事件数量

    Selector.select(1000);// 阻塞1000毫秒,监控所有注册的通道,当有对应的事件时,会将SelectionKey放入集合内部并返回

    Selector.selectedKeys;// 返回存有SelectionKey的集合

  2. SelectionKey

    image-20220418225852219

    • 常用方法
      • 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. 得到客户端通道,读取数据到缓冲区
    11. 给客户端回写数据
    12. 从集合中删除对应的事件, 因为防止二次处理.
  • 代码实现

    /**
     * 基于选择器实现服务端
     *
     * @name: NIOSelectorServer
     * @author: terwer
     * @date: 2022-04-18 23:07
     **/
    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
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务端已启动");
    
            while (true) {
                // 6. 检查选择器是否有事件
                int select = selector.select(2000);
    
                if (select == 0) {
                    continue;
                }
    
                // 7. 获取事件集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
    
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    // 8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
                    SelectionKey key = iterator.next();
                    // 9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
                    if (key.isAcceptable()) {
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        System.out.println("客户端已链接:" + socketChannel);
                        // 设置为非阻塞
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }
    
                    // 10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
                    if (key.isReadable()) {
                        // 11. 得到客户端通道,读取数据到缓冲区
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int read = channel.read(byteBuffer);
                        if (read > 0) {
                            System.out.println("获取到的客户端消息:" + new String(byteBuffer.array(), 0, read));
    
                            // 12. 给客户端回写数据
                            channel.write(ByteBuffer.wrap("给客户端的回复".getBytes(StandardCharsets.UTF_8)));
    
                            channel.close();
                        }
                    }
    
                    // 13. 从集合中删除对应的事件, 因为防止二次处理.
                    iterator.remove();
                }
            }
        }
    }
    
客户端

同NIOClient。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值