java 多用户即时通信系统的实现 万字详解

本文涉及的产品
系统运维管理,不限时长
简介: java 多用户即时通讯系统 提升功力推荐(难度较大)。

目录

前言

一、拾枝杂谈

       1.项目开发大体流程 :

       2.多用户即时通信系统分析 :

               1° 需求分析

               2° 整体分析

二、用户登录

       1.准备工作 :

       2.客户端 :

               1° 菜单界面

               2° 登录验证

               3° 线程创建

               4° 线程管理

       3.服务端 :

               1° 用户验证

               2° 线程创建

               3° 线程管理

       4.登录测试 :

三、在线列表

       1.扩充MessageType中的类型 :

       2.扩充UserClientService类中的方法 :

       3.扩充客户端线程类中的内容 :

       4.扩充ControlServerConnectClientThread类中的方法 :

       5.扩充服务端线程类中的内容 :

       6.拉取测试 :

四、退出系统

       1.需要解决的问题 :

       2.解决办法 :

               1° 总思路

               2° 客户端

               3° 服务端

五、私聊群聊

       1.私发消息 :

               1° 思路分析

               2° 代码实现

               3° 运行测试

       2.群发消息 :

               1° 客户端

               2° 服务端

               3° 运行测试

六、传输文件

       1.思路分析 :

       2.客户端 :

       3.服务端 :

       4.运行测试 :

七、最终代码

       1.客户端 :

               1° View

               2° UserClientService

               3° MessageClientService

               4° FileClientService

               5° ClientConnectServiceThread

               6° ControlClientConnectServiceThread

       2.服务端 :

               1° ChatServer

               2° ServerConnectClientThread

               3° ControlServerConnectClientThread

               4° ChatFrame

       3.公共部分 :

               1° Message

               2° MessageType

               3° User


前言

       本篇博文适合javaSE基础较为扎实的小伙伴儿们阅读,up会从实现层面和大家分享一个多用户即时通信系统,类似于QQ,微信这种可以实现登录,聊天,发文件,下线等功能的程序。但是声明一点,该多用户即时通信系统不是项目(up之后会专门开新的专栏来出项目),而只是对已学的java知识的联系和应用,可以理解为一个模拟项目,主要涉及到oop集合IO流多线程网络编程等内容。如果你想进一步巩固自己的java基础,这篇博文或许会是很好的选择。感谢阅读!

一、拾枝杂谈

       1.项目开发大体流程 :

       ①分析阶段 : 需求分析师会从“技术实现”和“行业情况”两方面综合考虑,出一个需求分析报告(通常是白皮书),包含客户的具体要求以及项目最终要实现的功能。需求分析在整个项目开发流程中所占用的时间和资源——往往与项目本身的大小成正比

      ②设计阶段 : 主要是架构师和项目经理揽活儿,有些公司会将二者合并。架构师/项目经理需要负责项目的设计工作(UML类图,流程图,模块设计,数据库,项目架构);并且要完成项目的原型开发——先在虚拟机上跑出一个预览的项目效果(不过多考虑性能),与客户进行对接,签订合约。一切就绪后,架构师/项目经理就会在公司的各个部门物色人选;比方说,当前项目是用java来实现的,架构师/项目经理就会挑选java技术牛逼的🐒。因此,有些时候会出现一个🐒同时在两个甚至多个项目组的情况,这时候这只牛逼的🐒会很忙,但是却痛并快乐着,因为它可以领到double甚至是multiple的工资(🐒们的工资往往由基本工资 + 项目提成构成),设计阶段在整个项目流程中所占用的时间往往比分析阶段短一些,但依然与项目本身的大小成正比

       ③实现阶段 : 不多解释,🐒儿们的主场。🐒儿们要负责把架构师/项目经理给的模块功能进行一一实现,完事儿后在自己run一run,看看自己负责的代码有没有bug。实现阶段在整个项目流程中所占的比重和项目本身成反比,即项目越大,实现阶段反而不如需求阶段和分析阶段重要。但是,实际情况是,小公司小项目的实现阶段往往是占比最大的一个,而且还会出现一边实现一边改需求的情况,即设计阶段和实现阶段缠一块儿了。

      ④测试阶段 :测试工程师,🐒儿们的天敌;负责把🐒儿们的代码拿来做各种测试,例如黑白盒测试,集成测试,单元测试等;因此,测试工程师与开发工程师往往打成一片,不可开交。在测试阶段,最怕的事情就是——高耦合性的代码出现了bug。

       ⑤实施阶段 : 实施工程师,需要将项目正确地部署到客户的平台,并保证其运行正常,需要有较强的开发能力和环境配置能力,以及较好的身体素质。客户的平台可能部署在不同的省市,甚至国家,因此实施工程师需要东奔西走,把每个平台的服务器,操作系统,环境配置等问题都给搞定。打个比方,小公司的实施工程师——使命召唤;大公司的实施工程师——塞尔达传说。

      ⑥维护阶段 : 解决程序后期出现的bug,解决项目升级相关的问题。大公司——运维工程师;小公司——背锅侠。

       2.多用户即时通信系统分析 :

               1° 需求分析

       ①用户登录        

       ②在线检测        (拉取在线用户列表)

       ③退出系统        (客户端与服务器端)

       ④私聊群聊        (实现单发和群发消息)

       ⑤传送文件      

               2° 整体分析

      Δ对于服务端——

       服务端上往往提供了不同的服务,因此服务端需要通过ServerSocket来监听不同的端口;

       每当有客户端成功连接到服务端,都会获得一个Socket对象;此时,启动一个线程,并令该线程持有Socket对象,即令每个线程都操纵一个自己的Socket对象,此举可以实现消息的群发;

       可以使用HashMap集合来管理服务端的多个线程

      Δ对于客户端——

      客户端采用对象的形式来与服务端进行通讯,此举可以发送更多的信息;可以使用对象处理流 ObjectInputStream和ObjectOutputStream来进行数据的读取

       当客户端成功根据"IP + 端口"成功连接到服务端后,客户端获得自己的Socket对象;此时,类似地,也启动一个线程,并令该线程持有Socket对象

       同样使用HashMap集合来管理客户端的多个线程。

       Δ如下图所示 :

image.png

image.gif

       User对象可以验证是否为合法的登录用户Message对象则包含了要传输的信息


二、用户登录

       1.准备工作 :

               在IDEA中创建一个新项目“ChatServer”,用来模拟通信系统的服务端;并另建一个新项目“ChatClient_0”用来模拟通信系统的一个客户端;如下图所示 :

image.png

               在服务端(ChatServer)项目中,src包下,创建一个mutual包,表示服务端和客户端共有的内容(用户信息和发送的消息)。在mutual包下创建User类,并令其实现Serializable接口;实现Serializable接口后User对象就可以序列化,进行网络传输,就可以被对象处理流操作。User类中定义用户名和用户密码两个属性。User类代码如下 :

packagemutual;
importjava.io.Serializable;
/*** @author : Cyan_RA9* @version : 21.0* @meaning : The shared User between Server and Client*/publicclassUserimplementsSerializable {
privatestaticfinallongserialVersionUID=1L;    //增强兼容性privateStringid;
privateStringpwd;
publicUser() {}
publicUser(Stringid, Stringpwd) {
this.id=id;
this.pwd=pwd;
    }
publicStringgetId() {
returnid;
    }
publicvoidsetId(Stringid) {
this.id=id;
    }
publicStringgetPwd() {
returnpwd;
    }
publicvoidsetPwd(Stringpwd) {
this.pwd=pwd;
    }
}

image.gif

               在mutual包下创建Message类,表示传输的消息类型,并令其实现Serializable接口Message类中应该包括消息的发送者,消息的接收者,消息的类型等属性,这样服务端解包后才知道这消息是发给谁的,以及消息的具体内容是什么。Message类代码如下 :

