【计算机网络_应用层】TCP应用与相关API守护进程

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)

文章目录

  • 1. 相关使用接口
  • 2. 代码实现
    • 2.1 日志组件
    • 2.2 Server端
    • 2.3 Client端
    • 2.3 bug解决
  • 3. 守护进程
    • 3.1 守护进程是什么
    • 3.2 守护进程相关的使用
    • 3.3 守护进程化的实现原理

1. 相关使用接口

tcp协议和udp协议的接口基本相似。使用逻辑也是:1. 创建对应的socket文件套接字对象; 2. bind自己的网络信息;3. 进行相关通信

只是由于tcp协议的相关特性,所以tcp通信方式有一些不同点。

1. 对于服务端

在创建对应socket文件套接字对象并bind完成后需要设置sockfd为监听状态,使用listen系统调用。

头文件:#include <sys/types.h>#include <sys/socket.h>
函数原型:int listen(int sockfd, int backlog);
参数解释:sockfd:要设置的文件套接字对象backlog:最多允许这么多个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5), 
函数描述:将sockfd文件套接对象设置为监听状态
返回值:调用成功返回0,失败返回-1同时设置错误码

在设置sockfd为监听状态之后,在底层进行”三次握手“之后,服务端需要调用accept接受客户端的连接。

头文件:#include <sys/types.h>#include <sys/socket.h>
函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数解释:sockfd:要设置的文件套接字对象(这里传的是监听的sockfd)addr:接受的连接对应的相关网络属性addrlen:addr对应的对象的大小
函数描述:服务端调用accept接受客户端的连接。如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来
返回值:调用成功返回一个新的文件套接字,用于进行本次的客户端和服务端通信,调用失败返回-1同时设置错误码

2. 对于客户端

同样在初始化的时候需要创建socket文件套接字,同样的不需要程序员显示bind。也不需要listen和accept。接下来需要做的事情就是发送连接请求,使用connect系统调用

头文件:#include <sys/types.h>#include <sys/socket.h>
函数原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解释:sockfd:发送链接请求的文件套接字对象addr:连接对应的相关网络属性addrlen:addr对应的对象的大小
函数描述:客户端使用sockfd向指定服务器的指定端口发起TCP链接请求
返回值:调用成功返回0,调用失败返回-1同时设置错误码

2. 代码实现

2.1 日志组件

一般来说,服务器在运行的时候,不会在当前shell输出相关的运行结果,而是在日志中输出,所以,这里我们现在封装一个日志的小组件

1. 组件需求

  1. 使用logMessage函数可以将相关日志信息写入预设的文件中(在当前目录创建对应文件)
  2. 每条日志信息都会有相关的日志等级,不同等级在不同文件中
  3. 日志内容支持format和可变参数

2. 代码实现

