项目——群英阁(galaxyHub)

news2024/11/28 16:37:12

目录

  • 一、项目概述
  • 二、设计思路
  • 三、项目流程
  • 四、项目代码
    • 头文件:👇
    • server端:👇
    • client端:👇
  • 五、运行效果

一、项目概述

项目中文名称:群英阁
项目英文名称:galaxyHub

利用UDP通信实现局域网内的多人在线聊天(即群聊),即所有用户处在同一局域网下,多个(大于等于2)用户在客户端登录系统,用户发送消息之后其他用户都可以在其终端收到发送者的用户信息极其发送的消息。我们知道QQ群聊中不仅有群聊的功能,还可以限定群中某个用户进行私聊(指定在线用户私法消息),以及群通知的功能,该项目也仿照QQ添加了私聊以及系统通知的功能。
该项目的实现可以对UDP通信,数据结构,进程线程进行综合运用。

二、设计思路

为什么采用UDP通信实现? 采用TCP当然也是可以的。TCP通信是面向连接的,有三次握手和四次挥手机制,且能提供高可靠性通信,但是要实现一个服务端响应多个客户端(即实现并发服务器)相对繁琐。其中并发服务器的实现有如下几种实现方式:

  • 多进程。服务端每来一个客户端连接,便开一个子进程来专门处理客户端的数据,实现简单,但是系统开销相对较大。
  • 多线程。每来一个客户端连接,开一个子线程来专门处理客户端的数据,实现简单,占用资源较少,属于使用比较广泛的模型。
  • IO多路复用。借助select、poll、epoll机制,将新连接的客户端描述符增加到描述符表中,只需要一个线程即可处理所有的客户端连接,在嵌入式开发中应用广泛,不过代码写起了稍显繁琐。

UDP通信是面向无连接的,没有三次握手和四次挥手机制,客户端不需要对服务端进行绑定连接,一个服务端可以响应多个客户端,这样便很简单的实现并发服务器。

项目总的通信传输协议是基于UDP的,客户端和服务端是面向无连接的,一个服务端可以与多个客户端通信,从而实现并发服务器。为了保证客户端和服务器之间通信正确无误,需建立一个“通信协议”,即建立一个存放消息结构体,该结构体包含了消息类型、消息所有者、消息内容。

假设一个服务端一个客户端,客户往服务端发消息,服务端可以收到,若有多个客户端一个服务器实现群聊,客户端不知道所连接的其他客户端,不能直接群发给其他客户实现群聊,那只能通过服务端间接实现,即一个客户端往服务器发送消息,服务器收到消息后将此消息转发给其他用户,实现群聊。那么问题来了,服务器如何获取客户端信息实现消息转发呢?我们知道只要服务端通过recvfrom函数收到了客户端的一条消息便可以获取到客户端的ip和端口(存入sockaddr_in结构体中),因此就满足了服务器消息发送的条件,我们可以在客户端发送登陆消息时在服务端获取到其通信结构体,并保存下来,登陆一个保存一个。问题又来了,通过什么数据结构呢?常用的主要是顺序表和链表,顺序表的大小在建立时便确定,链表大小不确定,随用随加,考虑到我们不确定有多少客户端连接,故采用链表来存储客户端的通信结构体,登录一个客户端,便将其通信结构加入链表保存。客户端的登录信息实现了存储,自然而然群聊功能就不难了,服务端收到客户端发来的群聊类型消息后,可以遍历保存有所有已登录客户通信结构体的链表,遍历一个节点发送一个实现群聊,但要注意遍历时要剔除发送消息的客户端。

私聊的话,首先客户端指定私聊对象,要是能直接指出私聊的客户端的ip和端口最好,但是不好拿,就算拿到了客户要私聊时在终端输入一串ip和端口未免显得繁琐,私聊对象名字就相对好拿到(因为在终端可以看到其他客户端的名字对应的登录信息以及聊天信息),可以指定名字,将私聊对象名发给服务器,服务器拿到私聊对象名字就可以在链表中遍历查找(因此链表中不仅包含客户端通信结构体,还应包含其对应的登录名称),找到后直接定向转发发送私聊信息,实现了私聊功能。