packagemutual;
importjava.io.Serializable;
/*** @author : Cyan_RA9* @version : 21.0* @message : information that are transmitted*/publicclassMessageimplementsSerializable {
privatestaticfinallongserialVersionUID=1L;    //增强兼容性privateStringsendTime;    //发送时间privateStringsender;      //发送者privateStringreceiver;    //接收者privateStringcontent;     //消息内容privateStringmesType;     //消息类型publicStringgetSendTime() {
returnsendTime;
    }
publicvoidsetSendTime(StringsendTime) {
this.sendTime=sendTime;
    }
publicStringgetSender() {
returnsender;
    }
publicvoidsetSender(Stringsender) {
this.sender=sender;
    }
publicStringgetReceiver() {
returnreceiver;
    }
publicvoidsetReceiver(Stringreceiver) {
this.receiver=receiver;
    }
publicStringgetContent() {
returncontent;
    }
publicvoidsetContent(Stringcontent) {
this.content=content;
    }
publicStringgetMesType() {
returnmesType;
    }
publicvoidsetMesType(StringmesType) {
this.mesType=mesType;
    }
}

image.gif

               还需要确定Message内容的具体类型,可以定义MessageType接口,在接口中定义不同的常量,以表示不同的消息类型;MessageType接口代码如下 :

package mutual;


/**

* @author : Cyan_RA9

* @version : 21.0

* @meaning : Types of message

*/

public interface MessageType {

   //定义常量

   String MESSAGE_LOGIN_SUCCESS = "1";     //表示登录成功

   String MESSAGE_LOGIN_FAIL = "0";        //表示登录失败

}


               最后,将mutual包拷贝一份到客户端,如下图所示 :

image.png

       2.客户端 :

               1° 菜单界面

               在客户端(ChatClient_0)新建一个包client,用户存放用户相关的类;在client包下,另建一个包menu,用于菜单的界面显示。在menu包下新建一个View类,View类代码如下 :

packageclient.menu;
importclient.service.UserClientService;
importjava.io.IOException;
importjava.util.Scanner;
/*** @author : Cyan_RA9* @version : 21.0* @function : 菜单界面的显示* @PS : Run -> Edit Configurations -> Modify options -> allow multiple instances*/publicclassView {
privatebooleanloop=true;        //控制是否显示菜单privateStringkey="";            //接收用户的键盘输入privatestaticScannersc=newScanner(System.in);
/*将UserClientService对象置为属性,该对象用于执行用户登录/注册等操作(该步骤将功能与界面联系起来)*/privateUserClientServiceuserClientService=newUserClientService();
publicstaticvoidmain(String[] args) throwsIOException {
newView().mainMenu();
System.out.println("客户端退出...");
    }
privatevoidmainMenu() throwsIOException {
while (loop) {
System.out.println("===========Welcome to the system of chat:===========");
System.out.println("\t\t1.登录系统");
System.out.println("\t\t9.退出系统");
System.out.print("请输入你的选择:");
key=sc.nextLine();
switch (key) {
case"1" :
//登录操作System.out.print("请输入用户名:");
StringuserID=sc.nextLine();
System.out.print("请输入密  码:");
Stringpassword=sc.nextLine();
//验证登录的用户是否合法(封装思想)if (userClientService.check(userID, password)) {     //验证成功System.out.println("\n===========Welcome user "+userID+"===========");
//向用户显示二级菜单while (loop) {
System.out.println("\n===========网络通信系统二级菜单(user:"+userID+")===========");
System.out.println("\t\t1.在线列表:");
System.out.println("\t\t2.群发消息:");
System.out.println("\t\t3.私发消息:");
System.out.println("\t\t4.文件发送:");
System.out.println("\t\t9.退出系统:");
System.out.print("请输入你的选择:");
key=sc.nextLine();
switch (key) {
case"1" :
System.out.println(1);
break;
case"2" :
System.out.println(2);
break;
case"3" :
System.out.println(3);
break;
case"4" :
System.out.println(4);
break;
case"9" :
loop=false;   //在二级菜单中用户也可以直接选择退出系统                            }
                        }
                    } else {        //验证失败System.out.println("登录失败!请重新尝试!");
                    }
break;
case"9" :
sc.close();
loop=false;       //将控制while循环的布尔变量设置为falsebreak;
            }
        }
    }
}

image.gif

               为了实现多用户登录,需要对View类进行配置依次点击Run -> Edit Configurations -> Modify options -> allow multiple instances,允许并行,如下图所示 :

image.gif

               2° 登录验证

               View类中有关“用户登录验证”部分的代码,利用封装的思想,将其封装到client.service包下的类UserClientService中,UserClientService类代码如下 :

packageclient.service;
importmutual.Message;
importmutual.MessageType;
importmutual.User;
importjava.io.IOException;
importjava.io.ObjectInputStream;
importjava.io.ObjectOutputStream;
importjava.net.InetAddress;
importjava.net.Socket;
/*** @author : Cyan_RA9* @version : 21.0* @function : 登录验证*/publicclassUserClientService {
/*将User对象设置成一个属性,可利用getter和setter修改User对象的引用,便于操作。Socket对象同样也可能在其他类中使用,因此也设置为属性。*/privateUseruser=newUser();
privateSocketsocket;
publicbooleancheck(StringuserID, Stringpassword) throwsIOException {
//局部变量booleanb=false;
//初始化User对象user.setId(userID);
user.setPwd(password);
//向服务端发送信息try {
//1.获取Socket对象socket=newSocket(InetAddress.getByName("127.0.0.1"), 8888);
//2.获取与Socket对象相关联的对象处理流(输出流)ObjectOutputStreamoos=newObjectOutputStream(socket.getOutputStream());
//3.序列化User对象,写入数据通道(向服务端发送一个User对象,服务端会对这个User对象进行验证)oos.writeObject(user);
//.........//4.获取与Socket对象相关联的对象处理流(输入流)ObjectInputStreamois=newObjectInputStream(socket.getInputStream());
//5.读取服务端传输过来的Message对象Messagemessage= (Message) ois.readObject();   //类型强转if (message.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCESS)) {
//创建线程对象(目的是为了与服务端保持通讯)ClientConnectServiceThreadccst=newClientConnectServiceThread(socket);
//启动线程ccst.start();
//将线程放入集合中统一管理ControlClientConnectServiceThread.addClientConnectServiceThread(userID, ccst);
b=true;
            } else {
//如果没有启动线程,关闭Socket对象。socket.close();
            }
        } catch (Exceptione) {
e.printStackTrace();
        } 
returnb;
    }
}

image.gif

               3° 线程创建

               为了保持通讯,需要让线程持有Socket对象;同时,利用HashMap集合来管理多个线程。UserClientService类中有关线程的部分,同样新建一个类ClientConnectServiceThread,在client.service包下,ClientConnectServiceThread类代码如下 :

packageclient.service;
importmutual.Message;
importjava.io.ObjectInputStream;
importjava.net.Socket;
/*** @author : Cyan_RA9* @version : 21.0* @function : 客户端用于和服务端进行通讯的线程*/publicclassClientConnectServiceThreadextendsThread{
//该线程需要持有Socket对象privateSocketsocket;
publicClientConnectServiceThread(Socketsocket) {
this.socket=socket;
    }
publicSocketgetSocket() {
returnsocket;
    }
@Overridepublicvoidrun() {
//∵Thread需要在后台与服务器通信,因此使用while循环while (true) {
try {
System.out.println("客户端线程,等待读取来自服务器端的消息...");
ObjectInputStreamois=newObjectInputStream(socket.getInputStream());
/*如果服务端没有发送Message对象到数据通道中,线程就会阻塞在这里。*/Messagemessage= (Message) ois.readObject();
            } catch (Exceptione) {
thrownewRuntimeException(e);
            }
        }
    }
}

image.gif

               4° 线程管理

               UserClientService类中涉及到线程“管理”,将相关代码进行封装,在client.service包下新建一个ControlClientConnectServiceThread类,代码如下 :

