06 java GUI 網絡編程:圖形界面聊天室

仿照第4篇筆記的形式,筆者決定將GUI和網絡編程部分用綜合練習的方式來總結。練習項目是有圖形界面的聊天室,用到了GUI中的javax.swing包和網絡編程中的TCP/socket編程。GUI部分的難點是圖形控件的API較爲複雜,設置不同屬性需要很多的方法和字段,需要參考API手冊和網上的一些圖形界面作品的代碼,不過GUI代碼的結構較爲固定(相對於聊天室簡單的界面而言),容易總結固定格式。網絡編程部分的難點是聊天室涉及到多客戶端之間通過服務器的通信,不僅服務器要使用多線程,而且每個服務線程都要求能夠調取其它服務線程中的socket以便向其它客戶端傳遞消息,這就需要專門的數據容器來儲存所有服務線程。網絡編程部分的另一個難點是消息的結構設計。由於一個socket只有一對輸入輸出流,來自客戶端和服務器的各種不同類型消息都要通過這對流來傳遞給對方,所以服務器和客戶端都要能根據消息的類型採取不同的動作。這需要仔細考慮消息的形式、結構和解析方法。


編程用了兩天時間,下面簡要介紹下實現的功能,GUI和網絡編程部分的思路,詳細說明可以見最後的代碼和註釋。

1. 實現功能

作爲聊天室軟件實現了:
(1)可顯示並即時更新當前在線列表。當新的客戶端連接上服務器,或者在線的客戶端退出時,客戶端向服務器發送消息,服務器會立刻更新所有客戶端的在線列表。在線列表顯示當前在線者(不包括自己)的網名、IP和端口;
(2)發送消息時用戶從在線列表中選擇消息接收者,數量從一個人到所有人皆可;
(3)接收消息時顯示發送者IP和端口;
(4)與服務器失去連接時可以在聊天窗口顯示異常信息;
(5)服務器用多線程方式工作,有靜態容器存儲所有服務線程。

2. GUI部分概要

只有客戶端需要圖形界面。這個界面具有:
(1)聊天窗口:顯示自己發送的和收到的信息(包括髮信人身份),用設置成不可編輯的JTextArea控件實現,用JScrollPane控件包裝來實現滾動條;
(2)打字窗口:輸入聊天消息,用設置成可編輯的JTextArea控件實現;
(3)當前在線列表:顯示當前在線的所有人的網名,IP和端口,由服務器即時更新。發送聊天消息時需要在表中選擇消息接收人,從一個人到所有人皆可。用JTable控件實現,被服務器更新時可動態插入或刪除行。用JScrollPane控件包裝來實現滾動條;
(4)發送按鈕:將打字窗口中的文字按照在線列表中選擇的收信人發給服務器,由後者轉發給收信人,隨即將打字窗口清屏,用JButton控件實現;
(5)清屏按鈕:將聊天窗口清屏,用JButton控件實現;
(6)退出按鈕:向服務器發出退出消息,關閉此客戶端程序。服務器接收到後更新所有客戶端的當前在線列表;
(7)收信人標籤:與在線列表中選擇的收信人一致,起提醒作用。若用戶沒有選擇任何收信人,則不能發送聊天消息。

3. 網絡編程部分概要

(1)使用TCP/Socket連接。服務器使用多線程工作,每個客戶端都享有一個服務線程;
(2)每個客戶端用自己的IP地址和端口號組成一個字符串作爲用戶標識(uid)
(3)客戶端和服務器之間每次通信都是傳遞一個字符串,這個字符串可能有這幾種結構:
     Exit/                          客戶端發往服務器,表示該客戶端退出
     Chat/收信人地址/聊天內容       客戶端發往服務器,表示該客戶端要對別的客戶端發送聊天消息
     Chat/發信人地址/聊天內容       服務器發往客戶端,表示服務器轉發給收信客戶端的聊天消息
     OnlineListUpdate/在線者地址    服務器發往客戶端,表示有客戶端加入或退出,要更新所有客戶端的當前在線列表
(4)收信人地址,發信人地址,在線者地址字符串都採用以下形式:
     第一個客戶端IP地址:第一個客戶端端口號,第二個客戶端IP地址:第二個客戶端端口號,.....
     如果是發信人地址,則只有一個客戶端IP地址和端口號
