【Netty】NIO 选择器 ( Selector ) 通道 ( Channel ) 缓冲区 ( Buffer ) 网络通信案例

举报
韩曙亮 发表于 2022/01/10 23:52:39 2022/01/10
【摘要】 文章目录 I . NIO 通信 服务器端 流程说明II . NIO 通信 服务器端代码III . NIO 通信 客户端 流程说明IV . NIO 通信 客户端代码V . NIO 通信 示例运行 ...





I . NIO 通信 服务器端 流程说明



NIO 网络通信 服务器端 操作流程 , 与 BIO 原理类似 , 基本流程是 启动服务器套接字通道 , 创建选择器 , 将服务器套接字通道注册给选择器 , 监听客户端连接事件 , 客户端连接成功后 , 创建套接字通道 , 将新创建的通道注册给选择器 , 然后监听该通道的读取事件 ;


启动 -> 创建选择器 ->
创建服务器通道 -> 注册服务器通道 -> 监听连接 ->
创建客户端通道 -> 注册客户端通道 -> 监听数据读取/客户端连接



1 . 创建 服务器套接字通道 ( ServerSocketChannel ) :


① 创建通道 : 调用 ServerSocketChannel.open() 创建 , 创建后需要绑定本地端口号 , 需要获取 ServerSocket 用于绑定端口号 ;

② 获取服务器套接字 : 可以通过服务器套接字通道的 serverSocketChannel.socket() 方法获取 ServerSocket ;

③ 绑定本地端口号 : 调用 serverSocket.bind(new InetSocketAddress(8888)) 方法为该 ServerSocket 绑定 8888 端口号 ;

④ 设置非阻塞网络通信模式 : 并设置该通道网络通信模式为非阻塞模式 serverSocketChannel.configureBlocking(false) , 注意这里设置了非阻塞模式 , 其 对应的客户端套接字通道 SocketChannel 也要设置非阻塞模式 , 否则会报 IllegalBlockingModeException 异常 ;



2 . 创建选择器并注册通道 :


① 创建 选择器 ( Selector ) : 调用 Selector 的静态方法 open() , 即可创建一个 选择器 , Selector.open() ;

② 将 服务器套接字通道 注册给 选择器 ( Selector ) : serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT) , 监听通道的 SelectionKey.OP_ACCEPT 事件 , 如果有客户端连接服务器 , 就会触发该事件 , 生成相应的 SelectionKey , 并放入 选择器 ( Selector ) 的集合中 , 如果 选择器 ( Selector ) 正在调用 select 方法 ( 3 3 3 种 阻塞 / 非阻塞 监听方法 ) 监听 , 那么就会解除阻塞 ( 如果之前发生监听阻塞 ) , 返回触发事件的个数 ;



3 . 选择器 ( Selector ) 阻塞监听 : 这里调用 selector.select() 方法 , 阻塞监听 , 如果有事件发生 , 就会返回触发事件的个数 , 之后再遍历 SelectionKey 集合 , 依次处理触发的事件 ; 阻塞监听代码逻辑如下 :

while (true){
    if(selector.select() <= 0){
        continue;
    }
    //监听到触发事件, 处理对应的 SelectionKey 事件
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6


4 . 处理客户端连接事件 :


① 判定 SelectionKey 的事件是否是连接事件 : 调用 key.isAcceptable() 方法 , 如果返回 true , 说明该事件是客户端连接事件 ;

② 创建 套接字通道 : 为该客户端创建一个对应的 SocketChannel 通道 , 调用 serverSocketChannel.accept() 方法 , 可以创建该客户端对应的 SocketChannel 通道 , 该方法是非阻塞的 , 因为该事件触发时已经知道有客户端连接 , 这里只是响应客户端的连接 ;

③ 设置非阻塞网络通信模式 : : sc.configureBlocking(false) 设置该通道是非阻塞通道 , 否则会报 IllegalBlockingModeException 异常 ;

④ 将通道注册给选择器 : 注册通道给选择器 , 并监听数据读取事件 , 同时设置通道对应的缓冲区 , 通道与客户端之间使用缓冲区进行交互 ; sc.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)) ;



5 . 处理数据读取事件 :


① 判定 SelectionKey 的事件是否是 读取 事件 : 调用 key.isReadable() 方法 , 如果返回 true , 说明该事件是 数据读取 事件 ;

② 获取通道 : 调用 SelectionKey 的 (SocketChannel) key.channel() 方法 , 获取该 SelectionKey 对应的通道 ;

③ 获取缓冲区 : 调用 (ByteBuffer) key.attachment() 获取对应的注册给 选择器 的缓冲区 ;

④ 读取缓冲区的数据 : 通道 socketChannel.read(byteBuffer) 方法 , 可以将数据读取数据到该缓冲区中 , 之后可以从缓冲区中获取数据 ;





II . NIO 通信 服务器端代码



服务器端代码 :

package kim.hsl.nio.demo;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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 Server {
    public static void main(String[] args) {

        try {

            //I . 创建 ServerSocketChannel 监听 8888 端口


            //创建 ServerSocketChannel, 等价于 BIO 中的 ServerSocket
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

            //绑定本地端口, 获取其内部封装的 ServerSocket, 绑定 ServerSocket 的 8888 端口
            ServerSocket serverSocket = serverSocketChannel.socket();
            serverSocket.bind(new InetSocketAddress(8888));

            //设置网络通信非阻塞模式
            serverSocketChannel.configureBlocking(false);


            //II . 创建选择器, 并注册监听事件


            //获取 选择器 ( Selector )
            Selector selector = Selector.open();
            //将 serverSocketChannel 通道注册给 选择器 ( Selector ), 这里注册连接事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);


            //III . 选择器监听


            //选择器 ( Selector ) 开始监听
            while (true){
                //III . 1 . 判定事件触发 :
                //阻塞监听, 查看是否有事件触发, 如果有就在下面处理
                //如果没有 continue 终止循环, 继续下一次循环
                System.out.println("服务器端开始阻塞监听 8888 端口事件");
                if(selector.select() <= 0){
                    continue;
                }

                //当前状态说明 :
                //如果能执行到该位置, 说明 selector.select(1000) 方法返回值大于 0
                //当前有 1 个或多个事件触发, 下面就是处理事件的逻辑

                //III . 2 . 处理事件集合 :
                //获取当前发生的事件的 SelectionKey 集合, 通过 SelectionKey 可以获取对应的 通道
                Set<SelectionKey> keys =  selector.selectedKeys();

                //使用迭代器迭代, 涉及到删除操作
                Iterator<SelectionKey> keyIterator = keys.iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();

                    //根据 SelectionKey 的事件类型, 处理对应通道的业务逻辑

                    //III . 2 . ( 1 ) 客户端连接服务器, 服务器端需要执行 accept 操作
                    if (key.isAcceptable()) {
                        System.out.println("服务器端 选择器 ( Selector ) 监听到客户端连接事件");
                        //创建通道 : 为该客户端创建一个对应的 SocketChannel 通道
                        //不等待 : 当前已经知道有客户端连接服务器, 因此不需要阻塞等待
                        //非阻塞方法 : ServerSocketChannel 的 accept() 是非阻塞的方法
                        SocketChannel sc = serverSocketChannel.accept();
                        //如果 ServerSocketChannel 是非阻塞的, 这里的 SocketChannel 也要设置成非阻塞的
                        //否则会报 java.nio.channels.IllegalBlockingModeException 异常
                        sc.configureBlocking(false);

                        //注册通道 : 将 SocketChannel 通道注册给 选择器 ( Selector )
                        //关注事件 : 关注事件时读取事件, 服务器端从该通道读取数据
                        //关联缓冲区 :
                        sc.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));

                    }

                    //III . 2 . ( 2 ) 客户端写出数据到服务器端, 服务器端需要读取数据
                    if(key.isReadable()){
                        System.out.println("服务器端 选择器 ( Selector ) 监听到客户发送数据事件");
                        //获取 通道 ( Channel ) : 通过 SelectionKey 获取
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        //获取 缓冲区 ( Buffer ) : 获取到 通道 ( Channel ) 关联的 缓冲区 ( Buffer )
                        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
                        //读取客户端传输的数据 ;
                        socketChannel.read(byteBuffer);
                        System.out.println("客户端向服务器端发送数据 : " + new String(byteBuffer.array()));
                    }

                    //处理完毕后, 从 Set 集合中移除该 SelectionKey
                    keyIterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108




III . NIO 通信 客户端 流程说明



NIO 网络通信 客户端 操作流程 : 首先创建客户端套接字通道 , 设置该通道为非阻塞通信模式 , 连接服务器的指定端口号 , 连接成功后 , 写出数据到服务器中 ;


创建套接字通道 -> 连接服务器 -> 写出数据到服务器


1 . 创建套接字通道 : 调用 SocketChannel.open() 方法 , 即可获取套接字通道 ( SocketChannel ) , 之后将该通道设置为 非阻塞通信模式 socketChannel.configureBlocking(false) ;


2 . 连接服务器 : 首先设置服务器的地址和端口号 new InetSocketAddress(“127.0.0.1”, 8888) , 然后调用 socketChannel 的 connect(address) 方法 , 即可连接服务器 , 该操作是非阻塞的操作 , 此时需要确保连接成功以后 , 再向服务器发送数据 ;


3 . 写出数据到服务器 : 先创建 缓冲区 Buffer , 将数据放入缓冲区 , ByteBuffer.wrap(“Hello World”.getBytes()) , 然后调用 套接字通道 ( socketChannel ) 的 write(buffer) 方法 , 将数据写出到服务器中 ;





IV . NIO 通信 客户端代码



NIO 通信 客户端代码 :

package kim.hsl.nio.demo;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client {
    public static void main(String[] args) {
        try {
            //1 . 客户端 SocketChannel : 先获取 SocketChannel, 相当于 BIO 中的 Socket, 设置非阻塞模式
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);

            //服务器地址 : 服务器的 IP 地址 和 端口号
            InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);

            //2 . 连接服务器 : 连接成功, 返回 true; 连接失败, 返回 false;
            boolean isConnect = socketChannel.connect(address);

            //没有连接成功
            if (!isConnect) {
                while (!socketChannel.finishConnect()){
                    System.out.println("等待连接成功");
                }
            }

            //当前时刻状态分析 : 执行到该位置, 此时肯定是连接成功了
            System.out.println("服务器连接成功");

            //3 . 发送数据 : 如果连接成功 , 发送数据到服务器端
            ByteBuffer buffer = ByteBuffer.wrap("Hello World".getBytes());
            System.out.println("客户端向服务器端发送数据 \"Hello World\"");
            socketChannel.write(buffer);

            //目的是为了阻塞客户端, 不能让客户端退出
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42




V . NIO 通信 示例运行



按照以下顺序操作


1 . 运行服务器端 : 服务器端运行后 , 选择器阻塞监听客户端的请求 , 主要是监听 客户端连接 和 数据读取 ( 服务器读取客户端发送的数据 ) 事件 ;

在这里插入图片描述


2 . 运行客户端 : 客户端运行后 , 连接服务器 , 然后向服务器写出 “Hello World” 字符串数据 ;

在这里插入图片描述


3 . 服务器端结果 : 服务器端监听到客户端连接 , 为客户端创建对应的通道 , 然后注册监听该通道的数据读取事件 , 之后继续监听客户端是否有数据写入 ;

在这里插入图片描述

文章来源: hanshuliang.blog.csdn.net,作者:韩曙亮,版权归原作者所有,如需转载,请联系作者。

原文链接:hanshuliang.blog.csdn.net/article/details/106392226

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。