packageclient.service;
importjava.util.HashMap;
/*** @author : Cyan_RA9* @version : 21.0* @function : 管理客户端的线程*/publicclassControlClientConnectServiceThread {
/*使用HashMap类来管理多个线程(模拟数据库),key表示用户的id,value表示线程。*/privatestaticHashMap<String, ClientConnectServiceThread>hashMap=newHashMap<>();
//添加线程的方法publicstaticvoidaddClientConnectServiceThread(StringuserID, ClientConnectServiceThreadccst) {
hashMap.put(userID, ccst);
    }
//取出线程的方法publicstaticClientConnectServiceThreadgetClientConnectServiceThread(StringuserID) {
returnhashMap.get(userID);
    }
}

image.gif

       3.服务端 :

               1° 用户验证

               服务端的代码与客户端类似,都需要创建一个类用于读取数据通道中的数据;还需要一个线程类来持有Socket对象;最后就是一个类来管理服务端的线程。

               在ChatServer包下创建server.service包,在该包下创建ChatServer类,用于接收客户端法来的User和Message对象,并给出回应。ChatServer类代码如下 :

packageserver.service;
importmutual.Message;
importmutual.MessageType;
importmutual.User;
importjava.io.IOException;
importjava.io.ObjectInputStream;
importjava.io.ObjectOutputStream;
importjava.net.ServerSocket;
importjava.net.Socket;
importjava.util.concurrent.ConcurrentHashMap;
/*** @author : Cyan_RA9* @version : 21.0* @function : 服务端*/publicclassChatServer {
//将ServerSocket设置为属性,写在main函数外privateServerSocketserverSocket=null;
/**将合法的用户放入集合中(使用“id + user”的泛型),建议使用ConcurrentHashMap集合,线程同步,可在多线程程序下安全使用。*/privatestaticConcurrentHashMap<String, User>validUsers=newConcurrentHashMap<>();
static {    //在静态代码块中初始化validUsers集合对象validUsers.put("Cyan", newUser("Cyan", "RA9"));
validUsers.put("Rain", newUser("Rain", "flo"));
validUsers.put("Ice", newUser("Ice", "ais"));
validUsers.put("Five", newUser("Five", "55555"));
validUsers.put("Kyrie", newUser("Kyrie", "lrving"));
    }
publicbooleancheckUser(StringuserID, Stringpassword) {
Useruser=validUsers.get(userID);
if (user==null) { //如果合法用户集合中不存在当前用户,直接返回false;returnfalse;
        }
if (!(user.getPwd().equals(password))) {    //如果存在该用户,但密码错误,返回false;returnfalse;
        }
returntrue;
    }
publicChatServer() {
//端口也可以写在配置文件中try {
System.out.println("服务端正在8888端口监听...");
serverSocket=newServerSocket(8888);
/*监听是不间断的,当服务端和某个客户端建立连接后,服务端会继续监听。*/while (true) {
//获取Socket类对象(服务端是通过accept方法来获取Socket对象的)Socketsocket=serverSocket.accept();
//获取Socket对象关联的输入流与输出流(对象处理流)ObjectInputStreamois=newObjectInputStream(socket.getInputStream());
ObjectOutputStreamoos=newObjectOutputStream(socket.getOutputStream());
//客户端第一次传过来的是User对象Useruser= (User) ois.readObject();
//暂时以单用户登录为例(id = Cyan, pwd = RA9)//创建一个Message对象,用于回复客户端是否连接成功(Message对象写在if-else语句外)Messagemessage=newMessage();
if (checkUser(user.getId(), user.getPwd())) {   //登录成功message.setMesType(MessageType.MESSAGE_LOGIN_SUCCESS);
//将包含“登录成功与否”信息的Message对象写入数据通道oos.writeObject(message);
//创建一个线程,与客户端保持通讯ServerConnectClientThreadscct=newServerConnectClientThread(socket, user.getId());
//启动线程scct.start();
//将线程放入集合中ControlServerConnectClientThread.getServerConnectClientThread(user.getId());
                } else {    //登录失败System.out.println("id = "+user.getId() +",pwd = "+user.getPwd() +" 验证失败!");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
socket.close(); //关闭Socket                }
            }
        } catch (Exceptione) {
thrownewRuntimeException(e);
        } finally {
//若退出while循环,说明服务端不再监听,需要关闭ServerSocket对象try {
serverSocket.close();
            } catch (IOExceptione) {
thrownewRuntimeException(e);
            }
        }
    }
}

image.gif

              2° 线程创建

               同样,为了保持通讯,需要让线程持有Socket对象,相关代码封装到service包下的ServerConnectClientThread类中,代码如下 :

packageserver.service;
importmutual.Message;
importjava.io.ObjectInputStream;
importjava.net.Socket;
/*** @author : Cyan_RA9* @version : 21.0* @function : 服务端的线程,用于和客户端保持通讯*/publicclassServerConnectClientThreadextendsThread {
privateSocketsocket;
privateStringuserID;  //当前连接到服务端的用户的idpublicServerConnectClientThread(Socketsocket, StringuserID) {
this.socket=socket;
this.userID=userID;
    }
publicSocketgetSocket() {
returnsocket;
    }
@Overridepublicvoidrun() {
while (true) {
try {
System.out.println("服务端与客户端"+userID+"保持通讯,读取数据中...");
ObjectInputStreamois=newObjectInputStream(socket.getInputStream());
Messagemessage= (Message) ois.readObject();
            } catch (Exceptione) {
thrownewRuntimeException(e);
            }
        }
    }
}

image.gif

              3° 线程管理

               涉及到线程管理的代码封装到ControlServerConnectClientThread类中,代码如下 :

packageserver.service;
importjava.util.HashMap;
/*** @author : Cyan_RA9* @version : 21.0* @function : 用于管理服务端的线程*/publicclassControlServerConnectClientThread {
privatestaticHashMap<String, ServerConnectClientThread>hashMap=newHashMap<>();
//添加线程到集合中publicstaticvoidaddServerConnectClientThread(StringuserID, ServerConnectClientThreadscct) {
hashMap.put(userID, scct);
    }
//根据用户的id获取对应的线程publicstaticServerConnectClientThreadgetServerConnectClientThread(StringuserID) {
returnhashMap.get(userID);
    }
}

image.gif

       4.登录测试 :

               在服务器端新建一个frame包,在该包下新建一个ChatFrame类,用于启动服务端(客户端在View类中启动)。ChatFrame类代码如下 :

packageframe;
importserver.service.ChatServer;
publicclassChatFrame {
publicstaticvoidmain(String[] args) {
newChatServer();
    }
}

image.gif

               同时启动ChatFrame类和View类,效果如下GIF图 :



三、在线列表

       1.扩充MessageType中的类型 :

               客户端如果想获取当前多用户通讯系统中的在线成员列表,需要通过Message对象向服务端申请,服务端再通过Message对象的形式,将系统的在线用户列表发送给客户端

               首先我们需要对MessageType中的类型进行扩充,如下图所示 :

image.png

image.gif

       2.扩充UserClientService类中的方法 :

               在客户端的UserClientService类中新增一个用于拉取在线用户列表的onlineList方法代码如下 :

publicvoidonlineList() {
//向服务端发送一个Message对象,类型是MESSAGE_GET_ONLINE_FRIENDSMessagemessage=newMessage();
message.setSender(user.getId());    //用户登录时已在check方法中设置了id的值,所以可直接用message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIENDS);
try {
//得到当前线程持有的Socket对象对应的对象处理流(输出流)ObjectOutputStreamoos=newObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
oos.writeObject(message);   //向服务端发送“拉取在线用户列表”的请求        } catch (IOExceptione) {
thrownewRuntimeException(e);
        }
    }

image.gif

       3.扩充客户端线程类中的内容 :

               在客户端的ClientConnectServerThread类中,run方法里面增加对于Message类型判断和处理的逻辑语句代码如下 :

