JAVA基礎知識之網絡編程——-基於TCP通信的簡單聊天室


下面將基於TCP協議用JAVA寫一個非常簡單的聊天室程序, 聊天室具有以下功能,

  • 在服務器端,可以接受客戶端注冊(用戶名),可以顯示注冊成功的賬戶
  • 在客戶端,可以注冊一個賬號,並用這個賬號發送信息
  • 發送信息有兩種模式,一種是群聊,所有在線用戶都可以看到消息,另一種是私聊,只針對指定賬戶發送消息

下面是主要的實現思路,

  1. 首先是服務器端, 需要使用多線程實現。 主線程用來循環監聽客戶端的連接請求, 一旦接收到一個請求,就為這個客戶端創建一個專用通信線程。
  2. 服務器端依靠一個經過重寫的map保存在線的客戶端賬戶以及建立連接后的通信句柄(inputStream/outputStream)
  3. 服務器端和客戶端通信使用約定好的自定義協議,將在雙方發送的消息中添加固定消息頭和消息尾。 通信雙發都使用socket的inputStream和outStream讀和寫消息,與本地IO區別不大。
  4. 當服務器端接收到客戶端的請求的時候,先解析出消息頭和尾,根據約定的通信協議來判斷消息類型,是注冊賬戶,還是群發消息,還是私聊消息
  5. 對於群發消息,服務器端將遍歷在線的所有用戶(線程),然后將消息廣播出去
  6. 對於私聊消息,服務器端根據客戶端發來的目的地址(收信賬戶),去map中查找到通信線程句柄(outputStream),然后將信息發送給指定賬戶
  7. 對於每個客戶端,都創建兩個線程。 主線程用來做鍵盤輸入, 輔線程用來接收服務器發回的消息
  8. 客戶端的主線程中,所有消息都是先發送到服務器端,再由服務器端決定分發策略。
  9. 包括注冊賬戶在內,服務器和客戶端雙方所有消息都是經過約定協議包裝過的,這樣服務器才能讀取消息的屬性,進行指定操作。

服務器端實現如下,

首先我們要自定義一個通信協議,服務器端和客戶端需要使用同一種協議,用來描述消息的屬性,

 1 package chat;
 2 
 3 public interface ChatProtocol {
 4     int PROTOCOL_LEN = 2;
 5     
 6     //協議字符串,會加入數據包中
 7     String MSG_ROND = "##";
 8     String USER_ROND = "@@";
 9     String LOGIN_SUCCESS = "1";
10     String NAME_REP = "-1";
11     String PRIVATE_ROND = "%%";
12     String SPLIT_SIGN = "}";
13     
14 }

服務器端的監聽線程(主線程)

 1 package chat;
 2 
 3 import java.io.IOException;
 4 import java.io.PrintStream;
 5 import java.net.ServerSocket;
 6 import java.net.Socket;
 7 
 8 public class Server {
 9     private static final int SERVER_PORT = 33000;
10     public static ChatMap<String, PrintStream> clients = new ChatMap();
11     public void init() {
12         try {
13             ServerSocket ss = new ServerSocket(SERVER_PORT) ;
14             while(true) {
15                 Socket socket = ss.accept();
16                 new ServerThread(socket).start();
17             }
18         } catch (IOException e) {
19             e.printStackTrace();
20         }
21     }
22     public static void main(String[] args) {
23         Server server = new Server();
24         server.init();
25     }
26 }

服務器端需要使用一個重寫的map來存放用戶名和對應的通信句柄, 這樣才能實現私聊功能,

 1 package chat;
 2 
 3 import java.util.Collections;
 4 import java.util.HashMap;
 5 import java.util.HashSet;
 6 import java.util.Map;
 7 import java.util.Set;
 8 
 9 //用map來保存用戶和socket輸出流的對應關系,
