【BIO】通過指定消息大小實現(xiàn)的多人聊天室-終極版本

# 前情提要:
  • 在上一篇文章BIO在聊天室項目中的演化中提到页畦,告知對方消息已經(jīng)發(fā)送完畢的方式有4種
    1. 關(guān)閉Socket連接
    2. 關(guān)閉輸出流内地,socket.shutdownOutput();
    3. 使用標志符號接剩,借助字符流,Reader.readLine()舟铜,該方法會在讀取到\r,\n或者\r\n時返回所讀取到的內(nèi)容菊霜。
    4. 通過指定本次發(fā)送的數(shù)據(jù)的字節(jié)大小。告知對方從輸入流中讀取指定大小的字節(jié)脊另。

本文使用第四種方案來實現(xiàn)聊天室

  • 思路為:
    • 客戶端在發(fā)送消息之前导狡,先計算出本次發(fā)送的數(shù)據(jù)量的字節(jié)大小,比如為N個字節(jié)偎痛。那么在向服務(wù)器發(fā)送數(shù)據(jù)的前旱捧,先約定好流中的前1個字節(jié)(或者前X個字節(jié),根據(jù)自己項目的實際情況來決定)為本次發(fā)送的數(shù)據(jù)量的大小踩麦。
    • 客戶端發(fā)送消息廊佩,先將計算出的字節(jié)大小N寫入輸出流,再將實際的內(nèi)容寫入輸出流靖榕。
    • 服務(wù)端在獲取到輸入流之后标锄,根據(jù)約定,先讀取前X個字節(jié)茁计,根據(jù)這個字節(jié)的值可以知道料皇,本次發(fā)送的數(shù)據(jù)量的大小,那么在讀取數(shù)據(jù)時星压,只需要讀取后續(xù)的N個字節(jié)即可践剂。
  • 溫馨提示: 注意看代碼注釋喲~

# 代碼實現(xiàn)

  • 客戶端
/**
 * @author futao
 * @date 2020/7/6
 */
public class BioChatClient {

    private static final Logger logger = LoggerFactory.getLogger(BioChatClient.class);

    private static final ExecutorService SINGLE_THREAD_EXECUTOR = Executors.newSingleThreadExecutor();

    /**
     * 啟動客戶端
     */
    public void start() {
        try {  //嘗試連接到聊天服務(wù)器
            Socket socket = new Socket("localhost", Constants.SERVER_PORT);
            logger.debug("========== 成功連接到聊天服務(wù)器 ==========");

            InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();

            //從輸入流中讀取數(shù)據(jù)
            SINGLE_THREAD_EXECUTOR.execute(() -> {
                try {
                    while (true) {
                        String message = IOUtils.messageReceiver(inputStream);
                        logger.info("接收到服務(wù)端消息:[{}]", message);
                    }
                } catch (IOException e) {
                    logger.error("發(fā)生異常", e);
                }
            });

            while (true) {
                //獲取用戶輸入的數(shù)據(jù)
                String message = new Scanner(System.in).nextLine();
                if (StringUtils.isBlank(message)) {
                    break;
                }
                //將內(nèi)容轉(zhuǎn)換為字節(jié)數(shù)組
                byte[] contentBytes = message.getBytes(Constants.CHARSET);
                //內(nèi)容字節(jié)數(shù)組的大小
                int length = contentBytes.length;
                //第一個字節(jié)寫入本次傳輸?shù)臄?shù)據(jù)量的大小
                outputStream.write(length);
                //寫入真正需要傳輸?shù)膬?nèi)容
                outputStream.write(contentBytes);
                //刷新緩沖區(qū)
                outputStream.flush();

                if (Constants.KEY_WORD_QUIT.equals(message)) {
                    //客戶端退出
                    SINGLE_THREAD_EXECUTOR.shutdownNow();
                    inputStream.close();
                    outputStream.close();
                    socket.close();
                    break;
                }
            }
        } catch (IOException e) {
            logger.error("發(fā)生異常", e);
        }
    }