有了上面的分析,系统通知功能就简单了。服务端在终端获取待发系统消息,只需遍历链表,同时对当前遍历到的节点对应的客户端发送系统消息即可。到这可以看出在客户端要有两个进程或者线程,一个进程(或线程)发送消息给服务器,另一个接收服务端消息;服务端同样要有两个进程或者线程,一个进程(或线程)发送系统消息,另一个接收客户端消息。这里要注意的是,若客户端采用两个进程,需处理好程序退出时进程资源的回收问题,避免僵尸进程或者孤儿进程的出现。

三、项目流程

在这里插入图片描述
在这里插入图片描述

四、项目代码

头文件:👇

#ifndef __HEAD_H__
#define __HEAD_H__
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <string.h>
#include <pthread.h>

//定义链表节点结构体,用来存放客户端的通信结构体
typedef struct node
{
    struct sockaddr_in addr; //登录客户通信结构体
    char name[32];           //登录客户用户名
    struct node *next;
} link_node;

//消息对应的结构体(保证客户端服务端同一个协议)
typedef struct msg_t
{
    int type;       //消息类型
    char name[32];  //登录客户用户名
    char text[128]; //消息正文
} MSG_t;

//枚举,表示“登录、聊天、退出”三种消息类型
enum un
{
    login,
    chat,
    quit,
};

#endif

server端:👇

/*
    项目中文名称:群英阁(服务端)
    项目英文名称:galaxyHub
    完成时间:2023/8/23
    作者:Sunqk5665
*/
#include "head.h"

void theForeword();                                                                      //说明
link_node *CreatEpLinkList();                                                            //创建空的有头单向链表
void login_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p);           //登录
void chat_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p);            //聊天
void quit_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p);            //退出
void privChat(int sockfd, MSG_t msg, struct sockaddr_in caddr, char *buf, link_node *p); //私聊
void *systemNotice(void *arg);                                                           //线程处理函数,给所有客户发送系统通知

int sockfd;
struct sockaddr_in saddr, caddr;