(5)服務器類有兩個靜態容器:
    一個是String數組,用來儲存當前在線的所有人的uid,
    一個是HashTable<String, 服務線程>, 存儲所有服務線程,可以根據uid取出對應的服務線程
    當客戶端加入或退出時,先更新服務器中的這兩個容器,添加或刪除相應元素,再向客戶端發消息更新其在線列表
(6)服務器用while(true)循環中持續監聽客戶端消息,根據消息類型作出反應。收到"Exit/"類型消息就向所有客戶端發出"OnlineListUpdate/在線者地址"類型消息,
    收到"Chat/收信人地址/聊天內容"類型消息就向收信客戶端發出"Chat/發信人地址/聊天內容"類型消息;
(7)客戶端用while(true)循環中持續監聽服務器消息,根據消息類型作出反應。收到"Chat/發信人地址/聊天內容"類型消息就在聊天窗口中顯示發信人地址和聊天內容,
    收到"OnlineListUpdate/在線者地址"類型消息更新在線列表控件顯示新的在線列表;
(8)服務器只有在收到客戶端消息時纔會發送消息;
(9)客戶端只有按發送或退出按鈕時纔會發送消息。

4. 功能示例




5. 服務器代碼

import java.io.*;
import java.util.*;
import java.net.*;
import java.text.*;

public class Server
{
    public static void main(String[] args) throws Exception
    {
        //建立服務器ServerSocket
        ServerSocket ss = new ServerSocket(10000);
        //提示Server建立成功
        System.out.println("Server online... " + ss.getInetAddress().getLocalHost().getHostAddress() + ", " + 10000);
        //監聽端口,建立連接並開啓新的ServerThread線程來服務此連接
        while(true)
        {
            //接收客戶端Socket
            Socket s = ss.accept();
            //提取客戶端IP和端口
            String ip = s.getInetAddress().getHostAddress();
            int port = s.getPort();
            //建立新的服務器線程, 向該線程提供服務器ServerSocket,客戶端Socket,客戶端IP和端口
            new Thread(new ServerThread(s, ss, ip, port)).start();
        }
    }
}

class ServerThread implements Runnable
{
    //獲取的客戶端Socket
    Socket s = null;
    //獲取的服務器ServerSocket
    ServerSocket ss = null;
    //獲取的客戶端IP
    String ip = null;
    //獲取的客戶端端口
    int port = 0;
    //組合客戶端的ip和端口字符串得到uid字符串
    String uid = null;
    
    //靜態ArrayList存儲所有uid,uid由ip和端口字符串拼接而成
    static ArrayList<String> uid_arr = new ArrayList<String>();
    //靜態HashMap存儲所有uid, ServerThread對象組成的對
    static HashMap<String, ServerThread> hm = new HashMap<String, ServerThread>();
    
    public ServerThread(Socket s, ServerSocket ss, String ip, int port)
    {
        this.s = s;
        this.ss = ss;
        this.ip = ip;
        this.port = port;
        uid = ip + ":" + port;
    }

    @Override
    public void run()
    {
        //將當前客戶端uid存入ArrayList
        uid_arr.add(uid);
        //將當前uid和ServerThread對存入HashMap
        hm.put(uid, this);

        //時間顯示格式
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        
        //控制檯打印客戶端IP和端口
        System.out.println("Client connected: " + uid);

        try
        {
            //獲取輸入流
            InputStream in = s.getInputStream();
            //獲取輸出流
            OutputStream out = s.getOutputStream();

            //向當前客戶端傳輸連接成功信息
            String welcome = sdf.format(new Date()) + "\n成功連接服務器...\n服務器IP: " + ss.getInetAddress().getLocalHost().getHostAddress() + ", 端口: 10000\n客戶端IP: " + ip + ", 端口: " + port + "\n";
            out.write(welcome.getBytes());

            //廣播更新在線名單 
            updateOnlineList(out);

            //準備緩衝區
            byte[] buf = new byte[1024];
            int len = 0;
        
            //持續監聽並轉發客戶端消息
            while(true)
            {
                len = in.read(buf);
                String msg = new String(buf, 0, len);
                System.out.println(msg);
                //消息類型:退出或者聊天
                String type = msg.substring(0, msg.indexOf("/"));
                //消息本體:空或者聊天內容
                String content = msg.substring(msg.indexOf("/") + 1);
                //根據消息類型分別處理
                //客戶端要退出
                if(type.equals("Exit"))
                {
                    //更新ArrayList和HashMap, 刪除退出的uid和線程
                    uid_arr.remove(uid_arr.indexOf(uid));
                    hm.remove(uid);
                    //廣播更新在線名單
                    updateOnlineList(out);
                   //控制檯打印客戶端IP和端口
                    System.out.println("Client exited: " + uid);
                    //結束循環,結束該服務線程
                    break;
                }
                //客戶端要聊天
                else if(type.equals("Chat"))
                {
                    //提取收信者地址
                    String[] receiver_arr = content.substring(0, content.indexOf("/")).split(",");
                    //提取聊天內容
                    String word = content.substring(content.indexOf("/") + 1);
                    //向收信者廣播發出聊天信息
                    chatOnlineList(out, uid, receiver_arr, word);
                }
            }
        }
        catch(Exception e){}
    }
    
