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();
    }
}

 

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