int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("Please input:%s <port>\n", argv[0]);
        return 0;
    }
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("sockfd err.");
        return -1;
    }
    //填充通信结构体(IPV4)
    // struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    //bind
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err.");
        return -1;
    }

    socklen_t len = sizeof(caddr);
    link_node *p = CreatEpLinkList(); //创建空的有头单向链表

    MSG_t msg; //创建消息结构体变量
    theForeword();

    //创建一个线程从终端获取消息,并向服务器发通知
    pthread_t tid;
    pthread_create(&tid, NULL, systemNotice, NULL);
    while (1)
    {
        if (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&caddr, &len) < 0)
        {
            perror("recvfrom err.");
            return -1;
        }
        switch (msg.type)
        {
        case login:                           //接收到的客户端消息为“登录”类型
            login_fun(sockfd, msg, caddr, p); //调用登录函数
            break;
        case chat:                           //接收到的客户端消息为“聊天”类型
            chat_fun(sockfd, msg, caddr, p); //调用聊天函数
            break;
        case quit:                           //接收到的客户端消息为“退出”类型
            quit_fun(sockfd, msg, caddr, p); //调用退出函数
            break;
        default:
            break;
        }
    }
    close(sockfd);
    free(p); //释放空间
    p = NULL;
    return 0;
}
//创建空的有头单向链表
link_node *CreatEpLinkList()
{
    link_node *h = (link_node *)malloc(sizeof(link_node));
    if (NULL == h)
    {
        printf("CreatEpLinkList error!\n");
        return NULL;
    }
    h->next == NULL;
    return h;
}
//登录函数
void login_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p)
{
    sprintf(msg.text, "[%s]上线了...", msg.name); //将客户端上线信息放入消息正文中
    while (p->next != NULL)        //将登录信息发送给其他客户端(不包括现在登录的客户端,故先遍历发送)
    {
        p = p->next;
        sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
    }
    //更新已连接的客户端信息:将当前登录的客户端所建立的通信结构体插入单链表尾部
    link_node *pnew = (link_node *)malloc(sizeof(link_node));
    pnew->addr = caddr;
    strcpy(pnew->name, msg.name);
    pnew->next = NULL;
    p->next = pnew;

    //客户端的信息已加入链表,给发登录消息的客户端回一个"Login ok"
    strcpy(msg.text, "Login ok");
    sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&caddr, sizeof(caddr));
    printf("[%s]上线了...ip=%s,port=%d\n", msg.name, inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
}
//聊天函数
void chat_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p)
{
    printf("[%s]>>%s\n", msg.name, msg.text); //服务端打印一下当前聊天信息
    char sname[32] = {0};
    char stext[128] = {0};
    if (msg.text[0] == '@') //私聊
    {
        int i = 1, j = 0;
        while (msg.text[i] != ' ')
        {
            sname[j] = msg.text[i]; //@abc abc
            i++;
            j++;
        }
        sname[j] = '\0';
        strcpy(stext, msg.text + i + 1);
        strcpy(msg.text, "(私聊信息)");
        strcat(msg.text, stext);
        privChat(sockfd, msg, caddr, sname, p);
    }
    else // 群聊
    {
        //将当前发消息的客户端的消息发送给其他客户端
        while (p->next != NULL)
        {
            p = p->next;
            if (memcmp(&caddr, &(p->addr), sizeof(caddr)) != 0) //相等返回0,这样保证消息不发给发送消息的客户端
            {
                sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
            }
        }
    }
}
//私聊函数
void privChat(int sockfd, MSG_t msg, struct sockaddr_in caddr, char *name, link_node *p)
{
    while (p->next != NULL) // 查找私聊对象
    {
        p = p->next;
        if (strcmp(name, p->name) == 0) //相等返回0
        {
            sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
        }
    }
}
//退出函数
void quit_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p)
{
    printf("[%s]下线了...\n", msg.name);          //服务端打印一下当前推出聊天室的客户端信息
    link_node *pre, *pdel;                        //pre:指向p的前一个结点,pdel:指向要删除结点
    sprintf(msg.text, "[%s]下线了...", msg.name); //将客户端下线信息放入消息正文中,以便下面的消息推送
    while (p->next != NULL)
    {
        //在链表中找到当前退出的客户端的caddr进行删除,并向其他在线的客户端推送消息
        pre = p;
        p = p->next;
        if (!memcmp(&caddr, &(p->addr), sizeof(caddr))) //是要退出的客户端,删除
        {
            pdel = p;
            pre->next = p->next;
            free(pdel);
        }
        else //不是退出的客户端的通信结构体,则将退出的客户端的消息发送给其他客户端
        {
            //将退出的客户端信息发送给其他客户端
            sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
        }
    }
}
void *systemNotice(void *arg)
{
    MSG_t msg;
    msg.type = chat;
    strcpy(msg.name, "<系统通知>@所有人");
    while (1)
    {
        fgets(msg.text, sizeof(msg.text), stdin);
        if (msg.text[strlen(msg.text) - 1] == '\n')
            msg.text[strlen(msg.text) - 1] = '\0';
        sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, sizeof(saddr));
    }
    return NULL;
}
void theForeword()
{
    printf("说明:你作为超级管理员,你可以向群聊用户发送「系统通知」,\n只需在终端输入消息,回车即可发送\n\n");
    printf("**********************************************\n");
    printf("* This project has been created by Sunqk5665 *\n");
    printf("**********************************************\n\n");
}

client端:👇

/*
    项目中文名称:群英阁(客户端)
    项目英文名称:galaxyHub
    完成时间:2023/8/23
    作者:Sunqk5665
*/
#include "head.h"

void menuShow();    //菜单
void theForeword(); //说明

