Java NIO 应用案例:实现一个简单的群聊系统

1 案例要求

  • 编写一个 NIO 多人群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞);
  • 服务器端功能:
    • 监测用户上线,离线;
    • 实现客户端消息的转发功能(将该客户端的消息转发给其它客户端);
  • 客户端功能:通过 channel 无阻塞地发送消息给其它所有用户,同时可以接受其它用户发送的消息(由服务器转发得到)。

2 服务器端代码

2.1 服务器初始化并监听 6667 端口

服务端初始化 实现思路分析

首先服务器端我们需要三个属性:

  • Selector:检测多个注册的通道上是否有事件发生,实现多路复用(所有的 channel 都需要注册到 Selector 上);
  • ServerSocketChannel:监听新的客户端 Socket 连接;
    • 注意:ServerSocketChannel 也需要注册到 Selector 上,事件类型为 OP_ACCEPT
  • PROT:ServerSocketChannel监听的端口。
public class GroupChatServer {
    
    
    private Selector selector;
    private ServerSocketChannel listenerChannel;
    private static final int PORT = 6667;

    public GroupChatServer() {
    
    
        try {
    
    
            selector = Selector.open();
            listenerChannel = ServerSocketChannel.open();
            // 绑定端口
            listenerChannel.socket().bind(new InetSocketAddress(PORT));
            // 设置非阻塞模式
            listenerChannel.configureBlocking(false);
            // 将 listenerChannel 注册到 selector,事件为:OP_ACCEPT
            listenerChannel.register(selector, SelectionKey.OP_ACCEPT);

        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
}

服务端监听 6667 端口 实现思路分析

我们在一个 while 循环中不断通过 selector.select(5000); 方法来获取有事件发生的通道的个数,如果个数大于0,再获取有事件发生的通道的 SelectionKey,然后针对不同事件做相应的处理:

  • 如果 channel 事件类型为 OP_ACCEPT,则说明有客户端请求连接,那么就需要为该客户端分配一个 SocketChannel,并将该 SocketChannel注册到 Selector 上,注册事件类型为 OP_READ。此时意味着客户端上线了;
  • 如果 channel 事件类型为 OP_READ,则说明有客户端发送消息到该 channel,那么我们就需要读取客户端消息,并转发到其它的客户端。(2.2小节中讲解
// 监听
public void listen() {
    
    
    try {
    
    
        while (true) {
    
    
            int count = selector.select(5000);
            if (count > 0) {
    
     // 说明有事件处理
                // 遍历 SelectionKey 集合进行处理
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
    
    
                    SelectionKey key = iterator.next();
                    // 不同事件做不同的处理
                    if (key.isAcceptable()) {
    
    // accept事件
                        SocketChannel socketChannel = listenerChannel.accept();
                        socketChannel.configureBlocking(false);// 设置非阻塞
                        // 注册到 selector
                        socketChannel.register(selector,SelectionKey.OP_READ);
                        System.out.println(socketChannel.getRemoteAddress()+" 上线了。。。");
                    }
                    if (key.isReadable()) {
    
     // 读事件
                        // 处理读(单独写一个方法):读取客户端消息
                        readData(key);
                    }
                    // 将当前的 selectionKey 删除
                    iterator.remove();
                }
            } else {
    
    
            	continue;
            }
        }

    } catch (Exception e) {
    
    
        e.printStackTrace();
    } finally {
    
    

    }
}

2.2 服务器接收客户端信息,并实现转发

服务器接收客户端信息 实现思路分析

在2.1小节中,我们获取到了发生事件的 channel 的 SelectionKey,如果 SelectionKey 对应的 channel 事件为 OP_READ,则说明 客户端发送了消息,那么我们就需要对消息进行处理,即读取客户端消息并转发给其它上线的客户端。处理逻辑如下:

  • 首先根据 2.1 中获取到的 SelectionKey 获取到对应的 SocketChannel,然后读取 channel 中的消息;
  • 将消息转发给其它客户端(注意:要排除自己);
  • 这里有一点很重要:如果捕获到了 IOException,则意味着客户端下线了,做出响应的处理。
private void readData(SelectionKey key) {
    
    
   // 定义一个 SocketChannel
    SocketChannel channel = null;
    try {
    
    
        channel = (SocketChannel) key.channel();
        // 创建 Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        int count = channel.read(buffer);
        if (count > 0) {
    
    
            String msg = new String(buffer.array());
            // 输出消息
            System.out.println("from 客户端: " + msg);
            // 向其它客户端(排除自己) 转发消息
            sendInfoToOtherClient(msg,channel);
        }
    } catch (IOException e) {
    
    // 如果捕获到异常就说明客户端下线了
        try {
    
    
            System.out.println(channel.getRemoteAddress() + "离线了。。。");
            // 取消注册
            key.cancel();
            // 关闭通道
            channel.close();
        } catch (IOException ioException) {
    
    
            ioException.printStackTrace();
        }
    }

}

服务器消息转发 实现思路分析

注意:进行消息转发的时候要排除自己。

// 转发消息给其它客户端
private void sendInfoToOtherClient(String msg,SocketChannel self) {
    
    
    System.out.println("服务器转发消息。。。");
    // 遍历所有注册到 selector 上的 SocketChannel 并排除自己
    for (SelectionKey key : selector.keys()) {
    
    
        // 通过 key 取出对应的 SocketChannel
        SelectableChannel targetChannel = key.channel();
        // 排除自己
        if ( targetChannel instanceof SocketChannel && targetChannel != self) {
    
    
            SocketChannel dest = (SocketChannel) targetChannel;

            // 将msg存储到buffer
            ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
            
            try {
    
     // 将buffer的数据写入通道
                dest.write(buffer);
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }

        }
    }
}

2.3 最终代码

public class GroupChatServer {
    
    
    private Selector selector;
    private ServerSocketChannel listenerChannel;
    private static final int PORT = 6667;

    public GroupChatServer() {
    
    
        try {
    
    
            selector = Selector.open();
            listenerChannel = ServerSocketChannel.open();
            // 绑定端口
            listenerChannel.socket().bind(new InetSocketAddress(PORT));
            // 设置非阻塞模式
            listenerChannel.configureBlocking(false);
            // 将 listenerChannel 注册到 selector,事件为:OP_ACCEPT
            listenerChannel.register(selector, SelectionKey.OP_ACCEPT);

        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
    // 监听
    public void listen() {
    
    
        try {
    
    
            while (true) {
    
    
                int count = selector.select(5000);
                if (count > 0) {
    
     // 说明有事件处理
                    // 遍历 SelectionKey 集合进行处理
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
    
    
                        SelectionKey key = iterator.next();
                        // 不同事件做不同的处理
                        if (key.isAcceptable()) {
    
    // accept事件
                            SocketChannel socketChannel = listenerChannel.accept();
                            socketChannel.configureBlocking(false);// 设置非阻塞
                            // 注册到 selector
                            socketChannel.register(selector,SelectionKey.OP_READ);
                            System.out.println(socketChannel.getRemoteAddress()+" 上线了。。。");
                        }
                        if (key.isReadable()) {
    
     // 读事件
                            // 处理读(单独写一个方法):读取客户端消息
                            readData(key);
                        }
                        // 将当前的 selectionKey 删除
                        iterator.remove();
                    }
                } else {
    
    
                    System.out.println("目前无事件发生,等待中。。。");
                }
            }

        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    

        }
    }
    // 读取客户端消息,并转发
    private void readData(SelectionKey key) {
    
    
        // 定义一个 SocketChannel
        SocketChannel channel = null;
        try {
    
    
            channel = (SocketChannel) key.channel();
            // 创建 Buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            int count = channel.read(buffer);
            if (count > 0) {
    
    
                String msg = new String(buffer.array());
                // 输出消息
                System.out.println("from 客户端: " + msg);
                // 向其它客户端(排除自己) 转发消息
                sendInfoToOtherClient(msg,channel);
            }
        } catch (IOException e) {
    
    // 如果捕获到异常就说明客户端下线了
            try {
    
    
                System.out.println(channel.getRemoteAddress() + "离线了。。。");
                // 取消注册
                key.cancel();
                // 关闭通道
                channel.close();
            } catch (IOException ioException) {
    
    
                ioException.printStackTrace();
            }
        }

    }
    // 转发消息给其它客户端
    private void sendInfoToOtherClient(String msg,SocketChannel self) {
    
    
        System.out.println("服务器转发消息。。。");
        // 遍历所有注册到 selector 上的 SocketChannel 并排除自己
        for (SelectionKey key : selector.keys()) {
    
    
            // 通过 key 取出对应的 SocketChannel
            SelectableChannel targetChannel = key.channel();
            // 排除自己
            if ( targetChannel instanceof SocketChannel && targetChannel != self) {
    
    
                SocketChannel dest = (SocketChannel) targetChannel;

                // 将msg存储到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());

                try {
    
     // 将buffer的数据写入通道
                    dest.write(buffer);
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }

            }
        }
    }

}

3 客户端代码

3.1 客户端初始化连接服务器

客户端初始化连接服务器 实现思路分析

首先客户端我们需要五个属性:

  • Selector:检测多个注册的通道上是否有事件发生,实现多路复用(所有的 channel 都需要注册到 Selector 上);
  • SocketChannel:和服务器端建立连接;
    • 注意:SocketChannel 也需要注册到 Selector 上,事件类型为 OP_READ
  • PROT:服务器端监听的端口。
  • HostClient:服务器端地址。
  • username:客户端名称,这里就用ip地址代替。

初始化步骤如下:

  • 初始化 selector;
  • 通过SocketChannel和服务器端建立连接,并将ScoketChannel注册到Selector,注册事件为 OP_READ
  • 将username设为ip地址
public class GroupChatClient {
    
    
    private final String HostClient = "127.0.0.1";
    private final int PORT = 6667;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    public GroupChatClient() {
    
    
        try {
    
    
            selector = Selector.open();
            socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1",PORT));
            socketChannel.configureBlocking(false);
            // 将channel注册到selector
            socketChannel.register(selector, SelectionKey.OP_READ);
            // 得到 username
            username = socketChannel.getLocalAddress().toString().substring(1);
            System.out.println(username + " is ok ...");
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
}

3.2 发送消息 & 接收消息 实现逻辑

客户端发送消息 实现思路分析

我们直接将要发送的消息写入buffer,然后调用 channel 的 write() 方法将消息写入 channel 中。

// 向服务器发送消息
public void sendInfo(String info) {
    
    
    info = username + "说:" + info;
    try {
    
    
        socketChannel.write(ByteBuffer.wrap(info.getBytes()));
    } catch (IOException e) {
    
    
        e.printStackTrace();
    }
}

客户端读取从服务器端回复的消息 实现思路分析

  • 通过 selector.select(2000); 非阻塞的获取有事件发生的 channel;
  • 如果 channel 发生的时间为 OP_READ,则通过将 channel 中的消息读取到 Buffer 中。
// 读取从服务器端回复的消息
public void readInfo() {
    
    
    try {
    
    
        int readChannels = selector.select(2000);

        if (readChannels > 0) {
    
    
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
    
    
                SelectionKey key = iterator.next();
                if (key.isReadable()) {
    
    
                    // 得到相关的通道
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    // 将通道中的数据读取到缓冲区
                    int count = socketChannel.read(buffer);
                    if (count > 0) {
    
    
                        String msg = new String(buffer.array());
                        // 输出消息
                        System.out.println(msg.trim());
                    }
                }
            }
            // 防止重复操作
           iterator.remove();
        } 
    } catch (Exception e) {
    
    
        e.printStackTrace();
    }
}

3.3 最终代码

public class GroupChatClient {
    
    
    private final String HostClient = "127.0.0.1";
    private final int PORT = 6667;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    public GroupChatClient() {
    
    
        try {
    
    
            selector = Selector.open();
            socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1",PORT));
            socketChannel.configureBlocking(false);
            // 将channel注册到selector
            socketChannel.register(selector, SelectionKey.OP_READ);
            // 得到 username
            username = socketChannel.getLocalAddress().toString().substring(1);
            System.out.println(username + " is ok ...");
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
    // 向服务器发送消息
    public void sendInfo(String info) {
    
    
        info = username + "说:" + info;
        try {
    
    
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
    // 读取从服务器端回复的消息
    public void readInfo() {
    
    
        try {
    
    
            int readChannels = selector.select(2000);

            if (readChannels > 0) {
    
    
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
    
    
                    SelectionKey key = iterator.next();
                    if (key.isReadable()) {
    
    
                        // 得到相关的通道
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        System.out.println("1111111111111111111");
                        // 将通道中的数据读取到缓冲区
                        int count = socketChannel.read(buffer);
                        if (count > 0) {
    
    
                            String msg = new String(buffer.array());
                            // 输出消息
                            System.out.println(msg.trim());
                        }
                    }
                }
            } else {
    
    
                System.out.println("没有可以用的通道...");
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
    
    
        GroupChatClient chatClient = new GroupChatClient();
        // 启动一个线程,每隔3秒读取从服务器端发送的数据
        new Thread() {
    
    
            @Override
            public void run() {
    
    
                while (true) {
    
    
                    chatClient.readInfo();
                    try {
    
    
                        Thread.currentThread().sleep(3000);
                    } catch (Exception e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        }.start();
        // 发送数据给服务器端
        Scanner sc = new Scanner(System.in);
        while (sc.hasNextLine()) {
    
    
            String s = sc.nextLine();
            chatClient.sendInfo(s);
        }
    }
}

4 服务端/客户端 测试

4.1 如何启动 服务端 / 测试端

启动客户端

  • 我们需要创建一个线程,不断地从服务器端读取服务器转发的其它客户端的消息;
  • 然后通过Scanner类模拟当前用户发送信息。
public static void main(String[] args) {
    
    
    GroupChatClient chatClient = new GroupChatClient();
    // 启动一个线程,每隔3秒读取从服务器端发送的数据
    new Thread() {
    
    
        @Override
        public void run() {
    
    
            while (true) {
    
    
                chatClient.readInfo();
                try {
    
    
                    Thread.currentThread().sleep(3000);
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }.start();
    // 发送数据给服务器端
    Scanner sc = new Scanner(System.in);
    while (sc.hasNextLine()) {
    
    
        String s = sc.nextLine();
        chatClient.sendInfo(s);
    }
}

启动服务器端

创建服务器类,然后里调用 listen() 方法即可。

public static void main(String[] args) {
    
    
    GroupChatServer chatServer = new GroupChatServer();
    chatServer.listen();
}

4.2 测试结果

我们启动一个服务端代码和三个客户端代码:
在这里插入图片描述
在这里插入图片描述

选择其中一个客户端发送消息,测试其它客户端是否接收成功:

在这里插入图片描述
其它客户端接收结果:
在这里插入图片描述
在这里插入图片描述

到这里一个基于NIO的简单群聊系统就成功实现了。

猜你喜欢

转载自blog.csdn.net/qq_36389060/article/details/124345238