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

文章目录

    • 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.mzph.cn/news/777242.shtml

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

相关文章

uniapp-Form示例(uviewPlus)

示例说明 Vue版本&#xff1a;vue3 组件&#xff1a;uviewPlus&#xff08;Form 表单 | uview-plus 3.0 - 全面兼容nvue的uni-app生态框架 - uni-app UI框架&#xff09; 说明&#xff1a;表单组建、表单验证、提交验证等&#xff1b; 截图&#xff1a; 示例代码 <templat…

O2OA(翱途)开发平台-快速入门开发一个门户实例

O2OA(翱途)开发平台[下称O2OA开发平台或者O2OA]拥有门户页面定制与集成的能力&#xff0c;平台通过门户定制&#xff0c;可以根据企业的文化&#xff0c;业务需要设计符合企业需要的统一信息门户&#xff0c;系统首页等UI界面。本篇主要介绍通过门户管理系统如何快速的进行一个…

学点儿Java_Day12_IO流

1 IO介绍以及分类 IO: Input Output 流是一组有顺序的&#xff0c;有起点和终点的字节集合&#xff0c;是对数据传输的总称或抽象。即数据在两设备间的传输称为流&#xff0c;流的本质是数据传输&#xff0c;根据数据传输特性将流抽象为各种类&#xff0c;方便更直观的进行数据…

C++取经之路(其二)——含数重载,引用。

含数重载: 函数重载是指&#xff1a;在c中&#xff0c;在同一作用域&#xff0c;函数名相同&#xff0c;形参列表不相同(参数个数&#xff0c;或类型&#xff0c;或顺序)不同&#xff0c;C语言不支持。 举几个例子&#xff1a; 1.参数类型不同 int Add(int left, int right)…

【任职资格】某大型制造型企业任职资格体系项目纪实

该企业以业绩、责任、能力为导向&#xff0c;确定了分层分类的整体薪酬模式&#xff0c;但是每一名员工到底应该拿多少工资&#xff0c;同一个岗位的人员是否应该拿同样的工资是管理人员比较头疼的事情。华恒智信顾问认为&#xff0c;通过任职资格评价能实现真正的人岗匹配&…

基于Transformer的医学图像分类研究

医学图像分类目前面临的挑战 医学图像分类需要研究人员同时具备医学图像分析和数字图像的知识背景。由于图像尺度、数据格式和数据类别分布的影响&#xff0c;现有的模型方法&#xff0c;如传统的机器学习的识别方法和基于深度卷积神经网络的方法&#xff0c;取得的识别准确度…

微软AI 程序员AutoDev,自主执行工程任务生成代码

全球首个 AI 程序员 Devin 的横空出世&#xff0c;可能成为软件和 AI 发展史上一个重要的节点。它掌握了全栈的技能&#xff0c;不仅可以写代码 debug&#xff0c;训模型&#xff0c;还可以去美国最大求职网站 Upwork 上抢单。 Devin 诞生之后&#xff0c;让码农纷纷恐慌。最近…

智慧光伏:企业无纸化办公

随着科技的快速发展&#xff0c;光伏技术不仅成为推动绿色能源革命的重要力量&#xff0c;更在企业办公环境中扮演起引领无纸化办公的重要角色。智慧光伏不仅为企业提供了清洁、可持续的能源&#xff0c;更通过智能化的管理方式&#xff0c;推动企业向无纸化办公转型&#xff0…

滑动窗口_水果成篮_C++

题目&#xff1a; 题目解析&#xff1a; fruits[i]表示第i棵树&#xff0c;这个fruits[i]所表示的数字是果树的种类例如示例1中的[1,2,1]&#xff0c;表示第一棵树 的种类是 1&#xff0c;第二个树的种类是2 第三个树的种类是1随后每一个篮子只能装一种类型的水果&#xff0c;我…

SQL Server事务复制操作出现的错误 进程无法在“xxx”上执行sp_replcmds

SQL Server事务复制操作出现的错误 进程无法在“xxx”上执行“sp_replcmds” 无法作为数据库主体执行&#xff0c;因为主体 "dbo" 不存在、无法模拟这种类型的主体&#xff0c;或您没有所需的权限