int main(int argc, char const *argv[])
{
    if (argc != 3)
    {
        printf("Please input:%s <ip> <port>\n", argv[0]);
        return 0;
    }
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("sockfd err.");
        return -1;
    }
    //填充服务端的通信结构体
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[2]));      //port
    saddr.sin_addr.s_addr = inet_addr(argv[1]); //ip

    socklen_t len = sizeof(saddr);
    MSG_t msg; //创建消息结构体变量
    char buf[32] = {0};
    //客户端上来先登录。给服务端发送登录消息,以便服务端获取到当前客户端的连接信息
    //登录-只登录一次(消息只发送一次)+判断是否登录成功,没有成功继续登录
    theForeword();
    menuShow();
    printf("请输入login登录!!!\n");
    while (1)
    {
        fgets(buf, 32, stdin);
        if (buf[strlen(buf) - 1] == '\n') //剔除换行符,改为'\0'
            buf[strlen(buf) - 1] = '\0';
        if (strcmp(buf, "login") == 0)
        {
            printf("请输入用户名:");
            fgets(msg.name, 32, stdin);
            if (msg.name[strlen(msg.name) - 1] == '\n') //剔除换行符,改为'\0'
                msg.name[strlen(msg.name) - 1] = '\0';
            msg.type = login; //将消息类型设置为login
            if (sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, len) < 0)
            {
                perror("sendto login err.");
                return -1;
            }
            // 接收服务端发来的客户端登录成功的消息,确保客户端成功登录
            if (recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL) < 0)
            {
                perror("recvfrom confirm err");
                printf("请输入login重新登录!!!\n");
                continue;
            }
            if (strcmp(msg.text, "Login ok") == 0) //收到了服务端发来的客户端登录成功的消息
            {
                printf("login ok...\n\n");
                break;
            }
            else //未收到重新登录
            {
                printf("请输入login重新登录!!!\n");
                continue;
            }
        }
        else if(strcmp(buf, "quit")==0)
        {
            exit(-1);
        }
        else if(strcmp(buf, "help")==0)
        {
            menuShow();
            printf("请输入login重新登录!!!\n");
            continue;
        }
        else
        {
            printf("请输入login重新登录!!!\n");
            continue;
        }
    }

    pid_t pid = fork(); //创建子进程
    if (pid < 0)
    {
        perror("fork err.");
        return -1;
    }
    else if (pid == 0) //子进程接收server端消息
    {
        while (1)
        {
            recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, &len);
            if (msg.type == login)
                printf("(上线提醒)%s\n", msg.text);
            else if (msg.type == chat)
                printf("[%s]:%s\n", msg.name, msg.text);
            else if (msg.type == quit)
                printf("(下线提醒)%s\n", msg.text);
        }
    }
    else //父进程向server端发送消息
    {
        while (1)
        {
            fgets(msg.text, sizeof(msg.text), stdin);
            if (msg.text[strlen(msg.text) - 1] == '\n') //剔除消息正文的换行符
                msg.text[strlen(msg.text) - 1] = '\0';
            if (strcmp(msg.text, "help") == 0)
            {
                menuShow();
            }
            else if (strcmp(msg.text, "quit") == 0) //客户端消息为"quit"退出命令
            {
                msg.type = quit; //消息类型设置为“quit”
                sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, len);
                kill(pid, SIGKILL); //杀死子进程
                wait(NULL);         //阻塞回收子进程
                exit(-1);           //父进程退出
            }
            else //执行到这,消息肯定为聊天类型的
            {
                msg.type = chat; //消息类型设置为“chat”
                sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, len);
            }
        }
    }
    close(sockfd);
    return 0;
}
void theForeword()
{
    printf("说明:用户登录成功后的状态即为「群聊模式」,要想「私聊」某一用户,每次只需作如下输入:\n");
    printf("   @私聊对象用户名 消息内容\n\n");
    printf("***************** Client *********************\n");
    printf("* This project has been created by Sunqk5665 *\n");
    printf("**********************************************\n\n");
}
void menuShow()
{
    printf("=========THE MENU===========\n");
    printf("*      help : 查看菜单     *\n");
    printf("*         默认群聊         *\n");
    printf("*   @<姓名> <消息> : 私聊  *\n");
    printf("*     quit  : 退出聊天     *\n");
    printf("============================\n");
}

