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

news2024/9/24 11:21:55

文章目录

    • 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.coloradmin.cn/o/1552031.html

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

相关文章

ssh 公私钥(github)

一、生成ssh公私钥 生成自定义名称的SSH公钥和私钥对&#xff0c;需要使用ssh-keygen命令&#xff0c;这是大多数Linux和Unix系统自带的标准工具。下面&#xff0c;简单展示如何使用ssh-keygen命令来生成具有自定义名称的SSH密钥对。 步骤 1: 打开终端 首先&#xff0c;打开我…

vue前端工程化

前言 本文介绍的是有关于vue方面的前端工程化实践&#xff0c;主要通过实践操作让开发人员更好的理解整个前端工程化的流程。 本文通过开发准备阶段、开发阶段和开发完成三个阶段开介绍vue前端工程化的整体过程。 准备阶段 准备阶段我将其分为&#xff1a;框架选择、规范制…

AI智能分析网关智慧食安监管系统方案

3.15晚会刚过不久&#xff0c;淀粉肠的“屈辱”终于得以洗清&#xff0c;但某些品牌奶茶、梅菜扣肉、预制菜等等&#xff0c;生产过程仍是触目惊心。如何提升食品安全管理水平&#xff0c;保障食品从生产到消费环节的质量和安全&#xff1f;TSINGSEE青犀智利用智能分析网关V4Ea…

mysql--事务四大特性与隔离级别

事务四大特性与隔离级别 mysql事务的概念事务的属性事务控制语句转账示例 并发事务引发的问题脏读脏读场景 不可重复读幻读幻读场景 事务的隔离级别读未提交读已提交可重复读&#xff08;MySQL默认&#xff09; 总结 mysql事务的概念 事务就是一组操作的集合&#xff0c;他是一…

x86的内存分段机制

8086 是 Intel 公司第一款 16 位处理器&#xff0c;诞生于 1978 年&#xff0c;所以说它很古老。 一.8086 的通用寄存器 8086 处理器内部共有 8 个 16 位的通用处理器&#xff0c;分别被命名为 AX、 BX、 CX、 DX、 SI、 DI、 BP、 SP。如下图所示。 “通用”的意思是…

幻兽帕鲁服务器多少钱?可真便宜呀

2024年全网最全的幻兽帕鲁服务器租用价格表&#xff0c;阿里云幻兽帕鲁游戏服务器26元1个月、腾讯云32元一个月、京东云26元一个月、华为云24元1个月&#xff0c;阿腾云atengyun.com整理最新幻兽帕鲁专用4核16G、8核16G、8核32G游戏服务器租用价格表大全&#xff1a; 阿里云幻…

8、鸿蒙学习-HAR

HAR&#xff08;Harmony Archive&#xff09;是静态共享包&#xff0c;可以包含代码、C库、资源和配置文件。通过HAR可以实现多个模块或多个工程共享ArkUI组件、资源等相关代码。HAR不同于HAP&#xff0c;不能独立安装运行在设备上。只能作为应用模块的依赖项被引用。 一、创建…

【案例·增】获取当前时间、日期(含,SQL中DATE数据类型)

问题描述&#xff1a; 需要使用当前时间、日期&#xff0c;可以使用 SQL 中的 CURDATE() 、NOW()、CURTIME()运算符 案例&#xff1a; INSERT INTO table_name(current_time, column_name2,...) VALUES (NOW(),, ...)规则(Date 相关函数)&#xff1a; 规则(Date数据类型)

初识C++之命名空间(namespace)

初识C之入门 命名空间(namespace) 文章目录 初识C之入门 命名空间(namespace)1.为什么要有命名空间2. 命名空间 namespace使用方法3. 作用域限定符(::&#xff09;和 命名空间(namespace)4. 命名空间的定义5. 命名空间的嵌套6. 命名空间的使用7. 总结 1.为什么要有命名空间 在C…

通过Caliper进行压力测试程序,且汇总压力测试问题解决

环境要求 第一步. 配置基本环境 部署Caliper的计算机需要有外网权限;操作系统版本需要满足以下要求:Ubuntu >= 16.04、CentOS >= 7或MacOS >= 10.14;部署Caliper的计算机需要安装有以下软件:python 2.7、make、g++(gcc-c++)、gcc及git。第二步. 安装NodeJS # …

Tensorflow2.0笔记 - 自定义Layer和Model

本笔记主要记录如何在tensorflow中实现自定的Layer和Model。详细内容请参考代码中的链接。 import time import tensorflow as tf from tensorflow import keras from tensorflow.keras import datasets, layers, optimizers, Sequential, metricstf.__version__ #关于自定义l…

软考 - 系统架构设计师 - 关系模型的完整性规则

前言 关系模型的完整性规则是一组用于确保关系数据库中数据的完整性和一致性的规则。这些规则定义了在关系数据库中如何存储、更新和查询数据&#xff0c;以保证数据的准确性和一致性。 详情 关系模型的完整性规则主要包括以下三类&#xff1a; 实体完整性规则 这是确保每个…

【Java程序设计】【C00373】基于(JavaWeb)Springboot的社区疫情返乡管控系统(有论文)

TOC 博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;博客中有上百套程序可供参考&#xff0c;欢迎共同交流学习。 项目简介 项目获取 &#x1f345;文末点击卡片…

计算机网络——26通用转发和SDN

通用转发和SDN 网络层功能&#xff1a; 转发&#xff1a; 对于从某个端口 到来的分组转发到合适的 输出端口路由&#xff1a; 决定分组从源端 到目标端的路径 网络层 传统路由器的功能 每个路由器(Per Route)的控制平面 &#xff08;传统&#xff09; 每个路由器上都有实…

线程创建方式、构造方法和线程属性

欢迎各位&#xff01;&#xff01;&#xff01;推荐PC端观看 文章重点&#xff1a;学会五种线程的创造方式 目录 1.开启线程的五种方式 2.线程的构造方法 3.线程的属性及获取方法 1.开启线程的五种方式 创造线程的基本两步&#xff1a;&#xff08;1&#xff09;使用run方法…

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

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

白盒测试、接口测试、自动化测试详解

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、什么是白盒测试 白盒测试是一种测试策略&#xff0c;这种策略允许我们检查程序的内部结构&…

NEO 学习之 MLE(最大似然估计)

文章目录 简单题目MLE 在不同的分布的运用正态分布指数分布均匀分布泊松分布 简单题目 此题问的是求丢色子&#xff0c;求得到偶数点的概率 求两次都得到硬币的背面的概率 拿球问题 符合的点数是 1,5,6 MLE 在不同的分布的运用 正态分布 对于给定的数据集 {1, 3, 4, 6, 7}&am…

linux:线程同步

个人主页 &#xff1a; 个人主页 个人专栏 &#xff1a; 《数据结构》 《C语言》《C》《Linux》 文章目录 前言线程同步条件变量接口简单示例pthread_cond_wait为什么要有mutex伪唤醒问题的解决 (if->while) 总结 前言 本文作为我对于线程同步知识总结 线程同步 同步&…

Appium设备交互API

设备交互API指的是操作设备系统中的一些固有功能&#xff0c;而非被测程序的功能&#xff0c;例如模拟来电&#xff0c;模拟发送短信&#xff0c;设置网络&#xff0c;切换横竖屏&#xff0c;APP操作&#xff0c;打开通知栏&#xff0c;录屏等。 模拟来电 make_gsm_call(phon…