【Java】JavaSocket编程开发聊天室-客户端核心部分
ClientChat.
本篇文章圍繞聊天室的聊天界面ClientChat,敘述其中各種功能的實現。開始我們還是給出ClientChat最終GUI效果的兩個展示,通過展示來直觀認識各種功能。
關于控件.
JSplitPane分割窗格的使用.
- 上面展示的GUI中實際上有三個分割窗格,比較明顯的是中間這一道,將整個UI分為左右兩邊的分割窗格。我們首先給出代碼段,結合代碼段和上面的UI來敘述。
上述代碼就對應于將整個界面分割為左右兩部分——MainChatPanel以及UserPanel的分割窗格SplitPane。首先是構造函數JSplitPane(),第一個參數是分割策略,代碼中使用的是水平分割,也就是被分割后的兩個部分是水平并列的關系,后兩個參數則是分割的兩個部分。后續的三個set()方法都是顧名思義能夠知道意思的。setDividerLocation()是設置分割窗格的位置;setDividerSize()設置分割窗格的粗細,實際上剩余的兩個分割窗格的size都是1,所以沒有那么明顯,如果都設置為10的話,效果圖如下所示:
如此可以很清楚地看出三個分割窗格的位置了。setOneTouchExpandable()就對應于中間那個分割窗格上的兩個實心黑三角形,顧名思義,它是設置伸縮策略的,某一部分可以被完全展開,效果圖如下所示:
setEnabled則是分割窗格是否可以自由地上下拖動。實際上這些方法都可以通過自己的試驗,來發現它的效果。
圖標型JButton的生成.
- 平時常見的JButton都是上圖中【Exit】樣式的,不免讓人覺得單一。注意到GUI的中間部分,有四個圖標型的按鈕,放到上面還會給出提示信息,清新簡潔。
圖標型JButton是從ImageIcon類型封裝而來的,實際上很簡單,只需要給出圖標的路徑即可。setMargin()方法用于設置按鈕邊框和標簽之間的空白,我們更改其中的參數為new Insets(10,10,10,10),可以發現確實按鈕邊框周圍多了一圈空白。
setToolTipText()則是用于設置提示文本,也就是我們前面UI中顯示出的"Nudge your friend",最近在微信上也很火。
簡單文本消息發送.
簡單文本消息的發送分為三步:
這部分的代碼過于冗長,只選出比較重要的語句進行展示邏輯關系(直接拷貝是無法運行的,代碼是錯的)。
public void SendMessage() {//Get text message.String ThisMessage = RemainToSendArea.getText();//Interaction between client and server.if(One_OneChat.isSelected()){ThisMessage.setReceiver(ChosenUser);}ThisMessage.setSender(ThisUser);Plea plea = new Plea();plea.setAction("Chat");plea.setData("Message", ThisMessage);try{ClientToServer.SendMessage(plea);}catch(IOException e){e.printStackTrace();}//This client should display message,//regardless of public chat or private chatRemainToSendArea.setText("");ClientToServer.appendText(ThisMessage.getContent());私聊設置.
私聊的設定類似于騰訊會議中的設定,用戶可以選擇另一個用戶發起私聊,而其余的用戶無法看到他們之間的聊天內容。展示如下,其中Hoe與Mega正在進行私聊:
而此時,第三位用戶的視角是這樣的:
顯然Hoe與Mega的對話只在他們兩人的界面上顯示了出來。
這段代碼在私聊選項框One_OneChat被選中的時候,如果此時選擇的用戶是一個合法的私聊對象,那么客戶端會在即將發送出去的請求中指明這一條消息的接收者,這就是Server用于區分群發消息和私聊消息的標識。在Server的代碼中,有如下的一段:
if(message.getReceiver()!=null) //Private chat. {//Get receiver's id.ServerRecordClient service = ServerDataStore.OnlineInfoMap.get(message.getReceiver().getID());//Only reply to receiver.SendReply(service, reply); } else //Group chat. {//Reply to all the users except sender.for(Long ID:ServerDataStore.OnlineInfoMap.keySet()){if(message.getSender().getID()==ID){//Skip sender.continue;}else{ServerRecordClient service=ServerDataStore.OnlineInfoMap.get(ID);SendReply(service,reply);}} }總結來說就是Server認為沒有指明接收方的消息是群發消息,所以它會向除了發送方以外的所有客戶端發送回復,而如果是一條私聊消息,Server就只會向接收方一個客戶端發送回復。接收到回復的客戶端,會根據回復中的標識,來進行相應的動作。
if(type==ReplyType.CHAT) {//Chat.Message message =(Message) reply.getData("TextMessage");ClientToServer.appendText(message.getContent()); }客戶端會從Server發送的回復中得知這一條消息的內容,之后將其顯示在自己的界面上。那么沒有收到回復的客戶端,自然也談不上顯示消息了。至此,我們大體上敘述完了的文本消息的發送以及接收的過程,并且展示了體現設計思路的代碼。
窗口抖動發送.
窗口抖動這一功能我們平時也不少用,ClientChat輔助功能按鈕中從左數第三個就是發送窗口抖動的按鈕,除了在聊天框中顯示出"xxx is shaking xxx"這樣的提示消息之外,也借助于Java-Swing組件中的setLocation方法,模擬了實際的抖動效果。下圖中Hoe在22:37:22抖動了Mega,并且Mega也確實接收到了抖動。動態的抖動效果可以自行下載項目的完整代碼查看。
我們將執行窗口抖動的方法綁定到ShakingButton上,當該按鈕被觸發并且符合發送抖動的條件時,客戶端就會封裝一個標記為【抖動】的請求,發送到Server,和處理文本消息類似的,Server也會向接收方發送一個回復,接收方的客戶端根據回復中的內容,做出正確的反應。下面是說明邏輯的代碼,首先是發送方請求"抖動":
Server在接收到【抖動】請求后,如果判斷該請求合理(類似抖動自己這樣的就是不合理的),就會向被抖動方,也就是接收方發出回復:
//Mark reply with "Shake" reply.setType(ReplyType.SHAKE_WINDOW); reply.setData("Shake", message);//Get receiver's id. ServerRecordClient service = ServerDataStore.OnlineInfoMap.get(message.getReceiver().getID());//Send reply. SendReply(service,reply);被抖動方接收到了Server發來的回復后,客戶端就要執行相應的動作了:
//Start shaking. new ShakeFrame(ThisFrame).StartShake();關于ShakeFrame效果的實現,實際上就是通過Java提供的定時器Timer來周期性地使用setLocation()方法更改窗口的位置,高頻的更改就會有抖動的效果。
ShakeTimer = new Timer((int)(SHAKE_CYCLE/5),new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {long pass = System.currentTimeMillis()-StartTime;double ShakeOffset = (pass%SHAKE_CYCLE)/SHAKE_CYCLE;double Angle = ShakeOffset*Math.PI;int Shake_x = (int)(Math.sin(Angle)*SHAKE_DISTANCE+Location.x);int Shake_y = (int)(Math.sin(Angle)*SHAKE_DISTANCE+Location.y);Frame.setLocation(Shake_x,Shake_y);if(pass>=SHAKE_DURATION){StopShake();}} });ShakeTimer.start();在線用戶以及【我】信息展示.
首先需要明確的是,在用戶進行登錄的時候,我們是可以知道這個用戶是誰的。換言之,我們可以記錄下這一用戶的信息,包括但不限于頭像、昵稱以及賬號,而后我們在初始化ClientChat界面時,就可以完成對【我】的信息的展示。代碼如下:
if(null!=ClientDataStore.thisUser) {ThisUserLabel.setForeground(Color.BLUE);ThisUserLabel.setIcon(new ImageIcon("D:\\NewDesktop\\" + ClientDataStore.thisUser.getProfile() + ".png"));ThisUserLabel.setText(ClientDataStore.thisUser.getNickname()+ "(" + ClientDataStore.thisUser.getID() + ")");ThisUserLabel.setOpaque(true);ThisUserLabel.setBackground(Color.WHITE); }至于所有用戶信息的展示,我們說過用戶在登錄時,客戶端是需要與Server發生交互的,我們在Server中專門設置了一個列表來記錄當前的在線用戶,而Server完全可以在給客戶端發送回復時,將它記錄的關于在線用戶的數據包含在回復中,所以客戶端也能夠知道當前在線的用戶情況。
當有用戶登錄時,Server會向每一個客戶端發送回復,其中就包含了當前在線用戶的數據:
然后客戶端從收到的回復中,提取出有關在線用戶信息的數據,而后根據數據設置界面上的顯示
ClientDataStore.onlineUsers=(List<ADT_of_User>)reply.getData("OnlineUsers");OnlineUsersCount.setText("Online Users List 【"+ClientDataStore.onlineUsers.getSize()+"】");后續的關于表示每一個用戶的可選項的顯示,就用了Java提供的ListCellRenderer接口,顧名思義,是用于呈現每一個小格子的接口。我們在實現該接口的UserCellRenderer類中有這樣一段代碼,用于設置顯示出來的內容:
ADT_of_User Abstract_User = (ADT_of_User)obj; String Name = Abstract_User.getNickname()+"【"+Abstract_User.getID()+"】"; setText(Name);而后在ClientChat中,我們借助于客戶端接收到的在線用戶的數據,完成了在線用戶列表的顯示:
OnlineUsersList = new JList(ClientDataStore.onlineUsers); OnlineUsersList.setCellRenderer(new UserCellRenderer()); OnlineUsersList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);文件發送.
文件的發送是所用功能中最復雜的一個,其基本流程分為以下三步:
上述流程的描述,是以發送方和接收方為主體的,但實際上它們之間并沒有直接的交流,它們都需要通過Server來同對方進行交互。
1.Sender發送【ToSendFile】請求.
當發送方用戶按下SendFile時,ClientChat界面會彈出文件選擇框讓該用戶選擇即將發送的文件,之后向Server發送一個標記為【即將發送文件】的請求,等待回應。文件選擇框的效果如下所示,在文件發送的最后我們介紹其用法:
能夠說明邏輯的代碼展示如下:
2.Server收到【ToSendFile】請求.
在上一步的代碼中,請求中已經指明了待發送文件的接收者,所以Server可以精準地向該用戶發送回復,并且在回復中標記上【即將發送文件】,從而使得接收方的客戶端能夠進行后續的處理。
//Mark 'reply' with 'GOING_TO_SEND_FILE'. reply.setType(ReplyType.GOING_TO_SENT_FILE); FileData file = (FileData)plea.getData("File"); reply.setData("SendFile", file);//Get reveiver id. ServerRecordClient service = get(file.getReceiver().getID());//Send reply. SendReply(service,reply);3.Receiver收到【GOING_TO_SEND_FILE】回復.
當Receiver客戶端收到Server發來的回復后,會彈出一個詢問框請求用戶的指示,如果用戶選擇了【同意接收】,那么接收方客戶端會從Server發來的回復中獲取待發送文件的信息,在本地選擇一個保存文件的位置,并且向Server發送一個【AgreeReceiveFile】的請求;如果用戶選擇【拒絕接收】,那么接收方客戶端直接向Server發送一個【RefuseReceiveFile】的請求。
//Inquiry the user. int Choice = JOptionPane.showConfirmDialog(ThisFrame,SenderInfo+" want to send a file【"+FileName+"】 to you.\nWill you agree?","Accept file.",JOptionPane.YES_NO_OPTION);if(Choice==JOptionPane.YES_OPTION) //Agree to receive file. {//Choose position to save file. FileChooser.showSaveDialog(ThisFrame);//Send a plea marked with 'AgreeReceiveFile'.plea.setAction("AgreeReceiveFile"); } else {//Send a plea marked with 'RefuseReceiveFile'.plea.setAction("RefuseReceiveFile"); }4.Server收到【Agree/RefuseReceiveFile】請求.
Server收到來自Receiver的【同意接收】或者【拒絕接收】請求后,都需要向Sender發送一個回復,用于讓Sender客戶端進行后續的處理。當Receiver發送【同意接收】請求后,Server一方面向Sender發送一個【接收方同意接收】的回復,指示Sender進行文件的實際傳輸;另一方面給Receiver發送一個【做好接收準備】的回復,Sender即將實際傳輸文件過來。
//Reply to sender.[AGREE_RECEIVE_FILE]. reply.setType(ReplyType.AGREE_RECEIVE_FILE); ServerRecordClient SenderRecord = get(file.getSender().getID()); SendReply(SenderRecord,reply);//Reply to receiver.[RECEIVE_FILE]. anotherReply.setType(ReplyType.RECEIVE_FILE); ServerRecordClient ReceiverRecord = get(file.getReceiver().getID()); SendReply(ReceiverRecord,anotherReply);5.Sender收到【AGREE_RECEIVE_FILE】回復.
當Sender收到表明接收方已經同意接收文件的【AGREE_RECEIVE_FILE】回復后,就通過Sender和Server之間建立的Socket連接中的輸出流進行實際的文件傳輸。
socket = new Socket(SendFile.getDestIP(),SendFile.getDestPort()); Buffer_OS = new BufferedOutputStream(socket.getOutputStream()); Buffer_OS.write(File);6.Receiver收到【RECEIVE_FILE】回復.
當Receiver收到表明自己應該做好接受準備的【RECEIVE_FILE】回復后,就通過自己和Server之間的Socket連接,將實際的文件寫到在第3步中已經選定好的保存位置。
Ssocket = new ServerSocket(SendFile.getDestPort()); socket = Ssocket.accept(); Buffer_OS = new BufferedOutputStream(new FileOutputStream(SendFile.getDestFilename())); Buffer_OS.write(File);★至此整個文件發送、接收流程已經結束,在下面展示出整個流程的UI界面。
首先是發送方選定要發送的文件:
接收方收到詢問:
接收方同意接收,選定保存位置:
整個傳輸過程完成:
關于JFileChooser.
這是一個JavaSwing中很強大的控件,提供的就是文件選擇功能,并且語法簡單易于使用。
//Create a FileChooser JFileChooser FileChooser = new JFileChooser();//Select a file. if(FileChooser.showOpenDialog(ClientChat.this)==JFileChooser.APPROVE_OPTION) {File file = FileChooser.getSelectedFile();FileToSend = new FileData();FileToSend.setSender(ThisUser);FileToSend.setReceiver(ChosenUser); }總結
以上是生活随笔為你收集整理的【Java】JavaSocket编程开发聊天室-客户端核心部分的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux安装mongodb(设置非ro
- 下一篇: Java面试题之有没有有顺序的Map实现