网络编程综合项目-多用户通信系统

文章目录

    • 1.项目所用技术栈
          • 本项目使用了java基础,面向对象,集合,泛型,IO流,多线程,Tcp字节流编程的技术
    • 2.通信系统整体分析
        • 主要思路(自己理解)
          • 1.如果不用多线程
          • 2.使用多线程
          • 3.对多线程的新理解
    • 3.功能实现——用户登录
        • 1.实现传输数据的三个类Message和User和MessageType
          • 1.首先创建两个模块QQSever和QQClient
          • 2.完成两个模块共有类的编写
        • 2.实现用户登录界面框架
          • 1.导入工具类utils/Utility.java
          • 2.编写基本用户界面view/QQView.java
        • 3.实现客户端的登录部分
          • 1.qqclient/service/UserClientService.java
          • 2.qqclient/service/ManageClientConnectServerThread.java
          • 3.qqclient/service/ClientConnectServerThread.java
          • 4.修改QQView.java中的验证用户是否合法语句
        • 4.实现服务器端的登录部分
          • 1.qqserver/service/QQServer.java
          • 2.qqserver/service/ServerConnectClientThread.java
        • 5.登录阶段运行调试过程
          • 1.第一次运行,报错!(用户名密码正确时)
          • 解决方法
          • 2.第二次运行,报错!(用户名密码不正确时)
          • 原因
          • 解决方法
        • 6.实现多个合法用户可以登录
          • qqserver/service/QQServer.java更新
    • 4.功能实现——拉取在线用户
        • 1.功能完成
          • 1.qqcommon/MessageType.java更新
          • 2.qqclient/service/ClientConnectServerThread.java更新
          • 3.qqclient/service/UserClientService.java更新
          • 4.view/QQView.java更新
          • 5.qqserver/service/ManageClientThreads.java更新
            • 添加方法
          • 6.qqserver/service/QQServer.java更新
            • 添加方法
          • 7.qqserver/service/ServerConnectClientThread.java更新
            • try语句更新
        • 2.调试阶段
          • 1.代码冗余
          • 2.线程同步问题
    • 5.功能实现——无异常退出系统
        • 1.功能完成
          • 1.qqcommon/MessageType.java更新
          • 2.qqclient/service/ClientConnectServerThread.java更新
            • try语句更新
          • 3.qqclient/service/UserClientService.java更新
            • 添加三个方法
          • 4.view/QQView.java更新
          • 5.qqserver/service/QQServer.java更新
            • 添加两个方法
          • 6.qqserver/service/ManageClientThreads.java更新
            • 添加方法
          • 7.qqserver/service/ServerConnectClientThread.java更新
        • 2.调试阶段
          • 1.出现空指针异常
          • 2.数据未同步
          • 3.安全性提升
    • 6.功能实现——私聊功能
        • 1.功能完成
          • 1.qqclient/service/ClientConnectServerThread.java更新
          • 2.qqclient/service/UserClientService.java更新
            • 添加方法
          • 3.view/QQView.java更新
          • 4.qqserver/service/QQServer.java更新
            • 添加方法
          • 5.qqserver/service/ServerConnectClientThread.java更新
        • 2.调试阶段
          • 并未发现错误
    • 7.功能实现——群发功能
        • 1.功能完成
          • 1.qqcommon/MessageType.java更新
          • 2.qqclient/service/ClientConnectServerThread.java更新
          • 3.qqclient/service/UserClientServer.java更新
            • 添加方法
          • 4.qqserver/service/QQServer.java更新
          • 5.qqserver/service/QQServer.java更新
            • 添加方法
          • 6.qqserver/service/ServerConnectClientThread.java更新
        • 2.调试阶段
          • 未发现错误
    • 8.功能实现——发文件
        • 1.功能完成
          • 1.qqcommon/MessageType.java更新
          • 2.qqcommon/Message.java更新
          • 3.qqclient/service/ClientConnectServerThread.java更新
          • 4.qqclient/service/UserClientServer.java更新
            • 添加方法
          • 5.view/QQView.java更新
          • 6.qqserver/service/ServerConnectClientThread.java更新
        • 2.调试阶段
          • 1.传输文件大小膨胀
    • 9.功能实现——服务器端推送新闻
        • 1.功能完成
          • 1.qqserver/service/SendAllThread.java
          • 2.qqserver/service/ServerConnectClientThread.java更新
        • 2.调试阶段
          • 1.子线程群发问题

1.项目所用技术栈

本项目使用了java基础,面向对象,集合,泛型,IO流,多线程,Tcp字节流编程的技术

2.通信系统整体分析

image-20240112092954924

主要思路(自己理解)
1.如果不用多线程
  1. 客户端A连接并发送消息:服务端B通过 accept 方法接受客户端A的连接,然后读取数据。
  2. 服务端处理并响应:服务端B处理客户端A的数据,发送响应,然后继续监听新的消息或关闭连接。如果服务器继续监听来自A的数据,它将继续阻塞在读操作上。
  3. 客户端A不再发送数据:如果客户端A在发送了一些数据之后停止发送,并且服务器端正在等待读取更多数据,这时服务端将阻塞在对A的读操作上,因为它正在等待A发送更多数据。
  4. 客户端B尝试连接:由于服务端B正在处理客户端A的连接并阻塞在读操作上,它无法接受客户端B的连接请求。直到服务端B处理完A的请求并返回到 accept 方法,客户端B才能连接。
2.使用多线程
  1. 客户端A向服务器端B建立连接,连接成功,客户端A和服务器端各自有一个socket
  2. 客户端A向服务器发送User对象(包含用户名和密码),服务器端获取内容并验证,验证结束之后将结果返回给客户端A
  3. 客户端A收到结果之后,如果登录成功,则开启一个子线程,将socket放进去,使得子线程能够对其进行操作,然后子线程一直读取通道中的信息,如果没有信息则会阻塞。而主线程则会继续执行界面的操作,两者互不干涉
  4. 此时服务器端则会也开启一个线程,将socket放到线程中,然后持续读取与客户端A通道中的信息,以执行特定的操作,然后服务器端的主线程会继续进行监听,如果有其他的客户端链接则直接连接上
  5. 此时客户端B链接服务器端,服务器端提供链接并且验证User,如果正确则服务器端再开一个线程执行跟上面同样的操作,而主线程依然继续监听,这样就实现了多用户连接。
