NIO实现简易的多人聊天室

一、基本概念

1.Selector

    选择器,常用方法有:

        Selector selector.open() 创建选择器;

        int selector.select() 监听客户端事件;

        Set<SelectionKey> selector.selectKeys() 返回选择器中已出触发事件的选择键;

     4中Selector域:

        int OP_ACCEPT:相当于ServerSocket中的accept操作;

        int OP_CONNECT:连接操作;

        int OP_READ:读操作;

        int OP_WRITE:写操作;

2.SocketChannel

    SelectableChannel的子类,用于向Selector注册。
   

3.ServerSocketChannel

    SelectableChannel的子类,用于向Selector注册。

    常用方法:

        SelectableChannel configureBlocking(boolean block) 设置通道阻塞模式;

        SelectionKey register(Selector sel,int ops) 注册到selector上;

        ServerSocketChannel  open() 打开服务器端套接字通道;

        ServerSocket socket() 返回与此通道关联的服务器套接字;

4.SelectionKey

    常用方法:

        SelectableChannel channel() 返回创建此key的通道;

        isAcceptable() 是否可以接受新连接;

        isConnectable()  是否完成套接字的连接;

        isReadable() 是否可进行读取操作;

        isWriteable()是否可以进行写操作;

二、服务端代码

/**
 * 网络多客户端聊天室
 * 功能1: 客户端通过Java NIO连接到服务端,支持多客户端的连接
 * 功能2:客户端初次连接时,服务端提示输入暱称,如果暱称已经有人使用,提示重新输入,如果暱称唯一,则登录成功,之后发送消息都需要按照规定格式带着暱称发送消息
 * 功能3:客户端登录后,发送已经设置好的欢迎信息和在线人数给客户端,并且通知其他客户端该客户端上线
 * 功能4:服务器收到已登录客户端输入内容,转发至其他登录客户端。
 *
 */
public class ChatRoomServer {
    //定义唯一selector
    private Selector selector = null;
    //端口
    static final int port = 8888;
    //用来记录在线人数,以及暱称
    private static HashSet<String> users = new HashSet<>();
    //用户名存在时的提示消息
    private static String USER_EXIST = "系统消息: 当前用户名已存在,请更换用户名!";
    //相当于自定义协议格式,与客户端协商好
    private static String USER_CONTENT_SPILIT = "#@#";

    /**
     * 服务端初始化
     * @throws IOException
     */
    public void init() throws IOException {
        //1.打开selector
        selector = Selector.open();
        //2.通过ServerSocketChannel创建Channel通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //3.为channel通道绑定监听端口,这里用ServerSocketChannel.open().socket().bind()也可以
        serverSocketChannel.bind(new InetSocketAddress(port));
        //非阻塞的方式
        serverSocketChannel.configureBlocking(false);
        //注册到选择器上,设置为监听状态
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("Server is listening now...");
        while(true) {
            int readyChannels = selector.select();
            //防止epoll空循环
            if(readyChannels == 0) continue;
            Set<SelectionKey> selectedKeys = selector.selectedKeys();  //可以通过这个方法,知道可用通道的集合
            Iterator keyIterator = selectedKeys.iterator();
            while(keyIterator.hasNext()) {
                SelectionKey sk = (SelectionKey) keyIterator.next();
                keyIterator.remove();
                dealWithSelectionKey(serverSocketChannel,sk);
            }
        }
    }

    /**
     * 处理事件
     * @param serverSocketChannel
     * @param sk
     * @throws IOException
     */
    public void dealWithSelectionKey(ServerSocketChannel serverSocketChannel,SelectionKey sk) throws IOException {
        //接入事件
        if(sk.isAcceptable()){
            SocketChannel sc = serverSocketChannel.accept();
            //非阻塞模式
            sc.configureBlocking(false);
            //注册选择器,并设置为读取模式,收到一个连接请求,然后起一个SocketChannel,并注册到selector上,之后这个连接的数据,就由这个SocketChannel处理
            sc.register(selector, SelectionKey.OP_READ);
            //将此对应的channel设置为准备接受其他客户端请求
            sk.interestOps(SelectionKey.OP_ACCEPT);
            System.out.println("Server is listening from client :" + sc.getRemoteAddress());
            sc.write(Charset.forName("UTF-8").encode("请输入用户名:"));
        }
        //处理来自客户端的数据读取请求
        if(sk.isReadable()){
            //返回该SelectionKey对应的 Channel,其中有数据需要读取
            SocketChannel sc = (SocketChannel)sk.channel();
            ByteBuffer buff = ByteBuffer.allocate(1024);
            StringBuilder content = new StringBuilder();
            try{
                while(sc.read(buff) > 0){
                    //重设缓冲区,使position回到初始位置,读取数据之前调用
                    buff.flip();
                    content.append(Charset.forName("UTF-8").decode(buff));
                }
                System.out.println("Server is listening from client " + sc.getRemoteAddress() + " data rev is: " + content);
                //将此对应的channel设置为准备下一次接受数据
                sk.interestOps(SelectionKey.OP_READ);
            }catch (IOException io){
                sk.cancel();
                if(sk.channel() != null){
                    sk.channel().close();
                }
            }
            if(content.length() > 0){
                String[] arrayContent = content.toString().split(USER_CONTENT_SPILIT);
                //注册用户
                if(arrayContent != null && arrayContent.length ==1) {
                    String name = arrayContent[0];
                    if(users.contains(name)) {
                        sc.write(Charset.forName("UTF-8").encode(USER_EXIST));
                    } else {
                        users.add(name);
                        int num = OnlineNum(selector);
                        String message = "欢迎 "+name+" 来到聊天室! 当前在线人数:"+num;
                        BroadCast(selector, null, message);
                    }
                } //注册完了,发送消息
                else if(arrayContent != null && arrayContent.length >1){
                    String name = arrayContent[0];
                    String message = content.substring(name.length()+USER_CONTENT_SPILIT.length());
                    message = name + " 说:" + message;
                    if(users.contains(name)) {
                        //不回发给发送此内容的客户端
                        BroadCast(selector, sc, message);
                    }
                }
            }

        }
    }

