多人聊天室的實現


多人聊天室

一、功能簡介

每個客戶端在連接到服務器端時,開始發送消息到服務端,服務端在接收到客戶端的連接時,首先輸出誰進入了聊天室,然后把客戶端發來的消息轉發給其他客戶端,實現群聊的功能,最終達到實現多功能(快速、實時、多人)的多人聊天給用戶帶來更好的體驗功能。

二、設計構想

  • 設計客戶端及服務器的界面

  1. 輸入框與輸出框
  2. 用戶昵稱
  3. 消息時間
  4. 添加滾動條
  5. 按鈕的添加
  6. 功能鍵的設置
  7. 用戶進出的顯示
  • 服務器端與客戶端的交互

  1. 首先服務器端要實例化一個ServerSocket對象,並用其中的accept()方法等待客戶端的連接。
    這里值得注意的是:ServreSocket類中的accept()方法是一個阻塞方法,也就是說accept()方法在獲取客戶端的連接之前會一直進行阻塞式的等待,不會讓其下面的代碼執行,直到得到客戶端的連接為止。

  2. 服務器一直等待的同時,我們就要建立客戶端並向服務器申請連接。那么在客戶端代碼中,我們要實例化一個Socket對象,其應該具有和服務器端ServreSocket相同的端口號,以此確保對服務器提出准確的連接。在成功實例化創建Socket對象后,就相當於成功和服務器建立了連接(前提是申請的服務器對象已創建並在執行accept()方法等待)。

  3. 在客戶端成功提出連接申請后,我們再回到服務器端accept()方法上來,accept()方法在成功得到連接申請后,返回的值是一個Socket對象,該Socket對象是通向該客戶端的連接。
  4. 在成功通過Socket對象和ServerSocket對象在客戶端與服務器間建立TCP連接后,我們就要開始進行服務器與客戶端間的信息交流了,其手段則是通過IO流的實現: 通過Socket類中的getInputStream()方法和getOutputStrea()方法可以獲取對應方向的輸入流和輸出流。

  5. 服務器端通過對accept()得到的Socke對象使用getOutputStrea()方法創建的OutputStream流可以向客戶端寫入信息,同理對該Socket對象使用getInputStream()方法獲取的InputStream流可以從客戶端中讀取信息。反之客戶端亦然。
  • 優化界面

  1. 解決后續BUG
  2. 將用戶界面更加簡化不繁瑣

三、實驗結果展示

  • 服務器端口

  •  客戶端端口

  • 消息的交互

四、具體代碼及步驟

  • 界面設計

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM