【Netty】NIO 网络编程 聊天室案例

举报
韩曙亮 发表于 2022/01/11 00:21:10 2022/01/11
【摘要】 文章目录 一、 NIO 聊天室需求二、 NIO 聊天室 服务器端 代码分析三、 NIO 聊天室 客户端 代码分析四、 NIO 聊天室 服务器端 完整代码五、 NIO 聊天室 客户端 完整代码六、 ...





一、 NIO 聊天室需求



1 . NIO 聊天室需求 :


① 服务器 客户端 通信 : 服务器 与 客户端 实现 双向通信 ; 服务器可以写出数据到客户端 , 也能读取客户端的数据 ; 客户端可以写出数据到服务器端 , 也可以读取服务器端的数据 ;

② 多人聊天 : 一个服务器 与 多个客户端 进行数据交互 , 同时还要实现将某一个客户端的数据转发给其它客户端 ;

③ 用户状态监测 : 服务器可以检测用户的 上线 , 离线 状态 ;



2 . 数据传输细节 :


① 上线监听 : 当有客户端连接时 , 服务器检测到用户上线 , 服务器将该用户上线状态通知给其它客户端 ;

② 下线监听 : 如果有客户端离线 , 服务器检测到连接断开 , 服务器将该用户离线的状态通知给聊天室的其它客户端 ;

③ 聊天信息转发 : 客户端发送消息时 , 服务器端接收到该数据 , 并转发给聊天室的其它用户客户端 ;





二、 NIO 聊天室 服务器端 代码分析



服务器端的连接管理流程 : 创建 服务器套接字通道 ( ServerSocketChannel ) , 将该通道注册给 选择器 ( Selector ) , 选择器开启监听 , 监听到客户端连接 , 就创建一个 套接字通道 ( SocketChannel ) , 注册给选择器 ;


服务器端的消息转发流程 : 服务器端收到客户端发送的消息 , 将该消息转发给除该客户端外的其它客户端 , 从选择器中可以获取到所有的 通道 , 注意 屏蔽 服务器套接字通道发送本消息的客户端对应的通道 ;


服务器连接监听 : 当客户端与服务器连接成功 , 即触发注册给 选择器 ( Selector ) 的 服务器套接字通道 ( ServerSocketChannel ) 的 SelectionKey.OP_ACCEPT 事件 , 表示有客户端连接服务器成功 , 用户上线 ;


服务器断开连接监听 : 当服务器端与客户端读写数据出现异常时 , 说明该客户端离线 , 在异常处理代码中可以判定某个客户端离线 ;



1 . 服务器套接字通道 : 调用 open 静态方法创建服务器套接字通道 , 并绑定 8888 端口 , 设置非阻塞网络通信模式 ;

// 创建并配置 服务器套接字通道 ServerSocketChannel
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);

  
 
  • 1
  • 2
  • 3
  • 4

2 . 服务器端选择器 : 调用 open 静态方法获取 选择器 , 注册之前创建的 服务器套接字通道 ;

// 获取选择器, 并注册 服务器套接字通道 ServerSocketChannel
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

  
 
  • 1
  • 2
  • 3

3 . 监听事件 : 阻塞监听, 如果有事件触发, 返回触发的事件个数 ; 被触发的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中 ;

