网络套接字编程(三)

网络套接字编程(三)

文章目录

  • 网络套接字编程(三)
    • 简易日志组件
      • 引入日志的原因
      • 日志等级
      • 打印日志函数
      • 将日志组件使用到服务端中
    • 守护进程
      • 概念
      • 进程组、终端、会话
      • 守护进程的实现原理
      • 守护进程化组件
      • 将守护进程化组件使用到服务端中
    • 补充知识
      • 关于inet_ntoa

在上一篇博客 网络套接字编程(二)-CSDN博客中讲解了单执行流、多执行流、线程池版的简易TCP程序的编写,本文将讲解与其有关的组件编写。

简易日志组件

引入日志的原因

在实际开发中,服务端是需要不间断运行来保证无论何时都能给客户端提供网络服务,因此在程序遇到某些不影响程序运行的问题,不会主动终止程序,而是将错误信息以日志的形式打印。服务端维护人员,会通过日志中记录的错误信息,来进行程序错误的定位和解决。

日志等级

在日志系统中,常常使用不同的日志等级来对日志进行分类和标记,以便根据重要性和紧急程度进行过滤、查看和处理。不同的日志等级通常表示了不同的日志消息类型和优先级。

以下是常见的日志等级,按照从高到低的顺序排列:

  1. 致命错误(Fatal):表示严重的错误或故障,导致系统无法正常运行或继续执行。这类错误需要立即解决,并可能需要中断程序的执行。
  2. 错误(Error):表示一些关键操作或功能发生了错误,但系统仍然可以继续运行。这类错误需要进行修复,以确保系统正常运行。
  3. 警告(Warning):表示一些潜在问题或异常情况,可能会影响系统的正常运行或导致错误。这类日志用于提示潜在的风险或不寻常的行为,需要进行检查和调查。
  4. 信息(Info/Information):表示一般的信息性消息,用于记录程序的正常运行状态、关键路径、重要操作等。这类日志用于追踪应用程序的运行情况和关键事件。
  5. 调试(Debug):表示开发过程中的调试信息,用于调试和分析程序的内部逻辑、变量状态等。这类日志通常只在开发和测试阶段启用,可用于排查问题和验证程序行为。

本文采用枚举的方式来表示各个日志等级:

enum
{
    Debug=0,
    Info,
    Warning,
    Error,
    Fatal,
    Unknown
};

打印日志函数

本文想实现的日志组件中,打印的日志格式为[日志等级][时间][进程pid][日志消息体],日志组件的实现中核心部分就是打印日志函数,日志组件的具体实现如下:

#pragma once

#include <cstdio>
#include <cstring>
#include <cstdarg>
#include <iostream>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>

const std::string filename = "./log.txt";

enum
{
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal,
    Unknown
};

static std::string toLevelString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Unknown";
    }
}

static std::string getTime()
{
    const time_t cur = time(nullptr);
    struct tm *tmp = localtime(&cur);
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", (tmp->tm_year) + 1900, tmp->tm_mon+1, tmp->tm_mday,
             tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
    return buffer;
}

// 等级 时间 pid 消息体
void LogMessage(int level, const char *format, ...)
{
    char logLeft[1024];
    std::string level_string = toLevelString(level);
    std::string curr_time = getTime();
    snprintf(logLeft, sizeof(logLeft), "[%s][%s][%d]\n", level_string.c_str(), curr_time.c_str(), getpid());

    char logRight[1024];
    va_list p;
    va_start(p, format);
    vsnprintf(logRight, sizeof(logRight), format, p);
    va_end(p);

    FILE *fp = fopen(filename.c_str(), "a");
    if (fp == nullptr)
        return;
    fprintf(fp, "%s%s\n", logLeft, logRight);
    fflush(fp);
    fclose(fp);
}

识别日志等级函数

我们预期的打印中,日志等级是通过字符串形式打印的,而不是枚举对应的数值,因此需要实现一个识别日志等级函数,将传入的枚举数值转换成字符串形式表示的日志等级。

该函数实现只需要简单的switch case语句将输入的数值对应成的字符串返回即可。

