天天看點

用Java Swing+NIO實作了一個C/S聊天室程式 支援群聊私聊功能

特别說明

我實作的這個C/S模式的聊天室項目,主要是為了友善大家學習Java NIO的。由于NIO的優勢在于單機能處理的并發連接配接數高,是以特别适合用于聊天程式的服務端。

為什麼使用Java Swing來做圖形界面呢?我們大家都知道,Java Swing現在已經過時了,并且Java的優勢不在于圖形界面,但我的需求并不需要漂亮美觀的界面,并且Java語言實作C端界面的首選就是Java Swing了,隻不過我隻用了相對簡單的圖形元件及元件互動,但這也足夠了。

閱讀本文需要一點NIO的前置知識,大家可以看一下與本文同專欄的的這篇文章: ​​Java NIO三大元件Buffer、Channel、Selector保姆級教程 附聊天室核心代碼​​

本文隻粘貼了項目的核心代碼,如希望了解項目全貌,請點選上述連結下載下傳項目,零積分便可下載下傳。

如遇到項目啟動問題或bug,可在評論區留言。

效果示範

話不多說,先來示範一下我做的聊天程式吧!雖然界面比較簡潔,并且隻實作了核心功能,但相較而言這些都不是重點!重點在于服務端與用戶端之間的NIO通信流程,稍後我會為大家分析講解代碼。

CS模式聊天程式示範

項目結構

├─nio-chat-client    用戶端項目
│  │  pom.xml
│  └─src
│      └─main
│          ├─java
│          │  └─com
│          │      └─bobo
│          │          │  ChatClientBootstrap.java    用戶端啟動類
│          │          ├─entity
│          │          │      FriendMsg.java    存放好友資訊
│          │          ├─handler    放各種處理器
│          │          │      EventHandler.java    圖形元件事件處理器
│          │          │      IOHandler.java    用戶端IO處理器
│          │          │      UIHandler.java    圖形元件處理器
│          │          │      
│          │          └─ui
│          │                  MainUI.java 繼承自JFrame,程式主界面
│          └─resources
│                  trumpet.jpeg    小喇叭圖示
│                  
├─nio-chat-common    放服務端與用戶端公共代碼
│  │  pom.xml
│  └─src
│      └─main
│          └─java
│              └─com
│                  └─bobo
│                      ├─constant
│                      │      CommonConstant.java    公共常量
│                      ├─entity
│                      │      ChatMsg.java    服務端與用戶端通信的實體類
│                      └─io    下面兩個類分别用于讀資料與寫資料,用分隔符解決TCP粘拆包問題
│                              ChatBufferReader.java
│                              ChatMsgWrapper.java
│                              
└─nio-chat-server    服務端項目
    │  pom.xml
    └─src
        └─main
            └─java
                └─com
                    └─bobo
                        │  ChatServerBootstrap.java    服務端啟動類
                        └─handler
                                ServiceHandler.java    服務端處理器,NIO核心代碼都在這個類裡      

主界面結構

用Java Swing+NIO實作了一個C/S聊天室程式 支援群聊私聊功能

說明:

  • localhost:6666為預設的服務端位址,可以修改成你自己的

用戶端核心代碼分析

入口ChatClientBootstrap

即main方法所在處。

public class ChatClientBootstrap {
    public static void main(String[] args) {
        // new一個用戶端主界面執行個體
        MainUI mainUI = new MainUI();
        // 注冊事件
        EventHandler.doRegister(mainUI);
    }
}      

NIO通信處理IOHandler

用于處理用戶端發送消息至服務端,以及接收從服務端發來的消息等。用戶端這邊也是用了NIO模式,将用戶端對應的SocketChannel注冊到Selector上,并在單獨的一個線程裡無線循環處理用戶端的讀事件,并通過UIHandler類的各種方法将處理結果渲染到UI界面,實作界面與資料的關聯。

public class IOHandler {
    private static IOHandler ioHandler = new IOHandler();
    private IOHandler(){}
    public static IOHandler getHandler(){
        return ioHandler;
    }
    // 存儲好友的資料
    private Map<String,FriendMsg> friendsData = new LinkedHashMap<>();
    // 目前用戶端的ID
    private String selfId;
    // 目前用戶端的SocketChannel
    private SocketChannel socketChannel;
    // 讀緩沖,以\n為分隔符讀取消息,用于解決TCP粘包拆包問題
    private ChatBufferReader chatBufferReader = new ChatBufferReader();
    // 給消息的最後添加一個分隔符\n
    private ChatMsgWrapper chatMsgWrapper = new ChatMsgWrapper();

