群聊-聊天室
群聊:任何时候,任何一个客户端都可以向其它客户端发送和接受数据,服务器只起到转发的作用。
1、首先创建一个聊天室的简易版(版本1)。
需求:可以多个用户同时访问服务端,并且可以不断各自请求服务端获取响应的数据。
可以多个用户同时访问服务端:这个需要在服务端创建多线程,使服务端的监听套接字,可以被多个客户端使用。
可以不断各自请求服务端获取响应的数据:这个只需要在客户端的数据发送和接受处加上一层死循环,在服务端的外层套上一层死循环即可。
需要改进的不足之处:
1、客户端只能自己对自己说话,还没有实现群聊的效果。
2、代码比较多,不易于维护。
3、客户端的接收和发送数据没有分离,必须等到指定者发送数据,才能接收指定者的消息。
服务端
代码:
1 package 在线聊天室;
2
3 import java.io.DataInputStream;
4 import java.io.DataOutputStream;
5 import java.io.IOException;
6 import java.net.ServerSocket;
7 import java.net.Socket;
8
9 /**
10 * 模拟单人聊天室
11 * @author liuzeyu12a
12 *
13 */
14 public class Chat {
15 public static void main(String[] args) throws Exception {
16 //建立服务器端套接字,绑定本地端口
17 ServerSocket server = new ServerSocket(9999);
18 while(true){
19 new Thread(()->{
20 Socket client = null;
21 try {
22 //监听客户端
23 client = server.accept();
24 } catch (IOException e) {
25 e.printStackTrace();
26 }
27 System.out.println("一个客户端建立了连接...");
28
29 //接受客户端的消息
30 DataInputStream dis = null;
31 DataOutputStream dos = null;
32 try {
33 dis = new DataInputStream(client.getInputStream());
34 dos = new DataOutputStream(client.getOutputStream());
35 } catch (IOException e) {
36 e.printStackTrace();
37 }
38
39 boolean isRunning = true;
40 while(isRunning) {
41 String msg = null;
42 try {
43 msg = dis.readUTF();
44 } catch (IOException e) {
45 e.printStackTrace();
46 }
47 //返回回去给客户端
48 try {
49 dos.writeUTF(msg);
50 dos.flush();
51 } catch (IOException e) {
52 isRunning = false; //客户端断开即停止读写数据
53 }
54 }
55
56 //释放资源
57 try {
58 if(null!=dos)
59 dos.close();
60 } catch (IOException e) {
61 e.printStackTrace();
62 }
63 try {
64 if(null!=dis)
65 dis.close();
66 } catch (IOException e) {
67 e.printStackTrace();
68 }
69 try {
70 if(null!=client)
71 client.close();
72 } catch (IOException e) {
73 e.printStackTrace();
74 }
75 }).start();
76
77 }
78 }
79 }
View Code
客户端
代码:
1 package 在线聊天室;
2
3 import java.io.BufferedReader;
4 import java.io.DataInputStream;
5 import java.io.DataOutputStream;
6 import java.io.IOException;
7 import java.io.InputStreamReader;
8 import java.net.Socket;
9
10 /**
11 * TCP模拟单人聊天室
12 * @author liuzeyu12a
13 *
14 */
15 public class Client {
16 public static void main(String[] args) throws IOException, IOException {
17 //建立客户端套接字
18 Socket client = new Socket("localhost",9999);
19
20 //发送数据
21 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
22 DataInputStream dis = new DataInputStream(client.getInputStream());
23 DataOutputStream dos = new DataOutputStream(client.getOutputStream());
24 boolean isRunning = true;
25 while(isRunning) {
26 String msg = reader.readLine();
27 dos.writeUTF(msg);
28 //客户端接收服务器的响应
29
30 String respond = dis.readUTF();
31 System.out.println(respond);
32 }
33 //关闭
34 client.close();
35 }
36 }
View Code
2、我们将版本1的2,3问题进行改善一下(版本2)。
封装:
1)将服务器的接受和发送数据用一个Channel 类进行封装,这样子一个client 就对应了一个Channel对象了
2)将客户端的接收和发送分离开,使用两个线程进行分割,这样子接收数据和发送数据就可以不用同时进行了(为群聊做准备)。
将关闭资源的系列用一个ChatUtils工具类包装起来,使用Closeable接口。
1 package 在线聊天室;
2
3 import java.io.Closeable;
4 import java.io.IOException;
5 /**
6 * 用于聊天室一些流释放资源
7 * @author liuzeyu12a
8 *
9 */
10 public class ChatUtils {
11
12 static public void close(Closeable...closeables) {
13 for(Closeable target :closeables) {
14 if(null!=target) {
15 try {
16 target.close();
17 } catch (IOException e) {
18 e.printStackTrace();
19 }
20 }
21 }
22 }
23 }
利用面向对象思想对服务端进行封装
1 package 在线聊天室;
2
3 import java.io.DataInputStream;
4 import java.io.DataOutputStream;
5 import java.io.IOException;
6 import java.net.ServerSocket;
7 import java.net.Socket;
8
9 /**
10 * 1)将服务器的接受和发送数据用一个Channel 类进行封装,
11 * 这样子一个client 就对应了一个Channel对象了
12
13 2)将客户端的接收和发送分离开,
14 使用两个线程进行分割,这样子接收数据和发送数据就可以不用同时进行了
15 (为群聊做准备)。
16 * @author liuzeyu12a
17 *
18 */
19 public class Chat2 {
20 public static void main(String[] args) throws Exception {
21 System.out.println("-----Server-----");
22 //建立服务器端地址,并绑定本地端口
23 ServerSocket server = new ServerSocket(8989);
24
25 //这边加上死循环是为了接受多个客户的请求
26 while(true) {
27 //监听
28 Socket client = server.accept();
29 System.out.println("一个客户端建立了连接");
30 new Thread(new Channel(client)).start();
31 }
32
33 }
34 //静态内部类,封装处理客户端的数据
35 static class Channel implements Runnable{
36 private DataInputStream dis;
37 private DataOutputStream dos;
38 private Socket client;
39 private boolean isRunning;
40 //构造器
41 public Channel(Socket client) {
42 this.client = client;
43 this.isRunning = true;
44 try {
45 dis = new DataInputStream(
46 client.getInputStream());
47 dos = new DataOutputStream(
48 client.getOutputStream());
49 } catch (IOException e) {
50 release();
51 }
52 }
53 @Override
54 public void run() {
55 while(isRunning) {
56 String msg = receive();
57 if(!msg.equals(""))
58 send(msg);
59 }
60 }
61
62 //发送数据
63 public void send(String msg) {
64 try {
65 dos.writeUTF(msg);
66 dos.flush();
67 } catch (IOException e) {
68 System.out.println("发送数据失败");
69 release();
70 }
71 }
72
73 //接受数据
74 public String receive() {
75 try {
76 String msg = "";
77 msg = dis.readUTF();
78 return msg;
79 } catch (IOException e) {
80 isRunning = false;
81 System.out.println("接受数据失败");
82 release();
83 }
84 return "";
85 }
86
87 //释放资源
88 public void release() {
89 ChatUtils.close(client,dos,dis);
90 }
91 }
92 }
View Code
客户端的发送:
1 package 在线聊天室;
2
3 import java.io.BufferedReader;
4 import java.io.DataOutputStream;
5 import java.io.IOException;
6 import java.io.InputStreamReader;
7 import java.net.Socket;
8
9 /**
10 * 为聊天室Client2 的发送功能服务
11 * @author liuzeyu12a
12 *
13 */
14 public class Send implements Runnable{
15 //准备数据
16 private BufferedReader reader;
17 //发送数据
18 private DataOutputStream dos;
19 private Socket client;
20 private boolean isRunning;
21 //构造器
22 public Send(Socket client) {
23 this.client = client ;
24 this.isRunning = true;
25 reader= new BufferedReader(
26 new InputStreamReader(System.in));
27 try {
28 dos= new DataOutputStream(client.getOutputStream());
29 } catch (IOException e) {
30 System.out.println("DataOutputStream对象创建失败");
31 release();
32 }
33 }
34 @Override
35 public void run() {
36 while(isRunning) {
37 send("");
38 }
39 }
40
41 //发送数据
42 public void send(String msg) {
43 try {
44 msg = getMsgFromConsole();
45 dos.writeUTF(msg);
46 dos.flush();
47 } catch (IOException e) {
48 System.out.println("数据发送失败");
49 release();
50 }
51 }
52
53 public String getMsgFromConsole(){
54 try {
55 return reader.readLine();
56 } catch (IOException e) {
57 e.printStackTrace();
58 }
59 return null;
60 }
61 //释放资源
62 public void release() {
63 isRunning = false;
64 ChatUtils.close(dos,client,reader);
65 }
66 }
View Code
客户端的接收:
1 package 在线聊天室;
2
3 import java.io.DataInputStream;
4 import java.io.IOException;
5 import java.net.Socket;
6
7 /**
8 * 为聊天室Client2 的接收功能服务
9 * @author liuzeyu12a
10 *
11 */
12 public class Receive implements Runnable{
13
14 private boolean isRunning;
15 private DataInputStream dis;
16 private Socket client;
17 //构造器
18 public Receive(Socket client) {
19 this.client = client;
20 this.isRunning = true;
21 try {
22 dis = new DataInputStream(client.getInputStream());
23 } catch (IOException e) {
24 System.out.println("DataInputStream对象创建失败失败");
25 release();
26 }
27 }
28 @Override
29 public void run() {
30 while(isRunning) {
31 String msg = "";
32 msg = recevie();
33 if(!msg.equals(""))
34 System.out.println(msg);
35 }
36 }
37
38 public String recevie() {
39 String respone = null;
40 try {
41 respone = dis.readUTF();
42 return respone;
43 } catch (IOException e) {
44 release();
45 System.out.println("数据接收失败");
46 }
47 return null;
48 }
49
50
51 //释放资源
52 public void release() {
53 isRunning = false;
54 ChatUtils.close(dis,client);
55 }
56 }
View Code
客户端的封装:
1 package 在线聊天室;
2
3 import java.io.IOException;
4 import java.net.Socket;
5
6 /**
7 * 1)将服务器的接受和发送数据用一个Channel 类进行封装,
8 * 这样子一个client 就对应了一个Channel对象了
9
10 2)将客户端的接收和发送分离开,
11 使用两个线程进行分割,这样子接收数据和发送数据就可以不用同时进行了
12 (为群聊做准备)。
13 * @author liuzeyu12a
14 *
15 */
16 public class Client2 {
17 public static void main(String[] args) throws IOException, IOException {
18 System.out.println("-----Server-----");
19 //创建Socket套接字,绑定服务器端口
20 Socket client =new Socket("localhost",8989);
21
22 //发送数据
23 new Thread(new Send(client)).start();
24 //接收数据
25 new Thread(new Receive(client)).start();
26 }
27 }
View Code
3、实现简单的群聊功能。
服务器可以实现数据的转发功能,客户端不再局限于自己对话自己,是一个典型的群聊案例,重点理解如何将name进行传递。
客户端接受:
1 package 在线聊天室过渡版;
2
3 import java.io.DataInputStream;
4 import java.io.IOException;
5 import java.net.Socket;
6
7 /**
8 * 为聊天室Client2 的接收功能服务
9 * @author liuzeyu12a
10 *
11 */
12 public class Receive implements Runnable{
13
14 private boolean isRunning;
15 private DataInputStream dis;
16 private Socket client;
17 //构造器
18 public Receive(Socket client) {
19 this.client = client;
20 this.isRunning = true;
21 try {
22 dis = new DataInputStream(client.getInputStream());
23 } catch (IOException e) {
24 System.out.println("DataInputStream对象创建失败失败");
25 release();
26 }
27 }
28 @Override
29 public void run() {
30 while(isRunning) {
31 String msg = "";
32 msg = receive();
33 if(!msg.equals("")) {
34 System.out.println(msg);
35 }
36
37 }
38 }
39 public String receive() {
40 String respone = null;
41 try {
42 respone = dis.readUTF();
43 return respone;
44 } catch (IOException e) {
45 release();
46 System.out.println("数据接收失败");
47 }
48 return "";
49 }
50
51 //释放资源
52 public void release() {
53 isRunning = false;
54 ChatUtils.close(dis,client);
55 }
56 }
View Code
客户端发送:
1 package 在线聊天室过渡版;
2
3 import java.io.BufferedReader;
4 import java.io.DataOutputStream;
5 import java.io.IOException;
6 import java.io.InputStreamReader;
7 import java.net.Socket;
8
9 /**
10 * 为聊天室Client2 的发送功能服务
11 * @author liuzeyu12a
12 *
13 */
14 public class Send implements Runnable{
15 //准备数据
16 private BufferedReader reader;
17 //发送数据
18 private DataOutputStream dos;
19 private Socket client;
20 private boolean isRunning;
21 private String name;
22 //构造器
23 public Send(Socket client,String name) {
24 this.client = client ;
25 this.isRunning = true;
26 //获取名称
27 this.name = name;
28 reader= new BufferedReader(
29 new InputStreamReader(System.in));
30 try {
31 dos= new DataOutputStream(client.getOutputStream());
32 send(name); //发送名称
33 } catch (IOException e) {
34 System.out.println("DataOutputStream对象创建失败");
35 release();
36 }
37 }
38 @Override
39 public void run() {
40 while(isRunning) {
41 String msg = null;
42 try {
43 msg = reader.readLine();
44 } catch (IOException e) {
45 System.out.println("数据写入失败");
46 release();
47 }
48 send(msg);
49
50 }
51 }
52 public void send(String msg) {
53 try {
54 dos.writeUTF(msg);
55 dos.flush();
56 } catch (IOException e) {
57 System.out.println("数据发送失败");
58 release();
59 }
60 }
61 //释放资源
62 public void release() {
63 ChatUtils.close(dos,client,reader);
64 }
65 }
View Code
客户端封装:
1 package 在线聊天室过渡版;
2
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.InputStreamReader;
6 import java.net.Socket;
7
8 /**
9 * 可以实现简单的群聊了。
10 * @author liuzeyu12a
11 *
12 */
13 public class Client2 {
14 public static void main(String[] args) throws IOException, IOException {
15 System.out.println("-----Client-----");
16 System.out.println("请输入用户名:");
17 BufferedReader reader = new BufferedReader(
18 new InputStreamReader(System.in));
19 String name = reader.readLine();
20
21 //创建Socket套接字,绑定服务器端口
22 Socket client =new Socket("localhost",8989);
23
24 //发送数据
25 new Thread(new Send(client,name)).start();
26 //接收数据
27 new Thread(new Receive(client)).start();
28 }
29 }
View Code
服务端:
1 package 在线聊天室过渡版;
2
3 import java.io.DataInputStream;
4 import java.io.DataOutputStream;
5 import java.io.IOException;
6 import java.net.ServerSocket;
7 import java.net.Socket;
8 import java.util.concurrent.CopyOnWriteArrayList;
9
10 /**
11 * 可以实现简单的群聊了。
12 * @author liuzeyu12a
13 *
14 */
15 public class Chat2 {
16 //用于存储客户端的容器,涉及到多线程的并发操作,
17 //使用CopyOnWriteArrayList保证线程的安全
18 private static CopyOnWriteArrayList<Channel> all =
19 new CopyOnWriteArrayList<Channel>();
20 public static void main(String[] args) throws Exception {
21 System.out.println("-----Server-----");
22 //建立服务器端地址,并绑定本地端口
23 ServerSocket server = new ServerSocket(8989);
24
25 //这边加上死循环是为了接受多个客户的请求
26 while(true) {
27 //监听
28 Socket client = server.accept();
29 Channel c = new Channel(client);
30 all.add(c); //添加一个客户端
31 System.out.println("一个客户端建立了连接");
32 new Thread(c).start();
33 }
34 }
35 //静态内部类,封装处理客户端的数据
36 static class Channel implements Runnable{
37 private DataInputStream dis;
38 private DataOutputStream dos;
39 private Socket client;
40 private boolean isRunning;
41 private String name;
42 //构造器
43 public Channel(Socket client) {
44
45 this.client = client;
46 this.isRunning = true;
47 try {
48 dis = new DataInputStream(
49 client.getInputStream());
50 dos = new DataOutputStream(
51 client.getOutputStream());
52 this.name = receive(); //接收客户端的名称
53 this.send("欢迎光临聊天室..");
54 this.sendOther(this.name+"来到了聊天室...", true);
55 } catch (IOException e) {
56 release();
57 }
58 }
59 @Override
60 public void run() {
61 while(isRunning) {
62 String msg = receive();
63 if(!msg.equals("")) {
64 //send(msg);
65 sendOther(msg,false);
66 }
67
68 }
69 }
70
71 //发送数据
72 public void send(String msg) {
73 try {
74 dos.writeUTF(msg);
75 dos.flush();
76 } catch (IOException e) {
77 System.out.println("发送数据失败");
78 release();
79 }
80 }
81 /**
82 * 获取自己的消息,然后发送给其它人
83 * boolean isSys表示是否为系统消息
84 * @return
85 */
86 public void sendOther(String msg,boolean isSys) {
87 for(Channel other:all) {
88 if(other == this) { //不再自己发给自己了
89 continue;
90 }
91 if(!isSys) {
92 other.send(this.name+"对大家说:"+msg); //发送给自己
93 }else {
94 other.send(msg);
95 }
96 }
97 }
98
99 //接受数据
100 public String receive() {
101 try {
102 String msg = "";
103 msg = dis.readUTF();
104 return msg;
105 } catch (IOException e) {
106 isRunning = false;
107 System.out.println("接受数据失败");
108 release();
109 }
110 return "";
111 }
112
113 //释放资源
114 public void release() {
115 this.isRunning = false;
116 ChatUtils.close(client,dos,dis);
117 all.remove(this);
118 this.sendOther(this.name+"离开了聊天室...", true);
119 }
120 }
121 }
View Code
4、群聊功能升级(可以实现私聊某一个人@)。
只需要在发送消息的地方动手脚,其它的代码不变即可。
约定私聊的格式:@xxx:消息内容
1 /**
2 * 获取自己的消息,然后发送给其它人
3 * boolean isSys表示是否为系统消息
4 * 添加私聊的功能:可以向某一特定的用户发送数据
5 * 约定格式:@xxx:数据
6 * @return
7 */
8 public void sendOther(String msg,boolean isSys) {
9 //判断数据是否以@开头
10 boolean isPrivate = msg.startsWith("@");
11 if(isPrivate) {
12 //寻找:
13 int index = msg.indexOf(":");
14 //截取名字
15 String targetName = msg.substring(1, index);
16 //截取消息内容
17 String datas = msg.substring(index+1);
18 for(Channel other :all) {
19 if(other.name.equals(targetName)) {
20 other.send(this.name+"悄悄对你说:"+datas);
21 }
22 }
23
24 }else {
25 for(Channel other:all) {
26 if(other == this) { //不再自己发给自己了
27 continue;
28 }
29 if(!isSys) {
30 other.send(this.name+"对大家说:"+msg); //发送给自己
31 }else {
32 other.send(msg);
33 }
34 }
35 }
36
37 }
服务端:
1 package 在线聊天室终极版;
2
3 import java.io.DataInputStream;
4 import java.io.DataOutputStream;
5 import java.io.IOException;
6 import java.net.ServerSocket;
7 import java.net.Socket;
8 import java.util.concurrent.CopyOnWriteArrayList;
9
10 /**
11 * 可以实现简单的群聊了。
12 * @author liuzeyu12a
13 *
14 */
15 public class Chat2 {
16 //用于存储客户端的容器,涉及到多线程的并发操作,
17 //使用CopyOnWriteArrayList保证线程的安全
18 private static CopyOnWriteArrayList<Channel> all =
19 new CopyOnWriteArrayList<Channel>();
20 public static void main(String[] args) throws Exception {
21 System.out.println("-----Server-----");
22 //建立服务器端地址,并绑定本地端口
23 ServerSocket server = new ServerSocket(8989);
24
25 //这边加上死循环是为了接受多个客户的请求
26 while(true) {
27 //监听
28 Socket client = server.accept();
29 Channel c = new Channel(client);
30 all.add(c); //添加一个客户端
31 System.out.println("一个客户端建立了连接");
32 new Thread(c).start();
33 }
34 }
35 //静态内部类,封装处理客户端的数据
36 static class Channel implements Runnable{
37 private DataInputStream dis;
38 private DataOutputStream dos;
39 private Socket client;
40 private boolean isRunning;
41 private String name;
42 //构造器
43 public Channel(Socket client) {
44
45 this.client = client;
46 this.isRunning = true;
47 try {
48 dis = new DataInputStream(
49 client.getInputStream());
50 dos = new DataOutputStream(
51 client.getOutputStream());
52 this.name = receive(); //接收客户端的名称
53 this.send("欢迎光临聊天室..");
54 this.sendOther(this.name+"来到了聊天室...", true);
55 } catch (IOException e) {
56 release();
57 }
58 }
59 @Override
60 public void run() {
61 while(isRunning) {
62 String msg = receive();
63 if(!msg.equals("")) {
64 //send(msg);
65 sendOther(msg,false);
66 }
67
68 }
69 }
70
71 //发送数据
72 public void send(String msg) {
73 try {
74 dos.writeUTF(msg);
75 dos.flush();
76 } catch (IOException e) {
77 System.out.println("发送数据失败");
78 release();
79 }
80 }
81 /**
82 * 获取自己的消息,然后发送给其它人
83 * boolean isSys表示是否为系统消息
84 * 添加私聊的功能:可以向某一特地呢的用户发送数据
85 * 约定格式:@xxx:数据
86 * @return
87 */
88 public void sendOther(String msg,boolean isSys) {
89 //判断数据是否以@开头
90 boolean isPrivate = msg.startsWith("@");
91 if(isPrivate) {
92 //寻找:
93 int index = msg.indexOf(":");
94 //截取名字
95 String targetName = msg.substring(1, index);
96 //截取消息内容
97 String datas = msg.substring(index+1);
98 for(Channel other :all) {
99 if(other.name.equals(targetName)) {
100 other.send(this.name+"悄悄对你说:"+datas);
101 }
102 }
103
104 }else {
105 for(Channel other:all) {
106 if(other == this) { //不再自己发给自己了
107 continue;
108 }
109 if(!isSys) {
110 other.send(this.name+"对大家说:"+msg); //发送给自己
111 }else {
112 other.send(msg);
113 }
114 }
115 }
116
117 }
118
119 //接受数据
120 public String receive() {
121 try {
122 String msg = "";
123 msg = dis.readUTF();
124 return msg;
125 } catch (IOException e) {
126 isRunning = false;
127 System.out.println("接受数据失败");
128 release();
129 }
130 return "";
131 }
132
133 //释放资源
134 public void release() {
135 this.isRunning = false;
136 ChatUtils.close(client,dos,dis);
137 all.remove(this);
138 this.sendOther(this.name+"离开了聊天室...", true);
139 }
140 }
141 }
View Code
截图:
小结:
1、模拟多人聊天室,代码比较多,重点在于多线程,TCP数据的传递,面向对象的封装。
2、利用面向对象的思想可以对代码进行封装,简化代码,提高代码的可维护性。
3、多个客户端可以在服务端使用容器进行装载。
3、在使用容器中,多线程如果涉及到多个同步的操作,如聊天室中可能在聊天中忽然有人退出,有人加入,
容易造成数据的错误发送和接收,可以使用JUC并发容器CopyOnWriteArrayList(再操作容器前copy一个副本存起来,一旦数据有错,就启用副本数据
来保证数据的安全性)。