时间获取函数

我们预期的打印中,包含一个以字符串形式打印的时间部分,并且希望这个时间具体到年月日,时分秒,因此我们需要实现一个时间获取函数。

要实现这个时间获取函数,需要用到如下两个函数:

#include <time.h>

time_t time(time_t *t);
  • t参数: 如果t指针不为NULL,则time函数会将计算出的时间值存储在t指向的变量中,并返回该值;否则,time函数直接返回计算出的时间值。
  • time函数返回自1970年1月1日经过的秒数(也称为Unix时间戳),其类型为time_t。
#include <time.h>

struct tm *localtime(const time_t *timep);
  • 该函数能够将传入的time_t类型的时间值转换为本地时间(系统默认时区)的tm结构体类型。
  • 返回值是转换后得到的tm结构体类型。
  • 由于localtime函数返回的tm结构体中有些成员的范围并不完全符合人们常用的表示方式,比如tm_mon表示的月份范围是0到11,因此在使用这些值进行操作和显示时,可能需要进行适当的转换和调整。

tm结构体的定义如下:

struct tm {
  int tm_sec;   // 秒 [0, 60]
  int tm_min;   // 分钟 [0, 59]
  int tm_hour;  // 小时 [0, 23]
  int tm_mday;  // 月份中的日期 [1, 31]
  int tm_mon;   // 月份 [0, 11],0表示一月
  int tm_year;  // 年份,减去1900后的值
  int tm_wday;  // 一周中的星期几 [0, 6],0表示星期日
  int tm_yday;  // 一年中的第几天 [0, 365],0表示1月1日
  int tm_isdst; // 夏令时标识符,负数表示不可确定状态
};

可变参数列表

C/C++语言标准库提供的 <cstdarg> 头文件中提供了可变参数列表的操作方法:

  • va_list 是一个类型,在实际的使用中,它通常被定义为指向变长参数列表的指针。
  • va_start 是一个宏函数,它用于对 va_list 类型的变量进行初始化,将其指向第一个可变参数的位置。在如上代码中,p是一个 va_list 类型的变量,format 是可变参数列表中的第一个参数。
  • vsnprintf 是一个函数,它可以根据提供的格式字符串 formatva_list 类型的变量 p,将可变参数列表中的值按照指定的格式进行格式化输出,并将结果存储到 logRight 字符数组中。
  • va_end 是一个宏函数,它用于结束可变参数的获取,进行必要的清理工作。在这个例子中,它将释放 p 变量所占用的资源。

将日志组件使用到服务端中

我们将日志组件使用在上一篇博客网络套接字编程(二)-CSDN博客中实现的线程池版的服务端中,添加日志组件需要需添加到服务端类内部的函数,添加日志组件后具体的代码实现如下:

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR
};
static const uint16_t default_port = 8081;
static const int backlog = 32;
class TcpServer
{
    public:
    TcpServer(uint16_t port = default_port) : _port(port) {}
    void InitServer()
    {
        // 创建套接字
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            LogMessage(Fatal, "create socket error, %d:%s", errno, strerror(errno)); // 打印日志
            exit(SOCKET_ERROR);
        }
        LogMessage(Info, "create socket success"); // 打印日志
        // 绑定IP地址和端口号
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(_port);
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            LogMessage(Fatal, "bind socket error, %d:%s", errno, strerror(errno)); // 打印日志
            exit(BIND_ERROR);
        }
        LogMessage(Info, "bind socket success"); // 打印日志
        // 监听
        if (listen(_listensock, backlog) < 0)
        {
            LogMessage(Fatal, "listen socket error, %d:%s", errno, strerror(errno)); // 打印日志
            exit(LISTEN_ERROR);
        }
        LogMessage(Info, "listen socket success"); // 打印日志
    }
    void StartServer()
    {
        ThreadPool<Task> tp;
        tp.Start();
        while (true)
        {
            // 获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sock < 0)
            {
                LogMessage(Fatal, "accept error, %d:%s", errno, strerror(errno)); // 打印日志
                continue;
            }
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            LogMessage(Info, "accept socket success, %s-%d,for%d", clientip.c_str() ,clientport, sock); // 打印日志
            // 网络服务
            Task t(sock, clientip, clientport);
            tp.PushTask(t);
        }
    }

    private:
    uint16_t _port;
    int _listensock;
};

