TCP创建多人聊天室-飞外

群聊-聊天室群聊:任何时候,任何一个客户端都可以向其它客户端发送和接受数据,服务器只起到转发的作用。

1、首先创建一个聊天室的简易版(版本1)。

需求:可以多个用户同时访问服务端,并且可以不断各自请求服务端获取响应的数据。

可以多个用户同时访问服务端:这个需要在服务端创建多线程,使服务端的监听套接字,可以被多个客户端使用。

可以不断各自请求服务端获取响应的数据:这个只需要在客户端的数据发送和接受处加上一层死循环,在服务端的外层套上一层死循环即可。

需要改进的不足之处:

1、客户端只能自己对自己说话,还没有实现群聊的效果。

2、代码比较多,不易于维护。

3、客户端的接收和发送数据没有分离,必须等到指定者发送数据,才能接收指定者的消息。

代码:

 1 package 在线聊天室; 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; 9 /**10 * 模拟单人聊天室11 * @author liuzeyu12a13 */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();27 System.out.println("一个客户端建立了连接...");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();39 boolean isRunning = true;40 while(isRunning) {41 String msg = null;42 try {43 msg = dis.readUTF();44 } catch (IOException e) {45 e.printStackTrace();47 //返回回去给客户端 48 try {49 dos.writeUTF(msg);50 dos.flush();51 } catch (IOException e) {52 isRunning = false; //客户端断开即停止读写数据56 //释放资源57 try {58 if(null!=dos)59 dos.close();60 } catch (IOException e) {61 e.printStackTrace();63 try {64 if(null!=dis)65 dis.close();66 } catch (IOException e) {67 e.printStackTrace();69 try {70 if(null!=client)71 client.close();72 } catch (IOException e) {73 e.printStackTrace();75 }).start();79 }
View Code

代码:

 1 package 在线聊天室; 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;10 /**11 * TCP模拟单人聊天室12 * @author liuzeyu12a14 */15 public class Client {16 public static void main(String[] args) throws IOException, IOException {17 //建立客户端套接字18 Socket client = new Socket("localhost",9999);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 //客户端接收服务器的响应30 String respond = dis.readUTF();31 System.out.println(respond);33 //关闭34 client.close();36 }
View Code

2、我们将版本1的2,3问题进行改善一下(版本2)。

封装:

1)将服务器的接受和发送数据用一个Channel类进行封装,这样子一个client就对应了一个Channel对象了

2)将客户端的接收和发送分离开,使用两个线程进行分割,这样子接收数据和发送数据就可以不用同时进行了(为群聊做准备)。

将关闭资源的系列用一个ChatUtils工具类包装起来,使用Closeable接口。


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();23 }

利用面向对象思想对服务端进行封装

 1 package 在线聊天室; 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; 9 /**10 * 1)将服务器的接受和发送数据用一个Channel 类进行封装,11 * 这样子一个client 就对应了一个Channel对象了13 2)将客户端的接收和发送分离开,14 使用两个线程进行分割,这样子接收数据和发送数据就可以不用同时进行了15 (为群聊做准备)。16 * @author liuzeyu12a18 */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);25 //这边加上死循环是为了接受多个客户的请求26 while(true) {27 //监听28 Socket client = server.accept(); 29 System.out.println("一个客户端建立了连接");30 new Thread(new Channel(client)).start();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();53 @Override54 public void run() {55 while(isRunning) {56 String msg = receive();57 if(!msg.equals(""))58 send(msg);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();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();84 return "";87 //释放资源88 public void release() {89 ChatUtils.close(client,dos,dis);92 }
View Code

客户端的发送:

 1 package 在线聊天室; 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; 9 /**10 * 为聊天室Client2 的发送功能服务11 * @author liuzeyu12a13 */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 } 34 @Override35 public void run() {36 while(isRunning) {37 send("");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();53 public String getMsgFromConsole(){54 try {55 return reader.readLine();56 } catch (IOException e) {57 e.printStackTrace();59 return null;61 //释放资源62 public void release() {63 isRunning = false;64 ChatUtils.close(dos,client,reader);66 }
View Code

客户端的接收:

 1 package 在线聊天室; 3 import java.io.DataInputStream; 4 import java.io.IOException; 5 import java.net.Socket; 7 /** 8 * 为聊天室Client2 的接收功能服务 9 * @author liuzeyu12a11 */12 public class Receive implements Runnable{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();28 @Override29 public void run() {30 while(isRunning) {31 String msg = "";32 msg = recevie();33 if(!msg.equals(""))34 System.out.println(msg);35 } 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("数据接收失败");47 return null;51 //释放资源52 public void release() {53 isRunning = false;54 ChatUtils.close(dis,client);56 }
View Code

客户端的封装:

 1 package 在线聊天室; 3 import java.io.IOException; 4 import java.net.Socket; 6 /** 7 * 1)将服务器的接受和发送数据用一个Channel 类进行封装, 8 * 这样子一个client 就对应了一个Channel对象了10 2)将客户端的接收和发送分离开,11 使用两个线程进行分割,这样子接收数据和发送数据就可以不用同时进行了12 (为群聊做准备)。13 * @author liuzeyu12a15 */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);22 //发送数据23 new Thread(new Send(client)).start();24 //接收数据25 new Thread(new Receive(client)).start();27 }
View Code

3、实现简单的群聊功能。

服务器可以实现数据的转发功能,客户端不再局限于自己对话自己,是一个典型的群聊案例,重点理解如何将name进行传递。

客户端接受:

 1 package 在线聊天室过渡版; 3 import java.io.DataInputStream; 4 import java.io.IOException; 5 import java.net.Socket; 7 /** 8 * 为聊天室Client2 的接收功能服务 9 * @author liuzeyu12a11 */12 public class Receive implements Runnable{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();28 @Override29 public void run() {30 while(isRunning) {31 String msg = "";32 msg = receive();33 if(!msg.equals("")) {34 System.out.println(msg);37 } 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("数据接收失败");48 return "";51 //释放资源52 public void release() {53 isRunning = false;54 ChatUtils.close(dis,client);56 }
View Code

客户端发送:

 1 package 在线聊天室过渡版; 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; 9 /**10 * 为聊天室Client2 的发送功能服务11 * @author liuzeyu12a13 */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 } 38 @Override39 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();48 send(msg);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();61 //释放资源62 public void release() {63 ChatUtils.close(dos,client,reader);65 }
View Code

客户端封装:

 1 package 在线聊天室过渡版; 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStreamReader; 6 import java.net.Socket; 8 /** 9 * 可以实现简单的群聊了。10 * @author liuzeyu12a12 */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();21 //创建Socket套接字,绑定服务器端口22 Socket client =new Socket("localhost",8989);24 //发送数据25 new Thread(new Send(client,name)).start();26 //接收数据27 new Thread(new Receive(client)).start();29 }
View Code

服务端:

 1 package 在线聊天室过渡版; 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; 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); 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) { 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 } 68 } 69 } 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 } 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 }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);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);37 }

服务端:

 1 package 在线聊天室终极版; 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; 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); 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) { 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 } 68 } 69 } 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 }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 }117 }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 }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一个副本存起来,一旦数据有错,就启用副本数据

来保证数据的安全性)。