3.对多线程的新理解
  1. 多线程就相当于一个独立于主线程之外,可以运行的实例中的run方法
  2. 主线程可以实例化为多个子线程,然后调用run方法,对当前实例进行操作
  3. 当仅仅靠主线程无法实现目标时就要使用多线程并发执行,单独开一个线程,执行特定的任务
  4. 多线程的设计,首先要明确这个线程要完成什么功能,需要给他传递什么属性,然后就可以开始设计这个单线程,最后还要考虑这个线程是不是要并发执行,如果要并发执行,则就要考虑,对象锁或者类锁实现同步

3.功能实现——用户登录

1.实现传输数据的三个类Message和User和MessageType
1.首先创建两个模块QQSever和QQClient
2.完成两个模块共有类的编写
  1. qqcommon/Message.java

    package qqcommon;import java.io.Serializable;/*** @author 孙显圣* @version 1.0* 表示客户端和服务器端通讯时的消息对象*/
    public class Message implements Serializable { //也需要进行序列化private String sender; //发送者private String getter; //接受者private String content; //消息内容private String sendTime; //发送时间private String mesType; //消息类型,在接口中定义已知的消息类型public String getSender() {return sender;}public void setSender(String sender) {this.sender = sender;}public String getGetter() {return getter;}public void setGetter(String getter) {this.getter = getter;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public String getSendTime() {return sendTime;}public void setSendTime(String sendTime) {this.sendTime = sendTime;}public String getMesType() {return mesType;}public void setMesType(String mesType) {this.mesType = mesType;}
    }
  2. qqcommon/MessageType.java

    package qqcommon;/*** @author 孙显圣* @version 1.0*/
    public interface MessageType {//在接口中定义了不同的常量//不同常量的值表示不同的消息类型String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功String MESSAGE_LOGIN_FAIL = "2"; //表示登录失败
    }
  3. qqcommon/User.java

    package qqcommon;import java.io.Serializable;/*** @author 孙显圣* @version 1.0* 表示一个用户/客户信息*/
    public class User implements Serializable { //由于需要序列化所以需要实现接口private String userId; //用户名private String passwd; //密码public User() {}public User(String userId, String passwd) {this.userId = userId;this.passwd = passwd;}public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}public String getPasswd() {return passwd;}public void setPasswd(String passwd) {this.passwd = passwd;}
    }