程序测试

启动服务端,并且查看日志文件,可以看到服务端将信息打印到了日志文件中:

image-20231101185408082

使用客户端连接服务端进行网络通信,退出客户端,查看日志内容:

image-20231101185549658

守护进程

概念

守护进程(daemon)是在操作系统后台运行且独立于终端会话的一种特殊进程。它通常用于在系统启动时执行某些长期运行的任务或服务,如网络服务等。守护进程在启动时会脱离当前终端会话,以独立的进程在后台运行,不受终端关闭或用户注销的影响。

将服务端变成守护进程后,即使关闭启动进程的终端,也不会影响守护进程的运行,只要运行守护进程的主机不关闭,并且进程不出错,就可以实现让进程无停止的运行。实现在网络服务端中,就是让该服务端始终能够给客户端提供网络服务。

进程组、终端、会话

指令查看进程相关信息

在Xshell下每打开一个窗口就是建立一个终端,建立两个终端,其中一个启动一个后台运行的sleep进程,另一个使用指令ps axj | head -1 && ps axj | grep sleep 查看这个进程的信息:

image-20231101201647143

其中有如下信息:

  • PGID-进程组ID
  • SID-会话ID
  • TTY-终端文件

其中TTY下为?的进程与终端没有关系,显示为pts/n,即表示该进程打开了n号终端。在操作系统看来,就是该进程打开了该终端对应的终端文件,这些终端文件存在/dev设备文件路径下。如果向终端文件写入数据,就会显示在对应终端上:

image-20231101202749698

会话和进程组的概念

在Xshell下每启动一个终端,Linux操作系统就会为其创建一个会话,会话中会存在若干个包含一个或多个进程的进程组,其中存在bash以及其所在的进程组。而后在终端下所作的所有操作都会在这个会话中进行。

image-20231101204308388

进程组可能不止一个进程,进程组ID是一组进程中的第一个进程的进程ID或者具有”血缘“的进程中”辈分“最大的进程,比如一对父子进程,父进程ID为进程组ID。

启动一组sleep进程,查看它们的进程组ID:

image-20231101211733311

该组进程的进程组ID为第一个进程的进程ID。

进程组的作用是完成任务,其中任务是操作系统分配给进程或线程执行的工作。也就是操作系统会将一项工作交给一个进程组来完成,这个工作可能一个进程就能完成,也可能需要多个进程完成。如果一个进程能完成,该进程就会独立成组,多个进程才能完成,就会让多个进程形成进程组。

在Linux操作系统下,使用jobs指令可以查看该会话下的任务:

image-20231101210057826

使用fg 任务编号指令可以让后台进程变成前台进程:

image-20231101210150361

使用ctrl+z指令可以暂停前台进程并让他变成后台进程:

image-20231101210236435

使用bg 任务编号可以让暂停的后台进程运行:

image-20231101210332884

如果将一个后台任务变成前台任务,之前的前台任务就无法运行了,一个会话下只能有一个前台任务运行。开启一个终端,Linux操作系统就会为了对应创建一个会话,如果关闭这个终端,Linux操作系统就会销毁对应的会话,会话销毁就会影响会话中原有的进程。

守护进程的实现原理

守护进程的实现是让进程脱离启动它的终端对应的会话,自身独自处于一个会话中:

image-20231101211928687

进程独自处于一个会话后,就不会受到其他会话的影响,只要操作系统不停止运行,该进程就能一直运行下去。这就是一些提供网络服务的服务端进程的运行原理。

守护进程化组件

守护进程化组件的功能是让调用它的进程变成守护进程。

为了让进程变成守护进程需要使用Linux操作系统下提供的创建会话让进程独享的setsid函数:

#include <unistd.h>