#include <unistd.h>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>// 这里是日志等级对应的宏
#define DEBUG (1 << 0)
#define NORMAL (1 << 1)
#define WARNING (1 << 2)
#define ERROR (1 << 3)
#define FATAL (1 << 4)#define NUM 1024 // 日志行缓冲区大小
#define LOG_NORMAL "log.txt" // 日志存放的文件名
#define LOG_ERR    "err.txt"const char *logLevel(int level) // 把日志等级转变为对应的字符串
{switch (level){case DEBUG:return "DEBUG";case NORMAL:return "NORMAL";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOW";}
}
//[日志等级][时间][pid]日志内容
void logMessage(int level, const char *format, ...) // 核心调用
{char logprefix[NUM]; // 存放日志相关信息time_t now_ = time(nullptr);struct tm *now = localtime(&now_);snprintf(logprefix, sizeof(logprefix), "[%s][%d年%d月%d日%d时%d分%d秒][pid:%d]",logLevel(level), now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min, now->tm_sec, getpid());char logcontent[NUM];va_list arg; // 声明一个变量arg指向可变参数列表的对象va_start(arg, format); // 使用va_start宏来初始化arg,将它指向可变参数列表的起始位置。// format是可变参数列表中的最后一个固定参数,用于确定可变参数列表从何处开始vsnprintf(logcontent, sizeof(logcontent), format, arg); // 将可变参数列表中的数据格式化为字符串,并将结果存储到logcontent中FILE *log =  fopen(LOG_NORMAL, "a");FILE *err = fopen(LOG_ERR, "a");if(log != nullptr && err != nullptr){FILE *curr = nullptr;if(level == DEBUG || level == NORMAL || level == WARNING) curr = log;if(level == ERROR || level == FATAL) curr = err;if(curr) fprintf(curr, "%s%s\n", logprefix, logcontent);fclose(log);fclose(err);}
}

2.2 Server端

/* tcpServer.hpp */
#pragma once#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include <string>#include "log.hpp"namespace Server
{enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;static const int gbacklog = 5;void serviceIO(int sock) // 服务端调用{char buffer[1024];while (true){ssize_t n = read(sock, buffer, sizeof(buffer) - 1);if (n > 0){// 目前我们把读到的数据当成字符串, 截止目前buffer[n] = 0;std::cout << "recv message: " << buffer << std::endl;std::string outbuffer = buffer;outbuffer += " server[echo]";write(sock, outbuffer.c_str(), outbuffer.size()); // 这里再把结果写进sock中,意为返回给客户端}else if (n == 0){// 代表client退出logMessage(NORMAL, "client quit, me too!");break;}}close(sock);}class tcpServer{public:tcpServer(uint16_t &port) : _port(port){}void initServer(){// 1. 创建socket文件套接字对象_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock == -1){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success:%d", _listensock);// 2.bind自己的网络信息sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;int n = bind(_listensock, (struct sockaddr *)&local, sizeof local);if (n == -1){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");// 3. 设置socket为监听状态if (listen(_listensock, gbacklog) != 0) // listen 函数{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}void start(){while (true){struct sockaddr_in peer;socklen_t len = sizeof peer;int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){logMessage(ERROR, "accept error, next");continue;}serviceIO(sock); // 使用close(sock); // 使用之后要关闭,否则会造成文件描述符泄露}}~tcpServer() {}private:uint16_t _port;int _listensock;};} // namespace Server/* tcpServer.cc */
#include <iostream>
#include <memory>#include "tcpServer.hpp"using namespace Server;static void Usage(const char *proc)
{std::cout << "\n\tUsage:" << proc << " local_port\n";
}int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<tcpServer> tsvr(new tcpServer(port));tsvr->initServer();tsvr->start();return 0;
}

2.3 Client端

/* tcpClient.hpp */
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#include <string>#include "log.hpp"namespace Client
{class tcpClient{public:tcpClient(uint16_t &port, std::string &IP) : _serverPort(port), _serverIP(IP), _sockfd(-1) {}void initClient(){// 1. 创建socket_sockfd = socket(AF_INET, SOCK_STREAM, 0);if(_sockfd == -1){std::cerr << "create socket error" << std::endl;exit(2);}}void run(){struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(_serverPort);server.sin_addr.s_addr = inet_addr(_serverIP.c_str());if(connect(_sockfd, (struct sockaddr*)&server, sizeof server) != 0){// 链接失败std::cerr << "socket connect error" << std::endl;}else{std::string msg;while(true){std::cout << "Please Enter# ";std::getline(std::cin, msg);write(_sockfd, msg.c_str(), msg.size());char buffer[NUM];int n = read(_sockfd, buffer, sizeof(buffer) - 1); // 按照字符串的形式读取if(n > 0){// 目前先把读到的数据当作字符串处理buffer[n] = 0;std::cout << "Server 回显# " << buffer << std::endl;}else{break;}}}}~tcpClient(){if(_sockfd >= 0) close(_sockfd); // 使用完关闭,防止文件描述符泄露(当然这里也可以不写,当进程结束之后一切资源都将被回收)}private:uint16_t _serverPort;std::string _serverIP;int _sockfd;};} // namespace Client
/* tcpClient.cc */
#include <memory>
#include <string>#include "tcpClient.hpp"
using namespace Client;static void Usage(const char *proc)
{std::cout << "\n\tUsage:" << proc << " server_ip server_port\n";
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}std::string IP = argv[1];uint16_t port = atoi(argv[2]);std::unique_ptr<tcpClient> tclt(new tcpClient(port, IP));tclt->initClient();tclt->run();return 0;
}

image-20240226002443205

2.3 bug解决

这里会出现一个问题:在此时如果再有另一个客户端进行通信,就会出现其他客户端被阻塞的问题

image-20240226002542298

这是因为我们在服务端的serviceIO中的执行没有结束,而且由于实现的是死循环,所以也不可能结束,这就造成了服务端一直在阻塞的情况。那么如何解决呢?

1. 实现多进程版本

多进程的实现思想就是:每次收到新请求的时候,都创建一个子进程,让子进程来执行对应任务,父进程继续监听,但是由于创建的子进程需要被父进程等待回收,否则就会出现僵尸进程。那么这里的解决方案就是:让子进程再创建一个子进程,最终让孙子进程来执行本次请求对应的任务,父进程直接exit,爷爷进程等待父进程结束后继续监听。此时孙子进程就变成了孤儿进程,由OS直接接收管理。

这里需要更改的就只有tcpServer.hpp文件中的start函数,这里附上更改后的代码

void start()
{while (true){struct sockaddr_in peer;socklen_t len = sizeof peer;int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){logMessage(ERROR, "accept error, next");continue;}pid_t id = fork();if (id == 0){close(_listensock); // 子进程不会使用监听socket,但是创建子进程的时候写时拷贝会拷贝,这里先关掉// 子进程再创建子进程if (fork() > 0)exit(0); // 父进程退出// 走到当前位置的就是子进程serviceIO(sock); // 使用close(sock); // 关闭对应的通信socket(这里也可以不关闭,因为此进程在下个语句就会退出)exit(0); // 孙子进程退出}// 走到这里的是监听进程(爷爷进程)pid_t n = waitpid(id, nullptr, 0);if(n > 0){logMessage(NORMAL, "wait success pid:%d", n);}close(sock); // 使用之后要关闭,否则会造成文件描述符泄露}
}

image-20240226082008918

现在再测试,服务器就能够同时处理多个客户端的请求。

2. 实现多线程版本

但是,我们知道OS在创建线程的时候,需要的成本是非常高的,但是线程就非常轻量级,所以使用线程来处理服务器请求是更加合理的,所以这里实现一下多线程的版本

void start()
{while (true){struct sockaddr_in peer;socklen_t len = sizeof peer;int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){logMessage(ERROR, "accept error, next");continue;}// version 3:多线程版本pthread_t tid;pthread_create(&tid, nullptr, routine, new ThreadData(this, sock)); // 创建新线程,让新线程调用routine然后去执行serviceIO}
}        
static void *routine(void *arg)
{// 由于不能让主线程等待新线程执行完毕,所以这里进行线程分离pthread_detach(pthread_self());ThreadData* args = static_cast<ThreadData*>(arg);serviceIO(args->_sock);close(args->_sock); // 使用完之后回收sockdelete args; // 回收空间return nullptr;
}

image-20240226083955051

3. 实现线程池版本

当然,上述的两种实现方式是具有一些优化空间的,因为每次在创建子进程/新线程的时候都会有消耗,这样会降低效率,而且当突然出现很多长时间的请求的时候,服务器就会同时接收到很多请求,会一直创建子进程/新线程,可能会导致服务器崩溃,所以可以使用我们之前写过的一个小组件线程池来改写

void start()
{ThreadPool<Task>::getInstance()->run(); // 初始化线程池,让他跑起来logMessage(NORMAL, "init thread pool success");while (true){struct sockaddr_in peer;socklen_t len = sizeof peer;int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){logMessage(ERROR, "accept error, next");continue;}// version 4:线程池版本ThreadPool<Task>::getInstance()->push(Task(sock, serviceIO));}
}
/* 小组件 */
// Task.hpp
#pragma once#include <string>
#include <iostream>
#include <functional>class Task
{
public:using func_t = std::function<void(int)>;public:Task() {}Task(int sock, func_t func): _sock(sock), _callback(func){}void operator()(){_callback(_sock);}private:int _sock;func_t _callback;
};
// Thread.hpp
#pragma once#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cassert>class Thread
{
public:using func_t = std::function<void *(void *)>; // 定义func_t类型static int number;                            // 线程编号,按照一次运行时的调用次数计数
public:Thread(){char *buffer = new char[64];name_ = "thread-" + std::to_string(++number);}static void *start_routine(void *args){Thread *_this = static_cast<Thread *>(args);void *ret = _this->run(_this->args_);return ret;}void *run(void *arg){return func_(arg);}void start(func_t func, void *args){func_ = func;args_ = args;int n = pthread_create(&tid_, nullptr, start_routine, this);assert(n == 0);(void)n;}void join(){int n = pthread_join(tid_, nullptr);assert(n == 0);(void)n;}std::string GetTaskName(){return name_;}~Thread() {}private:std::string name_; // 线程名pthread_t tid_;    // 线程idfunc_t func_;      // 线程调用的函数void *args_;       // 线程调用函数的参数
};
int Thread::number = 0;
// ThreadPool.hpp
#pragma once
#include "LockGuard.hpp"
#include "Thread.hpp"
#include <vector>
#include <queue>
#include <string>
#include <iostream>
#include <mutex>const int gnum = 5; // 线程池中默认的线程个数template <class T>
class ThreadPool; // 线程池类的声明/* 线程数据类,保存线程对应的内容包括线程池对象的指针和线程名 */
template <class T>
class ThreadData
{
public:ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n){};public:ThreadPool<T> *threadpool;std::string name;
};/* 线程池类的实现 */
template <class T>
class ThreadPool
{
public:static void *handleTask(void *args) // 线程需要执行的回调函数{ThreadData<T> *td = static_cast<ThreadData<T> *>(args);while (true){T t; // 构建任务对象{LockGuard lockGuard(td->threadpool->mutex()); // 上锁while (td->threadpool->isQueueEmpty()){// 如果任务队列为空,线程挂起,等待队列中被填充任务td->threadpool->threadWait();}t = td->threadpool->pop(); // 如果队列中有任务,就拿出任务}// 任务在锁外执行t();}delete td;return nullptr;}public: // 给handleTask调用的外部接口pthread_mutex_t *mutex() { return &_mutex; }bool isQueueEmpty() { return _task_queue.empty(); }void threadWait() { pthread_cond_wait(&_cond, &_mutex); }T pop() // 获取线程池中任务队列里需要执行的下一个任务{T t = _task_queue.front();_task_queue.pop();return t;}public:                               // 需要暴露给外部的接口void run() // 为所有线程对象创建真正的执行流,并执行对应的回调函数{for (const auto &thread : _threads){ThreadData<T> *td = new ThreadData<T>(this, thread->GetTaskName()); // 构造handleTask的参数对象thread->start(handleTask, td);                                      // 调用该线程的start函数,创建新线程执行指定的handleTask任务// std::cout << thread->GetTaskName() << " start..." << std::endl;}}void push(const T &in) // 将指定任务push到队列中{// 加锁LockGuard lockGuard(&_mutex); // 自动加锁,在当前代码段结束之后调用LockGuard的析构函数解锁_task_queue.push(in);pthread_cond_signal(&_cond); // 发送信号表示此时task_queue中有值,让消费者可以使用}~ThreadPool() // 析构函数,销毁互斥量和条件变量,delete所有thread对象指针,自动调用thread对象的析构函数{pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);for (auto &thread : _threads){delete thread;}}static ThreadPool<T> *getInstance(){if(nullptr == tp){std::lock_guard<std::mutex> lck(_singletonlock);if(nullptr == tp){tp = new ThreadPool<T> ();}}return tp;}
private: // 单例模式需要私有化的接口ThreadPool(const int &num = gnum) // 构造函数,初始化互斥量和条件变量,构建指定个数的Thread对象{pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);for (int i = 0; i < num; ++i){_threads.push_back(new Thread());}}//delete拷贝构造和析构函数ThreadPool(const ThreadPool<T> &) = delete;ThreadPool<T> *operator=(const ThreadPool<T> &) = delete;private:std::vector<Thread *> _threads; // 保存所有线程对象的指针std::queue<T> _task_queue;      // 需要被分配的任务队列pthread_mutex_t _mutex;         // 任务队列需要被互斥的访问pthread_cond_t _cond;           // 生产任务和消费任务之间需要进行同步static ThreadPool<T> *tp; // 静态成员,存放ThreadPool指针static std::mutex _singletonlock; // 创建线程安全的单例对象要加的锁
};
template<class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
template<class T>
std::mutex ThreadPool<T>::_singletonlock;

image-20240226085928431

3. 守护进程

3.1 守护进程是什么

在我们之前实现的代码中,所有的Server端在运行的时候都会占用前台的Shell,当这个Shell退出之后,对应的进程也就会退出

image-20240226091312689

但是我们知道:在实际的应用环境中,是不会出现这种情况的,这是因为在实际部署服务的时候,会将对应的服务守护进程化,所谓的守护进程化就是让对应的进程不受当前会话的影响

守护进程的理解

我们是使用远程命令行工具来连接我们的云服务器的,这个工具在Windows下会使用Xshell,macOS下使用自带的终端或者iTerm,或者会使用VScode远程连接带有的shell…

在我们登录成功之后,OS在内部会创建一个会话,在此会话内部创建一个前台进程bash进行命令行解释,此时我们就可以想bash中输入命令,OS帮我们执行。

在一个会话(session)中,同一时间只能有一个前台进程但是可以有任意个后台进程的存在

当这个会话结束之后,会话内所有的进程都将会退出,这也就是为什么我们的服务不能长久的在服务器中运行

3.2 守护进程相关的使用

1. &jobs

&可以让一个命令在后台运行

jobs可以查看当前会话的所有作业(现在可以理解成进程)

image-20240226092835820

  • 作业前面的[]内部的数字就是作业号

为什么这个服务运行起来后还能够输入命令?

这是因为这个服务变成后台作业了,一个会话在同一时刻有且只有一个前台进程

  • 通过PGID可以确定同一个进程组
  • 通过SID可以确定同一个会话

image-20240226093227728

  • fg+作业号:把对应作业放在前台
  • CTRL+z:暂停作业(一个任务在前台如果暂停了会立马放在后台)
  • bg+作业号:启动作业

image-20240226093745860

2. daemon

OS提供了一个守护进程化的接口,但是我们不建议使用,因为这个接口会产生一些未定义行为,所以我们自己封装一个小组件用于守护进程化。

image-20240226094239043

3.3 守护进程化的实现原理

守护进程化的实现原理就是:让这个进程自己成为一个会话组,独立出来就可以不受当前会话的影响

头文件:#include <unistd.h>
函数原型:pid_t setsid();
函数解释:对于一个非会话组组长的进程,使其成为一个新的会话组,并且调用进程成为组长
返回值:如果调用成功,返回一个新的SID(SID就是当前会话组的组长的pid);调用失败返回-1同时设置错误码

守护进程化组件的实现

// daemon.hpp
#pragma once#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>#define DEV "/dev/null" // 这个路径是一个“黑洞”,写入的所有数据都会被“吃掉”,不会被读取void deamonSelf(const char *curPath = nullptr) // 可选参数,如果传入非空,就更改“当前路径”
{// 1. 让调用进程忽略掉所有异常信号signal(SIGPIPE, SIG_IGN);// 2. 让当前进程成为非组长进程if (fork() > 0)exit(0); // 创建子进程,然后将父进程退出确保调用setsid的进程是非组长进程// 3. 调用setsid创建新的会话组pid_t n = setsid();assert(n != -1);// 4. 守护进程是脱离终端的,需要关闭或者重定向以前进程默认打开的文件,这里我们采用重定向的方法更安全int fd = open(DEV, O_RDWR);if(fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}else{close(0);close(1);close(2);}// 5. 可选:是否更改当前路径if (curPath != nullptr)chdir(curPath);
}
#include <iostream>
#include <memory>#include "tcpServer.hpp"
#include "daemon.hpp"using namespace Server;static void Usage(const char *proc)
{std::cout << "\n\tUsage:" << proc << " local_port\n";
}int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<tcpServer> tsvr(new tcpServer(port));tsvr->initServer();deamonSelf(); // 当前进程守护进程化tsvr->start();return 0;
}

image-20240226100859218


本节完…

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

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

相关文章

JVM类加载机制以及双亲委派模型的介绍

目录 1.类加载介绍 2.具体步骤 2.1加载 2.2验证 2.3准备 2.4解析 2.5初始化 3.加载过程中的策略-双亲委派模型 1.类加载介绍 类加载,指的是Java进程在运行的时候,把.class文件从硬盘读取到内存,并进行一系列校验解析的过程. .class文件>类对象.硬盘>内村 类加载…

智能驾驶规划控制理论学习03-基于采样的规划方法

目录 一、基于采样的规划方法概述 二、概率路图&#xff08;PRM&#xff09; 1、核心思想 2、实现流程 3、算法描述 4、节点连接处理 5、总结 三、快速搜索随机树&#xff08;RRT&#xff09; 1、核心思想 2、实现流程 3、总结 4、改进RRT算法 ①快速搜索随机图&a…

运筹学_1.1.2 线性规划问题-图解法

1.1.2 线性规划问题-图解法 一、图解法求解步骤&#xff08;只适用于两个决策变量问题&#xff09;二、图解法作图实例三、图解法分析线性规划几种解的情况1、唯一最优解2、无穷多最优解3、无界解4、无解或无可行解 四、图解法的几点启示 一、图解法求解步骤&#xff08;只适用…

深入浅出Redis(一):对象与数据结构

引言 Redis是一款基于键值对的数据结构存储系统&#xff0c;它的特点是基于内存操作、单线程处理命令、IO多路复用模型处理网络请求、键值对存储与简单丰富的数据结构等等 这篇文章主要围绕Redis中的对象与数据结构来详细说明键值对存储与简单丰富的数据结构这两大特点 Redi…

运筹学_1.1.4 线性规划问题-解的概念

1.1.4 线性规划问题-解的概念 一、可行解与最优解二、基的概念三、基变量、基向量&#xff1b;非基变量、非基向量&#xff1b;基解、基可行解&#xff1b;四、最优解与可行解、基可行解的关系五、用例题&#xff08;枚举法&#xff09;巩固基解、基可行解、最优解三个概念1、例…

5、DVWA代码审计(2)

一、csrf 1、csrf(low) 限制 复现 GET /vulnerabilities/csrf/?password_new123456&password_conf123456&ChangeChange HTTP/1.1 Host: ddd.com Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,…

电子电器架构 —— DoIP协议相关的介绍

电子电器架构 —— DoIP协议相关的介绍 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 没有人关注你。也无需有人关注你。你必须承认自己的价值,你不能站在他人的角度来反对自己。人生在世,最怕…

vue3编写H5适配横竖屏

具体思路如下&#xff1a; 1、监听浏览器屏幕变化&#xff0c;通过监听屏幕宽高&#xff0c;辨别出是横屏&#xff0c;还是竖屏状态 在项目的起始根页面进行监听&#xff0c;我就是在App.vue文件下进行监听 代码如下&#xff1a; <template><RouterView /> <…

【Spring IoC】实验四:特殊值处理

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

C++ //练习 10.16 使用lambda编写你自己版本的biggies。

C Primer&#xff08;第5版&#xff09; 练习 10.16 练习 10.16 使用lambda编写你自己版本的biggies。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块 /*******************************************************************…

BERTopic安装最全教程及报错处理

BERTopic安装 BERTopic的安装比较复杂,直接安装会报错 安装方法1,.whl文件安装 ERROR: Could not build wheels for hdbscan, which is required to install pyproject.toml-based projects正确安装流程 查看python能安装whl的版本pip debug --verbose Compatible tags: 2…

图表背后的智慧:办公场景中的数据可视化革新

在现代办公场景中&#xff0c;数据可视化的应用已经成为提高效率、推动创新的得力工具。无论是管理层还是普通员工&#xff0c;都能从数据可视化中受益匪浅。下面我就以可视化从业者的角度&#xff0c;简单聊聊这个话题。 首先&#xff0c;数据可视化提升了数据的易读性与理解性…

【研发日记】Matlab/Simulink技能解锁(三)——在Stateflow编辑窗口Debug

文章目录 前言 State断点 Transition断点 条件断点 按State步进 Watch Data Value Sequence Viewer 分析和应用 总结 前言 见《【研发日记】Matlab/Simulink技能解锁(一)——在Simulink编辑窗口Debug》 见《【研发日记】Matlab/Simulink技能解锁(二)——在Function编辑…

Flink状态存储-StateBackend

文章目录 前言一、MemoryStateBackend二、FSStateBackend三、RocksDBStateBackend四、StateBackend配置方式五、状态持久化六、状态重分布OperatorState 重分布KeyedState 重分布 七、状态过期 前言 Flink是一个流处理框架&#xff0c;它需要对数据流进行状态管理以支持复杂的…

10个技巧,3分钟教会你高效寻找开源项目

作为程序员&#xff0c;不论是开发还是学习&#xff0c;肯定会用到开源项目&#xff0c;那么怎么快速在开源网站找到这些项目呢&#xff1f; 常用的开源网站有&#xff1a;github 和 gitee github是全球最大的开源社区&#xff0c;今天就以github为例&#xff0c;演示一下 gi…

JavaWeb之 Servlet(2万6千字详解)

目录 前言1. Servlet 简介2. Servlet 前世今生3. Servlet 执行流程4. Servlet 快速入门5. 两种配置 Servlet程序 URL的方式5.1 使用 注解来配置 Servlet程序 的 URL5.1.1 urlPattern 的配置规则精确匹配目录匹配&#xff1a;使用 * 符号代表任意路径扩展名匹配任意匹配 5.1.2 小…

【MATLAB】语音信号识别与处理:SG滤波算法去噪及谱相减算法呈现频谱

1 基本定义 SG 滤波算法&#xff08;Savitzky - Golay 滤波算法&#xff09;是一种数字信号处理算法&#xff0c;用于对信号进行平滑处理。该算法利用最小二乘法拟合局部数据段&#xff0c;然后用拟合的函数来估计每个数据点的值&#xff0c;从而实现平滑处理。 SG 滤波算法的…

redis05 sprngboot整合redis

redis的Java客户端 整合步骤 添加redis的pom依赖 <!-- 引入redis依赖 --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency><!-- 引入redis连…

51单片机学习day02

基于普中的stc89c52&#xff0c; 串口&#xff1a; 通讯接口&#xff0c;51单片机自带UART&#xff08;通用异步收发器&#xff09;&#xff0c;可实现窗口通讯。 硬件电路&#xff1a; 简单双向串口通信有两根通信线&#xff08;发送端TXD和接收端RXD&#xff09;&#xff0…

HelixToolKit的模型旋转操作

前面加载了模型以后&#xff0c;鼠标拖动和缩放比较好操作&#xff1b;但是旋转似乎没有&#xff0c; 操作了一阵&#xff0c;也不是没有&#xff0c;应该是还不熟悉&#xff1b; 旋转的指示器在右下角&#xff0c;现在U面看到正面&#xff0c; 想看一下模型的背面&#xff0…