    //向所有已連接的客戶端更新在線名單
    public void updateOnlineList(OutputStream out) throws Exception
    {
        for(String tmp_uid : uid_arr)
            {
                //獲取廣播收聽者的輸出流
                out = hm.get(tmp_uid).s.getOutputStream();
                //將當前在線名單以逗號爲分割組合成長字符串一次傳送
                StringBuilder sb = new StringBuilder("OnlineListUpdate/");
                for(String member : uid_arr)
                {
                    sb.append(member);
                    //以逗號分隔uid,除了最後一個
                    if(uid_arr.indexOf(member) != uid_arr.size() - 1)
                        sb.append(",");
                }
                out.write(sb.toString().getBytes());
            }
    }

    //向指定的客戶端發送聊天消息
    public void chatOnlineList(OutputStream out, String uid, String[] receiver_arr, String word) throws Exception
    {
        for(String tmp_uid : receiver_arr)
            {
                //獲取廣播收聽者的輸出流
                out = hm.get(tmp_uid).s.getOutputStream();
                //發送聊天信息
                out.write(("Chat/" + uid + "/" + word).getBytes());
            }
    } 
}

6. 客戶端代碼

import java.io.*;
import java.net.*;
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import java.nio.charset.*;
import java.text.*;

public class Client1 
{
    //建立客戶端Socket
    static Socket s = null;
    //消息接收者uid
    static StringBuilder uidReceiver = null;

    public static void main(String[] args)
    {
        //創建客戶端窗口對象
        ClientFrame cframe = new ClientFrame();
        //窗口關閉鍵無效,必須通過退出鍵退出客戶端以便善後
        cframe.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        //獲取本機屏幕橫向分辨率
        int w = Toolkit.getDefaultToolkit().getScreenSize().width;
        //獲取本機屏幕縱向分辨率
        int h = Toolkit.getDefaultToolkit().getScreenSize().height;
        //將窗口置中
        cframe.setLocation((w - cframe.WIDTH)/2, (h - cframe.HEIGHT)/2);
        //設置客戶端窗口爲可見
        cframe.setVisible(true);
        
        try
        {
            //連接服務器
            s = new Socket(InetAddress.getLocalHost(), 10000);
            //獲取輸入流
            InputStream in = s.getInputStream();
            //獲取輸出流
            OutputStream out = s.getOutputStream();

            //獲取服務端歡迎信息
            byte[] buf = new byte[1024];
            int len = in.read(buf);
            //將歡迎信息打印在聊天消息框內
            cframe.jtaChat.append(new String(buf, 0, len));
            cframe.jtaChat.append("\n");

            //持續等待接收服務器信息直至退出
            while(true)
            {
                in = s.getInputStream();
                len = in.read(buf);
                System.out.println(len);
                //處理服務器傳來的消息
                String msg = new String(buf, 0, len);
                //消息類型:更新在線名單或者聊天
                String type = msg.substring(0, msg.indexOf("/"));
                //消息本體:更新後的名單或者聊天內容
                String content = msg.substring(msg.indexOf("/") + 1);
                //根據消息類型分別處理
                //更新在線名單
                if(type.equals("OnlineListUpdate"))
                {
                    //提取在線列表的數據模型
                    DefaultTableModel tbm = (DefaultTableModel) cframe.jtbOnline.getModel();
                    //清除在線名單列表
                    tbm.setRowCount(0);
                    //更新在線名單
                    String[] onlinelist = content.split(",");
                    //逐一添加當前在線者
                    for(String member : onlinelist)
                    {
                        String[] tmp = new String[3];
                        //如果是自己則不在名單中顯示
                        if(member.equals(InetAddress.getLocalHost().getHostAddress() + ":" + s.getLocalPort()))
                            continue;
                        tmp[0] = "";
                        tmp[1] = member.substring(0, member.indexOf(":"));
                        tmp[2] = member.substring(member.indexOf(":") + 1);
                        //添加當前在線者之一
                        tbm.addRow(tmp);
                    }
                    //提取在線列表的渲染模型
                    DefaultTableCellRenderer tbr = new DefaultTableCellRenderer();
                    //表格數據居中顯示
                    tbr.setHorizontalAlignment(JLabel.CENTER);
                    cframe.jtbOnline.setDefaultRenderer(Object.class, tbr);
                }
                //聊天
                else if(type.equals("Chat"))
                {
                    String sender = content.substring(0, content.indexOf("/"));
                    String word = content.substring(content.indexOf("/") + 1);
                    //在聊天窗打印聊天信息
                    cframe.jtaChat.append(cframe.sdf.format(new Date()) + "\n來自 " + sender + ":\n" + word + "\n\n");
                    //顯示最新消息
                    cframe.jtaChat.setCaretPosition(cframe.jtaChat.getDocument().getLength());
                }
            }
        }
        catch(Exception e)
        {
            cframe.jtaChat.append("服務器掛了.....\n");
            e.printStackTrace();
        }
    }
}