/*判断客户端读取到的Message的类型,并做出相应的业务处理。*/if (message.getMesType().equals(MessageType.MESSAGE_RETURN_ONLINE_FRIENDS)) {
//若Message的类型是返回的在线用户列表,取出在线列表并显示,使用空格分隔不同用户的idString[] onlineUsers=message.getContent().split(" ");
System.out.println("===========在线用户列表如下:===========");
for (inti=0; i<onlineUsers.length; i++) {
System.out.println("用户: "+onlineUsers[i]);
                    }
                } else {
System.out.println("...other content");
                }

image.gif

       4.扩充ControlServerConnectClientThread类中的方法 :

               拉取在线用户列表的操作要在服务端线程的run方法中进行,同样可以利用oop思想,将相关代码封装起来;考虑每个线程都保存了当前用户的id和对应的Socket对象,于是决定在服务端线程的管理类ControlServerConnectClientThread类中新增一个onlineList方法,用于拉取用户的在线列表,返回一个String类型的字符串给客户端,客户端的线程再对该字符串进行处理。onlineList方法代码如下 :

//获取在线用户列表publicstaticStringgetOnlineFriends() {
/*利用hashMap集合中的key是用户id的特点,可以对hashMap对象进行遍历,从而获取用户列表。*/Iterator<String>iterator=hashMap.keySet().iterator();
StringonlineUsers="";
while (iterator.hasNext()) {
onlineUsers+=iterator.next() +" ";   //加空格对应客户端的split方法。        }
returnonlineUsers;
    }

image.gif

       5.扩充服务端线程类中的内容 :

               有了拉取在线用户的方法,就可以在服务端的线程类中调用该方法了,如下图所示 :

image.png

image.gif

       6.拉取测试 :

               启动ChatFrame类和多个View类,如下GIF图所示 :  

image.gif


四、退出系统

       1.需要解决的问题 :

       当用户登录成功后,即客户端与服务端建立连接后,若我们在控制台输入9,整个进程并没有退出,如下图所示 :

image.png

image.gif

       这是因为主线程退出后,负责联络服务端的子线程还没有退出,还在不停运行,等待服务端发送数据,如下图所示 :

image.png

image.gif

       2.解决办法 :

               1° 总思路

               如果我们可以直接令客户端的整个进程关闭,就可以自动退出该进程下的所有线程;可以在客户端的View类下增加一个方法的调用,若用户输入9,就给服务器端发送一个Mesage对象,令服务端退出与当前对象相关联的线程

               服务端的线程类中保存了与当前用户相关联的Socket对象和当前用户的ID,如下所示 :

image.png

image.gif

               因此,服务端可以根据接收到的Message对象,关闭指定线程及Socket对象。

               然后,在客户端调用System.exit(0)方法退出当前进程

               2° 客户端

               在UserClientService类中的新定义一个方法logout,用于完成对服务端发送“关闭线程”的Message对象的功能logout方法代码如下 :

/** logout方法可以退出当前用户 */publicvoidlogout() {
Messagemessage=newMessage();
message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
message.setSender(user.getId());    //指定具体要退出的客户端//发送Message对象try {
ObjectOutputStreamoos=newObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
oos.writeObject(message);
System.out.println(user.getId() +" 退出系统...");
System.exit(0);     //0表示正常退出当前“进程”。        } catch (IOExceptione) {
thrownewRuntimeException(e);
        }
    }

image.gif

               同时,在View类中调用该方法,如下图所示 :

image.png

image.gif

               3° 服务端

               首先,在服务端的线程管理类ControlServerConnectClientThread类中,新定义一个方法用来删除服务端指定的线程,如下所示 :

image.png

               然后在服务端的线程类ServerConnectClientThread中新增一个else if的判断,并在其中调用该方法(可在删除前令线程休眠0.5s,以避免EOF异常),如下图所示 :

image.png

image.gif

               测试结果如下(成功):

image.gif


五、私聊群聊

       1.私发消息 :

               1° 思路分析

               不管是私发还是群发,一般情况下,一个客户端与另一个客户端都是无法直接通讯的,需要经过服务端来转发

               对于客户端它需要将要发送的信息打包成Message对象,然后发给服务端;同时,要接收来自其他用户的(经过服务端转发的)消息

               对于服务端它需要读取某个用户发送给另一个用户的消息,然后根据Message对象中的id信息获取到对应线程,继而获取到该线程持有的Socket,最后通过Socket将信息发送给另一个用户

               2° 代码实现

               客户端 :

               在client.service包下新建一个MessageClientService类,用于管理消息代码如下 :

packageclient.service;
importmutual.Message;
importmutual.MessageType;
importjava.io.IOException;
importjava.io.ObjectOutputStream;
/*** @author : Cyan_RA9* @version : 21.0* @function : 提供与消息有关的服务*/publicclassMessageClientService {
/*** @param receiver : 消息的接收者* @param content : 消息内容* @param sender : 消息的发送者*/publicvoidsendMessageToOne(Stringreceiver, Stringcontent, Stringsender) {
Messagemessage=newMessage();
message.setMesType(MessageType.MESSAGE_COMMON_MES);     //消息类型message.setReceiver(receiver);
message.setContent(content);
message.setSender(sender);
message.setSendTime(newjava.util.Date().toString());   //发送时间System.out.println(sender+" 对 "+receiver+" 说 \""+content+"\"");
try {
//获取发送消息的用户的输出流对象,并将上面的消息发给服务端ObjectOutputStreamoos=newObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(sender).getSocket().getOutputStream());
oos.writeObject(message);
        } catch (IOExceptione) {
thrownewRuntimeException(e);
        }
    }
}

image.gif

               在客户端的线程类中,增加一条else if语句,打印出服务端转发来的消息,如下图所示 :

image.png

image.gif

               最后,在客户端界面的相关部分(View类),调用MessageCilentService类的发送消息的方法,如下图所示 :

image.png

image.gif

               服务端 :

               在服务端与“发消息用户”通讯的线程中(ServerConnectClientThread类中),通过Message对象的信息,获取服务端与“要接收消息的用户”通讯的线程,然后通过该线程获取要接受消息的用户的Socket以及其对应的对象处理流;最后将消息写入到该Socket对应的数据通道中,实现消息的转发,如下图所示 :

image.png

image.gif

              3° 运行测试

               如下GIF图所示 :

image.gif

       2.群发消息 :

               群发消息,就是在私发消息的基础上,在服务端遍历在线用户列表;然后将自己排序后,把Message对象转发给其他所有的在线用户。这里要对MessageType接口进行扩充,增加一个消息类型,如下图所示 :

image.png

image.gif

               1° 客户端

               在MessageClientService类中定义一个群发消息的方法sendMessageToAll,代码如下 :

publicvoidsendMessageToAll(Stringcontent, Stringsender) {
Messagemessage=newMessage();
message.setMesType(MessageType.MESSAGE_COMMON_MES_ALL);     //消息类型message.setContent(content);                                //消息内容message.setSender(sender);                                  //消息发送者message.setSendTime(newjava.util.Date().toString());       //消息发送时间System.out.println(sender+" 对所有在线的👴们说 \""+content+"\"");
try {
//获取发送消息的用户的输出流对象,并将上面的消息发给服务端ClientConnectServiceThreadclientConnectServiceThread=ControlClientConnectServiceThread.getClientConnectServiceThread(sender);
ObjectOutputStreamoos=newObjectOutputStream(clientConnectServiceThread.getSocket().getOutputStream());
oos.writeObject(message);
        } catch (Exceptione) {
thrownewRuntimeException(e);
        }
    }

image.gif

               然后在View类中的相应区域调用该方法,如下图所示 :

image.png

image.gif

               接着,在客户端的线程类中增加一个else if 的判断,用于接收来自服务端转发的群发消息并显示在控制台,如下图所示 :

image.png

image.gif

               2° 服务端

               首先在管理线程的类中,定义一个可以返回hashMap的方法,如下图所示 :

image.png

image.gif

               然后,在服务端的线程类中,新增一个else if 的判断,如果判断Message类型是群发类型,就遍历集合,实现群发。如下图所示 :

image.png

image.gif

               3° 运行测试

              如下GIF图 :  

image.gif