    public void doConnect(MainUI mainUI) throws IOException {
        Selector selector = Selector.open();
        socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        // 擷取服務端ip:port
        try {
            String[] arr = mainUI.getServer().getText().split(":");
            if(arr.length != 2){
                throw new RuntimeException("服務端位址格式錯誤");
            }
            socketChannel.connect(new InetSocketAddress(arr[0], Integer.valueOf(arr[1])));
        } catch (Exception e) {
            e.printStackTrace();
            UIHandler.alert(mainUI,e.getMessage(),"提示",JOptionPane.ERROR_MESSAGE);
            return;
        }
        socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ, ByteBuffer.allocate(CommonConstant.BUFFER_SIZE));
        for (;;){
            int num = selector.select();
            if(num <= 0){
                continue;
            }
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                if(key.isConnectable()){
                    while(!socketChannel.finishConnect()){
                        UIHandler.setText(mainUI.getBottom(),"用戶端連接配接中...");
                    }
                    selfId = socketChannel.getLocalAddress().toString().substring(1);
                    // 連接配接成功後,渲染界面
                    UIHandler.setText(mainUI.getBottom(),"已成功連接配接伺服器");
                    UIHandler.alert(mainUI,"成功連接配接伺服器","提示",JOptionPane.INFORMATION_MESSAGE);
                    UIHandler.append(mainUI.getGroupArea(),"成功連接配接伺服器,您現在可以發送群聊消息");
                    UIHandler.setText(mainUI.getId(),selfId);
                    UIHandler.setVisible(mainUI.getConnect(),false);
                    UIHandler.setEditable(mainUI.getServer(),false);
                }else if (key.isReadable()){
                    // 緩沖讀,以\n為分隔符讀取消息,可解決TCP粘包拆包問題
                    String msg = chatBufferReader.readMsg(key);
                    ChatMsg chatMsg = JSONObject.parseObject(msg, ChatMsg.class);
                    if(CommonConstant.MSG_TYPE_SYNC_TO_NEW_CLIENT == chatMsg.getType()){
                        // 新用戶端(自己)上線,擷取服務端發過來的所有好友資訊
                        Map<String,String> map = JSONObject.parseObject(chatMsg.getMsg(), Map.class);
                        map.forEach((subject,name)->{
                            // 初始化FriendMsg
                            friendsData.put(subject,new FriendMsg(subject,name,0,new ArrayList()));
                        });
                        // 重新加載好友清單
                        UIHandler.reloadFriends(mainUI);
                    }else if(CommonConstant.MSG_TYPE_NOTICE_HAS_NEW_CLIENT == chatMsg.getType()){
                        // 有新的好友上線
                        UIHandler.setText(mainUI.getBottom(),String.format("有新的好友[%s]已上線",chatMsg.getMsg()));
                        // 初始化FriendMsg
                        friendsData.put(chatMsg.getMsg(),new FriendMsg(chatMsg.getMsg(),"",0,new ArrayList()));
                        // 重新加載好友清單
                        UIHandler.reloadFriends(mainUI);
                    }else if(CommonConstant.MSG_TYPE_NOTICE_OTHER_CLIENT_MODIFY_NAME == chatMsg.getType()){
                        // 好友修改了昵稱
                        UIHandler.setText(mainUI.getBottom(),
                                String.format("好友[%s]修改了昵稱[%s]",chatMsg.getSubject(),chatMsg.getMsg()));
                        // 修改FriendMsg
                        friendsData.get(chatMsg.getSubject()).setName(chatMsg.getMsg());
                        // 重新加載好友清單
                        UIHandler.reloadFriends(mainUI);
                        // 如果正在與該好友聊天,則更新私聊标題
                        UIHandler.updatePrivateTitle(mainUI,chatMsg.getSubject(),false);
                    }else if(CommonConstant.MSG_TYPE_RECV_GROUP == chatMsg.getType()){
                        // 接收群聊消息
                        UIHandler.append(mainUI.getGroupArea(),UIHandler.buildSessionMsg(chatMsg.getSubject(),chatMsg.getMsg()));
                    }else if(CommonConstant.MSG_TYPE_RECV_PRIVATE == chatMsg.getType()){
                        // 接收私聊消息
                        FriendMsg friendMsg = friendsData.get(chatMsg.getSubject());
                        // 将消息記錄到friendMsg的msgList中
                        friendMsg.getMsgList().add(UIHandler.buildSessionMsg(chatMsg.getSubject(),chatMsg.getMsg()));
                        if(mainUI.getPrivateTitle().getText().contains(chatMsg.getSubject())){
                            // 目前正在與該好友聊天,則還要将消息追加到私聊視窗
                            UIHandler.append(mainUI.getPrivateArea(),UIHandler.buildSessionMsg(chatMsg.getSubject(),chatMsg.getMsg()));
                        }else{
                            // 目前沒有在于該好友聊天,則未讀消息個數加1
                            friendMsg.setUnreadCount(friendMsg.getUnreadCount()+1);
                            // 底部通知欄添加通知
                            UIHandler.setText(mainUI.getBottom(),UIHandler.buildSessionMsg(chatMsg.getSubject(),"發來了一條新消息"));
                            // 重新加載好友清單
                            UIHandler.reloadFriends(mainUI);
                        }
                    }else if(CommonConstant.MSG_TYPE_NOTICE_OTHER_CLIENT_OFFLINE == chatMsg.getType()){
                        // 好友下線,底部通知欄添加通知
                        UIHandler.setText(mainUI.getBottom(),UIHandler.buildSessionMsg(chatMsg.getSubject(),"下線"));
                        // 如果正在與該好友聊天,則更新私聊标題
                        UIHandler.updatePrivateTitle(mainUI,chatMsg.getSubject(),true);
                        // 移除
                        friendsData.remove(chatMsg.getSubject());
                        // 重新加載好友清單
                        UIHandler.reloadFriends(mainUI);
                    }
                }
                // 最後移除此次發生處理的selectionKey,防止事件重複處理
                iterator.remove();
            }
        }
    }
    /**
     * 寫資料,chatMsgWrapper實作了自定義消息格式(\n)
     */
    public void writeMsg(int type,String subject,String text){
        try {
            socketChannel.write(chatMsgWrapper.wrap(new ChatMsg(type,subject,text)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public String getSelfId() {
        return selfId;
    }
    public Map<String, FriendMsg> getFriendsData() {
        return friendsData;
    }
}      

服務端核心代碼分析

入口ChatServerBootstrap

即main方法所在處,可随意修改綁定的位址與端口号,但也要對應修改用戶端連接配接時指定的服務端位址。

public class ChatServerBootstrap {
    public static void main(String[] args) throws IOException {
        new ServiceHandler().handle("localhost",6666);
    }
}      

NIO通信處理ServiceHandler

public class ServiceHandler {
    /**
     * 存儲用戶端昵稱
     */
    private Map<String,String> clientNameMap = new HashMap();
    // 讀緩沖,以\n為分隔符讀取消息,用于解決TCP粘包拆包問題
    private ChatBufferReader chatBufferReader = new ChatBufferReader();
    // 給消息的最後添加一個分隔符\n
    private ChatMsgWrapper chatMsgWrapper = new ChatMsgWrapper();

    public void handle(String ip,int port) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(ip,port));
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("聊天伺服器已就緒...");
        for(;;){
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                if(selectionKey.isAcceptable()){
                    SocketChannel newSocketChannel = serverSocketChannel.accept();
                    newSocketChannel.configureBlocking(false);
                    // 擷取新用戶端id
                    String id = newSocketChannel.getRemoteAddress().toString().substring(1);
                    System.out.println(String.format("用戶端上線[%s]",id));
                    // 先通知其他所有人,有新的用戶端上線
                    for (SelectionKey otherKey : selector.keys()) {
                        SelectableChannel selectableChannel = otherKey.channel();
                        if(selectableChannel instanceof SocketChannel && selectableChannel != newSocketChannel){
                            ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
                            otherAtt.clear();
                            otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_NOTICE_HAS_NEW_CLIENT,null,id)));
                            // 觸發write事件
                            otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
                        }
                    }
                    // 向新用戶端同步線上好友
                    ByteBuffer att = ByteBuffer.allocate(CommonConstant.BUFFER_SIZE);
                    att.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_SYNC_TO_NEW_CLIENT,null,JSONObject.toJSONString(clientNameMap))));
                    newSocketChannel.register(selector,SelectionKey.OP_READ | SelectionKey.OP_WRITE,att);
                    // 初始化新用戶端的昵稱
                    clientNameMap.put(id,"");
                }else if(selectionKey.isReadable()){
                    SocketChannel channel = (SocketChannel)selectionKey.channel();
                    String msg = null;
                    try {
                        msg = chatBufferReader.readMsg(selectionKey);
                    } catch (IOException e) {
                        // 通知所有用戶端下線
                        clientNameMap.remove(e.getMessage());
                        for (SelectionKey otherKey : selector.keys()) {
                            SelectableChannel selectableChannel = otherKey.channel();
                            if(selectableChannel instanceof SocketChannel && selectableChannel != channel){
                                ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
                                otherAtt.clear();
                                otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_NOTICE_OTHER_CLIENT_OFFLINE,e.getMessage(),e.getMessage())));
                                // 觸發write事件
                                otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
                            }
                        }
                        iterator.remove();
                        continue;
                    }
                    ChatMsg chatMsg = JSONObject.parseObject(msg, ChatMsg.class);
                    String id = channel.getRemoteAddress().toString().substring(1);
                    if(CommonConstant.MSG_TYPE_MODIFY_NAME == chatMsg.getType()){
                        // 通知其他人有好友修改了昵稱
                        clientNameMap.put(id,chatMsg.getMsg());
                        for (SelectionKey otherKey : selector.keys()) {
                            SelectableChannel selectableChannel = otherKey.channel();
                            if(selectableChannel instanceof SocketChannel && selectableChannel != channel){
                                ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
                                otherAtt.clear();
                                otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_NOTICE_OTHER_CLIENT_MODIFY_NAME,id,chatMsg.getMsg())));
                                // 觸發write事件
                                otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
                            }
                        }
                    }else if(CommonConstant.MSG_TYPE_SEND_GROUP == chatMsg.getType()){
                        // 轉發群聊消息
                        for (SelectionKey otherKey : selector.keys()) {
                            SelectableChannel selectableChannel = otherKey.channel();
                            if(selectableChannel instanceof SocketChannel && selectableChannel != channel){
                                ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
                                otherAtt.clear();
                                otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_RECV_GROUP,id,chatMsg.getMsg())));
                                // 觸發write事件
                                otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
                            }
                        }
                    }else if(CommonConstant.MSG_TYPE_SEND_PRIVATE == chatMsg.getType()){
                        // 轉發私聊消息
                        for (SelectionKey otherKey : selector.keys()) {
                            SelectableChannel selectableChannel = otherKey.channel();
                            if(selectableChannel instanceof SocketChannel && selectableChannel != channel
                                && ((SocketChannel) selectableChannel).getRemoteAddress().toString().substring(1).equals(chatMsg.getSubject())){
                                ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
                                otherAtt.clear();
                                otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_RECV_PRIVATE,id,chatMsg.getMsg())));
                                // 觸發write事件
                                otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
                                break;
                            }
                        }
                    }
                }else if(selectionKey.isWritable()){
                    // 寫事件
                    SocketChannel channel = (SocketChannel)selectionKey.channel();
                    ByteBuffer att = (ByteBuffer) selectionKey.attachment();
                    att.flip();
                    channel.write(att);
                    selectionKey.interestOps(SelectionKey.OP_READ);
                }
                // 最後移除此次發生處理的selectionKey,防止事件重複處理
                iterator.remove();
            }
        }
    }
}      