//客戶端窗口
class ClientFrame extends JFrame
{
    //時間顯示格式
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    //窗口寬度
    final int WIDTH = 700;
    //窗口高度
    final int HEIGHT = 700;
    
    //創建發送按鈕
    JButton btnSend = new JButton("發送");
    //創建清除按鈕
    JButton btnClear = new JButton("清屏");
    //創建退出按鈕
    JButton btnExit = new JButton("退出");

    //創建消息接收者標籤
    JLabel lblReceiver = new JLabel("對誰說?");

    //創建文本輸入框, 參數分別爲行數和列數
    JTextArea jtaSay = new JTextArea();

    //創建聊天消息框
    JTextArea jtaChat = new JTextArea();

    //當前在線列表的列標題
    String[] colTitles = {"網名", "IP", "端口"};
    //當前在線列表的數據
    String[][] rowData = null;
    //創建當前在線列表
    JTable jtbOnline = new JTable
                                (
                                    new DefaultTableModel(rowData, colTitles)
                                    {
                                        //表格不可編輯,只可顯示
                                        @Override
                                        public boolean isCellEditable(int row, int column)
                                        {
                                            return false;
                                        }
                                    }
                                );
    
    //創建聊天消息框的滾動窗
    JScrollPane jspChat = new JScrollPane(jtaChat);

    //創建當前在線列表的滾動窗
    JScrollPane jspOnline = new JScrollPane(jtbOnline);