六、传输文件

       1.思路分析 :

               发送文件与发送消息原理类似,都是以Message对象为载体;只不过发送文件时,Message对象中的内容是一个保存了图片的字节数组了

               对于客户端——

               先把要发送的文件读取到客户端(字节数组);然后把文件对应的字节数组封装到Message对象中;最后将Message对象发送给服务端;当然,客户端还需要接收来自服务端转发过来的Message对象,并将其中的文件内容保存到磁盘

               对于服务端——

               服务端接收到来自某一个用户发来的Message对象后,要进行拆包,获取到具体的接收者,然后实现转发即可

               还需要对MessageType接口进行扩充,如下图所示 :

image.png

image.gif

               对Message类进行扩充,如下图所示 :

image.png

image.gif

               提供一些新的属性以及它们对应的getter, setter方法。  

       2.客户端 :

               在client.service包下新定义一个FileClientService类,用于文件发送功能的实现,FileClientService类代码如下 :

packageclient.service;
importmutual.Message;
importmutual.MessageType;
importjava.io.File;
importjava.io.FileInputStream;
importjava.io.IOException;
importjava.io.ObjectOutputStream;
/*** @author : Cyan_RA9* @version : 21.0* @function : 实现发送文件相关的功能*/publicclassFileClientService {
/*** @param souPath : 数据源文件路径* @param desPath : 目的地文件路径* @param sender : 发送者(ID)* @param receiver : 接收者(ID)*/publicvoidsetFileToOne(StringsouPath, StringdesPath, Stringsender, Stringreceiver) {
Messagemessage=newMessage();
message.setMesType(MessageType.MESSAGE_FILE_TRANSMISSION);
message.setSouPath(souPath);
message.setDesPath(desPath);
message.setSender(sender);
message.setReceiver(receiver);
//1.读取文件/*利用File类的length方法(获取当前文件的大小,以字节计算),可以得知要创建的字节数组的大小;因为length方法的返回值是long类型,所以此处需要类型强转。*/byte[] file=newbyte[(int)newFile(souPath).length()];
//创建一个输入流FileInputStreamfileInputStream=null;
try {
fileInputStream=newFileInputStream(souPath);
fileInputStream.read(file);     //将file文件读取到字节数组中。//将文件包装到Message对象message.setFile(file);
        } catch (IOExceptione) {
thrownewRuntimeException(e);
        } finally {
//关闭输入流if (fileInputStream!=null) {
try {
fileInputStream.close();
                } catch (IOExceptione) {
thrownewRuntimeException(e);
                }
            }
        }
//2.提示信息System.out.println("\n"+sender+" 给 "+receiver+" 发送 "+souPath+"到对方电脑的目录"+desPath+"下...");
//3.发送文件try {
ObjectOutputStreamoos=newObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(sender).getSocket().getOutputStream());
oos.writeObject(message);
        } catch (IOExceptione) {
thrownewRuntimeException(e);
        }
    }
}

image.gif

               然后,还要在客户端的线程类中扩展一个else if 的判断语句,若接收到的Message对象为服务端转发来的文件消息,就将其读取并保存到本地磁盘中。如下图所示 :

image.png

               最后在View类中创建FileClientService对象,如下图所示 :

image.png

               然后再对应的部分调用该对象的方法,如下图所示:

image.png

image.gif

       3.服务端 :

               服务端还是老样子,在服务端的线程中增加一个else if 的Message类型判断,如果判断为发送文件的Message对象,就和私发消息一样给转发一下就OK了。如下图所示 :

               截图没截到的部分,就是之前的老样子——先通过线程管理类的得到线程的方法,根据接收者的id获取对应的线程;然后在根据获得的线程,获取其持有的Socket对象,最后再获取与该Socket对象相关联的输出流

image.png

image.gif

       4.运行测试 :

               如下GIF图演示 :

image.gif


七、最终代码

       1.客户端 :

               1° View

packageclient.menu;
importclient.service.FileClientService;
importclient.service.MessageClientService;
importclient.service.UserClientService;
importjava.io.IOException;
importjava.util.Scanner;
/*** @author : Cyan_RA9* @version : 21.0* @function : 菜单界面的显示* @PS : Run -> Edit Configurations -> Modify options -> allow multiple instances*/publicclassView {
privatebooleanloop=true;        //控制是否显示菜单privateStringkey="";            //接收用户的键盘输入privatestaticScannersc=newScanner(System.in); //静态扫描仪/*将userClientService对象置为属性,该对象用于执行用户登录/注册等操作(该步骤将功能与界面联系起来)*/privateUserClientServiceuserClientService=newUserClientService();
/*将messageClientService对象置为属性,该对象用于消息的管理(该步骤将功能与界面联系起来)*/privateMessageClientServicemessageClientService=newMessageClientService();
/*将fileClientService对象置为属性,该对象用于文件的发送(该步骤将功能与界面联系起来)*/privateFileClientServicefileClientService=newFileClientService();
publicstaticvoidmain(String[] args) throwsIOException {
newView().mainMenu();
System.out.println("客户端退出...");
sc.close();
    }
privatevoidmainMenu() throwsIOException {
while (loop) {
System.out.println("===========Welcome to the system of chat:===========");
System.out.println("\t\t1.登录系统");
System.out.println("\t\t9.退出系统");
System.out.print("请输入你的选择:");
key=sc.nextLine();
switch (key) {
case"1" :
//登录操作System.out.print("请输入用户名:");
StringuserID=sc.nextLine();
System.out.print("请输入密  码:");
Stringpassword=sc.nextLine();
//验证登录的用户是否合法(封装思想)if (userClientService.check(userID, password)) {     //验证成功System.out.println("\n===========Welcome user "+userID+"===========");
//向用户显示二级菜单while (loop) {
System.out.println("\n===========网络通信系统二级菜单(user:"+userID+")===========");
System.out.println("\t\t1.在线列表:");
System.out.println("\t\t2.群发消息:");
System.out.println("\t\t3.私发消息:");
System.out.println("\t\t4.文件发送:");
System.out.println("\t\t9.退出系统:");
System.out.print("请输入你的选择:");
key=sc.nextLine();
switch (key) {
case"1" :
userClientService.onlineList();
break;
case"2" :
System.out.println("请输入你要对大家说的话:");
Stringannouncement=sc.nextLine();
//调用群发消息的方法messageClientService.sendMessageToAll(announcement, userID);
break;
case"3" :
System.out.print("请输入你想聊天的对象(在线),receiver = ");
Stringreceiver=sc.nextLine();
System.out.print("请输入你要说的话: ");
Stringcontent=sc.nextLine();
//调用私发消息的方法messageClientService.sendMessageToOne(receiver, content, userID);
break;
case"4" :
System.out.print("请输入你想发送文件的对象(在线),receiver = ");
StringfileReceiver=sc.nextLine();
System.out.print("请输入数据源文件的路径, souPath = ");
StringsouPath=sc.nextLine();
System.out.print("请输入目的地文件的路径, desPath = ");
StringdesPath=sc.nextLine();
fileClientService.setFileToOne(souPath, desPath, userID, fileReceiver);
break;
case"9" :
userClientService.logout();
loop=false;   //在二级菜单中用户也可以直接选择退出系统break;
                            }
                        }
                    } else {        //验证失败System.out.println("登录失败!请重新尝试!");
                    }
break;
case"9" :
loop=false;       //将控制while循环的布尔变量设置为falsebreak;
            }
        }
    }
}

image.gif

              2° UserClientService