專題

如何解決TCP粘拆包問題?

解決此類問題,有許多的方法可以使用,如消息定長(Long類型消息固定為8個位元組、Int類型消息固定為4個位元組、随機大小如200位元組不夠則空格)、分隔符(\n、\r\n、其它自定義分隔符)等。

本項目基于分隔符\n的方案,實作消息的讀取與寫入。

消息的寫入在ChatMsgWrapper.java類中,如下代碼所示。

public class ChatMsgWrapper {
    public ByteBuffer wrap(ChatMsg chatMsg){
        String jsonString = JSONObject.toJSONString(chatMsg);
        return ByteBuffer.wrap((jsonString+"\n").getBytes(StandardCharsets.UTF_8));
    }
}      

由此可見,通過wrap方法,會在每個原生消息chatMsg的後面追加上一個換行符。

那怎麼讀取呢?别着急,消息的讀取在ChatBufferReader.java類中,如下代碼所示。

public class ChatBufferReader {
    /**
     * 緩沖區
     */
    private Map<SocketChannel,String> msgBuffer = new HashMap();

    public String readMsg(SelectionKey selectionKey) throws IOException {
        SocketChannel channel = (SocketChannel)selectionKey.channel();
        ByteBuffer buffer = (ByteBuffer)selectionKey.attachment();
        StringBuilder sb = new StringBuilder();
        try {
            String msg = "";
            // 循環讀,讀到有\n截止
            while (!msg.contains("\n")){
                buffer.clear();
                channel.read(buffer);
                msg = new String(buffer.array(), 0, buffer.position(), StandardCharsets.UTF_8);
                sb.append(msg);
            }
        } catch (IOException e) {
            String host = channel.getRemoteAddress().toString().substring(1);
            System.out.println(String.format("遠端機器下線[%s]",host));
            selectionKey.cancel();
            channel.close();
            throw new IOException(host);
        }
        // 此次讀到的消息
        String message = sb.toString();
        // 将緩沖區中上次讀到的消息放到前面來
        if (msgBuffer.containsKey(channel) && null != msgBuffer.get(channel)){
            message = msgBuffer.get(channel)+message;
        }
        // 這裡取第一個\n前面的資料作為本次讀取的消息,後面的按原樣放到緩沖區
        // 放到緩沖區
        msgBuffer.put(channel,message.length()<=message.indexOf("\n")+2?"":message.substring(message.indexOf("\n")+2));
        // 将本次讀取的消息傳回出去
        return message.substring(0,message.indexOf("\n"));
    }
}      

