Java-多用户即时通信系统-用户注册、退出系统以及拉取在线用户列表
一、思路分析
1.1 用户注册功能
- 客户端:
- 1.用户选择”用户注册“选项,输入账户名和密码;
- 2.将输入的信息封装成一个User对象;
- 3.通过新建的另一个Socket将该User对象发送给服务器端;
- 4.并不断监听服务器端返回的结果。
- 服务器端:
- 1.通过监听对应的端口获取socket;
- 2.读取客户端发送的User对象;
- 3.验证该用户是否存在,存在则返回注册失败,否则在用户集合中添加该用户并返回注册结果。
1.2 用户退出功能
-
客户端:
- 1.用户选择"退出系统"选项;
- 2.封装一个请求退出系统的Message消息对象,包括消息类型,请求退出用户的userID等
- 3.将该Message对象通过对应的Socket发送给服务器端;
- 4.退出打印菜单界面的while循环;
- 5.调用System.exit(0)无异常退出系统。
-
服务器端:
- 1.读取对应socket通道的Message对象;
- 2.判断该Message对象是否为用户退出类型;
- 3.如果是,根据该Message对象携带的userID获取到对应的socket,并从客户端连接服务器端的线程集合类中移除该userID对应的线程以及关闭该socket;
- 4.停止当前客户端连接服务器端通信的线程。
1.3 拉取在线用户列表
- 客户端:
- 1.封装一个请求用户列表类型的Message对象,并通过socket数据通道发送给服务器端;
- 2.在对应的客户端连接服务器端的线程中,监听服务器端返回的 特定格式 的用户列表,解析并打印该列表;
- 服务器端:
- 1.读取socket数据通道中的Message对象;
- 2.若为请求在线用户列表类型的Message对象,则通过客户端连接服务器线程管理类获取所有正在与服务器端通信的客户端的userID;
- 3.将所有获取到的在线用户的userID封装成特定格式返回给Message对象的发送方;
二、具体实现
2.1 客户端
1 实体类(放在common包下)
- 包括用户类(User)、消息类(Message)、消息类型接口(MessageType)
User
- 用户类
/**
* @author: SEA
* @date: 2023/5/4
*/
public class User implements Serializable {
private static final long serialVersionUID = 1L;//序列化ID,用来保证序列化和反序列化过程中,User类的一致性
private String userID;//用户名
private String password;//密码
public User() {
}
public User(String userID, String password) {
this.userID = userID;
this.password = password;
}
public String getuserID() {
return userID;
}
public void setuserID(String userID) {
this.userID = userID;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"userID='" + userID + '\'' +
", password='" + password + '\'' +
'}';
}
}
Message
- 消息类
/**
* @author: SEA
* @date: 2023/5/4
*/
public class Message implements Serializable {
private static final long serialVersionUID = 1L;//序列化ID,用来保证序列化和反序列化过程中,User类的一致性
private String sender_ip;//发送方ip
private String receiver_ip;//接收方ip
private String send_data;//发送内容
private String send_time;//发送时间
private String message_type;//消息类型(如:文本,图片,音频,视频等)
public Message() {
}
public Message(String sender_ip, String receiver_ip, String send_data, String send_time, String message_type) {
this.sender_ip = sender_ip;
this.receiver_ip = receiver_ip;
this.send_data = send_data;
this.send_time = send_time;
this.message_type = message_type;
}
public String getSender_ip() {
return sender_ip;
}
public void setSender_ip(String sender_ip) {
this.sender_ip = sender_ip;
}
public String getReceiver_ip() {
return receiver_ip;
}
public void setReceiver_ip(String receiver_ip) {
this.receiver_ip = receiver_ip;
}
public String getSend_data() {
return send_data;
}
public void setSend_data(String send_data) {
this.send_data = send_data;
}
public String getSend_time() {
return send_time;
}
public void setSend_time(String send_time) {
this.send_time = send_time;
}
public String getMessage_type() {
return message_type;
}
public void setMessage_type(String message_type) {
this.message_type = message_type;
}
}
MessageType
- 消息类型接口
- 增加:
- 注册消息类型;
- 聊天消息类型;
- 请求在线用户列表消息类型;
- 客户端请求退出消息类型;
/**
* @author: sea-365
* @date: 2023/5/4 20:54
*/
public interface MessageType {
//表示消息状态的常量
String MESSAGE_LOGIN_FAILED = "0";//表示登录失败
String MESSAGE_LOGIN_SUCCEED = "1";//表示登录成功
String MESSAGE_REGISTER_FAILED = "2";//表示注册失败
String MESSAGE_REGISTER_SUCCEED = "3";//表示注册成功
String MESSAGE_COMMON_MSG = "4";//普通信息包,即聊天文本消息
String MESSAGE_REQUEST_ONLINE_USER = "5";//请求 在线用户列表 的消息
String MESSAGE_RESPONSE_ONLINE_USER = "6";//返回 持有在线用户列表 的消息
String MESSAGE_CLIENT_EXIT = "7";//客户端请求退出
}
2 功能类(放在service包下)
UserClientService
- 实现用户登录、注册等功能的类;
- 增加:
- 用户注册功能;
- 向服务器端请求在线用户列表;
- 客户端请求退出功能;
/**
* 该类完成用户登录验证,用户注册等功能
* @author: SEA
* @date: 2023/5/5
*/
public class UserClientService {
User loginUser = new User();//User对象
User regUser = new User();//User对象
Socket loginSocket = null;//Socket对象
Socket regSocket = null;//Socket对象
/**
* 用户登录验证
* @param userID 用户名
* @param password 密码
* @return 返回登录验证结果
*/
public boolean checkUser(String userID, String password) throws IOException, ClassNotFoundException {
loginUser.setuserID(userID);
loginUser.setPassword(password);
//向服务器发送User对象,根据服务器返回的结果进行验证
loginSocket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(loginSocket.getOutputStream());
//发送user对象
objectOutputStream.writeObject(loginUser);
//socket.shutdownOutput();
//读取服务器端返回的验证结果
ObjectInputStream objectInputStream = new ObjectInputStream(loginSocket.getInputStream());
//读取返回的Message对象
Message message = (Message) objectInputStream.readObject();
if(message.getMessage_type().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {//登录成功
//登录成功,创建一个与服务器端保持通信的线程 -> 创建一个线程类ClientConnectServerThread
ClientConnectServerThread clientConnectServerThread = new ClientConnectServerThread(loginSocket);
//启动客户端与服务器端通信的线程
clientConnectServerThread.start();
//为了方便拓展,将该线程放在一个集合中进行管理
//【关于“拓展”】:比如,客户端在私聊另一个客户端的同时,还在与其他客户端发送文件,此时需要同时进行两个任务,使用多线程并发实现
ClientConnectServerThreadManage.addClientConnectServerThread(userID, clientConnectServerThread);
return true;
}
else {//登录失败
System.out.println(message.getSend_data());
//应该关闭打开的流 和 Socket对象
objectInputStream.close();
loginSocket.close();
return false;
}
}
/**
* 用户注册功能
* @param regUserID 注册用户ID
* @param regPassword 注册用户密码
* @return 注册是否成功
*/
public boolean registerUser(String regUserID, String regPassword) throws IOException, ClassNotFoundException {
regUser.setuserID(regUserID);
regUser.setPassword(regPassword);
//向服务器发送User对象,根据服务器返回的结果进行验证
regSocket = new Socket(InetAddress.getByName("127.0.0.1"), 10000);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(regSocket.getOutputStream());
//发送user对象
objectOutputStream.writeObject(regUser);
//socket.shutdownOutput();
//读取服务器端返回的验证结果
ObjectInputStream objectInputStream = new ObjectInputStream(regSocket.getInputStream());
//读取返回的Message对象
Message message = (Message) objectInputStream.readObject();
if(message.getMessage_type().equals(MessageType.MESSAGE_REGISTER_SUCCEED)) {//注册成功
return true;
}
else {//登录失败
System.out.println(message.getSend_data());
//应该关闭打开的流 和 Socket对象
objectInputStream.close();
objectOutputStream.close();
regSocket.close();
return false;
}
}
/**
* 向服务器端请求在线用户列表
* @param userID 用户ID
*/
public void requestOnlineUserList(String userID) throws IOException {
//封装请求在线用户列表的Message对象
Message message = new Message();
message.setMessage_type(MessageType.MESSAGE_REQUEST_ONLINE_USER);
message.setSender_ip(userID);
//发送该对象
Socket cur_socket = ClientConnectServerThreadManage.getClientConnectServerThread(userID).getSocket();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(cur_socket.getOutputStream());
objectOutputStream.writeObject(message);
}
/**
* 客户端请求退出
* @param userID 请求退出的客户端userID
*/
public void clientExit(String userID) throws IOException {
//封装请求退出的message对象
Message message = new Message();
message.setMessage_type(MessageType.MESSAGE_CLIENT_EXIT);
message.setSender_ip(userID);
//获取对象输出流
Socket socket = ClientConnectServerThreadManage.getClientConnectServerThread(userID).getSocket();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
//发送Message对象
objectOutputStream.writeObject(message);
}
}
ClientConnectServerThread
- 客户端保持与服务器端通信的线程类
- 增加:
- 处理 请求用户列表 类型的消息;
/**
* 客户端保持与服务器端通信的线程
* @author: SEA
* @date: 2023/5/5
*/
public class ClientConnectServerThread extends Thread{
//持有用于网络通信的Socket对象
private Socket socket;
public ClientConnectServerThread(){ }
public ClientConnectServerThread(Socket socket){
this.socket = socket;
}
public Socket getSocket() {
return socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//客户端需要在后台一直保持与服务器端通信,因此使用while循环;
//【因此这种阻塞式IO的方式,导致效率比较低!】
while(true){
try {
ObjectInputStream objectInputStream = new ObjectInputStream(this.getSocket().getInputStream());
//读取服务器端发送的数据,如果没有发送数据,则线程阻塞!
Message message = (Message) objectInputStream.readObject();
if(message.getMessage_type().equals(MessageType.MESSAGE_RESPONSE_ONLINE_USER)){
//规定服务器端返回的在线用户列表以","分隔
String[] onlineUser = message.getSend_data().split(",");
System.out.println("===================== 在线用户 =====================");
for (String s : onlineUser) {
System.out.println("\t\t\t用户:" + s);
}
System.out.println("==================================================\n");
}
else {
System.out.println("其他类型的消息暂时不处理!");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
ClientConnectServerThreadManage
- 管理ClientConnectServerThread线程的类
/**
* @author: SEA
* @date: 2023/5/5
*/
public class ClientConnectServerThreadManage {
//管理ClientConnectServerThread线程类的集合, 以userID为key;
private static HashMap<String, ClientConnectServerThread> clientConnectServerThreads = new HashMap<>();
public ClientConnectServerThreadManage() {
}
//向集合添加一个线程
public static void addClientConnectServerThread(String userID, ClientConnectServerThread clientConnectServerThread){
clientConnectServerThreads.put(userID, clientConnectServerThread);
}
//根据userID获取集合中的某个线程
public static ClientConnectServerThread getClientConnectServerThread(String userID){
return clientConnectServerThreads.get(userID);
}
}
3 工具类(放在utils包下)
Util_KeyBoardInput
- 用于处理用户键盘输入的工具类
/**
* @author: SEA
* @date: 2023/5/5
*/
public class Util_KeyBoardInput {
//静态属性scanner
private static Scanner scanner = new Scanner(System.in);
/**
* 读取键盘输入的指定长度的字符串(不能为空串,limit >= 1)
* @param limit 限定的长度
* @return 指定长度的字符串
*/
public static String readString(int limit){
return readKeyBoard(limit, false);
}
/**
* 功能: 读取一个字符串
* @param limit 读取的长度
* @param blankReturn 如果为true ,表示 可以读空字符串。
* 如果为false表示 不能读空字符串。
* 如果输入为空,或者输入大于limit的长度,就会提示重新输入。
* @return
*/
private static String readKeyBoard(int limit, boolean blankReturn) {
String line = "";
while(scanner.hasNextLine()){
line = scanner.nextLine();
//如果line.length=0, 即用户没有输入任何内容,直接回车
if (line.length() == 0) {
if (blankReturn) return line;//如果blankReturn=true,可以返回空串
else continue; //如果blankReturn=false,不接受空串,必须输入内容
}
//如果用户输入的内容大于了 limit,就提示重写输入
//如果用户如的内容 >0 <= limit ,我就接受
if (line.length() < 1 || line.length() > limit) {
System.out.print("输入长度(不能大于" + limit + ")错误,请重新输入:");
continue;
}
break;
}
return line;
}
}
4 界面类(放在view包下)
QQClientView
- 客户端界面类(目前没有前端,使用命令行输出的方式展示前端);
- 增加用户注册、退出系统,拉取在线用户列表功能;
/**
* @author: SEA
* @date: 2023/5/5
*/
public class QQClientView {
private boolean isLoop = true;//是否退出系统
private String userID, password;//用户输入的登录信息
private String regUserID, regPassword;//用户输入的注册信息
private UserClientService userClientService = new UserClientService();//用于用户登录注册等功能的对象
public void mainView() throws Exception {
String selectItem;
Scanner scanner = new Scanner(System.in);
while(isLoop){
System.out.println("=============== 欢迎登录网络通信系统 ===============");
System.out.println("\t\t\t\t 1 登录系统 \t\t");
System.out.println("\t\t\t\t 2 注册账号 \t\t");
System.out.println("\t\t\t\t 9 退出系统 \t\t");
System.out.print("请输入您的选择:");
selectItem = Util_KeyBoardInput.readString(1);
switch (selectItem){
case "1":
System.out.print("请输入用户名: ");
userID = Util_KeyBoardInput.readString(50);
System.out.print("请输入密 码:");
password = Util_KeyBoardInput.readString(50);
System.out.println("登录验证中......");
if(userClientService.checkUser(userID, password)){
System.out.println("=============== 欢迎您("+ userID +")登录网络通信系统 ===============");
//进入二级菜单
while(isLoop){
System.out.println("=============== 欢迎您("+ userID +"),这里是网络通信系统二级菜单 ===============");
System.out.println("\t\t\t\t 1 显示在线用户列表 \t\t");
System.out.println("\t\t\t\t 2 群发消息 \t\t");
System.out.println("\t\t\t\t 3 私聊消息 \t\t");
System.out.println("\t\t\t\t 4 发送文件 \t\t");
System.out.println("\t\t\t\t 9 退出系统 \t\t");
System.out.print("请输入您的选择:");
selectItem = Util_KeyBoardInput.readString(1);
switch (selectItem){
case "1":
System.out.println("正在显示在线用户列表......");
userClientService.requestOnlineUserList(userID);
Thread.sleep(1000);
break;
case "2":
System.out.println("群发消息中......");
break;
case "3":
System.out.println("私聊消息中......");
break;
case "4":
System.out.println("发送文件中......");
break;
case "9":
userClientService.clientExit(userID);
isLoop = false;
System.exit(0);
break;
default:
System.out.println("请输入正确的选项!");
break;
}
}
}
else{
System.out.println("登录失败!请检查用户名和密码是否正确。");
}
break;
case "2":
System.out.println("=============== 欢迎您注册,请输入注册信息! ===============");
System.out.print("请输入用户名: ");
regUserID = Util_KeyBoardInput.readString(50);
System.out.print("请输入密 码:");
regPassword = Util_KeyBoardInput.readString(50);
System.out.println("注册中......");
if(userClientService.registerUser(regUserID, regPassword)){
System.out.println("注册成功,请登录您的账号!");
System.out.print("请输入用户名: ");
userID = Util_KeyBoardInput.readString(50);
System.out.print("请输入密 码:");
password = Util_KeyBoardInput.readString(50);
System.out.println("登录验证中......");
if(userClientService.checkUser(userID, password)){
System.out.println("=============== 欢迎您("+ userID +")登录网络通信系统 ===============");
//进入二级菜单
while(isLoop){
System.out.println("=============== 欢迎您("+ userID +"),这里是网络通信系统二级菜单 ===============");
System.out.println("\t\t\t\t 1 显示在线用户列表 \t\t");
System.out.println("\t\t\t\t 2 群发消息 \t\t");
System.out.println("\t\t\t\t 3 私聊消息 \t\t");
System.out.println("\t\t\t\t 4 发送文件 \t\t");
System.out.println("\t\t\t\t 9 退出系统 \t\t");
System.out.print("请输入您的选择:");
selectItem = Util_KeyBoardInput.readString(1);
switch (selectItem){
case "1":
System.out.println("正在显示在线用户列表......");
break;
case "2":
System.out.println("群发消息中......");
break;
case "3":
System.out.println("私聊消息中......");
break;
case "4":
System.out.println("发送文件中......");
break;
case "9":
userClientService.clientExit(userID);
isLoop = false;
System.exit(0);
break;
default:
System.out.println("请输入正确的选项!");
break;
}
}
}
else{
System.out.println("登录失败!请检查用户名和密码是否正确。");
}
}
else{
System.out.println("注册失败!该用户已存在。");
}
break;
case "9":
isLoop = false;
break;
default:
System.out.println("请输入正确的选项!");
break;
}
}
}
public static void main(String[] args) {
QQClientView qqView = new QQClientView();
try {
qqView.mainView();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
2.2 服务器端
1 实体类(放在common包下)
- 也包括用户类(User)、消息类(Message)、消息类型接口(MessageType)
User
- 用户类
/**
* @author: SEA
* @date: 2023/5/4
*/
public class User implements Serializable {
private static final long serialVersionUID = 1L;//序列化ID,用来保证序列化和反序列化过程中,User类的一致性
private String userID;//用户名
private String password;//密码
public User() {
}
public User(String userID, String password) {
this.userID = userID;
this.password = password;
}
public String getuserID() {
return userID;
}
public void setuserID(String userID) {
this.userID = userID;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"userID='" + userID + '\'' +
", password='" + password + '\'' +
'}';
}
}
Message
- 消息类
/**
* @author: SEA
* @date: 2023/5/4
*/
public class Message implements Serializable {
private static final long serialVersionUID = 1L;//序列化ID,用来保证序列化和反序列化过程中,User类的一致性
private String sender_ip;//发送方ip
private String receiver_ip;//接收方ip
private String send_data;//发送内容
private String send_time;//发送时间
private String message_type;//消息类型(如:文本,图片,音频,视频等)
public Message() {
}
public Message(String sender_ip, String receiver_ip, String send_data, String send_time, String message_type) {
this.sender_ip = sender_ip;
this.receiver_ip = receiver_ip;
this.send_data = send_data;
this.send_time = send_time;
this.message_type = message_type;
}
public String getSender_ip() {
return sender_ip;
}
public void setSender_ip(String sender_ip) {
this.sender_ip = sender_ip;
}
public String getReceiver_ip() {
return receiver_ip;
}
public void setReceiver_ip(String receiver_ip) {
this.receiver_ip = receiver_ip;
}
public String getSend_data() {
return send_data;
}
public void setSend_data(String send_data) {
this.send_data = send_data;
}
public String getSend_time() {
return send_time;
}
public void setSend_time(String send_time) {
this.send_time = send_time;
}
public String getMessage_type() {
return message_type;
}
public void setMessage_type(String message_type) {
this.message_type = message_type;
}
@Override
public String toString() {
return "Message{" +
"sender_ip='" + sender_ip + '\'' +
", receiver_ip='" + receiver_ip + '\'' +
", send_data='" + send_data + '\'' +
", send_time='" + send_time + '\'' +
", message_type='" + message_type + '\'' +
'}';
}
}
MessageType
- 消息类型接口
- 增加:
- 注册消息类型;
- 聊天消息类型;
- 请求在线用户列表消息类型;
- 客户端请求退出消息类型;
/**
* @author: sea-365
* @date: 2023/5/4 20:54
*/
public interface MessageType {
//表示消息状态的常量
String MESSAGE_LOGIN_FAILED = "0";//表示登录失败
String MESSAGE_LOGIN_SUCCEED = "1";//表示登录成功
String MESSAGE_REGISTER_FAILED = "2";//表示注册失败
String MESSAGE_REGISTER_SUCCEED = "3";//表示注册成功
String MESSAGE_COMMON_MSG = "4";//普通信息包,即聊天文本消息
String MESSAGE_REQUEST_ONLINE_USER = "5";//请求 在线用户列表 的消息
String MESSAGE_RESPONSE_ONLINE_USER = "6";//返回 持有在线用户列表 的消息
String MESSAGE_CLIENT_EXIT = "7";//客户端请求退出
}
2 功能类(放在service包下)
QQServer
- QQ服务器端,监听9999端口;
- 增加:
- 1.使用HashMap/ConcurrentMap模拟数据库;
- 2.使用静态代码块初始化用户数据库;
- 3.将用户登录验证功能与用户注册功能分离,分别监听9999端口和10000端口进行实现;
/**
* QQ服务器端,监听9999端口,等待客户端连接,客户端连接成功后,保持该连接的通信
* @author: sea-365
* @date: 2023/5/5 20:23
*/
public class QQServer {
private ServerSocket serverSocket = null;
//考虑高并发,使用ConcurrentHashMap;
private static ConcurrentMap<String, User> UserDB = new ConcurrentHashMap<>();//使用HashMap模拟数据库
//这里要想到使用静态代码块,静态代码块在类加载时会执行一次
static {
UserDB.put("100", new User("100", "123456"));
UserDB.put("200", new User("200", "12345"));
UserDB.put("300", new User("300", "1234"));
UserDB.put("400", new User("400", "123"));
UserDB.put("500", new User("500", "12"));
UserDB.put("600", new User("600", "1"));
UserDB.put("700", new User("700", "123"));
}
//验证用户是否有效
private static boolean checkLoginUser(String userID, String password){
User user = UserDB.get(userID);
return user != null && user.getPassword().equals(password);
}
private static boolean checkRegUser(String userID, String password){
User user = UserDB.get(userID);
if(user == null)
return true;
else
return false;
}
public QQServer(){
loginUser();
registerUser();
}
/**
* 登录线程
*/
public void loginUser(){
Thread loginThread = new Thread(new Runnable() {
@Override
public void run() {
ServerSocket serverLoginSocket = null;
try{
//端口可以写在配置文件中,方便后续修改
serverLoginSocket = new ServerSocket(9999);
System.out.println("监听9999端口,等待登录...");
while(true) {
//有客户端连接则获取到Socket对象,否则阻塞
Socket socket = serverLoginSocket.accept();
System.out.println("连接成功,正在验证登录信息...");
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
//获取客户端发送的User对象
User loginUser = (User) objectInputStream.readObject();
//message用于回复该客户端,是否登录成功
Message message = new Message();
//这里需要连接数据库进行验证,但是目前没有增加数据库这一块,暂时先假设只有一个用户(100,123456)可以登录成功
//这里需要连接数据库进行验证,但是目前没有增加数据库这一块,暂时使用HashMap模拟数据库
// if (user.getuserID().equals("100") && user.getPassword().equals("123456")) {//登录成功
if (checkLoginUser(loginUser.getuserID(), loginUser.getPassword())) {//登录成功
System.out.println("用户(" + loginUser.getuserID() + ")登录成功!");
//返回登录成功message
message.setMessage_type(MessageType.MESSAGE_LOGIN_SUCCEED);
objectOutputStream.writeObject(message);
// socket.shutdownOutput();
//登录成功,创建一个线程,用于保持该连接的通信
ServerConnectClientThread serverConnectClientThread = new ServerConnectClientThread(socket, loginUser.getuserID());
serverConnectClientThread.start();
//加入集合方便管理
ServerConnectClientThreadManage.addServerConnectClientThread(loginUser.getuserID(), serverConnectClientThread);
} else {//登录失败
System.out.println("用户(" + loginUser.getuserID() + ")登录失败,取消连接");
message.setMessage_type(MessageType.MESSAGE_LOGIN_FAILED);
objectOutputStream.writeObject(message);
socket.shutdownOutput();
objectOutputStream.close();
objectInputStream.close();
socket.close();
}
}
}
catch (Exception e){
e.printStackTrace();
}
finally {
try {
if(serverLoginSocket != null)
serverLoginSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
loginThread.start();
}
/**
* 注册线程
*/
public void registerUser(){
Thread registerThread = new Thread(new Runnable() {
@Override
public void run() {
ServerSocket serverRegisterSocket = null;
try{
//端口可以写在配置文件中,方便后续修改
serverRegisterSocket = new ServerSocket(10000);
System.out.println("监听10000端口,等待注册...");
while(true) {
//有客户端连接则获取到Socket对象,否则阻塞
Socket socket = serverRegisterSocket.accept();
System.out.println("连接成功,正在验证注册信息...");
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
//获取客户端发送的User对象
User regUser = (User) objectInputStream.readObject();
//回复该客户端,是否注册成功
Message message = new Message();
if (checkRegUser(regUser.getuserID(), regUser.getPassword())) {//注册成功
System.out.println("用户(" + regUser.getuserID() + ")注册成功!");
UserDB.put(regUser.getuserID(), regUser);
//返回注册成功message
message.setMessage_type(MessageType.MESSAGE_REGISTER_SUCCEED);
objectOutputStream.writeObject(message);
} else {//注册失败
System.out.println("用户(" + regUser.getuserID() + ")注册失败!");
message.setMessage_type(MessageType.MESSAGE_REGISTER_FAILED);
objectOutputStream.writeObject(message);
socket.shutdownOutput();
objectOutputStream.close();
objectInputStream.close();
socket.close();
}
}
}
catch (Exception e){
e.printStackTrace();
}
finally {
try {
if(serverRegisterSocket != null)
serverRegisterSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
registerThread.start();
}
}
ServerConnectClientThread
- 用于保持服务器端与某一个客户端通信的线程类
- 增加:
- 1.增加 拉取在线用户列表 类型的消息处理;
- 2.增加 用于请求退出系统 类型的消息处理;
/**
* @author: sea-365
* @date: 2023/5/5 20:48
* 用于保持服务器端与某一个客户端通信的线程类
*/
public class ServerConnectClientThread extends Thread{
private Socket socket;
private String userID;//与某个用户(客户端)通信
public ServerConnectClientThread() {
}
public ServerConnectClientThread(Socket socket, String userID) {
this.socket = socket;
this.userID = userID;
}
public Socket getSocket() {
return socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
@Override
public void run() {
//与客户端一样,服务器端也需要不断接收客户端发送的数据,并且还可以向客户端发送数据
boolean isLoop = true;
while(isLoop){
try {
System.out.println("与用户" + userID + "保持连接,正在读取数据...");
ObjectInputStream objectInputStream = new ObjectInputStream(this.socket.getInputStream());
Message message = (Message) objectInputStream.readObject();
if(message.getMessage_type().equals(MessageType.MESSAGE_REQUEST_ONLINE_USER)){
//规定返回的在线用户列表以","分隔
//此处需要思考:从哪里可以拿到所有的在线用户呢?
//我一开始想的是:给User实体类增加属性status,用于表示用户的登录状态
//由于目前没有使用数据库,因此,此处只需要获取线程类集合中所有关联userID的线程即可得到所有在线用户
//使用的是HashMap,获取所有的在线用户只需获取keySet;
System.out.println("当前用户(" + message.getSender_ip() + ")正在请求在线用户列表");
String onlineUserList = ServerConnectClientThreadManage.getOnlineUserList();
//构建一个Message对象返回在线用户列表
Message respond_msg = new Message();
respond_msg.setMessage_type(MessageType.MESSAGE_RESPONSE_ONLINE_USER);
respond_msg.setSend_data(onlineUserList);
respond_msg.setReceiver_ip(message.getSender_ip());//设置当前message的接收方
ObjectOutputStream objectOutputStream = new ObjectOutputStream(this.socket.getOutputStream());
objectOutputStream.writeObject(respond_msg);
} else if (message.getMessage_type().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
//客户端请求退出:
//1.移除集合中的线程
//1.关闭对应的socket;
//2.停止线程
ServerConnectClientThreadManage.removeServerConnectClientThread(this.userID);
System.out.println("用户(" + this.userID + ")退出系统!") ;
this.socket.close();
isLoop = false;
} else{
System.out.println("其他类型的消息暂时不处理!");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
ServerConnectClientThreadManage
- 管理ServerConnectClientThread线程的类
- 增加:
- 1.在增加服务器与客户端通信线程的方法中,由put()替换为使用putIfAbsent()方法实现,能够保证线程安全;
- 2.增加removeServerConnectClientThread()方法;
/**
* @author: sea-365
* @date: 2023/5/5 20:59
*/
public class ServerConnectClientThreadManage {
private static HashMap<String, ServerConnectClientThread> serverConnectClientThreads = new HashMap<>();
public ServerConnectClientThreadManage() {
}
public static void addServerConnectClientThread(String userID, ServerConnectClientThread serverConnectClientThread){
//putIfAbsent 方法是一个原子操作,是线程安全的,多线程环境下可以安全地使用它来更新 Map 集合。
// 仅当Map中不存在具有相同键的映射时才会添加。
//此处,用于当该userID已经存在时,不创建新的线程与其交互;
serverConnectClientThreads.putIfAbsent(userID, serverConnectClientThread);
}
public static void removeServerConnectClientThread(String userID){
serverConnectClientThreads.remove(userID);
}
public static ServerConnectClientThread getserverConnectClientThread(String userID){
return serverConnectClientThreads.get(userID);
}
public static String getOnlineUserList(){
StringBuilder onlineUserlist = new StringBuilder();
for (String s : serverConnectClientThreads.keySet()) {
onlineUserlist.append(s).append(",");
}
return onlineUserlist.toString();
}
}
3 框架类(放在frame包下)
QQFrame
- 服务器端启动框架;
/**
* @author: sea-365
* @date: 2023/5/5 21:22
*/
public class QQFrame {
public static void main(String[] args) {
QQServer qqServer = new QQServer();
}
}
本文围绕Java多用户即时通信系统,介绍用户注册、退出及拉取在线用户列表功能。思路分析涵盖客户端与服务器端操作,如注册时客户端封装信息发送,服务器端验证;退出时客户端发送请求,服务器端移除对应线程等。还阐述了客户端和服务器端的具体实现,包括实体类、功能类等。

被折叠的 条评论
为什么被折叠?