五、运行效果

在这里插入图片描述

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

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

相关文章

HikariCP源码修改,使其连接池支持Kerberos认证

HikariCP-4.0.3 修改HikariCP源码,使其连接池支持Kerberos认证 修改后的Hikari源码地址:https://github.com/Raray-chuan/HikariCP-4.0.3 Springboot使用hikari连接池并进行Kerberos认证访问Impala的demo地址:https://github.com/Raray-chuan/springboot-kerberos-hikari-im…

Lambda表达式第三版,从3个方面分析。(①抽象方法无参数无返回值,②抽象方法带参数无返回值,③抽象方法带参数有返回值)

1、函数式编程思想概述 在数学中&#xff0c;函数就是有输入量、输出量的一套计算方案&#xff0c;也就是”拿数据做操作“面向对象思想强调”必须通过对象的形式来做事情“函数式思想则尽量忽略面向对象的复杂语法&#xff1a;”强调做什么&#xff0c;而不是以什么形式去做&a…

【javaweb】学习日记Day8 - Mybatis入门 Mysql 多表查询 事务 索引

之前学习过的SQL语句笔记总结戳这里→【数据库原理与应用 - 第六章】T-SQL 在SQL Server的使用_Roye_ack的博客-CSDN博客 【数据库原理与应用 - 第八章】数据库的事务管理与并发控制_一级封锁协议_Roye_ack的博客-CSDN博客 目录 一、多表查询 1、概述 &#xff08;1&#…

性能测试(测试系列10)

目录 前言&#xff1a; 1.什么是性能测试 1.1生活中遇到的软件问题 1.2性能测试的定义 1.3性能测试和功能测试有什么区别 1.4性能的好坏的区分 1.5影响一个软件性能的因素 2.为什么要进行性能测试 3.性能测试常见的术语以及衡量指标 3.1并发 3.2用户数 3.3响应时间 …

Jmeter如何设置中文版

第一步&#xff1a;找到 apache-jmeter-5.4.3\bin目录下的 jmeter.properties 第二步:打开 三&#xff0c;ctrf 输入languageen&#xff0c;注释掉&#xff0c;增加以行修改如下 四&#xff0c;ctrs 保存修改内容&#xff0c;重新打开jmeter就可以了

golang-bufio 缓冲写

1. 缓冲写 在阅读这篇博客之前&#xff0c;请先阅读上一篇&#xff1a;golang-bufio 缓冲读 // buffered output// Writer implements buffering for an io.Writer object. // If an error occurs writing to a Writer, no more data will be // accepted and all subsequent…

5.0: Dubbo服务导出源码解析

#Dubbo# 文章内容 Dubbo服务导出基本原理分析Dubbo服务注册流程源码分析Dubbo服务暴露流程源码分析服务导出的入口方法为ServiceBean.export(),此方法会调用ServiceConfig.export()方法,进行真正的服务导出。 1. 服务导出大概原理 服务导出的入口方法为ServiceBean.export…

stm32之28.ADC

须看原理图&#xff08;引脚、电压值、ADC几号通道&#xff09;配置 。 若对比值0~4096 模拟电压/参考电压4096/x 假设模拟电压2.1V&#xff0c;参考电压3.3v&#xff0c;4096/x3.3/2.1 ->3.3x2.1x4096 ->x2,606.5 也可反推出模拟电压 ADC转换时间 ADC时钟来源于…

leetcode645. 错误的集合(java)

错误的集合 题目描述优化空间代码演示 题目描述 难度 - 简单 LC645 - 错误的集合 集合 s 包含从 1 到 n 的整数。不幸的是&#xff0c;因为数据错误&#xff0c;导致集合里面某一个数字复制了成了集合里面的另外一个数字的值&#xff0c;导致集合 丢失了一个数字 并且 有一个数…

时序预测 | MATLAB实现CNN-GRU卷积门控循环单元时间序列预测(风电功率预测)