10 //K將會是String類型的用戶名,不允許重復
11 //V是從socket返回的outputStream對象,也不允許重復
12 public class ChatMap<K,V> {
13     public Map<K,V> map = Collections.synchronizedMap(new HashMap<K,V>());
14     
15     //根據outputStream對象刪除制定項
16     public synchronized void removeByValue(Object value) {
17         for (Object key : map.keySet()) {
18             if(map.get(key) == value) {
19                 map.remove(key);
20                 break;
21             }
22         }
23     }
24     
25     //獲取outputStream對象組成的Set
26     public synchronized Set<V> valueSet() {
27         Set<V> result = new HashSet<V>();
28         //遍歷map,將map的value存入Set
29         for(K key :  map.keySet()) {
30             result.add(map.get(key));
31         }
32         /*
33          for (Map.Entry<K, V> entry : map.entrySet()) {
34              result.add(entry.getValue());
35          }
36          */
37          return result;
38     }
39     
40     //根據ouputStream對象查找用戶名
41     public synchronized K getKeyByValue(V val) {
42         for(K key : map.keySet()) {
43             if (map.get(key) == val || map.get(key).equals(val)) {
44                 return key;
45             }
46         }
47         return null;
48     }
49     
50     //實現put,key和value都不允許重復
51     public synchronized V put(K key, V value) {
52         for (V val : valueSet() ) {
53                 if (val.equals(value) && val.hashCode() == value.hashCode()) {
54                     throw new RuntimeException("此輸入流已經被使用");
55             }
56         }
57         return map.put(key, value);
58     }
59 }

對每一個客戶端請求創建一個通信子線程

 1 package chat;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.IOException;
 5 import java.io.InputStreamReader;
 6 import java.io.PrintStream;
 7 import java.net.Socket;
 8 
 9 public class ServerThread extends Thread{
10     private Socket socket;
11     BufferedReader br = null;
12     PrintStream ps = null;
13     
14     public ServerThread(Socket socket) {
15         this.socket = socket;
16     }
17 
18     public void run() {
19         try {
20             br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
21             //一個客戶端的輸出流對象
22             ps = new PrintStream(socket.getOutputStream());
23             String line = null;
24             while((line = br.readLine()) != null) {
25                 //如果消息以ChatProtocol.USER_ROND開始,並以其結束
26                 //則可以確定讀到的是用戶登錄的用戶名
27                 if(line.startsWith(ChatProtocol.USER_ROND) &&
28                         line.endsWith(ChatProtocol.USER_ROND)) {
29                     String userName = getRealMsg(line);
30                     //用戶名不允許重復
31                     if(Server.clients.map.containsKey(userName)) {
32                         System.out.println("用戶名重復");
33                         ps.println(ChatProtocol.NAME_REP);
34                     } else {
35                         System.out.println("["+userName+"] 注冊成功,你可以開始聊天了!");
36                         ps.println(ChatProtocol.LOGIN_SUCCESS);
37                         //將用戶名和輸出流對象組成的鍵值關聯對存入前面經過改造的map
38                         Server.clients.map.put(userName, ps);
39                     }
40                 } //如果消息以ChatProtocol.PRIVATE_ROND開頭並以ChatProtocol.PRIVATE_ROND結尾
41                 //則可以確定是私聊信息
42                 else if (line.startsWith(ChatProtocol.PRIVATE_ROND ) && 
43                         line.endsWith(ChatProtocol.PRIVATE_ROND)) {
44                     String userAndMsg = getRealMsg(line);
45                     
46                     //以SPILT_SIGN分割字符串,前半是用戶名,后半是聊天信息
47                     String user = userAndMsg.split(ChatProtocol.SPLIT_SIGN)[0];
48                     String msg = userAndMsg.split(ChatProtocol.SPLIT_SIGN)[1];
49                     //根據用戶名在map中找出輸出流對象,進行私聊信息發送
50                     Server.clients.map.get(user).println("[私聊信息] [來自 "+Server.clients.getKeyByValue(ps)+"] : " + msg);
51                     
52                 }
53                 // 群聊信息,廣播消息
54                 else {
55                     String msg = getRealMsg(line);
56                     for(PrintStream clientPs :  Server.clients.valueSet()) {
57                         clientPs.println("[群發信息] [來自 "+Server.clients.getKeyByValue(ps)+"] : " + msg);
58                     }
59                 }
60             }
61         } catch (IOException e) {
62             //e.printStackTrace();
63             Server.clients.removeByValue(ps);
64             System.out.println(Server.clients.map.size());
65             try {
66                 if (br != null) {
67                     br.close();
68                 } 
69                 
70                 if (ps != null) {
71                     ps.close();
72                 }
73                 
74                 if (socket != null) {
75                     socket.close();
76                 }
77             } catch (IOException ex) {
78                 ex.printStackTrace();
79             }
80         }
81     }
82 
83     private String getRealMsg(String line) {
84         return line.substring(ChatProtocol.PROTOCOL_LEN, line.length() - ChatProtocol.PROTOCOL_LEN);
85     }
86 
87 
88 }

 

下面開始寫客戶端, 客戶端和服務器端是兩個完全獨立的應用, 可以新建工程寫一個客戶端,

為了簡單起見,我將服務器端和客戶端放在了同一個工程的同一個包下, 這樣可以共享一下協議接口 ChatProtocol.java

