Java開源-Talk:一個聊天系統

JosNsb 7年前發布 | 20K 次閱讀 Java Java開發

這是一個Java聊天系統,作為Java實驗課的內容,目前已基本完成,支持如下功能:

  • 群聊
  • 私聊
  • 消息提醒
  • 用戶狀態標記
  • 聊天記錄保存
  • 表情支持

效果如下圖:

這是私聊的界面,其中可以看到Master,表示群聊大廳,選中可以進行群聊,而選擇其他用戶,則表示私聊。

名字后面的(*)表示消息提醒,切換標簽即可查看,而(Offline)則標記用戶已經離線。

私聊

同樣,可以發送表情,不那么單調。

表情支持

這是服務端界面,主要是記錄用戶的登入、注銷。

服務端界面

下面我就來寫下我的設計思路吧。

設計思路

首先我要吐槽的是,界面真難寫,從代碼統計中可以看出,我的服務端230行左右,而客戶端達到700多行,同時客戶端也寫的一坨,純粹是面向過程的寫法了。

這里我把客戶端和服務端寫到一個項目里(有3個包,一個客戶端,一個服務端,一個公用數據),通過命令行參數來判斷是啟動客戶端,還是服務端--server,同時互不依賴。

在寫客戶端的過程中,發現Swing比較丑,查了下資料,最后選擇了JavaFX來構建界面。

剛開始是用openjdk來編寫,發現沒有內置JavaFX庫,最后還是老老實實用了Oracle-JDK

期間也查了不少資料,全是關于客戶端的細節處理。

公用數據包

這里主要定義了3個類,分別介紹如下。

Talk類就是程序的主入口了,通過判斷是否帶--server來啟動服務端或者客戶端。

TalkUser類,主要是為服務端使用的,標記了用戶名userName,以及該用戶收到的消息隊列message(為[*FROM <from>]格式,后面會講)。同時也定義了sendMsg(存儲用戶消息)和sendAll(存儲群聊消息)方法,來存儲消息。可能方法名有點誤導,比如說調用usr.sendMsg(from, msg),其實是usr存儲from發來的消息,而不是發送消息= =。sendAll類似。

TalkEmoji類,這個類比較智障,存儲了各個Emoji表情的Unicode碼,這里提前說下,其實早在2010年,Unicode編碼就已經納入了700多個Emoji表情,所以是可以支持表情的,只要加載支持Emoji表情的字庫即可。參考鏈接:How to support Emojis

emoji

服務端pers.netcan.talk.server

服務端接口設計,比較爛大街(傳統)的Master-Worker模型,設置一個Master主線程,專門用來監聽客戶端請求;當客戶端請求時,則創建一系列子線程Workers來處理各個客戶端的請求。

需要注意的是Java很容易產生Null指針異常操作問題,這里要仔細處理。

之后就是設計一套專用的協議,以實現服務端與客戶端的交互。

我設計的協議如下:

  • 客戶端請求

    • [REGISTER]<username>: 用戶注冊到服務器,服務器產生一個子線程專門來處理這個用戶的請求
    • [GETUSRS]: 服務端返回在線用戶列表[USERS]<uesr1>, <user2>...,用逗號隔開
    • [SENDTO <to>]<message>: 用戶發送message消息給名為to的用戶,若發送給Master,則[ALLFROM]響應。
    • [LOGOUT]:用戶注銷
  • 服務端響應

    • [USERS]<uesr1>, <user2>...: 響應客戶端的[GETUSRS]請求,返回各個在線用戶名
    • [OK]: 目前僅表示用戶的[REGISTER]請求成功,即登錄成功
    • [FAILED]: 目前僅表示用戶的[REGISTER]請求失敗,即登錄失敗,可能因為重名。
    • [FROM <from>]<message>: 表示當前用戶收到一個名為from用戶的message消息。
    • [ALLFROM <from>]<message>: 表示當前用戶收到一個名為from用戶的群發(在Master標簽中顯示)message消息。

好吧,應該就那么幾條指令,這樣對于一個聊天系統來說足夠了,需要注意的是發送的時候,用flush方法立即將發送緩沖區中的內容發送出去,而不是等到緩沖區滿了才發送,這樣就沒有交互性可言了。

