群聊-聊天室
群聊:任何时候,任何一个客户端都可以向其它客户端发送和接受数据,服务器只起到转发的作用。
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 }
客户端
代码:
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 }
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 }
客户端的发送:
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 }
客户端的接收:
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 }
客户端的封装:
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 }
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 }
客户端发送:
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 }
客户端封装:
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 }
服务端:
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 }
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 }
截图:
小结:
1、模拟多人聊天室,代码比较多,重点在于多线程,TCP数据的传递,面向对象的封装。
2、利用面向对象的思想可以对代码进行封装,简化代码,提高代码的可维护性。
3、多个客户端可以在服务端使用容器进行装载。
3、在使用容器中,多线程如果涉及到多个同步的操作,如聊天室中可能在聊天中忽然有人退出,有人加入,
容易造成数据的错误发送和接收,可以使用JUC并发容器CopyOnWriteArrayList(再操作容器前copy一个副本存起来,一旦数据有错,就启用副本数据
来保证数据的安全性)。