1.程序實現的功能
兩個客戶端之間,實現在線文字聊天,和接收離線消息。
2.程序總體結構
程序整體是C/S結構,用java中socket通信建立服務端和客戶端之間的UDP連接,消息都通過服務端轉發,客戶端之間不直接建立連接。
3.服務端介紹
(圖3.1 服務端初始界面)
首先,服務端程序運行后,需要點擊啟動按鈕,基於事件監聽機制,建立socket連接和等待接收數據報都在按鈕actionPerformed函數里進行:
1 BStart.addActionListener(new ActionListener() { //啟動按鈕 2 3 @Override 4 public void actionPerformed(ActionEvent e) { 5 thread t1 = new thread(); 6 t1.start(); 7 } 8 });
注意到,按鈕里的並沒有socket操作,只有一個線程的執行,其實,socket操作就放在這個線程里進行,之所以用到線程,原因在后文會給出。
通過繼承了Thread類的方式實現多線程:
1 static class thread extends Thread{ 2 public thread(){ 3 super(); 4 } 5 public void run(){ 6 //socket methods... 7 }
第一件事是建立socket,綁定到本機,並顯示在左邊的系統消息框內:
1 //建立數據報Socket並顯示 2 InetAddress addr = InetAddress.getByName("localhost"); 3 DatagramSocket ds =new DatagramSocket(PORT,addr); 4 ta.setText("【" + df.format(new Date()) +"】" + "UDP服務器已啟動:" + addr.getHostAddress() + addr.getHostName());
(3.2 服務端啟動連接界面)
接下里通過DatagramSocket的receive函數等待數據報的到來:
1 while(true){ 2 ds.receive(inDataPacket);//等待數據報的到來 3 //... 4 }
如果收到的是客戶端的連接請求,則更新用戶列表,返回確認連接報文,並在系統消息框內顯示客戶端連接的消息:
1 //數據的處理 2 String str = new String(inDataPacket.getData(), 0, inDataPacket.getLength()); 3 if(str.equals(new String("Request Connect"))){ //連接請求 4 String str2 = inDataPacket.getAddress() + "(" + inDataPacket.getPort() + ")"; 5 //更新用戶列表 6 if(ta2.getText().indexOf(str2) == -1){ 7 String history2 = ta2.getText(); 8 String now2 = String.format("%s%n", history2) + str2; 9 ta2.setText(now2); 10 } 11 12 //返回確認連接報文 13 String strRep = "Connection Confirm"; 14 DatagramPacket outDataPacket = new DatagramPacket(strRep.getBytes(),strRep.length(), 15 inDataPacket.getAddress(), inDataPacket.getPort()); 16 ds.send(outDataPacket); 17 18 //更新系統消息列表 19 String history_SM = ta.getText(); 20 String now_SM = String.format("%s%n", history_SM) + "【" + df.format(new Date()) +"】" +str2 + "成功連接到服務器"; 21 ta.setText(now_SM);
(圖3.3 客戶端連接到服務器)
如果不是請求連接報文,則為消息報文。數據報中消息的頭部加上了源端口和目的端口,方便服務端進行數據報的封裝,如果源端口為8888,目的端口為8889,則消息的頭部為:TO8889FR8888。如果用戶在線(在當前用戶列表上查找,有則判斷在線),則封裝數據報並投遞。如果不在線,則將消息儲存,等待此用戶上線后再發送該消息。只要收到消息,系統顯示框都會有顯示。
1 //消息處理 2 String Port = str.substring(2, 6); //提取收信方端口 3 if(ta2.getText().indexOf(Port) != -1){ //用戶在線 4 String strRep = str; 5 DatagramPacket outDataPacket = new DatagramPacket(strRep.getBytes(),strRep.getBytes().length, 6 addr, Integer.parseInt(Port)); 7 ds.send(outDataPacket); 8 9 }else{ //用戶不在線 10 if(Port.equals(new String("8888"))){ 11 S1 = str; 12 }else if(Port.equals(new String("8889"))){ 13 S2 = str; 14 }else{ 15 S3 = str; 16 } 17 } 18 String history = ta.getText(); 19 String now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + str.subSequence(8, 12) + "給" + Port + "發送了一條消息。"; 20 ta.setText(now);
(圖3.4 服務端顯示消息發送成功)
4.客戶端介紹
客戶端和服務端有很多類似,不管是界面還是代碼結構。為了演示的需要,共設置三個客戶端,由於都在本機運行,地址全為localhost,以端口號區分。
(圖4.1 客戶端初始界面)
客戶端的socket操作放在連接按鈕的監聽函數里,完成消息的獲取:
1 //建立數據報socket 2 addr = InetAddress.getByName("localhost"); 3 datagramSocket = new DatagramSocket(LocalPORT); 4 datagramSocket.setSoTimeout(3000); //設置3秒超時 5 6 //發送登錄消息給服務器 7 String str = "Request Connect"; 8 DatagramPacket login = new DatagramPacket(str.getBytes(), str.length(), addr, ServerPORT); 9 datagramSocket.send(login); 10 11 //接受服務器的確認數據報 12 byte[] msg = new byte[100]; 13 DatagramPacket inDataPacket = new DatagramPacket(msg, msg.length); 14 datagramSocket.receive(inDataPacket);
當連接按鈕按下后,右側的好友按鈕才會可用:
(圖4.2 客戶端連接到服務器)
此時,發送消息的准備工作已經完成。點擊要對話的好友按鈕,發送按鈕會變成可用,在下方的文本框里輸入要發送的信息,點擊發送按鈕即可完成:
1 //按下好友按鈕,發送按鈕可用 2 B8889.addActionListener(new ActionListener(){ 3 @Override 4 public void actionPerformed(ActionEvent e) { 5 6 current = "8889"; 7 BSend.setEnabled(true); 8 } 9 10 });
其中,current為消息將要到達的目標端口號的標識,點擊發送按鈕,消息將會被發到current所表示的端口客戶端。
發送按鈕里的socket操作:
1 //建立數據報socket 2 InetAddress addr = InetAddress.getByName("localhost"); 3 DatagramSocket datagramSocket = new DatagramSocket(LocalPORTS); 4 5 //構造數據報並發送 6 String str = "T0" + current + "FR" + "8888" + ":" + tf.getText(); 7 DatagramPacket out = new DatagramPacket(str.getBytes(), str.getBytes().length, addr, ServerPORT); 8 datagramSocket.send(out); 9 10 //清空發送框 11 tf.setText(null); 12 13 //顯示 14 String history = ta.getText(); 15 String now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + "消息發送成功!"; 16 ta.setText(now); 17 //關閉數據報 18 datagramSocket.close();
演示,用客戶端8888發送“網絡交談123”給客戶端8889:
(圖4.3 客戶端8888發送消息成功)
(圖4.4 客戶端8889收到消息)
5.遇到的問題及解決過程
實踐過程中碰到很多問題,持續時間很長,由於基礎不牢,有時小問題都會花很長時間,絕大多數時間都花在解決問題上了。下面就過程中碰到的3個問題進行簡要說明。
1)服務端點擊啟動后窗口無法正常關閉/啟動客戶端並點擊連接按鈕,窗口失效。
最開始是發現當服務端點擊啟動按鈕運行后,窗口就不能點擊右上角關閉了,需要用任務管理器才能關掉。之后在java課上,聽老師講到了阻塞的概念,才發現,DatagramSocket中receive函數在接受到數據報前一直處於阻塞狀態,就是說,當沒有收到消息時程序會停在這里,而這個時候,程序中其他部分就會處於不可用狀態。知道了原因,但還是不知道怎樣解決。在網上了解一番后,發現,將啟動按鈕中socket操作放在另外線程里能解決這個問題。就又去學習了線程的知識,才找到解決方案。
之后發現,啟動客戶端點擊連接按鈕后窗口上好友按鈕顯示可用,但是點擊沒有反應,界面底部的文本框也不能用,整個窗口像是卡住了一樣。很快發應過來,這還是receive函數阻塞的原因,因為連接按鈕的監聽函數里,放了DatagramSocket的receive函數,需要總是處於等待消息狀態。同樣,加入線程后就解決了。
2)服務端未啟動時,先啟動客戶端並點擊連接按鈕,需要將阻塞狀態終止,顯示連接超時消息。
很明顯這也是receive阻塞,因為客戶端的連接按鈕的監聽函數里通過DatagramSocket給服務器發送了請求連接報文,但是服務器未啟動,不能返回確認連接報文,receive阻塞。這個情況也可以通過加入線程和設置定時器的方法可以解決,但是我通過查閱java API文檔找到了更好的方法,發現可以給receive函數設置超時限制,既當receive函數在設置的時間里未能收到消息,會拋出SocketTimeoutException異常,通過捕獲這個異常,就可以中斷receive的阻塞,並可以顯示超時信息。
1 datagramSocket.setSoTimeout(3000); //設置3秒超時
1 Try{ 2 DatagramSocket.receive(inDataPacket); 3 }catch(SocketTimeoutException e1) { 4 String history = ta.getText(); 5 String now; 6 if(!history.equals("")){ 7 now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + "連接超時!"; 8 }else{ 9 now = "【" + df.format(new Date()) +"】" + "連接超時!"; 10 } 11 ta.setText(now); 12 datagramSocket.close(); 13 }
3)中文亂碼
開始只能發送英文和數字,發中文就亂碼,在網上查說是編碼的問題,改了半天才發現我封裝數據報的時候,字符串的長度計算不妥:
1 DatagramPacket out = new DatagramPacket(str.getBytes(),str.length(), addr, ServerPORT); //str為String類型
如果是英文或數字,str.length()計算的長度沒問題,但是如果是漢字,就不對了,漢字不只占一個字節。
之后改成:
1 DatagramPacket out = new DatagramPacket(str.getBytes(), str.getBytes().length, addr, ServerPORT);
就可以了。
6. 存在的不足和接下來的工作
對數據消息的儲存沒有花太多心思,客戶端只能顯示最后一條離線消息。
這個只是在本機上模擬多客戶端交談,要想放在不同機器上,需要獲得機器的ip地址,將DatagramSocket綁定到此地址即可。
圖片的傳輸可以嘗試。
7.完整代碼
1 /*服務端*/ 2 import java.awt.Button; 3 import java.awt.Color; 4 import java.awt.Label; 5 import java.awt.TextArea; 6 import java.awt.event.ActionEvent; 7 import java.awt.event.ActionListener; 8 import javax.swing.JFrame; 9 import java.io.*; 10 import java.net.*; 11 import java.text.SimpleDateFormat; 12 import java.util.Date; 13 14 public class talkOnline_Server { 15 public static final int PORT =8081; 16 private static TextArea ta; 17 private static TextArea ta2; 18 static SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//設置日期格式 19 public static void main(String[] args) { 20 JFrame f = new JFrame(); 21 f.setLayout(null); 22 23 //連接按鈕 24 Button BStart = new Button("啟動"); 25 BStart.setBounds(10, 10, 50, 20); 26 27 //系統消息框標簽 28 Label lb1 = new Label("--------系統消息框--------"); 29 lb1.setBounds(10, 55, 300, 20); 30 31 //系統消息框 32 ta = new TextArea(); 33 ta.setBackground(Color.gray); 34 ta.setEditable(false); 35 ta.setBounds(10, 80, 400, 400); 36 37 38 //當前用戶列表標簽 39 Label lb2 = new Label("--------當前用戶列表--------"); 40 lb2.setBounds(450, 55, 300, 20); 41 42 //當前用戶列表框 43 ta2 = new TextArea(); 44 ta2.setBackground(Color.gray); 45 ta2.setEditable(false); 46 ta2.setBounds(450, 80, 200, 400); 47 48 49 //將組件加入到框架 50 f.add(BStart); 51 f.add(lb1); 52 f.add(ta); 53 f.add(lb2); 54 f.add(ta2); 55 56 f.setSize(700,600); 57 f.setVisible(true); 58 f.setResizable(false); 59 f.setTitle("網絡交談_服務器"); 60 f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 61 62 BStart.addActionListener(new ActionListener() { //啟動按鈕 63 64 @Override 65 public void actionPerformed(ActionEvent e) { 66 thread t1 = new thread(); 67 t1.start(); 68 } 69 }); 70 71 } 72 73 static class thread extends Thread{ 74 public thread(){ 75 super(); 76 } 77 public void run(){ 78 InetAddress addr; 79 DatagramSocket ds = null; 80 String S1 = new String(), S2 = new String (), S3 = new String(); 81 try { 82 //建立數據報Socket並顯示 83 addr = InetAddress.getByName("localhost"); 84 ds =new DatagramSocket(PORT,addr); 85 ta.setText("【" + df.format(new Date()) +"】" + "UDP服務器已啟動:" + addr.getHostAddress() + "/" + addr.getHostName()); 86 87 //建立接受數據報 88 byte[] buf = new byte[1000]; 89 DatagramPacket inDataPacket = new DatagramPacket(buf, buf.length); 90 while(true){ 91 //等待數據報的到來 92 ds.receive(inDataPacket); 93 //數據的處理 94 String str = new String(inDataPacket.getData(), 0, inDataPacket.getLength()); 95 if(str.equals(new String("Request Connect"))){ //連接請求 96 String str2 = inDataPacket.getAddress() + "(" + inDataPacket.getPort() + ")"; 97 //更新用戶列表 98 if(ta2.getText().indexOf(str2) == -1){ 99 String history2 = ta2.getText(); 100 String now2 = String.format("%s%n", history2) + str2; 101 ta2.setText(now2); 102 } 103 104 //返回確認連接報文 105 String strRep = "Connection Confirm"; 106 DatagramPacket outDataPacket = new DatagramPacket(strRep.getBytes(),strRep.length(), 107 inDataPacket.getAddress(), inDataPacket.getPort()); 108 ds.send(outDataPacket); 109 110 //更新系統消息列表 111 String history_SM = ta.getText(); 112 String now_SM = String.format("%s%n", history_SM) + "【" + df.format(new Date()) +"】" +str2 + "成功連接到服務器"; 113 ta.setText(now_SM); 114 115 if(!S1.isEmpty()){ //有8888的消息 116 117 outDataPacket = new DatagramPacket(S1.getBytes(),S1.getBytes().length, 118 addr, 8888); 119 ds.send(outDataPacket); 120 S1 = new String(""); 121 } 122 123 if(!S2.isEmpty()){ //有8889的消息 124 outDataPacket = new DatagramPacket(S2.getBytes(),S2.getBytes().length, 125 addr, 8889); 126 ds.send(outDataPacket); 127 S2 = new String(""); 128 } 129 if(!S3.isEmpty()){ //有8890的消息 130 outDataPacket = new DatagramPacket(S3.getBytes(),S3.getBytes().length, 131 addr, 8890); 132 ds.send(outDataPacket); 133 S3 = new String(""); 134 } 135 136 } 137 else{ 138 //消息處理 139 String Port = str.substring(2, 6); //收信方端口 140 if(ta2.getText().indexOf(Port) != -1){ //用戶在線 141 String strRep = str; 142 DatagramPacket outDataPacket = new DatagramPacket(strRep.getBytes(),strRep.getBytes().length, 143 addr, Integer.parseInt(Port)); 144 ds.send(outDataPacket); 145 146 }else{ //用戶不在線 147 if(Port.equals(new String("8888"))){ 148 S1 = str; 149 }else if(Port.equals(new String("8889"))){ 150 S2 = str; 151 }else{ 152 S3 = str; 153 } 154 } 155 String history = ta.getText(); 156 String now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + str.subSequence(8, 12) + "給" + Port + "發送了一條消息。"; 157 ta.setText(now); 158 159 } 160 161 //.... 162 163 } 164 165 } catch (SocketException e1) { 166 // System.err.println("cannot open socket"); 167 ta.setText("cannot open socket"); 168 // System.exit(1); 169 } catch (IOException e1) { 170 // System.err.println("communication error"); 171 ta.setText("communication error"); 172 e1.printStackTrace(); 173 }finally{ 174 ds.close(); 175 } 176 177 } 178 } 179 180 } 181 182
1 /*客戶端*/ 2 import java.awt.Button; 3 import java.awt.Color; 4 import java.awt.Label; 5 import java.awt.TextArea; 6 import java.awt.TextField; 7 import java.awt.event.ActionEvent; 8 import java.awt.event.ActionListener; 9 import javax.swing.JFrame; 10 11 import java.io.*; 12 import java.net.*; 13 import java.text.SimpleDateFormat; 14 import java.util.Date; 15 16 public class talkOnline_Client8888 { 17 public static final int ServerPORT =8081; 18 public static final int LocalPORT =8888; 19 public static final int LocalPORTS =8878; 20 static String current = null; 21 static SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//設置日期格式 22 public static void main(String[] args) { 23 JFrame f = new JFrame(); 24 f.setLayout(null); 25 26 //連接按鈕 27 final Button BConnect = new Button("連接"); 28 BConnect.setBounds(10, 10, 50, 20); 29 30 //系統消息框標簽 31 Label lb1 = new Label("--------消息框--------"); 32 lb1.setBounds(10, 55, 300, 20); 33 34 //系統消息框 35 final TextArea ta = new TextArea(); 36 ta.setBackground(Color.gray); 37 ta.setEditable(false); 38 //ta.setSize(300, 300); 39 ta.setBounds(10, 80, 400, 400); 40 41 42 //當前在線好友列表標簽 43 Label lb2 = new Label("--------好友列表--------"); 44 lb2.setBounds(450, 55, 400, 20); 45 46 //好友列表框 47 final Button B8889 = new Button("8889"); 48 B8889.setBounds(450, 80, 200, 20); 49 B8889.setEnabled(false); 50 final Button B8890 = new Button("8890"); 51 B8890.setBounds(450, 120, 200, 20); 52 B8890.setEnabled(false); 53 54 //發送框 55 final TextField tf = new TextField(); 56 tf.setEditable(true); 57 tf.setBounds(10, 500, 400, 20); 58 59 //發送按鈕 60 final Button BSend = new Button("發送"); 61 BSend.setBounds(450, 500, 150, 20); 62 BSend.setEnabled(false); 63 64 65 //將組件加入到框架 66 f.add(BConnect); 67 f.add(lb1); 68 f.add(ta); 69 f.add(lb2); 70 f.add(tf); 71 f.add(BSend); 72 f.add(B8889); 73 f.add(B8890); 74 75 f.setSize(700,600); 76 f.setVisible(true); 77 f.setResizable(false); 78 f.setTitle("網絡交談_客戶端 8888"); 79 f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 80 81 82 83 BConnect.addActionListener(new ActionListener(){ 84 InetAddress addr; 85 DatagramSocket datagramSocket; 86 @Override 87 public void actionPerformed(ActionEvent e) { 88 89 try { 90 //建立數據報socket 91 addr = InetAddress.getByName("localhost"); 92 datagramSocket = new DatagramSocket(LocalPORT); 93 datagramSocket.setSoTimeout(3000); //設置3秒超時 94 95 //發送登錄消息給服務器 96 String str = "Request Connect"; 97 DatagramPacket login = new DatagramPacket(str.getBytes(), str.length(), addr, ServerPORT); 98 datagramSocket.send(login); 99 100 //接受服務器的確認數據報 101 byte[] msg = new byte[100]; 102 DatagramPacket inDataPacket = new DatagramPacket(msg, msg.length); 103 datagramSocket.receive(inDataPacket); 104 String receivedMsg = new String (inDataPacket.getData(), 0, inDataPacket.getLength()); 105 if(receivedMsg.equals("Connection Confirm")){ 106 ta.setText("【" + df.format(new Date()) +"】" + "UDP已建立鏈接~" + addr.getHostName()+ ":" + addr.getHostAddress()); 107 //將好友按鈕設為可用 108 B8889.setEnabled(true); 109 B8890.setEnabled(true); 110 BConnect.setEnabled(false); 111 try { 112 datagramSocket.setSoTimeout(0); 113 } catch (SocketException e1) { 114 // TODO Auto-generated catch block 115 e1.printStackTrace(); 116 } 117 Mythread t1 = new Mythread(datagramSocket, ta); 118 t1.start(); 119 120 }else{ 121 String history = ta.getText(); 122 String now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + "UDP連接建立失敗"; 123 ta.setText(now); 124 datagramSocket.close(); 125 } 126 }catch(SocketTimeoutException e1) { 127 String history = ta.getText(); 128 String now; 129 if(!history.equals("")){ 130 now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + "連接超時!"; 131 }else{ 132 now = "【" + df.format(new Date()) +"】" + "連接超時!"; 133 } 134 ta.setText(now); 135 datagramSocket.close(); 136 } catch (UnknownHostException e2) { 137 // TODO Auto-generated catch block 138 e2.printStackTrace(); 139 } catch (IOException e3) { 140 // TODO Auto-generated catch block 141 e3.printStackTrace(); 142 } 143 144 145 } 146 }); 147 148 //按下好友按鈕,發送按鈕可用 149 B8889.addActionListener(new ActionListener(){ 150 @Override 151 public void actionPerformed(ActionEvent e) { 152 153 current = "8889"; 154 BSend.setEnabled(true); 155 } 156 157 }); 158 159 B8890.addActionListener(new ActionListener(){ 160 @Override 161 public void actionPerformed(ActionEvent e) { 162 current = "8890"; 163 BSend.setEnabled(true); 164 } 165 166 }); 167 168 BSend.addActionListener(new ActionListener(){ 169 170 public void actionPerformed(ActionEvent e){ 171 try { 172 //建立數據報socket 173 InetAddress addr = InetAddress.getByName("localhost"); 174 DatagramSocket datagramSocket = new DatagramSocket(LocalPORTS); 175 176 //構造數據報並發送 177 String str = "T0" + current + "FR" + "8888" + ":" + tf.getText(); 178 DatagramPacket out = new DatagramPacket(str.getBytes(), str.getBytes().length, addr, ServerPORT); 179 datagramSocket.send(out); 180 181 //清空發送框 182 tf.setText(null); 183 184 //顯示 185 String history = ta.getText(); 186 String now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" + "消息發送成功!"; 187 ta.setText(now); 188 //關閉數據報 189 datagramSocket.close(); 190 191 } catch (UnknownHostException e1) { 192 // TODO Auto-generated catch block 193 e1.printStackTrace(); 194 } catch (IOException e1) { 195 // TODO Auto-generated catch block 196 e1.printStackTrace(); 197 } 198 } 199 }); 200 } 201 202 }
1 /*線程操作類*/ 2 import java.awt.TextArea; 3 import java.io.IOException; 4 import java.net.DatagramPacket; 5 import java.net.DatagramSocket; 6 import java.text.SimpleDateFormat; 7 import java.util.Date; 8 9 public class Mythread extends Thread{ 10 DatagramSocket datagramsocket; 11 private TextArea ta; 12 public Mythread(DatagramSocket datagramsocket, TextArea ta){ 13 this.datagramsocket = datagramsocket; 14 this.ta = ta; 15 } 16 17 public void run(){ 18 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//設置日期格式 19 byte[] buf = new byte[1000]; 20 DatagramPacket inDataPacket = new DatagramPacket(buf, buf.length); 21 while(true){ 22 try { //接收 23 datagramsocket.receive(inDataPacket); 24 } catch (IOException e) { 25 // TODO Auto-generated catch block 26 e.printStackTrace(); 27 } 28 String receivedMsg = ""; 29 receivedMsg = new String (inDataPacket.getData(), 0, inDataPacket.getLength()); 30 String PrintMsg = receivedMsg.substring(8); 31 String history = ta.getText(); 32 String now = String.format("%s%n", history) + "【" + df.format(new Date()) +"】" +PrintMsg; 33 ta.setText(now); 34 } 35 } 36 }