忘記說一點,我是這么處理用戶的消息的,在Master中有一個Users全局變量(這樣所有的線程都能訪問了),它的類型為<TalkUser>,就是前面公用數據包中提到的數據結構,每當接收到用戶的發送指令[SENDTO]時,就調用對應用戶的sendto方法來存儲消息到自己的消息隊列中。而每個Worker線程,都會在300ms內檢查各自處理用戶的消息隊列是否有消息,一有就立刻發送給對應的客戶端,讓客戶端展示出來。

之前和一個同學討論這個聊天軟件是怎么設計比較合適,他比較糾結一個問題,就是怎么調度各個用戶發送的消息,所以考慮用輪詢的做法,而我一開始就沒考慮過這個問題,因為很簡單啊,用戶A發消息給用戶B,用戶B直接展示出來就行了,反過來類似,如果同時發,怎么調度?當然是誰網速快就先處理誰的= =,同理,群聊也是,服務器先收到誰的,就立刻發送給各個用戶,先后順序完全由發送時間和網速來確定,所以不用考慮那么多的。(當然可能每個用戶的消息記錄顯示順序不一樣,這也是有可能的)。

客戶端pers.netcan.talk.client

客戶端寫的就比較凌亂了,它的職責無非就是解析服務器響應,展現給用戶,同時將用戶的操作(主要是發送命令)發送給服務器處理。

看起來比較容易,細節還是比較難處理的。客戶端開一個線程,每300ms發送一個[GETUSRS]報文給服務器,服務器響應報文,返回用戶列表,也就是說每300ms刷新一下用戶列表,這里起到2個作用,一是相當于心跳包,維持TCP長連接,二是實時獲取在線用戶,之后就是接收消息,每300ms接收一條消息是可以接受的。

然而這個專門用來刷新消息的線程,若修改UI會出錯,無奈查了大量資料,用Task<Void>來處理,將修改UI、刷新消息部分代碼放到如下代碼塊中處理。

Platform.runLater(new Runnable() {
    @Override
    public void run() {
        ...
    }
});

用戶狀態標記,這里當用戶離線的時候,就加個(Offline)標記,有新消息,就加個(*)標記,用正則表達式"([\\w\\d]+)( \\((\\*|Offline)\\))?"來匹配是哪種狀態,看起來夠難寫的。

發送消息,響應發送按鈕點擊事件,和回車事件,然后將發送框中的消息<msg>,用戶列表選中的用戶<to>,發送[SENDTO <to>]<msg>指令給服務端。需要注意的是,為了減少特殊字符(例如換行)帶來的麻煩,將消息字符串利用base64編碼再發送,接收的時候再base64解碼,就不用考慮那么多文本處理細節了。

接收消息,每300ms響應一下服務端,然后檢查是否有[*FROM]響應,并將消息存儲至消息記錄中。客戶端展現出來。

表情支持,將一些Emoji表情的Unicode碼存到按鈕中,然后響應按鈕事件,點擊按鈕就把表情附加到發送框中,這里又出現一個問題,我將emojis定義為一個按鈕數組,那么綁定事件會出現問題:

for(int i=0; i<TalkEmoji.emoji.length; ++i) { // 將表情顯示到按鈕上
    emojis[i].setOnAction((event) -> {
            sendMsg.appendText(emojis[i].getText());
    });
}

將編譯不過去,因為eventlambda表達式引用了i這個外部變量,這在Java中是不允許的(只能將外部變量聲明為final),this我也想過了,不行,沒辦法,又查了大量資料,解決如下:

((Button) event.getSource()).getText()

利用event.getSource()方法獲取是哪個對象響應的事件。

當用戶點擊退出按鈕的時候,就將內存中的聊天記錄以用戶名為文件名的方式保存到文件中,登錄的時候加載一下文件的內容到內存中即可。

還有一點要注意的是,保存/讀取文件需要指定編碼,否則在Win平臺下運行,保存/讀取的內容將亂碼。

TODO

  • 服務器接口
  • 完成客戶端
  • 聊天記錄保存至文件
  • 增加表情支持

 

 本文由用戶 JosNsb 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!