    public static void main(String[] args) {
        new BioChatClient().start();
    }
}
  • 從輸入流中讀取指定大小的數(shù)據(jù)
    /**
     * 從輸入流中讀取指定大小的字節(jié)數(shù)據(jù)并轉(zhuǎn)換成字符串
     *
     * @param inputStream 輸入流
     * @return 讀取到的字符串
     * @throws IOException
     */
    public static String messageReceiver(InputStream inputStream) throws IOException {
        //本次傳輸?shù)臄?shù)據(jù)量的大小
        int curMessageLength = inputStream.read();
        byte[] contentBytes = new byte[curMessageLength];
        //讀取指定長度的字節(jié)
        inputStream.read(contentBytes);
        return new String(contentBytes);
    }

  • 服務(wù)端
/**
 * @author futao
 * @date 2020/7/6
 */
public class BioChatServer {

    private static final Logger logger = LoggerFactory.getLogger(BioChatServer.class);

    /**
     * 可同時接入的客戶端數(shù)量
     */
    private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);


    /**
     * 當前接入的客戶端
     */
    private static final Set<Socket> CLIENT_SOCKET_SET = new HashSet<Socket>() {
        @Override
        public synchronized boolean add(Socket o) {
            return super.add(o);
        }

        @Override
        public synchronized boolean remove(Object o) {
            return super.remove(o);
        }
    };

    /**
     * 啟動服務(wù)端
     */
    public void start() {
        try {
            //啟動服務(wù)器,監(jiān)聽端口
            ServerSocket serverSocket = new ServerSocket(Constants.SERVER_PORT);
            logger.debug("========== 基于BIO的聊天室在[{}]端口啟動成功 ==========", Constants.SERVER_PORT);
            while (true) {
                //監(jiān)聽客戶端接入事件
                Socket socket = serverSocket.accept();
                THREAD_POOL.execute(() -> {
                    CLIENT_SOCKET_SET.add(socket);
                    int port = socket.getPort();
                    logger.debug("客戶端[{}]成功接入聊天服務(wù)器", port);
                    try {
                        InputStream inputStream = socket.getInputStream();
                        OutputStream outputStream = socket.getOutputStream();

                        while (true) {
                            //獲取到客戶端發(fā)送的消息
                            String message = IOUtils.messageReceiver(inputStream);
                            logger.info("接收到客戶端[{}]發(fā)送的消息:[{}]", port, message);
                            //客戶端是否退出
                            boolean isQuit = IOUtils.isQuit(message, socket, CLIENT_SOCKET_SET);
                            if (isQuit) {
                                socket.close();
                                break;
                            } else {
                                //消息轉(zhuǎn)發(fā)
                                IOUtils.forwardMessage(port, message, CLIENT_SOCKET_SET);
                            }
                        }
                    } catch (IOException e) {
                        logger.error("發(fā)生異常", e);
                    }
                });
            }
        } catch (IOException e) {
            logger.error("發(fā)生異常", e);
        }
    }


    public static void main(String[] args) {
        new BioChatServer().start();
    }
}
  • 客戶端下線與消息轉(zhuǎn)發(fā)
/**
     * 判斷客戶端是否下線娜膘,并且將需要下線的客戶端下線
     *
     * @param message         消息
     * @param socket          客戶端Socket
     * @param clientSocketSet 當前接入的客戶端Socket集合
     * @return 是否退出
     * @throws IOException
     */
    public static boolean isQuit(String message, Socket socket, Set<Socket> clientSocketSet) throws IOException {
        boolean isQuit = StringUtils.isBlank(message) || Constants.KEY_WORD_QUIT.equals(message);
        if (isQuit) {
            clientSocketSet.remove(socket);
            int port = socket.getPort();
            socket.close();
            logger.debug("客戶端[{}]下線", port);
        }
        return isQuit;
    }

    /**
     * 轉(zhuǎn)發(fā)消息
     *
     * @param curSocketPort   當前發(fā)送消息的客戶端Socket的端口
     * @param message         需要轉(zhuǎn)發(fā)的消息
     * @param clientSocketSet 當前接入的客戶端Socket集合
     */
    public static void forwardMessage(int curSocketPort, String message, Set<Socket> clientSocketSet) {
        if (StringUtils.isBlank(message)) {
            return;
        }
        for (Socket socket : clientSocketSet) {
            if (socket.isClosed() || socket.getPort() == curSocketPort) {
                continue;
            }
            if (socket.getPort() != curSocketPort) {
                try {
                    OutputStream outputStream = socket.getOutputStream();
                    byte[] messageBytes = message.getBytes(Constants.CHARSET);
                    outputStream.write(messageBytes.length);
                    //將字符串編碼之后寫入客戶端
                    outputStream.write(messageBytes);
                    //刷新緩沖區(qū)
                    outputStream.flush();
                } catch (IOException e) {
                    logger.error("消息轉(zhuǎn)發(fā)失敗", e);
                }
            }
        }
    }