pid_t setsid(void);
  • 该函数的功能是让调用的进程独自处于一个新的会话中。
  • 返回值: 调用成功,返回新的会话ID(调用该函数的进程的ID)。调用失败,返回-1,错误码被设置。
  • 注意: 调用该函数的进程不能是所处进程组的组长。

进程守护进程化的步骤:

  1. 忽略相关信号
  2. 让进程进程不再是组长
  3. 创建新的会话,让进程成为会话的首个进程
  4. (可选)更改进程的工作路径
  5. 处理进程的0,1,2号文件描述符

守护进程化组件的具体代码实现如下:

void Daemon()
{
    //忽略相关信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);
    //让自己不再是组长
    if (fork() > 0) exit(0);
    //创建新的会话,让自己成为首个进程
    pid_t ret = setsid();
    if ((int)ret == -1)
    {
        LogMessage(Fatal, "deamon error, code: %d, string: %s", errno, strerror(errno));//打印日志
        exit(SETSID_ERROR);
    }
    //处理0,1,2文件描述符
    int fd = open("/dev/null", O_RDWR);
    if (fd < 0 )
    {
        LogMessage(Fatal, "open error, code: %d, string: %s", errno, strerror(errno));//打印日志
        exit(OPEN_ERROR);
    }
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    close(fd);
}

守护进程是一种特殊的孤儿进程

由于调用setsid函数的进程不能是进程组组长,因此需要创建子进程完成setsid函数的调用,然后终止无用的父进程,因此最终完成任务的子进程,由于父进程终止了,因此守护进程是一种特殊的孤儿进程。执行任务是网络服务时,由于套接字操作中,始终是和相关的套接字文件进行操作,因此拷贝了父进程文件描述符表的子进程可以完成父进程的任务。

处理0,1,2文件描述符

由于守护进程不想受到外部设备输入的影响,也不想向外部设备输出,因此需要关闭0,1,2文件描述符,Linux操作系统中/dev/null是一个不会有任何实质性数据的文件,因此让守护进程从这个文件读取,不会读取到任何数据,让守护进程向这个文件写入,不会写入任何数据到外部设备上。落实到代码上就是让0,1,2文件描述符指向该文件。

将守护进程化组件使用到服务端中

使用守护进程化组件,只需要在创建服务端类并初始化后调用组件即可,具体的代码实现如下:

void Usage(const char *proc)
{
    std::cout << "Usage:\n\t" << proc << " port\n" << std::endl; 
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> tsvr(new TcpServer(port));
    tsvr->InitServer();
    Daemon();//守护进程化
    tsvr->StartServer();
    return 0;
}

程序测试

在一个终端中启动服务端进程,然后使用指令ps axj | head -1 && ps axj | grep sleep 查看进程信息:

image-20231101221051467

服务端进程独自使用了一个会话(该终端的会话ID和grep进程的一样),并且与终端无关。即使关闭终端也不会影响该服务端的运行了。

关闭该终端,启动一个新的终端运行客户端连接该服务器:

image-20231101221355151

另外还可以查看日志,了解这个服务端的信息:

image-20231102143300668

补充知识

关于inet_ntoa

inet_ntoa函数是系统提供的将四字节整形IP地址转换成char*类型的IP地址。

//inet_ntoa所在的头文件和函数声明
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

char *inet_ntoa(struct in_addr in);

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?

image-20231102144522988

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放。但man手册也提到了,每次调用该函数都会覆盖该缓冲区,如果您需要在多个地方使用返回的字符串,应该立即将其复制到另一个缓冲区中。如果一个线程一直使用该返回值指向的字符串作为参数进行操作,其他线程再调用该函数就会覆盖这个字符串,导致数据不一致。

因此,inet_ntoa不是线程安全的函数。但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁。在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。

#include <arpa/inet.h>

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • af参数: 指定了地址族,可以取值AF_INET或AF_INET6,分别表示IPv4和IPv6地址族。
  • src参数: 是一个指向待转换的二进制地址的指针。
  • dst参数: 是一个用于存储转换结果的缓冲区指针。
  • size参数: 指定缓冲区的大小。
    串,应该立即将其复制到另一个缓冲区中。如果一个线程一直使用该返回值指向的字符串作为参数进行操作,其他线程再调用该函数就会覆盖这个字符串,导致数据不一致。

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

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

