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

文章目录

    • 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=>N
            String 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 {
                        //退出这个switch
                        System.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);

        //获取客户端的socket
        Socket 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);
            //成功了,将返回值设置为true
            res = 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;
    }
    //更方便的得到Socket
    public Socket getSocket() {
        return socket;
    }

    //因为线程需要在后台一直保持和服务器的通信,因此使用while循环
    @Override
    public 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);
        }

        //由于可能会有很多的客户端发送信息,所以要使用循环监听,并且返回不同的socket
        try {
            while (true) {
                //每次有用户连接都获取socket
                Socket 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 {
                    //如果登录失败,就不能启动线程,将失败的消息返回给客户端则关闭socket
                    message.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 {
                //如果最终退出了循环,说明不再需要服务器端监听,所以,关闭ServerSocket
                ss.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,和对应的用户id
    private Socket socket;
    private String userId;

    public ServerConnectClientThread(Socket socket, String userId) {
        this.socket = socket;
        this.userId = userId;
    }

    //保持这个socket的运行
    @Override
    public 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;
    }
    //更方便的得到Socket
    public Socket getSocket() {
        return socket;
    }
    //刷新子线程状态
    public static void flushState() {
        STATE = false;
    }

    //因为线程需要在后台一直保持和服务器的通信,因此使用while循环
    @Override
    public 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);
        //获取线程中的socket
        Socket 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);
        //获取socket
        Socket socket = clientConnectServerThread.getSocket();
        //创建输出流
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
        //发送信息
        objectOutputStream.writeObject(message);
    }

    //向客户端发送请求退出的信息
    public void requestExit(String userId) throws IOException {
        //创建一个Message
        Message 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);
    }

    //服务器端,返回一个退出成功的数据包然后关闭socket

    public 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;
    @Override
    public 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{

    @Override
    public void run() {
        while (true) { //循环获取要推送的信息
            System.out.println("请输入要推送的消息");
            Scanner scanner = new Scanner(System.in);
            String content = scanner.next();
            //获取Message
            Message 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(); //所有的socket
            for (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.mfbz.cn/a/496838.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

智能车主控板原理图原理讲解

智能车主控板原理图原理讲解 综述&#xff1a;本篇文章对智能车主控板的一部分电路进行原理分析&#xff0c;文末附加整体原理图。 1. 电源电路 &#xff08;1&#xff09;通过外接电池供电并通过电源模块电路&#xff0c;运用稳压芯片lm2940&#xff0c;将电源电压转化为5V…

原生JS上传大文件分片

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

市场复盘总结 20240328

仅用于记录当天的市场情况&#xff0c;用于统计交易策略的适用情况&#xff0c;以便程序回测 短线核心&#xff1a;不参与任何级别的调整&#xff0c;采用龙空龙模式 一支股票 10%的时候可以操作&#xff0c; 90%的时间适合空仓等待 二进三&#xff1a; 进级率中 40% 最常用的…

代码随想录算法训练营第day60|84.柱状图中最大的矩形

84.柱状图中最大的矩形 力扣题目链接(opens new window) 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 思路&#xff1a; 为什么这么说呢&#xff…

第三十二天-PythonWeb主流框架-Django框架

目录 1.介绍 发展历史 介绍 2.使用 1.安装 2.创建项目 3.项目结构 4.启动 3.开发流程 1.设置ip可访问 2.创建模块 3.第一个页面 4.视图 5.include()参数 6.url与视图的关系 7.响应内容 4.视图处理业务逻辑 1.响应html 2.获取url参数 3.从文件响应html内容 …

趣味物理知识竞赛活动方案

为了丰富校园文化生活&#xff0c;创建格调高雅、学习氛围浓厚、青春气息浓郁的校园文化&#xff0c;注重多样性全方面的发展&#xff0c;推进了素质教育向纵深拓展&#xff0c;全面提升大学生的综合素质和精神文明建设全面发展。为进一步引导大学生了解前沿科技&#xff0c;普…

24计算机考研调剂 | 【官方】北京科技大学

北京科技大学 考研调剂招生信息 招生专业&#xff1a; 085404&#xff08;计算机技术&#xff09; 081200&#xff08;计算机科学与技术&#xff09; 调剂要求&#xff1a;&#xff08;调剂基本分数&#xff09; 我中心将在教育部“全国硕士生招生调剂服务系统”&#xff08…

面试算法-124-二叉树的最近公共祖先

题目 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个节点 p、q&#xff0c;最近公共祖先表示为一个节点 x&#xff0c;满足 x 是 p、q 的祖先且 x 的深度尽可能大&#xff08;一个节点也可以是它…

缓存雪崩问题及解决思路

实战篇Redis 2.7 缓存雪崩问题及解决思路 缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机&#xff0c;导致大量请求到达数据库&#xff0c;带来巨大压力。 解决方案&#xff1a; 给不同的Key的TTL添加随机值利用Redis集群提高服务的可用性给缓存业务添加降…

Requests教程-20-sign签名认证

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go 上一小节中&#xff0c;我们学习了requests的token认证方法&#xff0c;本小节我们讲解一下requests的sign签名认证。 在进行接口调用时&#xff0c;一般会要求进行签名操作&#xff0c;以确保接口的安全性和完整…

基于springboot实现课程作业管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现课程作业管理系统演示 摘要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;课程作业管理系统当然也不能排除在外。课程作业管理系统是以实际运用为开发背景…

项目实战:电影评论情感分析系统

目录 1.引言 2.数据获取与预处理 3.构建文本分类模型&#xff08;使用LSTM&#xff09; 4.结果评估与模型优化 4.2.结果评估 4.2.模型优化 5.总结 1.引言 在本篇文章中&#xff0c;将通过一个完整的项目实战来演示如何使用Python构建一个电影评论情感分析系统。涵盖从数…

OSCP靶场--pyLoader

OSCP靶场–pyLoader 考点(信息收集CVE-2023-0297) 1.nmap扫描 ┌──(root㉿kali)-[~/Desktop] └─# nmap -Pn -sC -sV 192.168.178.26 --min-rate 2500 Starting Nmap 7.92 ( https://nmap.org ) at 2024-03-28 09:14 EDT Nmap scan report for 192.168.178.26 Host is up…

二分图

数据结构、算法总述&#xff1a;数据结构/算法 C/C-CSDN博客 二分图&#xff1a;节点由两个集合组成&#xff0c;且两个集合内部没有边的图。换言之&#xff0c;存在一种方案&#xff0c;将节点划分成满足以上性质的两个集合。 染色法 目的&#xff1a;验证给定的二分图是否可…

房间采光不好怎么改造?这里有6个实用的解决方案!福州中宅装饰,福州装修

当装修中遇到房间采光不好的问题&#xff0c;可以从以下几个方面来解决&#xff1a; ①引入自然光源 尽可能减少光线阻碍物&#xff0c;例如可以考虑打通一些非承重墙&#xff0c;扩大窗户的面积&#xff0c;让阳光直接穿过阳台照射到室内。同时&#xff0c;也可以考虑在某些没…

YOLOV8逐步分解(2)_DetectionTrainer类初始化过程

接上篇文章yolov8逐步分解(1)--默认参数&超参配置文件加载继续讲解。 1. 默认配置文件加载完成后&#xff0c;创建对象trainer时&#xff0c;需要从默认配置中获取类DetectionTrainer初始化所需的参数args&#xff0c;如下所示 def train(cfgDEFAULT_CFG, use_pythonFalse…

17.注释和关键字

文章目录 一、 注释二、关键字class关键字 我们之前写的HelloWorld案例写的比较简单&#xff0c;但随着课程渐渐深入&#xff0c;当我们写一些比较难的代码时&#xff0c;在刚开始写完时&#xff0c;你知道这段代码是什么意思&#xff0c;但是等过了几天&#xff0c;再次看这段…

图片标注编辑平台搭建系列教程(3)——画布拖拽、缩放实现

简介 标注平台很关键的一点&#xff0c;对于整个图片为底图的画布&#xff0c;需要支持缩放、拖拽&#xff0c;并且无论画布位置在哪里&#xff0c;大小如何&#xff0c;所有绘制的点、线、面的坐标都是相对于图片左上角的&#xff0c;并且&#xff0c;拖拽、缩放&#xff0c;…

从零开始学习在VUE3中使用canvas(六):线条样式(线条宽度lineWidth,线条端点样式lineCap)

一、线条宽度lineWidth 1.1简介 值为一个数字 const ctx canvas.getContext("2d"); ctx.lineWidth 6; 1.2效果展示 1.3全部代码 <template><div class"canvasPage"><!-- 写一个canvas标签 --><canvas class"main" r…

图像处理与视觉感知---期末复习重点(5)

文章目录 一、膨胀与腐蚀1.1 膨胀1.2 腐蚀 二、开操作与闭操作 一、膨胀与腐蚀 1.1 膨胀 1. 集合 A A A 被集合 B B B 膨胀&#xff0c;定义式如下。其中集合 B B B 也称为结构元素&#xff1b; ( B ^ ) z (\hat{B})z (B^)z 表示 B B B 的反射平移 z z z 后得到的新集合。…
最新文章