// 阻塞监听, 如果有事件触发, 返回触发的事件个数
// 被触发的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中
// 下面开始遍历上述 selectedKeys 集合
try {
    int eventTriggerCount = selector.select();
} catch (IOException e) {
    e.printStackTrace();
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4 . 处理客户端连接事件 : 接受客户端连接 , 获取 网络套接字通道 ( SocketChannel ) , 并注册给 选择器 ( Selector ) , 监听 SelectionKey.OP_READ 数据读取事件 ;

// 客户端连接服务器, 服务器端需要执行 accept 操作
if (key.isAcceptable()) {
    //创建通道 : 为该客户端创建一个对应的 SocketChannel 通道
    //不等待 : 当前已经知道有客户端连接服务器, 因此不需要阻塞等待
    //非阻塞方法 : ServerSocketChannel 的 accept() 是非阻塞的方法
    SocketChannel socketChannel = null;
    try {
        socketChannel = serverSocketChannel.accept();
        //如果 ServerSocketChannel 是非阻塞的, 这里的 SocketChannel 也要设置成非阻塞的
        //否则会报 java.nio.channels.IllegalBlockingModeException 异常
        socketChannel.configureBlocking(false);
        //注册通道 : 将 SocketChannel 通道注册给 选择器 ( Selector )
        //关注事件 : 关注事件时读取事件, 服务器端从该通道读取数据
        //关联缓冲区 :
        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
        System.out.println(String.format("用户 %s 进入聊天室", socketChannel.getRemoteAddress()));
    } 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


5 . 处理客户端消息转发事件 :


① 读取客户端上传的数据 : 通过 SelectionKey 获取 通道缓冲区 , 使用 套接字通道 ( SocketChannel ) 读取 缓冲区 ( ByteBuffer ) 中的数据 , 然后记录显示该数据 ;

// 获取 通道 ( Channel ) : 通过 SelectionKey 获取
SocketChannel socketChannel = (SocketChannel) key.channel();
// 获取 缓冲区 ( Buffer ) : 获取到 通道 ( Channel ) 关联的 缓冲区 ( Buffer )
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
String remoteAddress = null;
String message = null;
try {
    // 读取客户端传输的数据
    int readCount = socketChannel.read(byteBuffer);
    byte[] messageBytes = new byte[readCount];
    byteBuffer.flip();
    byteBuffer.get(messageBytes);
    // 处理读取的消息
    message = new String(messageBytes);
    //重置以便下次使用
    byteBuffer.flip();
    remoteAddress = socketChannel.getRemoteAddress().toString();
    System.out.println(String.format("%s : %s", remoteAddress, message));
} catch (IOException e) {
    //e.printStackTrace();
    // 如果此处出现异常, 说明该客户端离线了, 服务器提示, 取消选择器上的注册信息, 关闭通道
    try {
        System.out.println( String.format("%s 用户离线 !", socketChannel.getRemoteAddress()) );
        key.cancel();
        socketChannel.close();
        //继续下一次循环
        continue;
    } catch (IOException ex) {
        ex.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

② 转发给其它客户端 :选择器 ( Selector )keys 集合 中获取所有注册的通道 , 然后除 ServerSocketChannel发送本信息的 客户端对应的 SocketChannel 通道 之外 , 其它所有的通道都转发一份聊天信息 ;

// 向其它客户端转发消息, 发送消息的客户端自己就不用再发送该消息了
// 遍历所有注册到 选择器 Selector 的 SocketChannel
Set<SelectionKey> selectionKeys = selector.keys();
for (SelectionKey selectionKey : selectionKeys) {
    // 获取客户端对应的 套接字通道
    // 这里不能强转成 SocketChannel, 因为这里可能存在 ServerSocketChannel
    Channel channel = selectionKey.channel();
    // 将自己排除在外, 注意这里是地址对比, 就是这两个类不能是同一个地址的类
    // 这个类的类型必须是 SocketChannel, 排除之前注册的 ServerSocketChannel 干扰
    if (socketChannel != channel && channel instanceof SocketChannel) {
        // 将通道转为 SocketChannel, 之后将字符串发送到客户端
        SocketChannel clientSocketChannel = (SocketChannel) channel;
        // 写出字符串到其它客户端
        try {
            clientSocketChannel.write(ByteBuffer.wrap( ( remoteAddress + " : " + message ).getBytes()));
        } catch (IOException e) {
            //e.printStackTrace();
            // 如果此处出现异常, 说明该客户端离线了, 服务器提示, 取消选择器上的注册信息, 关闭通道
            try {
                System.out.println( String.format("%s 用户离线 !", clientSocketChannel.getRemoteAddress()) );
                selectionKey.cancel();
                clientSocketChannel.close();
            } catch (IOException ex) {
                ex.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




三、 NIO 聊天室 客户端 代码分析



客户端的连接与数据接收 : 客户端的工作是连接服务器 , 得到与服务器通信的 套接字通道 ( SocketChannel ) , 注册该通道到 选择器 ( Selector ) , 监听 SelectionKey.OP_READ 读取数据事件 , 接收到数据后显示即可 ;



1 . 连接服务器 : 连接服务器 , 并设置网络通信非阻塞模式 ;

// 创建并配置 服务器套接字通道 ServerSocketChannel
socketChannel = SocketChannel.open(new InetSocketAddress(SERVER_ADDRESS, PORT));
socketChannel.configureBlocking(false);

  
 
  • 1
  • 2
  • 3

2 . 获取选择器并注册通道 : 获取 选择器 ( Selector ) , 并将 套接字通道 ( SocketChannel ) 注册给该选择器 ;

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

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

3 . 监听服务器端下发的消息 : 阻塞监听, 如果有事件触发, 返回触发的事件个数 ; 被触发的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中 ;

// 阻塞监听, 如果有事件触发, 返回触发的事件个数
// 被触发的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中
// 下面开始遍历上述 selectedKeys 集合
try {
    int eventTriggerCount = selector.select();
} catch (IOException e) {
    e.printStackTrace();
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4 . 处理服务器端发送的数据 : 如果监听到服务器下发数据 , 开始遍历当前触发事件的通道 , 调用该通道读取数据到缓冲区 , 之后显示该数据 ;

// 处理事件集合 :
// 获取当前发生的事件的 SelectionKey 集合, 通过 SelectionKey 可以获取对应的 通道
Set<SelectionKey> keys = selector.selectedKeys();
// 使用迭代器迭代, 涉及到删除操作
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    
    // 根据 SelectionKey 的事件类型, 处理对应通道的业务逻辑
    // 客户端写出数据到服务器端, 服务器端需要读取数据
    if (key.isReadable()) {
        // 获取 通道 ( Channel ) : 通过 SelectionKey 获取
        SocketChannel socketChannel = (SocketChannel) key.channel();
        // 获取 缓冲区 ( Buffer ) : 获取到 通道 ( Channel ) 关联的 缓冲区 ( Buffer )
        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
        
        String message = null;
        try {
            // 读取客户端传输的数据
            int readCount = socketChannel.read(byteBuffer);
            byte[] messageBytes = new byte[readCount];
            byteBuffer.flip();
            byteBuffer.get(messageBytes);
            // 处理读取的消息
            message = new String(messageBytes);
            byteBuffer.flip();
            System.out.println(String.format(message));
        } catch (IOException e) {
            //e.printStackTrace();
            // 客户端连接断开
            key.cancel();
            try {
                socketChannel.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }// try
    }// if (key.isReadable())

  
 
  • 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




四、 NIO 聊天室 服务器端 完整代码



package kim.hsl.nio.chat;

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

/**
 * 聊天室服务器端
 *
 * @author hsl
 * @date 2020-05-29 17:24
 */
public class Server {

    /**
     * 服务器监听的端口号
     */
    public static final int PORT = 8888;

    /**
     * 监听 ServerSocketChannel 通道和各个客户端对应的 SocketChannel 通道
     */
    private Selector selector;

    /**
     * 服务器端的套接字通道, 相当于 BIO 中的 ServerSocket
     */
    private ServerSocketChannel serverSocketChannel;

    /**
     * 初始化服务器相关操作
     */
    public Server() {
        initServerSocketChannelAndSelector();
    }

    /**
     * 初始化 服务器套接字通道 和
     */
    private void initServerSocketChannelAndSelector() {
        try {
            // 创建并配置 服务器套接字通道 ServerSocketChannel
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
            serverSocketChannel.configureBlocking(false);

            // 获取选择器, 并注册 服务器套接字通道 ServerSocketChannel
            selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Selector 开始执行 监听工作
     */
    private void selectorStartSelectOperation() {
        System.out.println("服务器端启动监听 :");
        while (true) {
            // 阻塞监听, 如果有事件触发, 返回触发的事件个数
            // 被触发的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中
            // 下面开始遍历上述 selectedKeys 集合
            try {
                int eventTriggerCount = selector.select();
            } catch (IOException e) {
                e.printStackTrace();
            }

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

            // 处理事件集合 :
            // 获取当前发生的事件的 SelectionKey 集合, 通过 SelectionKey 可以获取对应的 通道
            Set<SelectionKey> keys = selector.selectedKeys();
            // 使用迭代器迭代, 涉及到删除操作
            Iterator<SelectionKey> keyIterator = keys.iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();

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

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

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

                        System.out.println(String.format("用户 %s 进入聊天室", socketChannel.getRemoteAddress()));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }


                // 客户端写出数据到服务器端, 服务器端需要读取数据
                if (key.isReadable()) {
                    // 获取 通道 ( Channel ) : 通过 SelectionKey 获取
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    // 获取 缓冲区 ( Buffer ) : 获取到 通道 ( Channel ) 关联的 缓冲区 ( Buffer )
                    ByteBuffer byteBuffer = (ByteBuffer) key.attachment();

                    String remoteAddress = null;
                    String message = null;
                    try {
                        // 读取客户端传输的数据
                        int readCount = socketChannel.read(byteBuffer);
                        byte[] messageBytes = new byte[readCount];
                        byteBuffer.flip();
                        byteBuffer.get(messageBytes);
                        // 处理读取的消息
                        message = new String(messageBytes);
                        //重置以便下次使用
                        byteBuffer.flip();
                        remoteAddress = socketChannel.getRemoteAddress().toString();
                        System.out.println(String.format("%s : %s", remoteAddress, message));
                    } catch (IOException e) {
                        //e.printStackTrace();
                        // 如果此处出现异常, 说明该客户端离线了, 服务器提示, 取消选择器上的注册信息, 关闭通道
                        try {
                            System.out.println( String.format("%s 用户离线 !", socketChannel.getRemoteAddress()) );
                            key.cancel();
                            socketChannel.close();
                            //继续下一次循环
                            continue;
                        } catch (IOException ex) {
                            ex.printStackTrace();
                        }
                    }

                    // 向其它客户端转发消息, 发送消息的客户端自己就不用再发送该消息了
                    // 遍历所有注册到 选择器 Selector 的 SocketChannel
                    Set<SelectionKey> selectionKeys = selector.keys();
                    for (SelectionKey selectionKey : selectionKeys) {
                        // 获取客户端对应的 套接字通道
                        // 这里不能强转成 SocketChannel, 因为这里可能存在 ServerSocketChannel
                        Channel channel = selectionKey.channel();
                        // 将自己排除在外, 注意这里是地址对比, 就是这两个类不能是同一个地址的类
                        // 这个类的类型必须是 SocketChannel, 排除之前注册的 ServerSocketChannel 干扰
                        if (socketChannel != channel && channel instanceof SocketChannel) {
                            // 将通道转为 SocketChannel, 之后将字符串发送到客户端
                            SocketChannel clientSocketChannel = (SocketChannel) channel;

                            // 写出字符串到其它客户端
                            try {
                                clientSocketChannel.write(ByteBuffer.wrap( ( remoteAddress + " : " + message ).getBytes()));
                            } catch (IOException e) {
                                //e.printStackTrace();
                                // 如果此处出现异常, 说明该客户端离线了, 服务器提示, 取消选择器上的注册信息, 关闭通道
                                try {
                                    System.out.println( String.format("%s 用户离线 !", clientSocketChannel.getRemoteAddress()) );
                                    selectionKey.cancel();
                                    clientSocketChannel.close();
                                } catch (IOException ex) {
                                    ex.printStackTrace();
                                }

                            }
                        }

                    }

                }

                // 处理完毕后, 当前的 SelectionKey 已经处理完毕
                // 从 Set 集合中移除该 SelectionKey
                // 防止重复处理
                keyIterator.remove();
            }
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.selectorStartSelectOperation();
    }
}

  
 
  • 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
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192




五、 NIO 聊天室 客户端 完整代码



package kim.hsl.nio.chat;

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

public class Client {

    /**
     * 服务器地址
     */
    public final static String SERVER_ADDRESS = "127.0.0.1";

    /**
     * 服务器监听的端口号
     */
    public static final int PORT = 8888;

    /**
     * 监听 SocketChannel 通道的 选择器
     */
    private Selector selector;

    /**
     * 服务器端的套接字通道, 相当于 BIO 中的 ServerSocket
     */
    private SocketChannel socketChannel;


    public Client() {
        initClientSocketChannelAndSelector();
    }

    /**
     * 初始化 服务器套接字通道 和
     */
    private void initClientSocketChannelAndSelector() {
        try {
            // 创建并配置 服务器套接字通道 ServerSocketChannel
            socketChannel = SocketChannel.open(new InetSocketAddress(SERVER_ADDRESS, PORT));
            socketChannel.configureBlocking(false);

            // 获取选择器, 并注册 服务器套接字通道 ServerSocketChannel
            selector = Selector.open();
            //注册通道 : 将 SocketChannel 通道注册给 选择器 ( Selector )
            //关注事件 : 关注事件时读取事件, 服务器端从该通道读取数据
            //关联缓冲区 :
            socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 向服务器端发送消息
     * @param message
     */
    public void sendMessageToServer(String message){
        try {
            socketChannel.write(ByteBuffer.wrap(message.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readMessageFromServer(){
        // 阻塞监听, 如果有事件触发, 返回触发的事件个数
        // 被触发的 SelectionKey 事件都存放到了 Set<SelectionKey> selectedKeys 集合中
        // 下面开始遍历上述 selectedKeys 集合
        try {
            int eventTriggerCount = selector.select();
        } catch (IOException e) {
            e.printStackTrace();
        }

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

        // 处理事件集合 :
        // 获取当前发生的事件的 SelectionKey 集合, 通过 SelectionKey 可以获取对应的 通道
        Set<SelectionKey> keys = selector.selectedKeys();
        // 使用迭代器迭代, 涉及到删除操作
        Iterator<SelectionKey> keyIterator = keys.iterator();
        while (keyIterator.hasNext()) {
            SelectionKey key = keyIterator.next();

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

            // 客户端写出数据到服务器端, 服务器端需要读取数据
            if (key.isReadable()) {
                // 获取 通道 ( Channel ) : 通过 SelectionKey 获取
                SocketChannel socketChannel = (SocketChannel) key.channel();
                // 获取 缓冲区 ( Buffer ) : 获取到 通道 ( Channel ) 关联的 缓冲区 ( Buffer )
                ByteBuffer byteBuffer = (ByteBuffer) key.attachment();

                String message = null;
                try {
                    // 读取客户端传输的数据
                    int readCount = socketChannel.read(byteBuffer);
                    byte[] messageBytes = new byte[readCount];
                    byteBuffer.flip();
                    byteBuffer.get(messageBytes);
                    // 处理读取的消息
                    message = new String(messageBytes);
                    byteBuffer.flip();
                    System.out.println(String.format(message));
                } catch (IOException e) {
                    //e.printStackTrace();
                    // 客户端连接断开
                    key.cancel();
                    try {
                        socketChannel.close();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }// try

            }// if (key.isReadable())

            // 处理完毕后, 当前的 SelectionKey 已经处理完毕
            // 从 Set 集合中移除该 SelectionKey
            // 防止重复处理
            keyIterator.remove();
        }
    }

    public static void main(String[] args) {
        Client client = new Client();

        // 接收服务器端数据线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    //不停地从服务器端读取数据
                    client.readMessageFromServer();
                }
            }
        }).start();

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()){
            String message = scanner.nextLine();
            client.sendMessageToServer(message);
        }
    }
}

  
 
  • 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
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152




六、 NIO 聊天室 运行



按照如下步骤演示聊天室功能 ( 一定要按照顺序执行 ) ;


1 . 服务器启动 : 首先

在这里插入图片描述

2 . 设置客户端多个 : 点击下图绿框中的下拉菜单 , 选择 Edit Configuration , 弹出如下对话框 , 配置 Client 应用 , 勾选 Allow parallel run 选项 , 之后 Client 程序就可以运行多个 , 否则只能运行一个 ;

在这里插入图片描述

3 . 客户端 1 1 1 连接 : 运行客户端程序即可 ;

在这里插入图片描述

4 . 客户端 2 2 2 连接 : 运行客户端程序即可 ;

在这里插入图片描述

5 . 客户端 3 3 3 连接 : 运行客户端程序即可 ;

在这里插入图片描述

6 . 客户端 2 2 2 发送消息 :

服务器显示 : 客户端 2 2 2 将消息发送给服务器 , 服务器显示该消息 ;

在这里插入图片描述
客户端 1 1 1 显示 : 这是由服务器转发的客户端 2 2 2 发送的消息 , 客户端 3 3 3 也收到该消息 ;

在这里插入图片描述

7 . 客户端 2 2 2 退出 : 终止客户端 2 2 2 程序 , 服务器端检测到异常 , 就可以将客户端 2 2 2 退出 ;

在这里插入图片描述

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

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

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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