# 測試一下~

  • 服務(wù)端啟動逊脯,客戶端接入
image.png
  • 客戶端接入
image.png
  • 客戶端發(fā)送消息
image.png
  • 服務(wù)端打印并轉(zhuǎn)發(fā)消息
image.png
  • 聊天室內(nèi)的其他小伙伴收到服務(wù)器轉(zhuǎn)發(fā)的消息
image.png
  • 小馬哥客戶端下線
image.png
  • 服務(wù)器收到小馬哥的下線通知
image.png

# 總結(jié)

  • 非常優(yōu)雅~??

# 注意

  • 本文約定的是第一個字節(jié)為消息大小的標記,一個字節(jié)可以表示的最大值為255竣贪,所以一次最多傳輸255個字節(jié)军洼,如果超過這個值巩螃,會造成業(yè)務(wù)錯誤,需要注意匕争。
  • 所以使用幾個字節(jié)來作為標識需要從業(yè)務(wù)的角度來考慮
    • 一個字節(jié)8位避乏,可表示的最大值為 255 = 255B
    • 二個字節(jié)16位,可表示的最大值為 65535 = 64KB
    • 三個字節(jié)24位甘桑,可表示的最大值為 16777215 = 16MB
    • 四個字節(jié)32位拍皮,可表示的最大值為 4294967295 = 4GB
    • 以此類推....

# 系列文章

歡迎在評論區(qū)留下你看文章時的思考,及時說出跑杭,有助于加深記憶和理解铆帽,還能和像你一樣也喜歡這個話題的讀者相遇~

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市德谅,隨后出現(xiàn)的幾起案子爹橱,更是在濱河造成了極大的恐慌,老刑警劉巖女阀,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異屑迂,居然都是意外死亡浸策,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門惹盼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來庸汗,“玉大人,你說我怎么就攤上這事手报◎遣眨” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵掩蛤,是天一觀的道長枉昏。 經(jīng)常有香客問我,道長揍鸟,這世上最難降的妖魔是什么兄裂? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮阳藻,結(jié)果婚禮上晰奖,老公的妹妹穿的比我還像新娘。我一直安慰自己腥泥,他們只是感情好匾南,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛔外,像睡著了一般蛆楞。 火紅的嫁衣襯著肌膚如雪溯乒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天臊岸,我揣著相機與錄音橙数,去河邊找鬼。 笑死帅戒,一個胖子當著我的面吹牛灯帮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逻住,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼钟哥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瞎访?” 一聲冷哼從身側(cè)響起腻贰,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扒秸,沒想到半個月后播演,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡伴奥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年写烤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拾徙。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡洲炊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出尼啡,到底是詐尸還是另有隱情暂衡,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布崖瞭,位于F島的核電站狂巢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏书聚。R本人自食惡果不足惜隧膘,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寺惫。 院中可真熱鬧疹吃,春花似錦、人聲如沸西雀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艇肴。三九已至腔呜,卻和暖如春叁温,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背核畴。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工膝但, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谤草。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓跟束,卻偏偏與公主長得像,于是被迫代替她去往敵國和親丑孩。 傳聞我的和親對象是個殘疾皇子冀宴,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355