    //設置默認窗口屬性,連接窗口組件
    public ClientFrame()
    {
        //標題
        setTitle("聊天室");
        //大小
        setSize(WIDTH, HEIGHT);
        //不可縮放
        setResizable(false);
        //設置佈局:不適用默認佈局,完全自定義
        setLayout(null);

        //設置按鈕大小和位置
        btnSend.setBounds(20, 600, 100, 60);
        btnClear.setBounds(140, 600, 100, 60);
        btnExit.setBounds(260, 600, 100, 60);

        //設置標籤大小和位置
        lblReceiver.setBounds(20, 420, 300, 30);

        //設置按鈕文本的字體
        btnSend.setFont(new Font("宋體", Font.BOLD, 18));
        btnClear.setFont(new Font("宋體", Font.BOLD, 18));
        btnExit.setFont(new Font("宋體", Font.BOLD, 18));

        //添加按鈕
        this.add(btnSend);
        this.add(btnClear);
        this.add(btnExit);

        //添加標籤
        this.add(lblReceiver);

        //設置文本輸入框大小和位置
        jtaSay.setBounds(20, 460, 360, 120);
        //設置文本輸入框字體
        jtaSay.setFont(new Font("楷體", Font.BOLD, 16));
        //添加文本輸入框
        this.add(jtaSay);
        
        //聊天消息框自動換行
        jtaChat.setLineWrap(true);
        //聊天框不可編輯,只用來顯示
        jtaChat.setEditable(false);
        //設置聊天框字體
        jtaChat.setFont(new Font("楷體", Font.BOLD, 16));

        //設置滾動窗的水平滾動條屬性:不出現
        jspChat.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        //設置滾動窗的垂直滾動條屬性:需要時自動出現
        jspChat.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        //設置滾動窗大小和位置
        jspChat.setBounds(20, 20, 360, 400);
        //添加聊天窗口的滾動窗
        this.add(jspChat);

        //設置滾動窗的水平滾動條屬性:不出現
        jspOnline.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        //設置滾動窗的垂直滾動條屬性:需要時自動出現
        jspOnline.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        //設置當前在線列表滾動窗大小和位置
        jspOnline.setBounds(420, 20, 250, 400);
        //添加當前在線列表
        this.add(jspOnline);

        //添加發送按鈕的響應事件
        btnSend.addActionListener
                                (
                                     new ActionListener()
                                     {
                                        @Override
                                        public void actionPerformed(ActionEvent event)
                                        {
                                            //顯示最新消息
                                            jtaChat.setCaretPosition(jtaChat.getDocument().getLength());
                                            try
                                            {
                                                //有收信人才發送
                                                if(Client1.uidReceiver.toString().equals("") == false)
                                                {
                                                    //在聊天窗打印發送動作信息
                                                    jtaChat.append(sdf.format(new Date()) + "\n發往 " + Client1.uidReceiver.toString() + ":\n");
                                                    //顯示發送消息
                                                    jtaChat.append(jtaSay.getText() + "\n\n");
                                                    //向服務器發送聊天信息
                                                    OutputStream out = Client1.s.getOutputStream();
                                                    out.write(("Chat/" + Client1.uidReceiver.toString() + "/" + jtaSay.getText()).getBytes());
                                                } 
                                            }
                                            catch(Exception e){}
                                            finally
                                            {
                                                //文本輸入框清除
                                                jtaSay.setText("");
                                            }
                                        }
                                     }
                                );
        //添加清屏按鈕的響應事件
        btnClear.addActionListener
                                (
                                     new ActionListener()
                                     {
                                        @Override
                                        public void actionPerformed(ActionEvent event)
                                        {
                                            //聊天框清屏
                                            jtaChat.setText("");
                                        }
                                     }
                                );
        //添加退出按鈕的響應事件
        btnExit.addActionListener
                                (
                                     new ActionListener()
                                     {
                                        @Override
                                        public void actionPerformed(ActionEvent event)
                                        {
                                            try
                                            {
                                                //向服務器發送退出信息
                                                OutputStream out = Client1.s.getOutputStream();
                                                out.write("Exit/".getBytes());
                                                //退出
                                                System.exit(0);
                                            }
                                            catch(Exception e){}
                                        }
                                     }
                                );
        //添加在線列表項被鼠標選中的相應事件
        jtbOnline.addMouseListener
                                (
                                    new MouseListener()
                                    {
                                        @Override
                                        public void mouseClicked(MouseEvent event)
                                        {
                                            //取得在線列表的數據模型
                                            DefaultTableModel tbm = (DefaultTableModel) jtbOnline.getModel();
                                            //提取鼠標選中的行作爲消息目標,最少一個人,最多全體在線者接收消息
                                            int[] selectedIndex = jtbOnline.getSelectedRows();
                                            //將所有消息目標的uid拼接成一個字符串, 以逗號分隔
                                            Client1.uidReceiver = new StringBuilder("");
                                            for(int i = 0; i < selectedIndex.length; i++)
                                            {
                                                Client1.uidReceiver.append((String) tbm.getValueAt(selectedIndex[i], 1));
                                                Client1.uidReceiver.append(":");
                                                Client1.uidReceiver.append((String) tbm.getValueAt(selectedIndex[i], 2));
                                                if(i != selectedIndex.length - 1)
                                                    Client1.uidReceiver.append(",");
                                            }
                                            lblReceiver.setText("發給:" + Client1.uidReceiver.toString());
                                        }
                                        @Override
                                        public void mousePressed(MouseEvent event){};
                                        @Override
                                        public void mouseReleased(MouseEvent event){};
                                        @Override
                                        public void mouseEntered(MouseEvent event){};
                                        @Override
                                        public void mouseExited(MouseEvent event){};
                                    }
                                );
    }
}<span style="color:#3333ff;">
</span>

7. 總結

聊天室軟件綜合運用了類集框架,多線程,GUI和網絡編程的知識。在寫程序時筆者發現兩個靜態容器非常關鍵,它們是溝通不同客戶端的橋樑。另外應當重視註釋,否則像代碼稍多的程序維護起來就會很困難。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章