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)的私人消息。

 

posted @ 2016-11-20 18:05  fysola  阅读(3820)  评论(0编辑  收藏  举报