多人聊天室
一、功能簡介
每個客戶端在連接到服務器端時,開始發送消息到服務端,服務端在接收到客戶端的連接時,首先輸出誰進入了聊天室,然后把客戶端發來的消息轉發給其他客戶端,實現群聊的功能,最終達到實現多功能(快速、實時、多人)的多人聊天給用戶帶來更好的體驗功能。
二、設計構想
-
設計客戶端及服務器的界面
- 輸入框與輸出框
- 用戶昵稱
- 消息時間
- 添加滾動條
- 按鈕的添加
- 功能鍵的設置
- 用戶進出的顯示
-
服務器端與客戶端的交互
-
首先服務器端要實例化一個ServerSocket對象,並用其中的accept()方法等待客戶端的連接。
這里值得注意的是:ServreSocket類中的accept()方法是一個阻塞方法,也就是說accept()方法在獲取客戶端的連接之前會一直進行阻塞式的等待,不會讓其下面的代碼執行,直到得到客戶端的連接為止。 -
服務器一直等待的同時,我們就要建立客戶端並向服務器申請連接。那么在客戶端代碼中,我們要實例化一個Socket對象,其應該具有和服務器端ServreSocket相同的端口號,以此確保對服務器提出准確的連接。在成功實例化創建Socket對象后,就相當於成功和服務器建立了連接(前提是申請的服務器對象已創建並在執行accept()方法等待)。
- 在客戶端成功提出連接申請后,我們再回到服務器端accept()方法上來,accept()方法在成功得到連接申請后,返回的值是一個Socket對象,該Socket對象是通向該客戶端的連接。
-
在成功通過Socket對象和ServerSocket對象在客戶端與服務器間建立TCP連接后,我們就要開始進行服務器與客戶端間的信息交流了,其手段則是通過IO流的實現: 通過Socket類中的getInputStream()方法和getOutputStrea()方法可以獲取對應方向的輸入流和輸出流。
- 服務器端通過對accept()得到的Socke對象使用getOutputStrea()方法創建的OutputStream流可以向客戶端寫入信息,同理對該Socket對象使用getInputStream()方法獲取的InputStream流可以從客戶端中讀取信息。反之客戶端亦然。
-
優化界面
- 解決后續BUG
- 將用戶界面更加簡化不繁瑣
三、實驗結果展示
-
服務器端口
-
客戶端端口
-
消息的交互
四、具體代碼及步驟
-
界面設計
1.客戶端
public void init(){ this.setTitle("客戶端窗口"); this.add(sp,BorderLayout.CENTER); this.add(tf,BorderLayout.SOUTH); this.setBounds(300,300,300,400); //給輸入框設置監聽 tf.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String strSend = tf.getText(); //獲取輸入框中的文本 if(strSend.trim().length()==0){ //若輸入框為空則不進行操作(檢查) return; } //strSend發送服務器的方法 send(strSend); // 將文本內容送到發送服務器方法中 tf.setText(""); // 文本在輸入完后消失在文本框 } }); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //關閉窗口及關閉程序 ta.setEditable(false); //不能在顯示框中打字 tf.requestFocus(); //光標聚焦 try { s= new Socket(CONNSTR,CONNPORT); //表示連上服務器 isConn = true; } catch (UnknownHostException e1) { // TODO 自動生成的 catch 塊 e1.printStackTrace(); } catch (IOException e1) { // TODO 自動生成的 catch 塊 e1.printStackTrace(); } this.setVisible(true); //將構造好的數據模型顯示出來 new Thread(new Receive()).start();//啟動多線程
2.服務器端
public ServerChat(){ this.setTitle("服務器端"); this.add(sp,BorderLayout.CENTER); btnTool.add(startBtn); btnTool.add(stopBtn); this.add(btnTool,BorderLayout.SOUTH); this.setBounds(0,0,500,500); if(isStart){ //判斷服務器是否啟動 serverta.append("服務器已啟動\n"); }else{ serverta.append("服務器還未啟動,請點擊按鈕啟動\n"); } //給窗口關閉鍵賦予監聽 this.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { isStart = false ; try { if (ss!=null){ ss.close(); } System.out.println("服務器停止!"); serverta.append("服務器斷開"); System.exit(0); //退出程序 下面關閉按鈕同理 } catch (IOException e1) { // TODO 自動生成的 catch 塊 e1.printStackTrace(); } } }); //給關閉按紐加監聽 stopBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { try { if (ss!=null){ ss.close(); //關閉服務器連接端口 isStart = false; } System.exit(0); serverta.append("服務器斷開"); System.out.println("服務器停止!"); } catch (IOException e1) { // TODO 自動生成的 catch 塊 e1.printStackTrace(); } } }); //給啟動按鈕設置一個監聽 startBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("成功啟動"); try { if(ss == null){ ss= new ServerSocket(PORT); //創建一個服務器端口號 } isStart = true; //啟動條件變為真 serverta.append("服務器已經啟動了!"+"\n"); } catch (IOException e1) { // TODO 自動生成的 catch 塊 e1.printStackTrace(); } } }); this.setVisible(true); //將構造好的數據模型顯示出來 startServer(); }
-
客戶端與服務器端口的交互
1.在用戶端建立與服務器端的通道
dos = new DataOutputStream(s.getOutputStream()); //建立一根發送管道
2.在服務器上為客戶端創建端口號
try{ try{ ss = new ServerSocket(PORT); //創建端口號 isStart=true; }catch (IOException e2){ e2.printStackTrace(); } //可以接收多個客戶端的連接 while(isStart){ //在這使用是否啟動來作為循環判斷條件 Socket s =ss.accept(); //在這等着接客戶端的信息 一個客戶端連一個服務器接口 ccList.add(new ClientConn(s)); //每來一個將信息加入到集合中 System.out.println("一個客戶端連接服務器:"+s.getInetAddress()+"/"+s.getPort()); serverta.append("一個客戶端連接服務器:"+s.getInetAddress()+"/"+s.getPort()+"\n"); }
3.客戶端發送消息
public void send(String str){ //得到要發送的文本 try { dos = new DataOutputStream(s.getOutputStream()); //建立一根發送管道 dos.writeUTF(str); } catch (IOException e) { // TODO 自動生成的 catch 塊 e.printStackTrace(); } }
4.服務器端接收消息
public void run() { try { DataInputStream dis =new DataInputStream(s.getInputStream()); //為了能讓服務器收到每個客戶端多句話 while(isStart){ //在這使用是否啟動來作為循環判斷條件 String str =dis.readUTF(); //接收文本 System.out.println(s.getLocalAddress()+"|"+s.getPort()+"說:"+str+"\n");//顯示在控制台上 serverta.append(s.getLocalAddress()+"|"+s.getPort()+"說:"+str+"\n"); //顯示在服務器端 String strSend = s.getLocalAddress()+"|"+s.getPort()+"說:"+str+"\n"; //遍歷 ccList 調用send方法 在客戶端接收信息是多線程的接收信息(多線程的發送消息) java.util.Iterator<ClientConn> it = ccList.iterator(); while(it.hasNext()){ ClientConn o = it.next(); o.send(strSend); } } } catch (SocketException e){ System.out.println("一個客戶端下線了\n"); serverta.append(s.getLocalAddress()+"|"+s.getPort()+"客戶端下線了\n"); //建立發送的管道 }catch (IOException e) { // TODO 自動生成的 catch 塊 e.printStackTrace(); } }
5.服務器發送消息
public void send(String str){ try { DataOutputStream dos = new DataOutputStream(this.s.getOutputStream()); dos.writeUTF(str); } catch (IOException e) { // TODO 自動生成的 catch 塊 e.printStackTrace(); } }
6.客戶端接收消息
class Receive implements Runnable{ @Override public void run() { try { while(isConn){ DataInputStream dis = new DataInputStream(s.getInputStream()); String str = dis.readUTF(); ta.append(str); } } catch (SocketException e) { System.out.println("服務器意外終止!"); ta.append("服務器意外終止!\n"); }catch (IOException e) { // TODO 自動生成的 catch 塊 e.printStackTrace(); } } }
7.遍歷客戶端發來的消息(為了防止消息發送回自身)
java.util.Iterator<ClientConn> it = ccList.iterator(); while(it.hasNext()){ ClientConn o = it.next(); o.send(strSend); }
五、總結
- 經過查閱大料博客以及相關文獻來完成多人聊天室項目,雖然我們的代碼能力提高的並不是很明顯,但是經過此役之后,我的java功底加深了,設計以及對項目的思考有了一定的廣度和深度,還有就是在查閱相關博客的同時也就是在取之精華進行總結歸納
- 做完多人聊天項目之后,對我最大的幫助就是,讓我了解到一個中間層次 Socket的概念
-
我們原一直以為,客戶端發送消息之后服務端必須立刻馬上進行處理,但其實還可以設置一些中間轉發的過程,比如消息隊列用於保存發送過程中的數據,這樣做可提高系統的響應速率及系統穩定性,從而避免了一些不確定因素,比如(消息高峰時的解耦和),如果發送方和接收方步頻不一致,中間轉發可以達到彌補生產者消費者步頻的不一致問題
- 在編寫服務端代碼的過程中,處處有坑,報出的錯誤要一一的進行處理以達到更好的優化
小組成員:
劉龍軍、郭潤方、惠文凱、邢潤虎
鏈接:https://pan.baidu.com/s/1kP3PY71-Ev-e_ulc7MNomg
提取碼:4ezx