    /**
     * 统计在线人数
     * @param selector
     * @return
     */
    public static int OnlineNum(Selector selector) {
        int res = 0;
        for(SelectionKey key : selector.keys()){
            Channel targetchannel = key.channel();
            if(targetchannel instanceof SocketChannel){
                res++;
            }
        }
        return res;
    }

    /**
     * 广播消息
     * @param selector
     * @param except
     * @param content
     * @throws IOException
     */
    public void BroadCast(Selector selector, SocketChannel except, String content) throws IOException {
        //广播数据到所有的SocketChannel中
        for(SelectionKey key : selector.keys()){
            Channel targetchannel = key.channel();
            //如果except不为空,不回发给发送此内容的客户端
            if(targetchannel instanceof SocketChannel && targetchannel!=except){
                SocketChannel dest = (SocketChannel)targetchannel;
                dest.write(Charset.forName("UTF-8").encode(content));
            }
        }
    }
    public static void main(String[] args) throws IOException{
        new ChatRoomServer().init();
    }
}

三、客户端代码


public class ChatRoomClient {

    private Selector selector = null;
    static final int port = 8888;
    private SocketChannel sc = null;
    private String name = "";
    private static String USER_EXIST = "系统消息: 当前用户名已存在,请更换用户名!";
    private static String USER_CONTENT_SPILIT = "#@#";

    /**
     * 初始化客户端
     * @throws IOException
     */
    public void init() throws IOException{
        selector = Selector.open();
        //连接远程主机的IP和端口
        sc = SocketChannel.open(new InetSocketAddress("127.0.0.1",port));
        sc.configureBlocking(false);
        sc.register(selector, SelectionKey.OP_READ);
        //开辟一个新线程来读取从服务器端的数据
        new Thread(new ClientThread()).start();
        //在主线程中 从键盘读取数据输入到服务器端
        Scanner scan = new Scanner(System.in);
        while(scan.hasNextLine())
        {
            String line = scan.nextLine();
            if("".equals(line)) continue; //不允许发空消息
            if("".equals(name)) {
                name = line;
                line = name+USER_CONTENT_SPILIT;
            } else {
                line = name+USER_CONTENT_SPILIT+line;
            }
            sc.write(Charset.forName("UTF-8").encode(line));//sc既能写也能读,这边是写
        }

    }
    private class ClientThread implements Runnable{
        public void run(){
            try{
                while(true) {
                    int readyChannels = selector.select();
                    //防止epoll空循环
                    if(readyChannels == 0) continue;
                    Set<SelectionKey> selectedKeys = selector.selectedKeys();  //可以通过这个方法,知道可用通道的集合
                    Iterator keyIterator = selectedKeys.iterator();
                    while(keyIterator.hasNext()) {
                        SelectionKey sk = (SelectionKey) keyIterator.next();
                        keyIterator.remove();
                        //处理事件
                        dealWithSelectionKey(sk);
                    }
                }
            }catch (IOException io){}
        }

        /**
         * 处理事件
         * @param sk
         * @throws IOException
         */
        private void dealWithSelectionKey(SelectionKey sk) throws IOException {
            //可读事件
            if(sk.isReadable()){
                //使用 NIO 读取 Channel中的数据,这个和全局变量sc是一样的,因为只注册了一个SocketChannel
                //sc既能写也能读,这边是读
                SocketChannel sc = (SocketChannel)sk.channel();
                ByteBuffer buff = ByteBuffer.allocate(1024);
                String content = "";
                while(sc.read(buff) > 0){
                    buff.flip();
                    content += Charset.forName("UTF-8").decode(buff);
                }
                //若系统发送通知名字已经存在,则需要换个暱称
                if(USER_EXIST.equals(content)) {
                    name = "";
                }
                System.out.println(content);
                sk.interestOps(SelectionKey.OP_READ);
            }
        }
    }

    public static void main(String[] args) throws IOException{
        new ChatRoomClient().init();
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章