术语技巧:如何格式化网页中的术语

术语是语言服务中的核心语言资产。快速处理英汉对照的术语是我们在翻译技术学习过程中需要掌握的必备技能。 通常&#xff0c;我们需要把在权威网站上收集到的术语放到word当中&#xff0c;调整正左右对齐的样式&#xff0c;便于打印学习或者转化为Excel表。 如何快速实现这一…

加密流量分类torch实践5:TrafficClassificationPandemonium项目更新3

加密流量分类torch实践5&#xff1a;TrafficClassificationPandemonium项目更新3 更新日志 代码已经推送开源至露露云的github&#xff0c;如果能帮助你&#xff0c;就给鼠鼠点一个star吧&#xff01;&#xff01;&#xff01; 我的CSDN博客 我的Github Page博客 3/23日更新…

iOS - Runtime-API

文章目录 iOS - Runtime-API1. Runtime应用1.1 字典转模型1.2 替换方法实现1.3 利用关联对象给分类添加属性1.4 利用消息转发机制&#xff0c;解决方法找不到的异常问题 2. Runtime-API2.1 Runtime API01 – 类2.1.1 动态创建一个类&#xff08;参数&#xff1a;父类&#xff0…

【Pt】马灯贴图绘制过程 02-制作锈迹

目录 一、边缘磨损效果 二、刮痕效果 三、边缘磨损与刮痕的混合 四、锈迹效果 本篇效果&#xff1a; 一、边缘磨损效果 将智能材质“Iron Forge Old” 拖入图层 打开“Iron Forge Old” 文件夹&#xff0c;选中“Sharpen”&#xff08;锐化&#xff09;&#xff0c;增大“…

2010-2021年银行网点及员工信息数据

2010-2021年银行网点及员工信息数据 1、时间&#xff1a;2010-2021年 2、来源&#xff1a;整理自csmar 3、指标&#xff1a;银行代码、股票代码、银行中文简称、统计截止日期、分行数量、机构网点数量、其中&#xff1a;境内网点数量、其中&#xff1a;境外网点数量、在职员…

Linux集群

目录 一、什么是集群&#xff1f; 二、 搭建(tomcatnginxkeepalived)集群 一、JDK安装 二、Tomcat安装 三、Nginx 3.1、什么是Nginx&#xff1f; 3.2、下载Nginx 3.3、安装 四、搭建NginxTomcat的实现集群 配置nginx.comf文件 五&#xff1a;Nginx搭建图片服务器 …

【Java程序设计】【C00392】基于(JavaWeb)Springboot的校园生活服务平台(有论文)

基于&#xff08;JavaWeb&#xff09;Springboot的校园生活服务平台&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过…

VUE 实现文件夹上传(保留目录结构)

代码&#xff1a;https://gitee.com/xproer/up6-vue-cli 1.引入up6组件 2.配置接口地址 接口地址分别对应&#xff1a;文件初始化&#xff0c;文件数据上传&#xff0c;文件进度&#xff0c;文件上传完毕&#xff0c;文件删除&#xff0c;文件夹初始化&#xff0c;文件夹删除&…

2024年做视频号小店是不是明智之举?这篇文章告诉你答案

大家好&#xff0c;我是电商糖果 视频号自从去年电商的知名度打开之后&#xff0c;不少朋友都盯上这块肥肉。 要知道现在可是短视频电商的时代&#xff0c;抖音&#xff0c;快手靠做电商赚了不少钱。 视频号又怎么会放过这次的风口呢&#xff1f; 也有不少想做电商的朋友问…

灯哥驱动器端口讲解----foc电机驱动必看

CS:是电流采样的引脚&#xff0c;三项采样电流&#xff0c;现在只给了两路&#xff0c;另外一路算出来就行了 in:三项电流输入&#xff0c;驱动电机使用。 en:没有用 SDA,SCL&#xff1a;I2C的引脚用来读取编码器的计数值 tx,rx&#xff1a;引出来了一路串口&#xff0c;没有用…