通過while (!msg.contains("\n"))循環,我們可能會讀好幾次,直到讀到的資料有換行符為止,為什麼呢?因為換行符代表一個消息的結束,如果沒有換行符就代表還沒有讀到消息的末尾,那就肯定還要繼續讀啊。

讀完了之後,怎麼處理呢?由于TCP粘包機制,是以讀到的資料可能會長這樣:abcdefg\nhijk,\n代表了一個消息的結束,那麼abcdefg就是一條完整的消息,但hijk是屬于下一條消息的,隻不過被粘在一起了,我們需要做的是将hijk放到緩沖區緩存起來,然後将abcdefg傳回出去(代表本次讀到的消息)。

緩存起來怎麼辦呢?難道就不管了嗎?當然不是!當我們嘗試讀下一條消息時,讀到的資料可能是這樣的:lmn\nopq,通過上面的論述,我們已經知道了opq是屬于下一條消息的,要放緩沖區裡,但lmn就是一條完整的消息嗎?當然不是,lmn是消息的下半部分,那上半部分在哪?在緩沖區呀!

這就是緩沖區的作用,緩沖區可以将消息的上半部分先緩存起來,等到消息的下半部分也讀到了,再組合在一塊兒,就是一條完整的消息了。

服務端和用戶端通信的消息類型有很多種,如何區分?