2.实现用户登录界面框架
1.导入工具类utils/Utility.java
package utils;/**工具类的作用:处理各种情况的用户输入,并且能够按照程序员的需求,得到用户的控制台输入。
*/import java.util.Scanner;/***/
public class Utility {//静态属性。。。private static Scanner scanner = new Scanner(System.in);/*** 功能:读取键盘输入的一个菜单选项,值:1——5的范围* @return 1——5*/public static char readMenuSelection() {char c;for (; ; ) {String str = readKeyBoard(1, false);//包含一个字符的字符串c = str.charAt(0);//将字符串转换成字符char类型if (c != '1' && c != '2' && c != '3' && c != '4' && c != '5') {System.out.print("选择错误,请重新输入:");} else break;}return c;}/*** 功能:读取键盘输入的一个字符* @return 一个字符*/public static char readChar() {String str = readKeyBoard(1, false);//就是一个字符return str.charAt(0);}/*** 功能:读取键盘输入的一个字符,如果直接按回车,则返回指定的默认值;否则返回输入的那个字符* @param defaultValue 指定的默认值* @return 默认值或输入的字符*/public static char readChar(char defaultValue) {String str = readKeyBoard(1, true);//要么是空字符串,要么是一个字符return (str.length() == 0) ? defaultValue : str.charAt(0);}/*** 功能:读取键盘输入的整型,长度小于2位* @return 整数*/public static int readInt() {int n;for (; ; ) {String str = readKeyBoard(10, false);//一个整数,长度<=10位try {n = Integer.parseInt(str);//将字符串转换成整数break;} catch (NumberFormatException e) {System.out.print("数字输入错误,请重新输入:");}}return n;}/*** 功能:读取键盘输入的 整数或默认值,如果直接回车,则返回默认值,否则返回输入的整数* @param defaultValue 指定的默认值* @return 整数或默认值*/public static int readInt(int defaultValue) {int n;for (; ; ) {String str = readKeyBoard(10, true);if (str.equals("")) {return defaultValue;}//异常处理...try {n = Integer.parseInt(str);break;} catch (NumberFormatException e) {System.out.print("数字输入错误,请重新输入:");}}return n;}/*** 功能:读取键盘输入的指定长度的字符串* @param limit 限制的长度* @return 指定长度的字符串*/public static String readString(int limit) {return readKeyBoard(limit, false);}/*** 功能:读取键盘输入的指定长度的字符串或默认值,如果直接回车,返回默认值,否则返回字符串* @param limit 限制的长度* @param defaultValue 指定的默认值* @return 指定长度的字符串*/public static String readString(int limit, String defaultValue) {String str = readKeyBoard(limit, true);return str.equals("")? defaultValue : str;}/*** 功能:读取键盘输入的确认选项,Y或N* 将小的功能,封装到一个方法中.* @return Y或N*/public static char readConfirmSelection() {System.out.println("请输入你的选择(Y/N): 请小心选择");char c;for (; ; ) {//无限循环//在这里,将接受到字符,转成了大写字母//y => Y n=>NString str = readKeyBoard(1, false).toUpperCase();c = str.charAt(0);if (c == 'Y' || c == 'N') {break;} else {System.out.print("选择错误,请重新输入:");}}return c;}/*** 功能: 读取一个字符串* @param limit 读取的长度* @param blankReturn 如果为true ,表示 可以读空字符串。 * 					  如果为false表示 不能读空字符串。* 			*	如果输入为空,或者输入大于limit的长度,就会提示重新输入。* @return*/private static String readKeyBoard(int limit, boolean blankReturn) {//定义了字符串String line = "";//scanner.hasNextLine() 判断有没有下一行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;}
}
2.编写基本用户界面view/QQView.java
package view;import utils.Utility;/*** @author 孙显圣* @version 1.0* 客户端的菜单界面*/
public class QQView {public static void main(String[] args) {new QQView().mainMenu();}private boolean loop = true; //控制主菜单循环执行//显示主菜单的方法private void mainMenu() {while (loop) { //循环显示菜单System.out.println("==========欢迎登录网络通信系统==========");System.out.println("          1 登录系统");System.out.println("          9 退出系统");System.out.print("请输入您的选择:");String s = Utility.readString(1); //读取一个字符//根据选择执行操作switch (s) {case "1":System.out.println("请输入用户号");String userId = Utility.readString(50);System.out.println("请输入密  码");String passwd = Utility.readString(50);//去服务端验证该用户是否合法//1.假设合法if (false) {//循环输出菜单while (loop) {System.out.println("==========网络通信系统二级菜单==========");System.out.println("          1 显示在线用户列表");System.out.println("          2 群发消息");System.out.println("          3 私聊消息");System.out.println("          4 发送文件");System.out.println("          9 退出系统");System.out.print("请输入您的选择:");String key = Utility.readString(1);//根据选择做出相应操作switch (key) {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":System.out.println("==========用户退出系统==========");loop = false;break;}}}//2.不合法else {//退出这个switchSystem.out.println("==========用户名或密码不正确!==========");break;}break;case "9":System.out.println("==========用户退出系统==========");loop = false;break;}}}
}
3.实现客户端的登录部分
1.qqclient/service/UserClientService.java
package qqclient.service;import com.sun.org.apache.xpath.internal.operations.Variable;
import qqcommon.Message;
import qqcommon.MessageType;
import qqcommon.User;import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;/*** @author 孙显圣* @version 1.0* 完成用户登录验证和用户注册等等功能*/
public class UserClientService {private User user = new User(); //由于可能在其他地方需要使用到这个User对象,所以将其设置为这个类的属性//根据前端输入的用户名和密码,封装成User对象并且发送到服务器端,接受服务器端返回的Message对象,并根据mesType来确定是否符合要求public boolean checkUser(String userId, String pwd) throws IOException, ClassNotFoundException {//设置一个临时变量,用于返回值boolean res = false;//将用户名和密码封装到User对象中user.setUserId(userId);user.setPasswd(pwd);//获取客户端的socketSocket socket = new Socket(InetAddress.getLocalHost(), 9999);//获取客户端的输出流OutputStream outputStream = socket.getOutputStream();//将其转换成对象处理流ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);//将user对象发送objectOutputStream.writeObject(user);//获取从服务器端回复的Message对象//获取客户端的输入流InputStream inputStream = socket.getInputStream();//转换为对象处理流ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);//读取message对象Message o = (Message) objectInputStream.readObject(); //此时我们确定读取的一定是Message对象,所以将其向下转型//根据获取的mesType来确定是否成功if (o.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {//创建一个和服务器端保持通信的线程ClientConnectServerThread clientConnectServerThread = new ClientConnectServerThread(socket);//启动客户端的线程,使其等待服务器的信息clientConnectServerThread.start();//为了后面客户端的扩展,放到一个集合中ManageClientConnectServerThread.addClientConnectServerThread(userId, clientConnectServerThread);//成功了,将返回值设置为trueres = true;} else {//如果登录失败则虽然没有启动线程但是还是开启了一个socket,所以要关闭socket.close();}return res;}}
2.qqclient/service/ManageClientConnectServerThread.java
package qqclient.service;import java.util.HashMap;/*** @author 孙显圣* @version 1.0* 该类管理客户端连接到服务器端的线程的类*/
public class ManageClientConnectServerThread {//把多个线程放到一个HashMap的集合中,key是用户id,value是线程private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();//将某个线程放到集合中public static void addClientConnectServerThread(String userId, ClientConnectServerThread clientConnectServerThread) {hm.put(userId, clientConnectServerThread);}//通过userId可以得到该线程public static ClientConnectServerThread getClientConnectServerThread(String userId) {return hm.get(userId);}}
3.qqclient/service/ClientConnectServerThread.java
package qqclient.service;import qqcommon.Message;import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;/*** @author 孙显圣* @version 1.0* 这个线程持有socket*/
public class ClientConnectServerThread extends Thread {private Socket socket;//该构造器可以接受一个Socket对象public ClientConnectServerThread(Socket socket) {this.socket = socket;}//更方便的得到Socketpublic Socket getSocket() {return socket;}//因为线程需要在后台一直保持和服务器的通信,因此使用while循环@Overridepublic void run() {while (true) {System.out.println("客户端线程,等待读取从服务器端发送的信息");try {//获取该线程socket的对象输入流ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());//读取信息Message o = (Message) objectInputStream.readObject(); //如果没有数据传进来,则这个线程则会阻塞} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}}
}
4.修改QQView.java中的验证用户是否合法语句
4.实现服务器端的登录部分
1.qqserver/service/QQServer.java
package qqserver.service;import qqcommon.Message;
import qqcommon.MessageType;
import qqcommon.User;import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;/*** @author 孙显圣* @version 1.0* 这是服务器,监听9999,等待客户端的连接并且保持通信*/
public class QQServer {private ServerSocket ss = null;public QQServer() {System.out.println("服务端在9999端口监听。。。");try {ss = new ServerSocket(9999); //开一个9999端口监听User对象} catch (IOException e) {throw new RuntimeException(e);}//由于可能会有很多的客户端发送信息,所以要使用循环监听,并且返回不同的sockettry {while (true) {//每次有用户连接都获取socketSocket socket = ss.accept();//读取客户端的User对象ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());User o  = (User) objectInputStream.readObject();//创建一个Message用于回复客户端Message message = new Message();//输出流ObjectOutputStream objectOutputStream = null;//对其进行验证,先写死if (o.getUserId().equals("100") && o.getPasswd().equals("123456")) {message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);//获取输出流回复客户端objectOutputStream = new ObjectOutputStream(socket.getOutputStream());objectOutputStream.writeObject(message);//回复完客户端之后,需要创建一个线程,用来管理socket用来保持与客户端的通信ServerConnectClientThread serverConnectClientThread = new ServerConnectClientThread(socket, o.getUserId());serverConnectClientThread.start();//使用集合来管理线程ManageClientThreads.addClientThread(o.getUserId(), serverConnectClientThread);}else {//如果登录失败,就不能启动线程,将失败的消息返回给客户端则关闭socketmessage.setMesType(MessageType.MESSAGE_LOGIN_FAIL);objectOutputStream.writeObject(message);socket.close();}}} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);} finally {try {//如果最终退出了循环,说明不再需要服务器端监听,所以,关闭ServerSocketss.close();} catch (IOException e) {throw new RuntimeException(e);}}}
}
2.qqserver/service/ServerConnectClientThread.java
package qqserver.service;import qqcommon.Message;import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;/*** @author 孙显圣* @version 1.0* 该类对应的一个对象和某个客户端保持连接,*/
public class ServerConnectClientThread extends Thread{//管理一个socket,和对应的用户idprivate Socket socket;private String userId;public ServerConnectClientThread(Socket socket, String userId) {this.socket = socket;this.userId = userId;}//保持这个socket的运行@Overridepublic void run() {while (true) {System.out.println("服务端和客户端保持通信,读取数据。。。");try {//读取数据ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());Message o = (Message) objectInputStream.readObject(); //由于之前已经接受过User对象了,现在就是接受的Message对象} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}}
}
5.登录阶段运行调试过程
1.第一次运行,报错!(用户名密码正确时)

Connect reset。。。。

解决方法
  1. 在两个序列化的类添加这行代码:private static final long serialVersionUID = 1L;
  2. 修改之后,密码正确的时候可以正常显示
2.第二次运行,报错!(用户名密码不正确时)

image-20240112170539595

原因

image-20240112170627654

在执行else语句时,由于没有运行if,所以是空的

解决方法

image-20240112170834454

由于if和else都会用到,所以提出来在外边初始化

成功运行image-20240112170946821

6.实现多个合法用户可以登录
qqserver/service/QQServer.java更新
  1. 添加以下内容:

        //创建一个集合,存放多个用户,如果是这些用户登录,就认为是合法的//可以使用ConcurrentHashMap,这样就避免了线程安全问题,HashMap线程不安全的private static ConcurrentHashMap<String, User> vaildUsers = new ConcurrentHashMap<>();//使用静态代码块初始化static {vaildUsers.put("100", new User("100", "123456"));vaildUsers.put("200", new User("200", "123456"));vaildUsers.put("300", new User("300", "123456"));vaildUsers.put("400", new User("400", "123456"));}//验证用户是否有效的方法private boolean checkUser(User user) {String userId = user.getUserId(); //获取键String passwd = user.getPasswd(); //获取密码//过关斩将//首先查找键是否存在if (!vaildUsers.containsKey(userId)) {return false;}if (!vaildUsers.get(userId).getPasswd().equals(passwd)) {return false;}return true;}
    
  2. 修改验证逻辑image-20240112195818402

4.功能实现——拉取在线用户

1.功能完成
1.qqcommon/MessageType.java更新
    String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功String MESSAGE_LOGIN_FAIL = "2"; //表示登录失败String MESSAGE_COMM_MES = "3"; //普通信息包String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出
2.qqclient/service/ClientConnectServerThread.java更新
package qqclient.service;import qqcommon.Message;import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;/*** @author 孙显圣* @version 1.0* 这个线程持有socket*/
public class ClientConnectServerThread extends Thread {private Message message; //存放信息private Socket socket;public static Boolean STATE = false; //子线程任务完成状态,用于线程同步//该构造器可以接受一个Socket对象public ClientConnectServerThread(Socket socket) {this.socket = socket;}//更方便的得到Socketpublic Socket getSocket() {return socket;}//刷新子线程状态public static void flushState() {STATE = false;}//因为线程需要在后台一直保持和服务器的通信,因此使用while循环@Overridepublic void run() {while (true) {System.out.println("客户端线程,等待读取从服务器端发送的信息");try {//获取该线程socket的对象输入流ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());//读取信息message = (Message) objectInputStream.readObject(); //如果没有数据传进来,则这个线程则会阻塞switch (message.getMesType()) {case "3": //普通信息包break;case "5": //返回在线用户列表System.out.println(message.getContent());break;}STATE = true; //更新状态} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}}
}
3.qqclient/service/UserClientService.java更新
    //向服务器端发送请求在线用户的数据包public void onlineFriendList(String userId) throws IOException, ClassNotFoundException, InterruptedException {//获取一个消息包Message message = new Message();//设置参数message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);//获取当前用户名对应的线程ClientConnectServerThread currentThread = ManageClientConnectServerThread.getClientConnectServerThread(userId);//获取线程中的socketSocket socket = currentThread.getSocket();//获取对象输出流ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());//输出对象objectOutputStream.writeObject(message);while (!ClientConnectServerThread.STATE); //等待子线程完成ClientConnectServerThread.flushState(); //刷新状态}
4.view/QQView.java更新

image-20240113101956600

5.qqserver/service/ManageClientThreads.java更新
添加方法
    //获取线程集合public static HashMap<String, ServerConnectClientThread> getHm() {return hm;}
6.qqserver/service/QQServer.java更新
添加方法
    //遍历当前用户列表并发送到前端public static void getCurrentOnlineFriendList(Socket socket) throws IOException {//获取当前用户列表HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();//遍历并保存到数据包中Message message = new Message(); //创建一个数据包//设置数据类型message.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND); //类型为返回在线用户列表//记录返回的内容StringBuilder res = new StringBuilder();//获取所有的key,使用迭代器遍历Set<String> strings = hm.keySet();Iterator<String> iterator1 = strings.iterator();int i = 0; //统计用户个数while (iterator1.hasNext()) {String next = iterator1.next();res.append("用户" + (++i) + ": ").append(next).append(" "); //拼接}//将结果放到数据包中message.setContent(res.toString());//根据目前的socket来发送数据ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());objectOutputStream.writeObject(message);}
7.qqserver/service/ServerConnectClientThread.java更新
try语句更新
                //读取数据ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());Message o = (Message) objectInputStream.readObject(); //由于之前已经接受过User对象了,现在就是接受的Message对象//根据读到的信息类型进行处理switch (o.getMesType()) {case "3": //普通信息包break;case "4": //返回当前在线用户列表QQServer.getCurrentOnlineFriendList(socket); //将目前的socket给他break;case "6": //客户端请求退出break;}
2.调试阶段
1.代码冗余
  1. 我最开始自己实现时,获取服务器端的socket是在线程数组中通过客户端传过来的姓名来获取的,后来发现没这么麻烦
  2. 服务器端的一个线程就对应一个通道的socket,并且在不断读取,如果读取到了,则此时的线程实例中的属性socket,就应该是与发送信息的客户端连通的那个socket,直接使用就可以了
2.线程同步问题

image-20240113103809616