相关文章

STM32:使用蓝牙模块

一、蓝牙概要 蓝牙是一种常见的无线通信协议&#xff0c;通常用于短距离通信。蓝牙分为经典蓝牙和低功耗蓝牙(BLE)。经典蓝牙通常用于需要持续传输数据的设备&#xff0c;比如蓝牙耳机等。低功耗蓝牙通常用于只需要间歇性传输数据的设备&#xff0c;比如运动手环。 蓝牙…

51单片机电子钟闹钟温度LCD1602液晶显示设计( proteus仿真+程序+原理图+设计报告+讲解视频)

51单片机电子钟闹钟温度液晶显示设计( proteus仿真程序原理图设计报告讲解视频&#xff09; 1.主要功能&#xff1a;2.仿真3. 程序代码4. 原理图5. 设计报告6. 设计资料内容清单&&下载链接资料下载链接&#xff08;可点击&#xff09;&#xff1a; &#x1f31f;51单片…

【Redis】Java连接Redis及Java操作Redis常用数据类型

一&#xff0c;Java连接Redis 1.1 连接前端服务器 打开RedisDesktopManager并连接Redis 不知道可看我上一篇文章&#xff1a; 【Redis】安装(Linux&window)及Redis的常用命令-CSDN博客 1.2 后端依赖 导入相关的jedis依赖 注意&#xff1a;要在dependencies标签中导入…

【深度学习】pytorch——实现CIFAR-10数据集的分类

笔记为自我总结整理的学习笔记&#xff0c;若有错误欢迎指出哟~ 往期文章&#xff1a; 【深度学习】pytorch——快速入门 CIFAR-10分类 CIFAR-10简介CIFAR-10数据集分类实现步骤一、数据加载及预处理实现数据加载及预处理归一化的理解访问数据集Dataset对象Dataloader对象 二、…

【css3】涟漪动画

效果展示 dom代码 <div class"mapSelfTitle66"><div></div> </div> 样式代码 .mapSelfTitle66{width:120px;height:60px;position: relative;&>div{width:100%;height:100%;background: url("~/assets/images/video_show/err…

数据结构:邻接矩阵与邻接表

模型图 邻接矩阵 用于反应图中任意两点之间的关联&#xff0c;用二维数组表示比较方便 以行坐标为起点&#xff0c;列坐标为终点如果两个点之间有边&#xff0c;那么标记为绿色&#xff0c;如图&#xff1a; 适合表示稠密矩阵 邻接表 用一维数组 链表的形式表示&#xff…

离散数学实践(2)-编程实现关系性质的判断

*本文为博主本人校内的离散数学专业课的实践作业。由于实验步骤已经比较详细&#xff0c;故不再对该实验额外提供详解&#xff0c;本文仅提供填写的实验报告内容与代码部分&#xff0c;以供有需要的同学学习、参考。 -------------------------------------- 编程语言&#xff…

高效处理异常值的算法:One-class SVM模型的自动化方案

一、引言 数据清洗和异常值处理在数据分析和机器学习任务中扮演着关键的角色。清洗数据可以提高数据质量&#xff0c;消除噪声和错误&#xff0c;从而确保后续分析和建模的准确性和可靠性。而异常值则可能对数据分析结果产生严重影响&#xff0c;导致误导性的结论和决策。因此&…

人工智能与卫星:颠覆性技术融合开启太空新时代

人工智能与卫星&#xff1a;颠覆性技术融合开启太空新时代 摘要&#xff1a;本文将探讨人工智能与卫星技术的融合&#xff0c;并介绍其应用、发展和挑战。通过深入了解这一领域的前沿动态&#xff0c;我们将展望一个由智能卫星驱动的未来太空时代。 一、引言 近年来&#xf…

uniapp小程序砸金蛋抽奖

砸之前是金蛋png图片&#xff0c;点击砸完之后切换砸金蛋动效gif图片&#xff1b; 当前代码封装为砸金蛋的组件&#xff1b; vue代码&#xff1a; <template><view class"page" v-if"merchantInfo.cdn_static"><image class"bg&qu…

第6章_多表查询

文章目录 多表查询概述1 一个案例引发的多表连接1.1 案例说明1.2 笛卡尔积理解演示代码 2 多表查询分类讲解2.1 等值连接 & 非等值连接2.1.1 等值连接2.1.2 非等值连接 自连接 & 非自连接内连接与外连接演示代码 3 SQL99语法实现多表查询3.1 基本语法3.2 内连接&#x…

HTML脚本、字符实体、URL

HTML脚本&#xff1a; JavaScript 使 HTML 页面具有更强的动态和交互性。 <script> 标签用于定义客户端脚本&#xff0c;比如 JavaScript。<script> 元素既可包含脚本语句&#xff0c;也可通过 src 属性指向外部脚本文件。 JavaScript 最常用于图片操作、表单验…

【机器学习】几种常用的机器学习调参方法

在机器学习中&#xff0c;模型的性能往往受到模型的超参数、数据的质量、特征选择等因素影响。其中&#xff0c;模型的超参数调整是模型优化中最重要的环节之一。超参数&#xff08;Hyperparameters&#xff09;在机器学习算法中需要人为设定&#xff0c;它们不能直接从训练数据…

Locust:可能是一款最被低估的压测工具

01、Locust介绍 开源性能测试工具https://www.locust.io/&#xff0c;基于Python的性能压测工具&#xff0c;使用Python代码来定义用户行为&#xff0c;模拟百万计的并发用户访问。每个测试用户的行为由您定义&#xff0c;并且通过Web UI实时监控聚集过程。 压力发生器作为性…

Nacos报错Connection refused (Connection refused)(最后原因醉了,非常醉)

目录 一、问题产生二、排查思路1.nacos拒绝连接&#xff0c;排查思路&#xff1a;2.Nacos启动成功但是拒绝连接的几种原因&#xff1a; 三、实操过程&#xff08;着急解决问题直接看这个&#xff09;1.启动Nacos2.查看Nacos启动日志3.根据日志处理问题4.修改Nacos5.重启Nacos 一…

CSS基础知识点速览

1 基础认识 1.1 css的介绍 CSS:层叠样式表(Cascading style sheets) CSS作用&#xff1a; 给页面中的html标签设置样式 css写在style标签里&#xff0c;style标签一般在head标签里&#xff0c;位于head标签下。 <style>p{color: red;background-color: green;font-size…

Git客户端软件 Tower mac中文版特点说明

Tower mac是一款Mac OS X系统上的Git客户端软件&#xff0c;它提供了丰富的功能和工具&#xff0c;帮助用户更加方便地管理和使用Git版本控制系统。 Tower mac软件特点 1. 界面友好&#xff1a;Tower的界面友好&#xff0c;使用户能够轻松地掌握软件的使用方法。 2. 多种Git操…

探索主题建模:使用LDA分析文本主题

在数据分析和文本挖掘领域&#xff0c;主题建模是一种强大的工具&#xff0c;用于自动发现文本数据中的隐藏主题。Latent Dirichlet Allocation&#xff08;LDA&#xff09;是主题建模的一种常用技术。本文将介绍如何使用Python和Gensim库执行LDA主题建模&#xff0c;并探讨主题…

STM32F407的系统定时器

文章目录 系统定时器SysTick滴答定时器寄存器STK_CTRL 控制寄存器STK_LOAD 重载寄存器STK_VAL 当前值寄存器STK_CALRB 校准值寄存器 非系统初始化 Systick 定时器SysTick_InitSysTick_CLKSourceConfig delay_us寄存器delay_us库函数delay_xms短时delay_ms长时SysTick_Config 系…

Firefox修改缓存目录的方法

打开Firefox&#xff0c;在地址栏输入“about:config” 查找是否有 browser.cache.disk.parent_directory&#xff0c;如果没有就新建一个同名的字符串&#xff0c;然后修改值为你要存放Firefox浏览器缓存的目录地址&#xff08;E:\FirefoxCacheFiles&#xff09; 然后重新…