packageclient.service;
importmutual.Message;
importmutual.MessageType;
importmutual.User;
importjava.io.IOException;
importjava.io.ObjectInputStream;
importjava.io.ObjectOutputStream;
importjava.net.InetAddress;
importjava.net.Socket;
/*** @author : Cyan_RA9* @version : 21.0* @function : 登录验证*/publicclassUserClientService {
/*将User对象设置成一个属性,可利用getter和setter修改User对象的引用,便于操作。Socket对象同样也可能在其他类中使用,因此也设置为属性。*/privateUseruser=newUser();
privateSocketsocket;
/** check方法可以向服务端发起用户登录的验证 */publicbooleancheck(StringuserID, Stringpassword) throwsIOException {
//局部变量booleanb=false;
//初始化User对象user.setId(userID);
user.setPwd(password);
//向服务端发送信息try {
//1.获取Socket对象socket=newSocket(InetAddress.getByName("127.0.0.1"), 8888);
//2.获取与Socket对象相关联的对象处理流(输出流)ObjectOutputStreamoos=newObjectOutputStream(socket.getOutputStream());
//3.序列化User对象,写入数据通道(向服务端发送一个User对象,服务端会对这个User对象进行验证)oos.writeObject(user);
//.........//4.获取与Socket对象相关联的对象处理流(输入流)ObjectInputStreamois=newObjectInputStream(socket.getInputStream());
//5.读取服务端传输过来的Message对象Messagemessage= (Message) ois.readObject();   //类型强转if (message.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCESS)) {
//创建线程对象(目的是为了与服务端保持通讯)ClientConnectServiceThreadccst=newClientConnectServiceThread(socket);
//启动线程ccst.start();
//将线程放入集合中统一管理ControlClientConnectServiceThread.addClientConnectServiceThread(userID, ccst);
b=true;
            } else {
//如果没有启动线程,关闭Socket对象。socket.close();
            }
        } catch (Exceptione) {
e.printStackTrace();
        }
returnb;
    }
/** onlineList方法可以向服务端请求拉取在线列表 */publicvoidonlineList() {
//向服务端发送一个Message对象,类型是MESSAGE_GET_ONLINE_FRIENDSMessagemessage=newMessage();
message.setSender(user.getId());    //用户登录时已在check方法中设置了id的值,所以可直接用message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIENDS);
try {
//得到当前线程持有的Socket对象对应的对象处理流(输出流)ObjectOutputStreamoos=newObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
oos.writeObject(message);   //向服务端发送“拉取在线用户列表”的请求        } catch (IOExceptione) {
thrownewRuntimeException(e);
        }
    }
/** logout方法可以退出当前用户 */publicvoidlogout() {
Messagemessage=newMessage();
message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
message.setSender(user.getId());    //指定具体要退出的客户端//发送Message对象try {
ObjectOutputStreamoos=newObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).getSocket().getOutputStream());
oos.writeObject(message);
ControlClientConnectServiceThread.getClientConnectServiceThread(user.getId()).setLoop(false);
System.out.println(user.getId() +" 退出系统...");
System.exit(0);     //0表示正常退出当前“进程”。        } catch (IOExceptione) {
thrownewRuntimeException(e);
        }
    }
}

image.gif

              3° MessageClientService

packageclient.service;
importmutual.Message;
importmutual.MessageType;
importjava.io.ObjectOutputStream;
/*** @author : Cyan_RA9* @version : 21.0* @function : 提供与消息有关的服务*/publicclassMessageClientService {
/*** @param receiver : 消息的接收者* @param content : 消息内容* @param sender : 消息的发送者*/publicvoidsendMessageToOne(Stringreceiver, Stringcontent, Stringsender) {
Messagemessage=newMessage();
message.setMesType(MessageType.MESSAGE_COMMON_MES);     //消息类型message.setReceiver(receiver);                          //消息接收者message.setContent(content);                            //消息内容message.setSender(sender);                              //消息发送者message.setSendTime(newjava.util.Date().toString());   //发送时间System.out.println(sender+" 对 "+receiver+" 说 \""+content+"\"");
try {
//获取发送消息的用户的输出流对象,并将上面的消息发给服务端ClientConnectServiceThreadclientConnectServiceThread=ControlClientConnectServiceThread.getClientConnectServiceThread(sender);
ObjectOutputStreamoos=newObjectOutputStream(clientConnectServiceThread.getSocket().getOutputStream());
oos.writeObject(message);
        } catch (Exceptione) {
thrownewRuntimeException(e);
        }
    }
/*** @param content : 群发消息的内容* @param sender : 群发消息的发送者*/publicvoidsendMessageToAll(Stringcontent, Stringsender) {
Messagemessage=newMessage();
message.setMesType(MessageType.MESSAGE_COMMON_MES_ALL);     //消息类型message.setContent(content);                                //消息内容message.setSender(sender);                                  //消息发送者message.setSendTime(newjava.util.Date().toString());       //消息发送时间System.out.println(sender+" 对所有在线的👴们说 \""+content+"\"");
try {
//获取发送消息的用户的输出流对象,并将上面的消息发给服务端ClientConnectServiceThreadclientConnectServiceThread=ControlClientConnectServiceThread.getClientConnectServiceThread(sender);
ObjectOutputStreamoos=newObjectOutputStream(clientConnectServiceThread.getSocket().getOutputStream());
oos.writeObject(message);
        } catch (Exceptione) {
thrownewRuntimeException(e);
        }
    }
}

image.gif

               4° FileClientService

packageclient.service;
importmutual.Message;
importmutual.MessageType;
importjava.io.File;
importjava.io.FileInputStream;
importjava.io.IOException;
importjava.io.ObjectOutputStream;
/*** @author : Cyan_RA9* @version : 21.0* @function : 实现发送文件相关的功能*/publicclassFileClientService {
/*** @param souPath : 数据源文件路径* @param desPath : 目的地文件路径* @param sender : 发送者(ID)* @param receiver : 接收者(ID)*/publicvoidsetFileToOne(StringsouPath, StringdesPath, Stringsender, Stringreceiver) {
Messagemessage=newMessage();
message.setMesType(MessageType.MESSAGE_FILE_TRANSMISSION);
message.setSouPath(souPath);
message.setDesPath(desPath);
message.setSender(sender);
message.setReceiver(receiver);
//1.读取文件/*利用File类的length方法(获取当前文件的大小,以字节计算),可以得知要创建的字节数组的大小;因为length方法的返回值是long类型,所以此处需要类型强转。*/byte[] file=newbyte[(int)newFile(souPath).length()];
//创建一个输入流FileInputStreamfileInputStream=null;
try {
fileInputStream=newFileInputStream(souPath);
fileInputStream.read(file);     //将file文件读取到字节数组中。//将文件包装到Message对象message.setFile(file);
        } catch (IOExceptione) {
thrownewRuntimeException(e);
        } finally {
//关闭输入流if (fileInputStream!=null) {
try {
fileInputStream.close();
                } catch (IOExceptione) {
thrownewRuntimeException(e);
                }
            }
        }
//2.提示信息System.out.println("\n"+sender+" 给 "+receiver+" 发送 "+souPath+" 到对方电脑的目录 "+desPath+" 下...");
//3.发送文件try {
ObjectOutputStreamoos=newObjectOutputStream(ControlClientConnectServiceThread.getClientConnectServiceThread(sender).getSocket().getOutputStream());
oos.writeObject(message);
        } catch (IOExceptione) {
thrownewRuntimeException(e);
        }
    }
}

image.gif

              5° ClientConnectServiceThread