首先是客戶端主程序,用來完成鍵盤輸入操作, 其中注冊用戶名的地方調用了一點點java的gui編程接口swi,彈出對話框輸入用戶名,

 1 package chat;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.IOException;
 5 import java.io.InputStreamReader;
 6 import java.io.PrintStream;
 7 import java.net.Socket;
 8 
 9 import javax.swing.JOptionPane;
10 
11 public class Client {
12     private static final int SERVER_PORT = 33000;
13     private Socket socket;
14     private PrintStream ps;
15     private BufferedReader brServer;
16     private BufferedReader keyIn;
17     
18     public void init() {
19         try {
20             keyIn = new BufferedReader(new InputStreamReader(System.in));
21             socket = new Socket("127.0.0.1", SERVER_PORT);
22             ps = new PrintStream(socket.getOutputStream());
23             brServer = new BufferedReader(new InputStreamReader(socket.getInputStream()));
24             String tip = "";
25             while(true) {
26                 String userName = JOptionPane.showInputDialog(tip + "輸入用戶名");
27                 ps.println(ChatProtocol.USER_ROND + userName + ChatProtocol.USER_ROND);
28                 
29                 //服務器端響應
30                 String result = brServer.readLine();
31                 if(result.equals(ChatProtocol.NAME_REP)) {
32                     tip = "用戶名重復,請重新輸入";
33                     continue;
34                 }
35                 //登錄成功
36                 if(result.equals(ChatProtocol.LOGIN_SUCCESS)) {
37                     System.out.println("登錄成功,賬號: ["+ userName +"]");
38                     break;
39                 }
40             }
41         } catch (IOException ex) {
42             ex.printStackTrace();
43         }
44         
45         new ClientThread(brServer).start();
46     }
47     
48     private void readAndSend() {
49         try {
50             String line = null;
51             while((line = keyIn.readLine()) != null) {
52                 //如果發送的消息中帶有冒號,且以//開頭,則認為是私聊信息
53                 if(line.indexOf(":") > 0 && line.startsWith("//")) {
54                     line = line.substring(2);
55                     ps.println(ChatProtocol.PRIVATE_ROND 
56                             + line.split(":")[0] 
57                             + ChatProtocol.SPLIT_SIGN 
58                             + line.split(":")[1]
59                             + ChatProtocol.PRIVATE_ROND);
60                 } else {
61                     ps.println(ChatProtocol.MSG_ROND + line + ChatProtocol.MSG_ROND);
62                 }
63             }
64         } catch (IOException ex) {
65             ex.printStackTrace();
66         }
67     }
68 
69     private void closeRs() {
70         try {
71             if (keyIn != null) {
72                 keyIn.close();
73             }
74             
75             if (brServer != null) {
76                 brServer.close();
77             }
78             
79             if (ps != null) {
80                 ps.close();
81             }
82             
83             if (socket != null) {
84                 socket.close();
85             }
86         } catch (IOException ex) {
87             ex.printStackTrace();
88         }
89     }
90     
91     public static void main(String[] args) {
92         Client client = new Client();
93         client.init();
94         client.readAndSend();
95     }
96 }

 

鍵盤操作中,區分群發消息和私聊消息是看消息以什么開頭,以//開頭就是私聊,否則是群發,

私聊時,用冒號隔開收信人和消息內容, 一條私聊消息格式是這樣的    //b:hi, i'm a

下面是客戶端的子線程,專門用來回顯服務器端發回來的消息,

 1 package chat;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.IOException;
 5 
 6 public class ClientThread extends Thread {
 7 
 8     BufferedReader br = null;
 9     
10     public ClientThread(BufferedReader brServer) {
11         this.br = brServer;
12     }
13     
14     public void run() {
15         try {
16             String line = null;
17             while((line = br.readLine()) != null) {
18                 System.out.println(line);
19             }
20         } catch (IOException ex) {
21             ex.printStackTrace();
22         } finally {
23             try {
24                 if (br != null) {
25                     br.close();
26                 }
27             } catch (IOException e) {
28                 e.printStackTrace();
29             }
30         }
31     }
32 
33 }

 

下面是執行結果, 先啟動一個Server端的進程,可以看到啟動之后Server端處於監聽阻塞狀態,

接着分別啟動兩個Client端進程,每次啟動Client進程的時候都會要求輸入用戶名,要保證用戶名不能重復,

接着就可以發送消息了,普通消息將會發送給所有人,即群發, 指定格式的消息將是私聊,例如 //bbb:hi I'm aaa     (這是發給賬戶bbb)的私人消息。

 


免責聲明!

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



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