NIO群聊系统的实现

一、前言

通过NIO编写简单版聊天室,客户端通过控制台输入发送消息到其他客户端。注意:并未处理粘包半包问题。

二、逻辑简述

服务器:

java 复制代码
1)创建服务器NIO通道,绑定端口并启动服务器
2)开启非阻塞模式
3)创建选择器、并把通道注册到选择器上,关心的事件为新连接
4)循环监听选择器的事件,
5)监听到新连接事件:
       5.1) 建立连接、创建客户端通道
       5.2)客户端通道设置非阻塞
       5.3)客户端注册到选择器上,关心的事件为读
6)监听到读 事件
       6.1)获取到发送数据的客户端通道
       6.2)把通道数据写入到一个缓冲区中
       6.3)打印数据
       6.4)发送给其他注册在选择器上的客户端,排除自己

客户器:

java 复制代码
1)创建客户端通道,连接服务器 ip和端口
2)创建选择器,注册客户端通道到选择器上,关心的事件为读
3)开启一个线程 循环监听选择器事件
4)监听到读事件后
       4.1)从通道中把数据读到缓冲区中
       4.2)打印数据
5)主线程循环用scanner 来监听控制台输入
       5.1)有输入后 发送给服务器

三、代码

服务器:

java 复制代码
 
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
 
/**
 
 */
public class GroupChatServer {
 
    private int port = 8888;
 
    private ServerSocketChannel serverSocketChannel;
 
    private Selector selector;
 
 
    public GroupChatServer() throws IOException {
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(port));
 
        //创建选择器
        selector = Selector.open();
        //通道注册到选择器上,关心的事件为 OP_ACCEPT:新连接
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("server is ok");
    }
 
    public void listener() throws IOException {
        for (; ; ) {
            if (selector.select() == 0) {
                continue;
            }
            //监听到时间
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isAcceptable()) {//新连接事件
                    newConnection();
                }
                if (selectionKey.isReadable()) {//客户端消息事件
                    clientMsg(selectionKey);
                }
                iterator.remove();
            }
        }
    }
 
    /**
     * 客户端消息处理
    
     */
    private void clientMsg(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
        try {
            //通道数据读取到 byteBuffer缓冲区
            socketChannel.read(byteBuffer);
            //创建一个数组用于接受 缓冲区的本次写入的数据。
            byte[] bytes = new byte[byteBuffer.limit()];
            //转换模式 写->读
            byteBuffer.flip();
            //获取数据到 bytes 中 从位置0开始到limit结束
            byteBuffer.get(bytes, 0, byteBuffer.limit());
            String msg = socketChannel.getRemoteAddress() + "说:" + new String(bytes, "utf-8");
            //倒带这个缓冲区。位置设置为零,标记为-1.这样下次写入数据会从0开始写。但是如果下次的数据比这次少。那么使用 byteBuffer.array方法返回的byte数组数据会包含上一次的部分数据
            //例如 上次写入了 11111 倒带后 下次写入了 22 读取出来 却是 22111
            byteBuffer.rewind();
            System.out.println(msg);
            //发送给其他客户端
            sendOuterClient(msg, socketChannel);
        } catch (Exception e) {
            System.out.println(socketChannel.getRemoteAddress() + ":下线了");
            socketChannel.close();
        }
    }
 
    /**
     * 发送给其他客户端
     *
     * @param msg           要发送的消息
     * @param socketChannel 要排除的客户端
     * @throws IOException
     */
    private void sendOuterClient(String msg, SocketChannel socketChannel) throws IOException {
        //获取selector上注册的全部通道集合
        Set<SelectionKey> keys = selector.keys();
        for (SelectionKey key : keys) {
            SelectableChannel channel = key.channel();
            //判断通道是客户端通道(因为服务器的通道也注册在该选择器上),并且排除发送人的通道
            if (channel instanceof SocketChannel && !channel.equals(socketChannel)) {
                try {
                    ((SocketChannel) channel).write(ByteBuffer.wrap(msg.getBytes()));
                } catch (Exception e) {
                    channel.close();
                    System.out.println(((SocketChannel) channel).getRemoteAddress() + ":已下线");
                }
            }
        }
    }
 
    /**
     * 新连接处理方法
     * @throws IOException
     */
    private void newConnection() throws IOException {
        //连接获取SocketChannel
        SocketChannel socketChannel = serverSocketChannel.accept();
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //注册到选择器上,关心的事件是读,并附带一个ByteBuffer对象
        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
        System.out.println(socketChannel.getRemoteAddress() + " 上线了");
    }
 
    public static void main(String[] args) throws IOException {
        GroupChatServer groupChatServer = new GroupChatServer();
        //启动监听
        groupChatServer.listener();
    }
}

客户端:

java 复制代码
 
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
 
/**    
 */
public class GroupChatClient {
 
    private Selector selector;
 
    private SocketChannel socketChannel;
 
    public GroupChatClient(String host, int port) throws IOException {
        socketChannel = SocketChannel.open(new InetSocketAddress(host, port));
        socketChannel.configureBlocking(false);
        selector = Selector.open();
        //注册事件,关心读事件
        socketChannel.register(selector, SelectionKey.OP_READ);
        System.out.println("我是:" + socketChannel.getLocalAddress());
    }
    /**
     * 读消息
     */
    private void read() {
        try {
            if(selector.select() == 0){
                //没有事件,return
                return;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                if(selectionKey.isReadable()){//判断是 读 事件
                    SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    //读取数据到  byteBuffer 缓冲区
                    socketChannel.read(byteBuffer);
                    //打印数据
                    System.out.println(new String(byteBuffer.array()));
                }
                iterator.remove();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 发送数据
     * @param msg 消息
     * @throws IOException
     */
    private void send(String msg) throws IOException {
        socketChannel.write(ByteBuffer.wrap(new String(msg.getBytes(),"utf-8").getBytes()));
    }
 
 
    public static void main(String[] args) throws IOException {
        //创建客户端 指定 ip端口
        GroupChatClient groupChatClient = new GroupChatClient("127.0.0.1",8888);
        //启动一个线程来读取数据
        new Thread(()->{
            while (true){
                groupChatClient.read();
            }
        }).start();
        //Scanner 发送数据
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()){
            String s = scanner.nextLine();
            //发送数据
            groupChatClient.send(s);
        }
 
    }
}
相关推荐
打码人的日常分享2 分钟前
企业人力资源管理,人事档案管理,绩效考核,五险一金,招聘培训,薪酬管理一体化管理系统(源码)
java·数据库·python·需求分析·规格说明书
27669582923 分钟前
京东e卡滑块 分析
java·javascript·python·node.js·go·滑块·京东
爱写代码的刚子5 分钟前
C++知识总结
java·开发语言·c++
冷琴199612 分钟前
基于java+springboot的酒店预定网站、酒店客房管理系统
java·开发语言·spring boot
daiyang123...38 分钟前
IT 行业的就业情况
java
爬山算法1 小时前
Maven(6)如何使用Maven进行项目构建?
java·maven
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
爱学的小涛1 小时前
【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解
java·开发语言·笔记·后端·nio
吹老师个人app编程教学1 小时前
详解Java中的BIO、NIO、AIO
java·开发语言·nio
爱学的小涛1 小时前
【NIO基础】NIO(非阻塞 I/O)和 IO(传统 I/O)的区别,以及 NIO 的三大组件详解
java·开发语言·笔记·后端·nio