packageclient.service;
importmutual.Message;
importmutual.MessageType;
importjava.io.FileOutputStream;
importjava.io.ObjectInputStream;
importjava.net.Socket;
/*** @author : Cyan_RA9* @version : 21.0* @function : 客户端用于和服务端进行通讯的线程*/publicclassClientConnectServiceThreadextendsThread {
//该线程需要持有Socket对象privateSocketsocket;
privatebooleanloop=true;
publicClientConnectServiceThread(Socketsocket) {
this.socket=socket;
    }
publicSocketgetSocket() {
returnsocket;
    }
publicvoidsetLoop(booleanloop) {
this.loop=loop;
    }
@Overridepublicvoidrun() {
//∵Thread需要在后台与服务器通信,因此使用while循环while (loop) {
try {
System.out.println("客户端线程,等待读取来自服务器端的消息...");
ObjectInputStreamois=newObjectInputStream(socket.getInputStream());
/*如果服务端没有发送Message对象到数据通道中,线程就会阻塞在这里。*/Messagemessage= (Message) ois.readObject();
/**判断客户端读取到的Message的类型,并做出相应的业务处理。*/if (message.getMesType().equals(MessageType.MESSAGE_RETURN_ONLINE_FRIENDS)) {
//若Message的类型是返回的在线用户列表,取出在线列表并显示,使用空格分隔不同用户的idString[] onlineUsers=message.getContent().split(" ");
System.out.println("\n===========在线用户列表如下:===========");
for (inti=0; i<onlineUsers.length; i++) {
System.out.println("用户: "+onlineUsers[i]);
                    }
                } elseif (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES_ALL)) {
System.out.println("\n"+message.getSender() +" 对所有在线的👴说 \""+message.getContent() +"\"");
                } elseif (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES)) {
System.out.println("\n"+message.getSender() +" 对 "+message.getReceiver() +" 说 \""+message.getContent() +"\"");
                } elseif (message.getMesType().equals(MessageType.MESSAGE_FILE_TRANSMISSION)) {
System.out.println("\n"+message.getSender() +" 给 "+message.getReceiver() +" 发送 "+message.getSouPath() +" 到对方电脑的目录 "+message.getDesPath() +"下...");
FileOutputStreamfileOutputStream=newFileOutputStream(message.getDesPath());
fileOutputStream.write(message.getFile());
fileOutputStream.close();
System.out.println("\n保存文件成功!");
                } else {
System.out.println("...other content");
                }
            } catch (Exceptione) {
e.printStackTrace();
            }
        }
    }
}

image.gif

              6° ControlClientConnectServiceThread

packageclient.service;
importjava.util.HashMap;
/*** @author : Cyan_RA9* @version : 21.0* @function : 管理客户端的线程*/publicclassControlClientConnectServiceThread {
/*使用HashMap类来管理多个线程(模拟数据库),key表示用户的id,value表示线程。*/privatestaticHashMap<String, ClientConnectServiceThread>hashMap=newHashMap<>();
//添加线程的方法publicstaticvoidaddClientConnectServiceThread(StringuserID, ClientConnectServiceThreadccst) {
hashMap.put(userID, ccst);
    }
//取出线程的方法publicstaticClientConnectServiceThreadgetClientConnectServiceThread(StringuserID) {
returnhashMap.get(userID);
    }
}

image.gif

       2.服务端 :

               1° ChatServer

packageserver.service;
importmutual.Message;
importmutual.MessageType;
importmutual.User;
importjava.io.IOException;
importjava.io.ObjectInputStream;
importjava.io.ObjectOutputStream;
importjava.net.ServerSocket;
importjava.net.Socket;
importjava.util.concurrent.ConcurrentHashMap;
/*** @author : Cyan_RA9* @version : 21.0* @function : 服务端*/publicclassChatServer {
//将ServerSocket设置为属性,写在main函数外privateServerSocketserverSocket=null;
/**将合法的用户放入集合中(使用“id + user”的泛型),建议使用ConcurrentHashMap集合,线程同步,可在多线程程序下安全使用。*/privatestaticConcurrentHashMap<String, User>validUsers=newConcurrentHashMap<>();
static {    //在静态代码块中初始化validUsers集合对象validUsers.put("Cyan", newUser("Cyan", "RA9"));
validUsers.put("Rain", newUser("Rain", "flo"));
validUsers.put("Ice", newUser("Ice", "ais"));
validUsers.put("Five", newUser("Five", "55555"));
validUsers.put("Kyrie", newUser("Kyrie", "lrving"));
    }
publicbooleancheckUser(StringuserID, Stringpassword) {
Useruser=validUsers.get(userID);
if (user==null) { //如果合法用户集合中不存在当前用户,直接返回false;returnfalse;
        }
if (!(user.getPwd().equals(password))) {    //如果存在该用户,但密码错误,返回false;returnfalse;
        }
returntrue;
    }
publicChatServer() {
//端口也可以写在配置文件中try {
System.out.println("服务端正在8888端口监听...");
serverSocket=newServerSocket(8888);
/*监听是不间断的,当服务端和某个客户端建立连接后,服务端会继续监听。*/while (true) {
//获取Socket类对象(服务端是通过accept方法来获取Socket对象的)Socketsocket=serverSocket.accept();
//获取Socket对象关联的输入流与输出流(对象处理流)ObjectInputStreamois=newObjectInputStream(socket.getInputStream());
ObjectOutputStreamoos=newObjectOutputStream(socket.getOutputStream());
//客户端第一次传过来的是User对象Useruser= (User) ois.readObject();
//创建一个Message对象,用于回复客户端是否连接成功(Message对象写在if-else语句外)Messagemessage=newMessage();
if (checkUser(user.getId(), user.getPwd())) {   //登录成功message.setMesType(MessageType.MESSAGE_LOGIN_SUCCESS);
//将包含“登录成功与否”信息的Message对象写入数据通道oos.writeObject(message);
//创建一个线程,与客户端保持通讯ServerConnectClientThreadscct=newServerConnectClientThread(socket, user.getId());
//启动线程scct.start();
//将线程放入集合中ControlServerConnectClientThread.addServerConnectClientThread(user.getId(), scct);
                } else {    //登录失败System.out.println("id = "+user.getId() +",pwd = "+user.getPwd() +" 验证失败!");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
socket.close(); //关闭Socket                }
            }
        } catch (Exceptione) {
thrownewRuntimeException(e);
        } finally {
//若退出while循环,说明服务端不再监听,需要关闭ServerSocket对象try {
serverSocket.close();
            } catch (IOExceptione) {
thrownewRuntimeException(e);
            }
        }
    }
}

image.gif

               2° ServerConnectClientThread

packageserver.service;
importmutual.Message;
importmutual.MessageType;
importjava.io.ObjectInputStream;
importjava.io.ObjectOutputStream;
importjava.net.Socket;
importjava.util.HashMap;
importjava.util.Iterator;
/*** @author : Cyan_RA9* @version : 21.0* @function : 服务端的线程,用于和客户端保持通讯*/publicclassServerConnectClientThreadextendsThread {
privateSocketsocket;
privateStringuserID;  //当前连接到服务端的用户的idpublicServerConnectClientThread(Socketsocket, StringuserID) {
this.socket=socket;
this.userID=userID;
    }
publicSocketgetSocket() {
returnsocket;
    }
@Overridepublicvoidrun() {
while (true) {
try {
System.out.println("服务端与客户端"+userID+"保持通讯,读取数据中...");
ObjectInputStreamois=newObjectInputStream(socket.getInputStream());
Messagemessage= (Message) ois.readObject();
/**判断服务端读取到的Message的类型,并做出相应的业务处理。*/if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIENDS)) {
System.out.println(message.getSender() +" 请求拉取在线用户列表。");
StringonlineUsers=ControlServerConnectClientThread.getOnlineFriends();
//构建Message对象,将获取到的在线用户列表的信息发送给客户端Messagemessage2=newMessage();
message2.setMesType(MessageType.MESSAGE_RETURN_ONLINE_FRIENDS);
message2.setContent(onlineUsers);
message2.setReceiver(message.getSender());  //发送者 ——> 接收者/*对象处理流写在相应业务里面,各是各的,各用各的,不易冲突。*/ObjectOutputStreamoos=newObjectOutputStream(socket.getOutputStream());
oos.writeObject(message2);
                } elseif (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
//删除负责与当前用户通信的线程System.out.println(message.getSender() +" 退出...");
Thread.sleep(500);    //删除线程前让当前线程休眠0.5秒,避免EOF异常ControlServerConnectClientThread.removeServerConnectClientThread(userID);
//关闭Socket(若忽略此步骤,客户端无异常退出,服务端仍然异常。socket.close();
//退出while循环break;
                } elseif (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES_ALL)) {
//遍历管理线程的集合HashMap<String, ServerConnectClientThread>hashMap=ControlServerConnectClientThread.getHashMap();
Iterator<String>iterator=hashMap.keySet().iterator();
while (iterator.hasNext()) {
StringonlUser=iterator.next();
//排除自己if (!onlUser.equals(message.getSender())) {
ObjectOutputStreamoos=newObjectOutputStream(hashMap.get(onlUser).getSocket().getOutputStream());
oos.writeObject(message);
                        }
                    }
                } elseif (message.getMesType().equals(MessageType.MESSAGE_COMMON_MES)) {
/*根据message对象中的receiver信息,获取对应的线程;进而获取线程持有的Socket,以及与该Socket相关联的对象处理流,利用对象处理流将信息发送给另一个用户*/ObjectOutputStreamoos=newObjectOutputStream(ControlServerConnectClientThread.getServerConnectClientThread(message.getReceiver()).getSocket().getOutputStream());
oos.writeObject(message);   //转发(注意:要使用正确的输出流)/*拓展 : 如果用户不在线,可以将消息保存到数据库,实现离线留言/离线发文件。*/                } elseif (message.getMesType().equals(MessageType.MESSAGE_FILE_TRANSMISSION)) {
ObjectOutputStreamoos=newObjectOutputStream(ControlServerConnectClientThread.getServerConnectClientThread(message.getReceiver()).getSocket().getOutputStream());
oos.writeObject(message);
                } else {
System.out.println("...other content");
                }
            } catch (Exceptione) {
thrownewRuntimeException(e);
            }
        }
    }
}

