Java网络编程之用TCP模拟聊天室
最近在B站跟着尚学堂的高琪老师学Java,课程很不错,老师不光在讲知识点,更重要的是在讲涉及思路,推荐大家来学习!
B站传送门:点我进入
上一篇博客简单模拟的Java的TCP的基本步骤
这次将是充分对之前知识点的扩展
多人聊天室功能:
1.多个客户端可以同时进行聊天
2.可以自定义进入客户端的用户名
3.一个客户端发消息可被其他客户端收到
4.可以进行私聊@Zhangsan:Hello Zhangsan!
首先分析简单的功能需求,一点一点来看
1.多个客户端,必然要用到多线程,且客户端可以同时<接受消息&&发送消息>。
2.在每个客户端类里创建个字符串name,通过控制台输入即可。
3.一个客户端和服务端通信会先建立一条管道,我们把管道们存放在一个数组里,当给其他客户端发消息时,只需遍历一遍管道们,通过管道给所对应的客户端发消息即可。
4.当服务端接收到一个客户端发来的消息时,我们先进行解剖。私聊的格式:@用户名:消息内容,看看符不符合这个格式,符合的话,我们提取出用户名与消息内容,遍历管道 找到用户名对应的管道只给他发消息即可。不符合私聊格式的话,我们默认给全部人都发,符合第3点。
服务端
存储管道们的时候,要考虑到多个客户端的同时写与读
可以考虑使用CopyOnWriteArrayList,底层是用volatile transient声明的数组array,实现了读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array。
CopyOnWriteArrayList<mxy> all=new CopyOnWriteArrayList<mxy>(); //用来存储管道们
建立连接
ServerSocket server=new ServerSocket(5655);
开始不停的监听接受客户端们的连接
while(true){
Socket client=server.accept(); //时刻准备着客户端们!
mxy dog=new mxy(client);
all.add(dog); //容器管理所有的客户端
new Thread(dog).start(); //实现能够同时接受、发送的多线程
}
发送和接受的多线程类
class mxy implements Runnable{
private Socket client;
private DataInputStream dis;
private DataOutputStream dos;
private String namee;
public mxy(Socket client) throws IOException {
// TODO Auto-generated constructor stub
this.client=client;
try {
dis=new DataInputStream(client.getInputStream());
dos=new DataOutputStream(client.getOutputStream());
//先获取输入的用户名 用于下一步操作
namee=dis.readUTF();
//发送XX来了到别的客户端
for(mxy other:all) {
if(other.equals(this)) {//如果是客户端时自己的话就不重复发送了
continue;
}
else {
other.dos.writeUTF(namee+"进入了聊天室!");
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
//释放资源
dis.close();
dos.close();
client.close();
}
}
@Override
public void run() {
// TODO Auto-generated method stub
boolean flag=true;
while(flag)
{
String datas = null; //存储整个一条消息
String[] siliaodata=null; //存储私聊内容
String[] siliaouser=null; //存储私聊用户名
try {
datas = dis.readUTF();
//System.out.println(datas.charAt(0));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
//释放资源
try {
dis.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
dos.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
client.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
try {
//开始判断是不是私聊,是的话-----------------------------------------
if(datas.charAt(0)=='@')
{
siliaodata=datas.split(":"); //用spilt分割冒号
siliaouser=siliaodata[0].split("@"); //用spilt分割@
for(mxy other:all) { //遍历客户端开始寻找
if(other.namee.equals(siliaouser[1])) {
other.dos.writeUTF(namee+"对你说"+siliaodata[1]);//开始给私聊的客户端发送消息
}
}
}
//下面是不是的话,代表给全体发的-------------------------------------
else {
for(mxy other:all) {
if(other.equals(this)) {
continue;
}
else {
other.dos.writeUTF(namee+"对所有人说"+datas);
}
}
}
//--------------------------------------------------------------
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
//释放资源
try {
dis.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
dos.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
client.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
}
}
客户端
客户端的话,需要用多线程实现接受和发送消息,每个客户端需要先输入自己的用户名,在new 发送类的时候顺便把用户名也传进去,首先用DataOutputStream先把用户名传给服务端,用于给别的客户端提示:XXX进入了聊天室。其他的也没什么了
建立连接
Socket client=new Socket("localhost",5655);
public class Client {
static String namee;
static Scanner sc=new Scanner(System.in);
public static void main(String[] args) throws Exception, IOException {
Socket client=new Socket("localhost",5655);
System.out.println("请输入您的名字");
namee=sc.next();
new Thread(new Send(client,namee)).start(); //在Send构造器里传进去用户名
new Thread(new Receive(client)).start();
// client.close(); 这句该死的代码!错了一晚上,不能把客户端关掉!
}
发送类
发送的时候,创建构造器,先把用户名丢进去,服务端那边读出来发给其他客户端
class Send implements Runnable{
Scanner sc=new Scanner(System.in);
private DataOutputStream dos;
private Socket client;
private String namee;
public Send(Socket client, String namee) {
this.namee=namee;
this.client=client;
try {
dos=new DataOutputStream(client.getOutputStream());
dos.writeUTF(namee); //先通过管道把用户名丢进去 用于提示XXX进入了聊天室
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void run() {
// TODO Auto-generated method stub
boolean flag=true;
while(flag)
{
String msg;
msg=sc.next(); //键盘读入要发送的信息
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
try {
dos.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
}
}
接收类
没什么好说的,跟之前的一样
class Receive implements Runnable{
private DataInputStream dis;
private Socket client;
private String namee;
public Receive(Socket client) {
this.client=client;
try {
dis=new DataInputStream(client.getInputStream());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
//释放资源
try {
dis.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
client.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
@Override
public void run() {
boolean flag=true;
while(flag)
{
try {
String res=dis.readUTF();
System.out.println(res);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
try {
dis.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
}
}
代码效果图:
我没有把IO流、资源释放用方法封装起来,所以看起来可能有点乱,其实道理都是一样的,还望大家见谅!
如有不妥的地方,欢迎大家在评论区指正!