這個好辦,用一個實體類就能搞定。

ChatMsg.java類定義如下。

public class ChatMsg {
    private int type;
    private String subject;
    private String msg;
    public ChatMsg() {
    }
    public ChatMsg(int type, String subject, String msg) {
        this.type = type;
        this.subject = subject;
        this.msg = msg;
    }
    // 省略getter/setter方法
}      

ChatMsg就是封裝的服務端與用戶端之間通信的消息對象,并且通過type屬性區分消息類型,消息類型定義在CommonConstant.java類中。

public interface CommonConstant {
    /**
     * 0 向新用戶端同步線上好友
     * 1 通知有新用戶端上線
     * 2 修改昵稱
     * 3 通知其它人修改昵稱
     * 4 發送群聊消息
     * 5 轉發群聊消息
     * 6 發送私聊消息
     * 7 轉發私聊消息
     * 8 通知有用戶端下線
     */
    int MSG_TYPE_SYNC_TO_NEW_CLIENT = 0; // server->client
    int MSG_TYPE_NOTICE_HAS_NEW_CLIENT = 1; // server->client
    int MSG_TYPE_MODIFY_NAME = 2; // client->server
    int MSG_TYPE_NOTICE_OTHER_CLIENT_MODIFY_NAME = 3; // server->client
    int MSG_TYPE_SEND_GROUP = 4; // client->server
    int MSG_TYPE_RECV_GROUP = 5; // server->client
    int MSG_TYPE_SEND_PRIVATE = 6; // client->server
    int MSG_TYPE_RECV_PRIVATE = 7; // server->client
    int MSG_TYPE_NOTICE_OTHER_CLIENT_OFFLINE = 8; // server->client
}      

