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{//建立服務器ServerSocketServerSocket ss = new ServerSocket(10000);//提示Server建立成功System.out.println("Server online... " + ss.getInetAddress().getLocalHost().getHostAddress() + ", " + 10000);//監聽端口,建立連接并開啟新的ServerThread線程來服務此連接while(true){//接收客戶端SocketSocket 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 {//獲取的客戶端SocketSocket s = null;//獲取的服務器ServerSocketServerSocket ss = null;//獲取的客戶端IPString 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;}@Overridepublic void run(){//將當前客戶端uid存入ArrayListuid_arr.add(uid);//將當前uid和ServerThread對存入HashMaphm.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()) + " 成功連接服務器... 服務器IP: " + ss.getInetAddress().getLocalHost().getHostAddress() + ", 端口: 10000 客戶端IP: " + ip + ", 端口: " + port + " ";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 {//建立客戶端Socketstatic Socket s = null;//消息接收者uidstatic 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(" ");//持續等待接收服務器信息直至退出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()) + " 來自 " + sender + ": " + word + "");//顯示最新消息cframe.jtaChat.setCaretPosition(cframe.jtaChat.getDocument().getLength());}}}catch(Exception e){cframe.jtaChat.append("服務器掛了..... ");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){//表格不可編輯,只可顯示@Overridepublic 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(){@Overridepublic void actionPerformed(ActionEvent event){//顯示最新消息jtaChat.setCaretPosition(jtaChat.getDocument().getLength());try{//有收信人才發送if(Client1.uidReceiver.toString().equals("") == false){//在聊天窗打印發送動作信息jtaChat.append(sdf.format(new Date()) + " 發往 " + Client1.uidReceiver.toString() + ": ");//顯示發送消息jtaChat.append(jtaSay.getText() + "");//向服務器發送聊天信息OutputStream out = Client1.s.getOutputStream();out.write(("Chat/" + Client1.uidReceiver.toString() + "/" + jtaSay.getText()).getBytes());} }catch(Exception e){}finally{//文本輸入框清除jtaSay.setText("");}}});//添加清屏按鈕的響應事件btnClear.addActionListener(new ActionListener(){@Overridepublic void actionPerformed(ActionEvent event){//聊天框清屏jtaChat.setText("");}});//添加退出按鈕的響應事件btnExit.addActionListener(new ActionListener(){@Overridepublic void actionPerformed(ActionEvent event){try{//向服務器發送退出信息OutputStream out = Client1.s.getOutputStream();out.write("Exit/".getBytes());//退出System.exit(0);}catch(Exception e){}}});//添加在線列表項被鼠標選中的相應事件jtbOnline.addMouseListener(new MouseListener(){@Overridepublic 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());}@Overridepublic void mousePressed(MouseEvent event){};@Overridepublic void mouseReleased(MouseEvent event){};@Overridepublic void mouseEntered(MouseEvent event){};@Overridepublic void mouseExited(MouseEvent event){};});} }<span style="color:#3333ff;"> </span>7. 總結
聊天室軟件綜合運用了類集框架,多線程,GUI和網絡編程的知識。在寫程序時筆者發現兩個靜態容器非常關鍵,它們是溝通不同客戶端的橋梁。另外應當重視注釋,否則像代碼稍多的程序維護起來就會很困難。
總結
以上是生活随笔為你收集整理的06 java GUI 网络编程:图形界面聊天室的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《程序员》2012年4期精彩内容:创业
- 下一篇: 【100%通过率】华为OD机试真题 Py