时序预测 | MATLAB实现CNN-GRU卷积门控循环单元时间序列预测&#xff08;风电功率预测&#xff09; 目录 时序预测 | MATLAB实现CNN-GRU卷积门控循环单元时间序列预测&#xff08;风电功率预测&#xff09;预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.时序预测 | MA…

Python中的绝对和相对导入

在本文中&#xff0c;我们将看到Python中的绝对和相对导入。 Python中导入的工作 Python中的import类似于C/C中的#include header_file。Python模块可以通过使用import导入文件/函数来访问其他模块的代码。import语句是调用import机制的最常见方式&#xff0c;但它不是唯一的…

常见数学名词的物理意义(更新中)

BetterExplained – Math lessons that click 一、复数 i 的物理意义就是旋转&#xff0c;i 就是逆时针旋转90&#xff0c;i*i 就是逆时针旋转180 加法&#xff1a;实部相加&#xff0c;虚部相加 乘法&#xff1a; 复数zxyi控制了函数的放缩和旋转 ——x 放缩&#xff0c;…

Shell开发实践:服务器的磁盘、CPU、内存的占用监控

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

基于vue-cli创建后台管理系统前端页面——element-ui,axios,跨域配置,布局初步,导航栏

目录 引出安装npm install安装element-ui安装axios 进行配置main.js中引入添加jwt前端跨域配置 进行初始布局HomeView.vueApp.vue 新增页面和引入home页面导航栏总结 引出 1.vue-cli创建前端工程&#xff0c;安装element-ui&#xff0c;axios和配置&#xff1b; 2.前端跨域的配…

SQL查询本年每月的数据

--一、以一行数据的形式&#xff0c;显示本年的12月的数据&#xff0c;本示例以2017年为例&#xff0c;根据统计日期字段判断&#xff0c;计算总和&#xff0c;查询语句如下&#xff1a;selectsum(case when datepart(month,统计日期)1 then 支付金额 else 0 end) as 1月, sum…

UE4 春节鞭炮

先搞个基类&#xff0c;一个鞭炮的 搞个鞭炮类&#xff0c;存多个鞭炮 在构造函数的位置先生成对应的鞭炮数 将鞭炮绑定到绳子上&#xff0c;随绳子摆动而一起摆动 在基类里面写爆炸事件 最后用Timer去调用

一篇文章搞懂Redis缓存

目录 一、什么是缓存缓存的优缺点缓存的优点缓存的缺点 二、Redis缓存三、缓存的更新策略主动更新策略 四、缓存穿透解决方案 五、缓存雪崩解决方案 六、缓存击穿解决方案 一、什么是缓存 我们都知道在计算机中内存的速度比磁盘要快非常多&#xff0c;如果每次都要去磁盘获取数…

《Python魔法大冒险》004第一个魔法程序

在图书馆的一个安静的角落,魔法师和小鱼坐在一张巨大的桌子前。桌子上摆放着那台神秘的笔记本电脑。 魔法师: 小鱼,你已经学会了如何安装魔法解释器和代码编辑器。是时候开始编写你的第一个Python魔法程序了! 小鱼:(兴奋地两眼放光)我准备好了! 魔法师: 不用担心,…

Lesson6---案例:人脸案例

学习目标 了解opencv进行人脸检测的流程了解Haar特征分类器的内容 1 基础 我们使用机器学习的方法完成人脸检测&#xff0c;首先需要大量的正样本图像&#xff08;面部图像&#xff09;和负样本图像&#xff08;不含面部的图像&#xff09;来训练分类器。我们需要从其中提取特…

解决Debian系统通过cifs挂载smb后,中文目录乱码问题

解决Debian系统通过cifs挂载smb后&#xff0c;中文目录乱码问题 //$smb_server/share /mnt/nas_share cifs credentials/root/.smbcredentials,iocharsetutf8 0 0默认通过以上命令挂载smb&#xff0c;但是在查看文件目录时&#xff0c;中文乱码 解决问题方式&#xff1a; de…