image.gif

               3° ControlServerConnectClientThread

packageserver.service;
importjava.util.HashMap;
importjava.util.Iterator;
/*** @author : Cyan_RA9* @version : 21.0* @function : 用于管理服务端的线程*/publicclassControlServerConnectClientThread {
privatestaticHashMap<String, ServerConnectClientThread>hashMap=newHashMap<>();
publicstaticHashMap<String, ServerConnectClientThread>getHashMap() {
returnhashMap;
    }
//添加线程到集合中publicstaticvoidaddServerConnectClientThread(StringuserID, ServerConnectClientThreadscct) {
hashMap.put(userID, scct);
    }
//根据用户的id获取对应的线程publicstaticServerConnectClientThreadgetServerConnectClientThread(StringuserID) {
returnhashMap.get(userID);
    }
//获取在线用户列表publicstaticStringgetOnlineFriends() {
/*利用hashMap集合中的key是用户id的特点,可以对hashMap对象进行遍历,从而获取用户列表。*/Iterator<String>iterator=hashMap.keySet().iterator();
StringonlineUsers="";
while (iterator.hasNext()) {
onlineUsers+=iterator.next() +" ";   //加空格对应客户端的split方法。        }
returnonlineUsers;
    }
//删除指定线程publicstaticvoidremoveServerConnectClientThread(StringuserID) {
hashMap.remove(userID);
    }
}

image.gif

              4° ChatFrame

packageframe;
importserver.service.ChatServer;
publicclassChatFrame {
publicstaticvoidmain(String[] args) {
newChatServer();
    }
}

image.gif

       3.公共部分 :

               1° Message

packagemutual;
importjava.io.Serializable;
/*** @author : Cyan_RA9* @version : 21.0* @message : information that are transmitted*/publicclassMessageimplementsSerializable {
privatestaticfinallongserialVersionUID=1L;    //增强兼容性privateStringsendTime;    //发送时间privateStringsender;      //发送者privateStringreceiver;    //接收者privateStringcontent;     //消息内容privateStringmesType;     //消息类型//与文件相关的属性privatebyte[] file;        //文件privateintfileLen;        //文件大小privateStringsouPath;     //数据源文件路径privateStringdesPath;     //目的地文件路径publicbyte[] getFile() {
returnfile;
    }
publicvoidsetFile(byte[] file) {
this.file=file;
    }
publicintgetFileLen() {
returnfileLen;
    }
publicvoidsetFileLen(intfileLen) {
this.fileLen=fileLen;
    }
publicStringgetSouPath() {
returnsouPath;
    }
publicvoidsetSouPath(StringsouPath) {
this.souPath=souPath;
    }
publicStringgetDesPath() {
returndesPath;
    }
publicvoidsetDesPath(StringdesPath) {
this.desPath=desPath;
    }
publicStringgetSendTime() {
returnsendTime;
    }
publicvoidsetSendTime(StringsendTime) {
this.sendTime=sendTime;
    }
publicStringgetSender() {
returnsender;
    }
publicvoidsetSender(Stringsender) {
this.sender=sender;
    }
publicStringgetReceiver() {
returnreceiver;
    }
publicvoidsetReceiver(Stringreceiver) {
this.receiver=receiver;
    }
publicStringgetContent() {
returncontent;
    }
publicvoidsetContent(Stringcontent) {
this.content=content;
    }
publicStringgetMesType() {
returnmesType;
    }
publicvoidsetMesType(StringmesType) {
this.mesType=mesType;
    }
}

image.gif

               2° MessageType

packagemutual;
/*** @author : Cyan_RA9* @version : 21.0* @meaning : Types of message*/publicinterfaceMessageType {
//定义常量StringMESSAGE_LOGIN_SUCCESS="1";     //表示登录成功StringMESSAGE_LOGIN_FAIL="0";        //表示登录失败StringMESSAGE_COMMON_MES="2";                //表示普通信息包StringMESSAGE_COMMON_MES_ALL="6";            //表示群发的信息包StringMESSAGE_GET_ONLINE_FRIENDS="3";        //表示请求拉取在线用户的列表StringMESSAGE_RETURN_ONLINE_FRIENDS="4";     //表示返回在线用户的列表StringMESSAGE_CLIENT_EXIT="5";               //表示客户端请求退出系统StringMESSAGE_FILE_TRANSMISSION="8";         //表示文件传输}

image.gif

              3° User

               User类并无改动,准备工作中的User类,即是最终的User类。

       System.out.println("END-------------------------------------------------------------------------------");

目录
相关文章
|
16天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的服装商城管理系统
基于Java+Springboot+Vue开发的服装商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的服装商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
47 2
基于Java+Springboot+Vue开发的服装商城管理系统
|
2天前
|
Java Apache Maven
Java百项管理之新闻管理系统 熟悉java语法——大学生作业 有源码!!!可运行!!!
文章提供了使用Apache POI库在Java中创建和读取Excel文件的详细代码示例,包括写入数据到Excel和从Excel读取数据的方法。
15 6
Java百项管理之新闻管理系统 熟悉java语法——大学生作业 有源码!!!可运行!!!
|
13天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
基于Java+Springboot+Vue开发的大学竞赛报名管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的大学竞赛报名管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
31 3
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
|
14天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的蛋糕商城管理系统
基于Java+Springboot+Vue开发的蛋糕商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的蛋糕商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
38 3
基于Java+Springboot+Vue开发的蛋糕商城管理系统
|
14天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的美容预约管理系统
基于Java+Springboot+Vue开发的美容预约管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的美容预约管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
24 3
基于Java+Springboot+Vue开发的美容预约管理系统
|
16天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的房产销售管理系统
基于Java+Springboot+Vue开发的房产销售管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的房产销售管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
30 3
基于Java+Springboot+Vue开发的房产销售管理系统
|
17天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的反诈视频宣传系统
基于Java+Springboot+Vue开发的反诈视频宣传系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的反诈视频宣传管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
45 4
基于Java+Springboot+Vue开发的反诈视频宣传系统
|
18天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的健身房管理系统
基于Java+Springboot+Vue开发的健身房管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的健身房管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
47 5
基于Java+Springboot+Vue开发的健身房管理系统
|
17天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的医院门诊预约挂号系统
基于Java+Springboot+Vue开发的医院门诊预约挂号系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的门诊预约挂号管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
42 2
基于Java+Springboot+Vue开发的医院门诊预约挂号系统
|
18天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的家具管理系统
基于Java+Springboot+Vue开发的家具管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的家具管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
33 2
基于Java+Springboot+Vue开发的家具管理系统