  1. 我在拉取在线用户时,在QQ的前端界面调取一个方法,来向服务器端发送Message来请求获取在线用户。然后服务器端发送信息给客户端,此时的客户端是子线程在接收数据,而主线程运行前端页面
  2. 由于主线程只是发送了个消息就直接退出case进行下一次循环,而子线程还要根据信息处理并返回,所以一定比主线程慢,所以我在子线程里面加了一个布尔型的状态常量,并且设置了一个方法可以刷新状态,这样在主线程调用的方法中,可以使用一个while循环持续等待,直到子线程输出数据,然后再刷新状态

5.功能实现——无异常退出系统

image-20240113104920189

1.功能完成
1.qqcommon/MessageType.java更新
package qqcommon;/*** @author 孙显圣* @version 1.0*/
public interface MessageType {//在接口中定义了不同的常量//不同常量的值表示不同的消息类型String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功String MESSAGE_LOGIN_FAIL = "2"; //表示登录失败String MESSAGE_COMM_MES = "3"; //普通信息包String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出String MESSAGE_SERVICE_EXIT_SUCCESS = "7"; //服务器端退出成功
}
2.qqclient/service/ClientConnectServerThread.java更新
try语句更新
                //获取该线程socket的对象输入流ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());//读取信息message = (Message) objectInputStream.readObject(); //如果没有数据传进来,则这个线程则会阻塞switch (message.getMesType()) {case "3": //普通信息包break;case "5": //返回在线用户列表System.out.println(message.getContent());break;case "7": //服务端退出成功new UserClientService().exitAllThreads(socket, objectInputStream); //关闭资源以及退出主线程loop = false; //退出线程循环break;}STATE = true; //更新状态
3.qqclient/service/UserClientService.java更新
添加三个方法
    //向客户端发送信数据包的方法public void sendMessageToService(String userId, Message message) throws IOException {//获取当前线程ClientConnectServerThread clientConnectServerThread = ManageClientConnectServerThread.getClientConnectServerThread(userId);//获取socketSocket socket = clientConnectServerThread.getSocket();//创建输出流ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());//发送信息objectOutputStream.writeObject(message);}//向客户端发送请求退出的信息public void requestExit(String userId) throws IOException {//创建一个MessageMessage message = new Message();message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);message.setSender(userId); //告诉服务器端发送者是谁,这样可以清除集合中的线程//发送数据包sendMessageToService(userId, message);}//退出子线程以及主线程public void exitAllThreads(Socket socket, ObjectInputStream objectInputStream) throws IOException {objectInputStream.close();socket.close();System.exit(0);}
4.view/QQView.java更新

image-20240113151346682

5.qqserver/service/QQServer.java更新
添加两个方法
    //服务器端发送给客户端数据包的方法public static void sendToClientMessage(Socket socket, Message message) throws IOException {//获取输出流ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());objectOutputStream.writeObject(message);}//服务器端,返回一个退出成功的数据包然后关闭socketpublic static void ServiceExit(Socket socket, ObjectInputStream objectInputStream) throws IOException {//创建一个数据包Message message = new Message();//放入数据message.setMesType(MessageType.MESSAGE_SERVICE_EXIT_SUCCESS); //服务器端退出成功//发送sendToClientMessage(socket, message);objectInputStream.close();socket.close();}
6.qqserver/service/ManageClientThreads.java更新
添加方法
    //根据userId删除public static void deleteByUserId(String userId) {hm.remove(userId);}
7.qqserver/service/ServerConnectClientThread.java更新
    //保持这个socket的运行private boolean loop = true;@Overridepublic void run() {while (loop) {System.out.println("服务端和客户端" + userId + "线程保持通信,读取数据。。。");try {//读取数据ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());Message o = (Message) objectInputStream.readObject(); //由于之前已经接受过User对象了,现在就是接受的Message对象//根据读到的信息类型进行处理switch (o.getMesType()) {case "3": //普通信息包break;case "4": //返回当前在线用户列表QQServer.getCurrentOnlineFriendList(socket); //将目前的socket给他break;case "6": //客户端请求退出ManageClientThreads.deleteByUserId(o.getSender()); //清除列表元素QQServer.ServiceExit(socket, objectInputStream);//关闭流和套接字loop = false;break;}} catch (IOException e) {throw new RuntimeException(e);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}}
2.调试阶段
1.出现空指针异常
  1. 我第一次在关闭客户端的时候,在服务器端出现了空指针异常
  2. 原因:我在处理关闭服务器端的时候只是关闭了流和套接字,并没有关闭run方法的循环,导致子线程继续在读,但是由于套接字已经关闭,读的时候还要使用它获取流,所以出现异常
2.数据未同步
  1. 修改完异常之后,可以正常退出,但是我在测试拉取在线用户时出现了异常
  2. 原因:客户端已经退出,但是服务器端的线程集合中的元素并没有清除,所以导致了异常
3.安全性提升
  1. 原来的的退出系统逻辑就是客户端向服务器端发送退出的请求,然后服务器端收到请求就直接退出
  2. 这样是不安全的,因为客户端的主线程向服务器端发送完请求之后就直接退出,但是有个问题,如果服务器端接受到信息的速度慢了一点,导致客 户端先关闭了socket,那么服务器端在使用socket的时候就会报异常
  3. 我的解决方案:让客户端通知服务器端请求关闭连接的时候,在服务器的socket关闭之前向客户端发送一条消息,就是服务器端关闭成功,当客户端接收到这个消息的时候再退出

6.功能实现——私聊功能

1.功能完成
1.qqclient/service/ClientConnectServerThread.java更新

image-20240113165228777

2.qqclient/service/UserClientService.java更新
添加方法
    //私聊消息public void privateMessages(String sender) throws IOException {//展示所有用户之后//获取用户名称System.out.print("请输入你要聊天的用户名称:");String getter = new Scanner(System.in).next();//获取聊天的内容System.out.print("请输入聊天的内容");String content = new Scanner(System.in).nextLine();//创建一个数据包Message message = new Message();message.setMesType(MessageType.MESSAGE_COMM_MES); //普通消息message.setContent(content);message.setSender(sender);message.setGetter(getter);//发送到服务器端sendMessageToService(sender, message);}//读取私聊消息public void readPrivateMessage(Message message) {String sender = message.getSender();String content = message.getContent();System.out.println("\n========== " + sender + "对你说" + " ==========");System.out.println(content);}
3.view/QQView.java更新

image-20240113165447314

4.qqserver/service/QQServer.java更新
添加方法
    //转发消息public static void forwordMessage(Message message) throws IOException {//获取信息String content = message.getContent();String sender = message.getSender();String getter = message.getGetter();//根据姓名获取线程ServerConnectClientThread sendThread = ManageClientThreads.getServerConnectClientThread(getter);//发送包sendToClientMessage(sendThread.getSocket(), message);}
5.qqserver/service/ServerConnectClientThread.java更新

image-20240113165742602

2.调试阶段
并未发现错误

7.功能实现——群发功能

1.功能完成
1.qqcommon/MessageType.java更新
package qqcommon;/*** @author 孙显圣* @version 1.0*/
public interface MessageType {//在接口中定义了不同的常量//不同常量的值表示不同的消息类型String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功String MESSAGE_LOGIN_FAIL = "2"; //表示登录失败String MESSAGE_COMM_MES = "3"; //普通信息包String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出String MESSAGE_SERVICE_EXIT_SUCCESS = "7"; //服务器端退出成功String MESSAGE_SEND_ALL_USER = "8"; //群发消息
}
2.qqclient/service/ClientConnectServerThread.java更新

image-20240113184829218

3.qqclient/service/UserClientServer.java更新
添加方法
    //群发消息public void sendToAllUser(String userId) throws IOException {System.out.println("==========请输入你要发送的内容==========");Scanner scanner = new Scanner(System.in);String content = scanner.nextLine();//创建一个数据包Message message = new Message();message.setMesType(MessageType.MESSAGE_SEND_ALL_USER);message.setContent(content);message.setSender(userId);//发送数据包sendMessageToService(userId, message);}//读取群发消息public void readAllSendMessage(Message message) {//获取信息String sender = message.getSender();String content = message.getContent();System.out.println("\n========== " + sender +" 的群发消息==========");System.out.println(content);}
4.qqserver/service/QQServer.java更新

image-20240113185243937

5.qqserver/service/QQServer.java更新
添加方法
    //群发消息public static void sendToAllUser(Message message, String userId) throws IOException {//遍历在线用户集合,发送消息HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();Collection<ServerConnectClientThread> threads = hm.values();for (ServerConnectClientThread thread : threads) {if (hm.get(userId) == thread) { //不用发送给本用户continue;}//发送包sendToClientMessage(thread.getSocket(), message);}}
6.qqserver/service/ServerConnectClientThread.java更新

image-20240113185445558

2.调试阶段
未发现错误

8.功能实现——发文件

1.功能完成
1.qqcommon/MessageType.java更新
package qqcommon;/*** @author 孙显圣* @version 1.0*/
public interface MessageType {//在接口中定义了不同的常量//不同常量的值表示不同的消息类型String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功String MESSAGE_LOGIN_FAIL = "2"; //表示登录失败String MESSAGE_COMM_MES = "3"; //普通信息包String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出String MESSAGE_SERVICE_EXIT_SUCCESS = "7"; //服务器端退出成功String MESSAGE_SEND_ALL_USER = "8"; //群发消息String MESSAGE_SEND_FILE = "9"; //发送文件
}
2.qqcommon/Message.java更新
package qqcommon;import java.io.Serializable;/*** @author 孙显圣* @version 1.0* 表示客户端和服务器端通讯时的消息对象*/
public class Message implements Serializable { //也需要进行序列化private String sender; //发送者private String getter; //接受者private String content; //消息内容private String sendTime; //发送时间private String mesType; //消息类型,在接口中定义已知的消息类型private String path; //记录路径private byte[] bytes; //存储文件private int length; //记录长度public int getLength() {return length;}public void setLength(int length) {this.length = length;}private static final long serialVersionUID = 1L;public byte[] getBytes() {return bytes;}public void setBytes(byte[] bytes) {this.bytes = bytes;}public String getPath() {return path;}public void setPath(String path) {this.path = path;}public String getSender() {return sender;}public void setSender(String sender) {this.sender = sender;}public String getGetter() {return getter;}public void setGetter(String getter) {this.getter = getter;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public String getSendTime() {return sendTime;}public void setSendTime(String sendTime) {this.sendTime = sendTime;}public String getMesType() {return mesType;}public void setMesType(String mesType) {this.mesType = mesType;}
}
3.qqclient/service/ClientConnectServerThread.java更新

image-20240113210046856

4.qqclient/service/UserClientServer.java更新
添加方法
    //发送文件public void sendFile(String setter) throws IOException {//获取用户名称System.out.print("请输入要发送文件的用户名称:");Scanner scanner = new Scanner(System.in);String getter = scanner.next();//获取本地文件路径System.out.print("请输入本地文件路径:");String path1 = scanner.next();//获取对方文件路径System.out.print("请输入对方文件路径:");String path2 = scanner.next();//读取本地文件BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(path1));//设置缓冲byte[] bytes = new byte[1024 * 10];//记录长度int len = 0;while ((len = inputStream.read(bytes)) != -1) {Message message = new Message();//创建一个数据包message.setSender(setter);message.setGetter(getter);message.setMesType(MessageType.MESSAGE_SEND_FILE);message.setPath(path2);message.setBytes(bytes);message.setLength(len);//发送sendMessageToService(setter, message);}//关闭inputStream.close();//最后发送一个普通信息包,通知用户Message message = new Message();message.setMesType(MessageType.MESSAGE_COMM_MES);message.setContent("用户" + setter + "向你发送了一个文件,路径为" + path2);message.setGetter(getter);message.setSender(setter);sendMessageToService(setter, message);}//读取文件public void readFile(Message message) throws IOException {String sender = message.getSender();String path = message.getPath();byte[] bytes = message.getBytes();int length = message.getLength();//写入到本地路径BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(path, true));bufferedOutputStream.write(bytes,0, length);//关闭bufferedOutputStream.close();}
5.view/QQView.java更新

image-20240113210634198

6.qqserver/service/ServerConnectClientThread.java更新

image-20240113210743722

2.调试阶段
1.传输文件大小膨胀
  1. 一开始由于Message要传输的内容是String类型的,所以我就将文件分成很多byte[1024*10]的部分进行传输并且转换成了String
  2. 但是这个导致了文件变大了很多
  3. 解决方法:在Message中添加属性,来保存byte类型的数组和读取到的长度,然后再将其放到包中传输,在读取的时候以byte数组的形式读取就行

9.功能实现——服务器端推送新闻

1.功能完成
1.qqserver/service/SendAllThread.java
package qqserver.service;import qqcommon.Message;
import qqcommon.MessageType;import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.Scanner;/*** @author 孙显圣* @version 1.0* 用来向客户端推送新闻*/
public class SendAllThread extends Thread{@Overridepublic void run() {while (true) { //循环获取要推送的信息System.out.println("请输入要推送的消息");Scanner scanner = new Scanner(System.in);String content = scanner.next();//获取MessageMessage message = new Message();message.setMesType(MessageType.MESSAGE_COMM_MES);message.setSender("系统");message.setContent(content);//遍历所有用户并群发HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();Collection<ServerConnectClientThread> values = hm.values(); //所有的socketfor (ServerConnectClientThread Thread : values) {//获取线程的socket,从而获取对象输出流try {ObjectOutputStream objectOutputStream = new ObjectOutputStream(Thread.getSocket().getOutputStream());//输出普通信息包objectOutputStream.writeObject(message);System.out.println("服务器端推送消息:" + content);} catch (IOException e) {throw new RuntimeException(e);}}}}
}
2.qqserver/service/ServerConnectClientThread.java更新

image-20240113225413422

2.调试阶段
1.子线程群发问题
  1. 我最初是把Message的内容写好,然后调用群发方法发送给各个用户
  2. 但是我只开了一个用户,然后一直测试发现群发不了,但是后来想起来,我的那个群发方法,设置的是不发送给当前的用户,真是醉了
  3. 解决方案:自己遍历所有用户,群发消息

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/289716.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用Jenkins打包时执行失败,但手动执行没有问题如ERR_ELECTRON_BUILDER_CANNOT_EXECUTE

具体错误信息如&#xff1a; Error output: Plugin not found, cannot call UAC::_ Error in macro _UAC_MakeLL_Cmp on macroline 2 Error in macro _UAC_IsInnerInstance on macroline 1 Error in macro _If on macroline 9 Error in macro FUNCTION_INSTALL_MODE_PAGE_FUNC…

【QT+QGIS跨平台编译】040:【geos_c+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

点击查看专栏目录 文章目录 一、geos_c介绍二、文件下载三、文件分析四、pro文件五、编译实践一、geos_c介绍 GEOS_C(GEOS C++接口)是GEOS库的C语言版本,它提供了一套丰富的API,允许开发者在C++程序中执行复杂的几何形状处理和空间关系分析。GEOS_C是基于JTS(Java Topolog…

【黑马头条】-day04自媒体文章审核-阿里云接口-敏感词分析DFA-图像识别OCR-异步调用MQ

文章目录 day4学习内容自媒体文章自动审核今日内容 1 自媒体文章自动审核1.1 审核流程1.2 内容安全第三方接口1.3 引入阿里云内容安全接口1.3.1 添加依赖1.3.2 导入aliyun模块1.3.3 注入Bean测试 2 app端文章保存接口2.1 表结构说明2.2 分布式id2.2.1 分布式id-技术选型2.2.2 雪…

IP组播基础

原理概述 IANA ( Internet Assigned Numbers Authority &#xff09;将 IP 地址分成了 A 、 B 、 C 、 D 、 E5类&#xff0c;其中的 D 类为组播 IP 地址&#xff0c;范围是224.0.0.0~239.255.255.255。 一个 IP 报文&#xff0c;其目的地址如果是单播 IP 地址&#xff…

Unity3d使用Jenkins自动化打包(Windows)(二)

文章目录 前言一、Unity工程准备二、Unity调取命令行实战一实战二实战三实战四实战五 总结 前言 自动化打包的价值在于让程序员更轻松地创建和管理构建工具链&#xff0c;提高编程效率&#xff0c;将繁杂的工作碎片化&#xff0c;变成人人&#xff08;游戏行业特指策划&#x…

混合编程:在Go中与Python共舞

1. 引言 在软件开发领域&#xff0c;Go语言和Python都是备受推崇的高级编程语言&#xff0c;它们各自具有独特的优势和适用场景。Go语言以其简洁、高效的特性而闻名&#xff0c;而Python则因其简单易学、灵活多样的语法而备受青睐。本文将探讨Go语言与Python的优势&#xff0c…

VsCode的json文件不允许注释的解决办法

右下角找到注释点进去 输入Files: Associations搜索出此项 改为项为*.json值为jsonc保存即可 然后会发现VsCode的json文件就允许注释了

黑苹果睡眠(电源设置参考),英特尔 NUC9 黑苹果 Sonoma 14.1.1

机型&#xff1a;英特尔 NUC9 i9-9980HK处理器 之前电源配置没设置好&#xff0c;导致经常睡眠被无故唤醒&#xff0c;处理好之后是这样子的设置&#xff0c;我是台式机&#xff0c;其它的不太清楚&#xff0c;可以提供一个参考给大家。 EFI 暂时没时间上传共享&#xff0c;到时…

uni-app(自定义题色变量)

1.安装sass npm i sass -D 2.安装sass-loader npm i sass-loader10.1.1 -D 3.创建自定义文件 在根目录static目录下&#xff0c;创建scss->_them.scss&#xff0c;目录名称及文件名称自定义即可。 4.定义颜色变量 在_them.scss中&#xff0c;自定义颜色变量&#xff0…

Flink系列之:Flink SQL Gateway

Flink系列之&#xff1a;Flink SQL Gateway 一、Flink SQL Gateway二、部署三、启动SQL Gateway四、运行 SQL 查询五、SQL 网关启动选项六、SQL网关配置七、支持的端点 一、Flink SQL Gateway SQL 网关是一项允许多个客户端从远程并发执行 SQL 的服务。它提供了一种简单的方法…

面试算法-122-翻转二叉树

题目 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1] 解 class Solution {public TreeNode invertTree(TreeNode root) {return dfs(…

实战 | 微调训练TrOCR识别弯曲文本

导 读 本文主要介绍如何通过微调训练TrOCR实现弯曲文本识别。 背景介绍 TrOCR&#xff08;基于 Transformer 的光学字符识别&#xff09;模型是性能最佳的 OCR 模型之一。在我们之前的文章中&#xff0c;我们分析了它们在单行打印和手写文本上的表现。 TrOCR—基于Transforme…

Starrocks基于主机和容器的读写测试

背景介绍 在云原生时代&#xff0c;存算分离架构显然已经是当下大数据架构的必备选型&#xff0c;但是在不同的虚拟化计算资源&#xff08;主机、容器&#xff09;之上&#xff0c;是否能有差异点以及对于不同服务的性能损耗程度如何&#xff1f;来判断应该在什么样的场景下选…

设计模式之原型模式讲解

原型模式本身就是一种很简单的模式&#xff0c;在Java当中&#xff0c;由于内置了Cloneable 接口&#xff0c;就使得原型模式在Java中的实现变得非常简单。UML图如下&#xff1a; 我们来举一个生成新员工的例子来帮助大家理解。 import java.util.Date; public class Employee…

macOS Sonoma如何查看隐藏文件

在使用Git进行项目版本控制时&#xff0c;我们可能会遇到一些隐藏文件&#xff0c;比如.gitkeep文件。它通常出现在Git项目的子目录中&#xff0c;主要作用是确保空目录也可以被跟踪。 终端命令 在尝试查看.gitkeep文件时&#xff0c;使用Terminal命令来显示隐藏文件 default…

win11蓝牙图标点击变灰,修复过程

问题发现 有一天突然心血来潮想着连接蓝牙音响放歌来听,才发现win11系统右下角菜单里的蓝牙开关有问题。 打开蓝牙设置,可以正常直接连上并播放声音,点击右下角菜单里的蓝牙开关按钮后,蓝牙设备也能正常断开,但是按钮直接变深灰色,无法再点击打开。 重启电脑,蓝牙开关显…

企微侧边栏开发(内部应用内嵌H5)

一、背景 公司的业务需要用企业微信和客户进行沟通&#xff0c;而客户的个人信息基本都存储在内部CRM系统中&#xff0c;对于销售来说需要一边看企微&#xff0c;一边去内部CRM系统查询&#xff0c;比较麻烦&#xff0c;希望能在企微增加一个侧边栏展示客户的详细信息&#xf…

GNU Radio之OFDM Carrier Allocator底层C++实现

文章目录 前言一、OFDM Carrier Allocator 简介二、底层 C 实现1、make 函数2、ofdm_carrier_allocator_cvc_impl 函数3、calculate_output_stream_length 函数4、work 函数5、~ofdm_carrier_allocator_cvc_impl 函数 三、OFDM 数据格式 前言 OFDM Carrier Allocator 是 OFDM …

(免费分享)基于springboot,vue超市管理系统

开发工具&#xff1a;IDEA 服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8 项目构建&#xff1a;maven 数据库&#xff1a;mysql5.7 项目采用前后端分离 前端技术&#xff1a;vueelementUI 服务端技术&#xff1a;springbootmybatis-plusredis 本项目分为系统管理员、…

24. UE5 RPG制作属性面板(二)

在上一篇中&#xff0c;我们创建属性面板的大部分样式&#xff0c;这一篇里面接着制作。 在这一篇里我们需要有以下几个方面&#xff1a; 在界面增加一个属性按钮。属性按钮增加事件&#xff0c;点击时可以打开属性面板&#xff0c;属性面板打开时无法再次点击按钮。点击属性面…