到時候,無論是用戶端讀到了服務端發來的消息,還是服務端讀到了用戶端發來的消息,都可以先将消息反序列化為ChatMsg對象,然後拿到type屬性做if判斷,确定本次要處理的是何種業務。

寫事件有什麼用?我直接寫不行嘛,為啥還需要等到寫事件發生才能寫呢?

在ServiceHandler.java類中,會處理用戶端發來的消息,比如用戶端A發來群聊的消息,那麼服務端需要将該消息寫給除用戶端A之外的所有用戶端,即消息的轉發。

那麼在這裡,我們服務端不是直接寫資料的,而是将資料先放到緩沖區裡,然後觸發一個寫事件,在寫事件發生時才将資料寫資料。

群聊業務處理代碼如下所示。

// 轉發群聊消息
for (SelectionKey otherKey : selector.keys()) {
    SelectableChannel selectableChannel = otherKey.channel();
    if(selectableChannel instanceof SocketChannel && selectableChannel != channel){
        ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
        otherAtt.clear();
        otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_RECV_GROUP,id,chatMsg.getMsg())));
        // 觸發write事件
        otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
    }
}      

處理寫事件代碼如下所示。

if(selectionKey.isWritable()){
    // 寫事件
    SocketChannel channel = (SocketChannel)selectionKey.channel();
    ByteBuffer att = (ByteBuffer) selectionKey.attachment();
    att.flip();
    channel.write(att);
    selectionKey.interestOps(SelectionKey.OP_READ);
}      

那麼不直接寫,而是等寫事件發生才寫,這麼做的好處是什麼呢?

寫事件的作用:沒有寫事件也可以,直接将使用者buffer資料拷貝到socket發送緩沖區,但是高并發情況下(使用者buffer頻繁往socket buffer拷貝資料)以及網絡環境很差的情況下(socket 發送緩沖區将資料發出去的速度很慢),socket發送緩沖區很快就滿了,這樣最終會導緻CPU使用率100%。

是以可以用寫事件優化,當socket發送緩沖區沒有滿時,即有空閑會觸發可寫事件,此時才去寫資料,而當滿了的時候,就不會觸發可寫事件,這樣能讓CPU歇一歇。

另外,在寫事件處理結束之後,一定要調用一下selectionKey.interestOps(SelectionKey.OP_READ)取消寫事件,如果不調用則selectionKey.isWritable()一直會傳回true,而實際并沒有可寫的資料,這樣會使CPU空轉。

私聊記錄如何儲存?

IOHandler.java中有一個屬性,如下所示。

private Map<String,FriendMsg> friendsData = new LinkedHashMap<>();      
/**
 * 存儲與好友聊天資訊
 */
public class FriendMsg {
    /**
     * 好友的id(ip:port)
     */
    private String subject;
    /**
     * 好友的昵稱
     */
    private String name;
    /**
     * 未讀消息個數
     */
    private int unreadCount;
    /**
     * 存儲我與該好友的聊天記錄
     */
    private List<String> msgList;

    public FriendMsg(){}

    public FriendMsg(String subject, String name, int unreadCount, List<String> msgList) {
        this.subject = subject;
        this.name = name;
        this.unreadCount = unreadCount;
        this.msgList = msgList;
    }
    // 省略getter/setter方法
}      

繼續閱讀