利用线程池多线程并发实现TCP两端通信交互,并将服务端设为守护进程

文章目录

  • 实现目标
  • 实现步骤
  • 封装日志类
  • 封装线程池
    • 封装线程
    • 封装锁
    • 封装线程池
  • TCP通信的接口和注意事项
    • accept
  • TCP
    • 封装任务
    • 客户端
      • Client.hpp
      • Client.cc
    • 服务端
      • Server.hpp
    • Server.cc
    • 实现效果
  • 守护进程
    • 服务端守护进程化

实现目标

利用线程池多线程并发实现基于TCP通信的多个客户端与服务端之间的交互,客户端发送数据,服务端接收后处理数据并返回。服务端为守护进程

实现步骤

  1. 封装一个记录日志的类,将程序运行的信息保存到文件
  2. 封装线程类、服务端处理任务类以及将锁进行封装,为方便实现线程池
  3. 实现服务端,使服务端能接收客户端所发来的数据,处理数据后返回。服务端采用多线程并发处理
  4. 封装守护进程方法,使服务端为守护进程
  5. 实现客户端,可以向服务端发送数据,并接收到服务端发送回来的数据

封装日志类

将程序运行的信息保存到指定文件,例如创建套接字成功或者失败等信息。以【状态】【时间】【信息】的格式保存。

状态可分为五种:“DEBUG”,“NORMAL”,“WARNING”,“ERROR”,“FATAL”

日志类保存的信息需带有可变参数

#pragma once#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>using namespace std;#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4const char *to_levelstr(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 nullptr;}
}void LogMessage(int level, const char *format, ...)
{
#define NUM 1024char logpre[NUM];snprintf(logpre, sizeof(logpre), "[%s][%ld][%d]", to_levelstr(level), (long int)time(nullptr), getpid());char line[NUM];// 可变参数va_list arg;va_start(arg, format);vsnprintf(line, sizeof(line), format, arg);// 保存至文件FILE* log = fopen("log.txt", "a");FILE* err = fopen("log.error", "a");if(log && err){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", logpre, line);fclose(log);fclose(err);}
}

封装线程池

封装线程

将线程的创建,等待封装成类的成员函数。不再需要单个的条用线程库接口,以对象的方式创建。

需要注意:在类里面的线程回调方法必须设为static类型,而静态的方法是不能访问类内成员的,因此传给回调函数的参数需要将整个对象传过去,通过对象来获取类内成员

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>typedef std::function<void *(void *)> func_t;class Thread
{
private:// 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为staticstatic void *start_routine(void *args) // 类内成员,有缺省参数!{Thread *_this = static_cast<Thread *>(args);return _this->callback();}public:// 构造函数里直接生成线程名,利用静态变量从1开始Thread(){char namebuffer[1024];snprintf(namebuffer, sizeof namebuffer, "thread-NO.%d", threadnum++);_name = namebuffer;}// 线程启动void start(func_t func, void *args = nullptr){_func = func;_args = args;// 由于静态的方法是不能访问类内成员的,// 因此传给回调函数的参数需要将整个对象传过去,通过对象来获取类内成员// 也就是this指针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;}~Thread(){}void *callback(){return _func(_args);}private:std::string _name; // 类名func_t _func;      // 线程回调函数void *_args;       // 线程回调函数的参数pthread_t _tid;    // 线程idstatic int threadnum; // 线程的编号,为生成线程名
};
// static的成员需在类外初始化
int Thread::threadnum = 1;

封装锁

同样的为了不再需要一直调用系统接口,可以将整个方法封装成类,通过类的对象实现加锁过程

#pragma once#include <iostream>
#include <pthread.h>// 加锁 解锁
class Mutex
{
public:Mutex(pthread_mutex_t *lock_p = nullptr) : _lock_p(lock_p){}// 加锁void lock(){if (_lock_p)pthread_mutex_lock(_lock_p);}// 解锁void unlock(){if (_lock_p)pthread_mutex_unlock(_lock_p);}~Mutex(){}private:pthread_mutex_t *_lock_p;
};// 锁的类
class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex) : _mutex(mutex){_mutex.lock(); // 在构造函数中进行加锁}~LockGuard(){_mutex.unlock(); // 在析构函数中进行解锁}private:Mutex _mutex;
};

封装线程池

在类里面的线程回调方法必须设为static类型,而静态的方法是不能访问类内成员的,因此传给回调函数的参数需要将整个对象传过去,通过对象来获取类内成员。

线程池需要实现为单例模式:

  1. 第一步就是把构造函数私有,再把拷贝构造和赋值运算符重载delete
  2. 在设置获取单例对象的函数的时候,注意要设置成静态成员函数,因为在获取对象前根本没有对象,无法调用非静态成员函数
  3. 可能会出现多个线程同时申请资源的场景,所以还需要一把锁来保护这块资源,而这把锁也得设置成静态,因为单例模式的函数是静态的
#pragma once#include "Thread.hpp"
#include "log.hpp"
#include "Lock.hpp"
#include <vector>
#include <queue>
#include <mutex>
#include <pthread.h>
#include <unistd.h>using namespace std;// 线程池类定义位于下面,因此属性类想要获取到
// 就必须在前面声明
template <class T>
class ThreadPool;template <class T>
class ThreadData
{
public:ThreadPool<T> *threadpool; // 线程所在的线程池,获取到线程的this指针std::string _name;         // 线程的名字public:ThreadData(ThreadPool<T> *tp, const std::string &name) : threadpool(tp), _name(name){}
};template <class T>
class ThreadPool
{
private:// 线程最终实现的方法static void *handlerTask(void *args){ThreadData<T> *td = (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;}ThreadPool(const int &num = 10) : _num(num){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);for (int i = 0; i < _num; i++){_threads.push_back(new Thread());}}void operator=(const ThreadPool &) = delete;ThreadPool(const ThreadPool &) = delete;public:// 将加锁 解锁 判断任务队列是否为空 和条件变量等待全部封装成类内方法// 方便在线程的回调方法中通过对象直接调用void lockQueue() { pthread_mutex_lock(&_mutex); }void unlockQueue() { pthread_mutex_unlock(&_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;}pthread_mutex_t *mutex(){return &_mutex;}public:// 让每个线程对象调用其启动函数,并将线程辅助类和最终执行的任务方法传入函数中// 线程的辅助类对象里包含了线程当前线程池对象,也就是可以// 通过辅助类对象可以调用到线程池对象里的成员void run(){for (const auto &t : _threads){ThreadData<T> *td = new ThreadData<T>(this, t->threadname());t->start(handlerTask, td);// 创建成功后打印日志LogMessage(DEBUG, "%s start ...", t->threadname().c_str());}}// 往任务队列里插入一个任务void push(const T &in){LockGuard lockguard(&_mutex);_task_queue.push(in);pthread_cond_signal(&_cond);}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);for (const auto &t : _threads)delete t;}// 实现单例模式static ThreadPool<T> *getInstance(){if (nullptr == tp){_singlock.lock();if (nullptr == tp){tp = new ThreadPool<T>();}_singlock.unlock();}return tp;}private:int _num;//线程的数量std::vector<Thread *> _threads;//线程组std::queue<T> _task_queue;//任务队列pthread_mutex_t _mutex;//锁pthread_cond_t _cond;//条件变量static ThreadPool<T> *tp;static std::mutex _singlock;
};template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;template <class T>
std::mutex ThreadPool<T>::_singlock;

TCP通信的接口和注意事项

为了实现TCP版的通信,首先来了解一下相关接口和注意事项

  1. TCP需要在通信前先创建链接,因此在TCP没有链接之前其创建的套接字并不是用来通信的,而是用来监听的。一旦创建链接成功后,才会返回一个用来通信的套接字
  2. TCP时面向字节流的,因此其通信就是往文件上IO,因此不用指定的调用某接口去完成,直接用文件接口读写就可以完成

accept

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

这就是用来创建链接的接口

参数一为负责监听的套接字

参数二就是socket的结构体

参数三为结构体的大小

返回值,成功创建链接之后会返回一个值,这个值就是负责通信的套接字,也就是后面利用文件通信的文件描述符

TCP

封装任务

因为上述说到TCP是可以直接使用文件操作来完成通信的,那么也就是说其通信根本就用不到其他的成员了,只需要知道一个套接字即可。那么这个方法就可以不放在类内,因为这就是线程最后的执行目的,因此可以将这个任务单独放到一个头文件中。因为线程池是一个模板类,则可以封装一个任务类。

#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <functional>
#include "log.hpp"// TCP的通信
// 线程的最终执行方法
void ServerIO(int sock)
{char buffer[1024];while (true){ssize_t n = read(sock, buffer, sizeof(buffer) - 1);if (n > 0){// readbuffer[n] = 0;std::cout << "recv message: " << buffer << std::endl;// writestd::string outbuffer = buffer;outbuffer += " server[echo]";write(sock, outbuffer.c_str(), outbuffer.size());}else if (n == 0){// 代表client退出LogMessage(NORMAL, "client quit, me too!");break;}}close(sock);
}// 任务类
// 为了最终执行的方法而服务
class Task
{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;
};

客户端

客户端不需要显示的绑定端口号,而是由操作系统随机去绑定。TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept。TCP的客户端只需要向服务端发起链接请求

Client.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "log.hpp"using namespace std;class Client
{
public:Client(const string &serverip, const uint16_t &port): _serverip(serverip), _port(port), _sock(-1){}void Init(){// 创建套接字_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){LogMessage(FATAL, "create socket error");exit(1);}// TCP的客户端也不需要显示绑定端口,让操作系统随机绑定// TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept// TCP的客户端只需要向服务端发起链接请求}void start(){// 向服务端发起链接请求struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_serverip.c_str());if (connect(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)LogMessage(ERROR, "connect socket error");// 和服务端通信else{string line;while (1){cout << "Please cin: " << endl;getline(cin, line);// 向服务端写write(_sock, line.c_str(), line.size());// 读服务端返回来的数据char buff[1024];int n = read(_sock, buff, sizeof(buff) - 1);if (n > 0){buff[n] = 0;cout << "接收到的消息为:" << buff << endl;}elsebreak;}}}~Client(){if(_sock >= 0)close(_sock);}private:int _sock;string _serverip;uint16_t _port;
};

Client.cc

#include "Client.hpp"
#include <memory>// 输出命令错误函数
void Usage(string proc)
{cout << "Usage:\n\t" << proc << " local_ip local_port\n\n";
}int main(int argc, char* argv[])
{// 再运行客户端时,输入的指令需要包括主机ip和端口号if(argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t port = atoi(argv[2]);unique_ptr<Client> client(new Client(serverip, port));client->Init();client->start();return 0;
}

服务端

那么对于服务端而言,必须要显式的去绑定端口号。则创建的套接字并不是负责通信的。创建好套接字和绑定完网络信息后,需要设置创建的套接字为监听状态。和UDP一样,服务端是不能指定IP的.

还需要注意的是:因为封装的线程池是单例模式,所以不需要创建对象,直接调用静态对象去调用类方法即可

步骤可分为:

  1. 创建监听套接字
  2. 绑定网络信息
  3. 设置套接字为监听状态
  4. 获取链接,得到通信的套接字
  5. 通信
  6. 关闭不需要的套接字

Server.hpp

#pragma once#include "Task.hpp"
#include "ThreadPool.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class Server
{
public:Server(const uint16_t &port = 8000): _port(port){}void Init(){// 创建负责监听的套接字 面向字节流_listenSock = socket(AF_INET, SOCK_STREAM, 0);if (_listenSock < 0){LogMessage(FATAL, "create socket error!");exit(1);}LogMessage(NORMAL, "create socket %d success!", _listenSock);// 绑定网络信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_listenSock, (struct sockaddr *)&local, sizeof(local)) < 0){LogMessage(FATAL, "bind socket error!");exit(3);}LogMessage(NORMAL, "bind socket success!");// 设置socket为监听状态if (listen(_listenSock, 5) < 0){LogMessage(FATAL, "listen socket error!");exit(4);}LogMessage(NORMAL, "listen socket success!");}void start(){while (1){// 因为线程池时单例模式,所以直接调用初始化ThreadPool<Task>::getInstance()->run();LogMessage(NORMAL, "Thread init success");// server获取建立新连接struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));socklen_t len = sizeof(peer);// 创建通信的套接字// accept的返回值才是真正用于通信的套接字_sock = accept(_listenSock, (struct sockaddr *)&peer, &len);if (_sock < 0){// 获取通信的套接字失败并不影响未来的操作,只是当前的链接失败而已LogMessage(ERROR, "accept socket error, next");continue;}LogMessage(NORMAL, "accept socket %d success", _sock);cout << "sock: " << _sock << endl;// 往线程池的任务队列里插入任务ThreadPool<Task>::getInstance()->push(Task(_sock, ServerIO));}}private:int _listenSock; // 负责监听的套接字int _sock;       // 通信的套接字uint16_t _port;  // 端口号
};

Server.cc

#include "Server.hpp"
#include "daemon.hpp"
#include <memory>// 输出命令错误函数
void Usage(string proc)
{cout << "Usage:\n\t" << proc << " local_ip local_port\n\n";
}int main(int argc, char* argv[])
{// 启动服务端不需要指定IPif(argc != 2){Usage(argv[0]);exit(1);}uint16_t port = atoi(argv[1]);unique_ptr<Server> server(new Server(port));server->Init();server->start();return 0;
}

实现效果

image-20230804003006475

可以看到多个客户端同时访问也没有问题,并且所对应的套接字也就是文件描述符也不一样。

守护进程

守护进程是一种特殊的孤儿进程,其运行于后台,生存期较长并且独立与终端周期性的执行任务或者等待处理任务

进程分为前台运行和后台运行,每一个进程都会属于一个会话组里。每一个会话组都有且只有能一个前台进程。像上述的服务端,当运行服务端时,操作系统会将其分到含有bash的会话组内,并且将服务端置为前台任务进程,因此服务端运行时bash把放置后台这也就是为什么用户不能再bash继续输入命令的原因。

每一个会话组都会有一个组长,一般而言在bash中输入命令执行的进程都会分到bash的会话组内,这个会话组的组长即为bash。可以通过查看进程的SID确认进程的会话组

image-20230805132436861

可以看到上述图片中运行了三个进程并置于后台,他们的SID也就是会话组都是一样的。那么如果将他们置于前台运行会发生什么呢

image-20230805133033613

可以看到,置于前台运行后,命令行输入什么都没有反应了。也就是说,此时的bash被自动的放到了后台运行,证实了一个会话组只能有一个前台进程

image-20230805133230899

输入ctr + Z 之后前台的进程就会把切回后台,但是切回后台后进程是阻塞状态的,因此输入bg + 作业号就可让进程启动。

服务端守护进程化

那么很显然,在业务逻辑上服务端肯定是需要守护进程化的。因为服务端没有特殊情况是不会关闭的,需要一直运行。如果服务端是前台进程的话,那服务端运行时bash都不能用了,显然不符合。

这里要介绍一个接口:

#include <unistd.h>
pid_t setsid(void);

这个接口的作用是使调用的进程独立成为一个会话组并且为该组的组长。但是调用这个接口是有前置条件的:调用这个接口的进程不能为某个会话组的组长

守护进程化的步骤:

  1. 让调用进程忽略掉异常信号,因为其不受终端控制的
  2. 让调用进程不为组长
  3. 关闭或者重定向之前默认打开的文件,如0 1 2文件描述符
#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 daemonSelf(const char *currPath = nullptr)
{// 1. 让调用进程忽略掉异常的信号signal(SIGPIPE, SIG_IGN);// 2. 让自己不是组长,setsidif (fork() > 0)exit(0);// 子进程 -- 守护进程,精灵进程,本质就是孤儿进程的一种!pid_t n = setsid();assert(n != -1);// 3. 守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件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);}
}

接着只需要服务端在初始化完成后调用这个函数,将自己设为守护进程化即可

image-20230805134141141

一起来看看效果:

image-20230805134304036

可以看到服务端启动后并不会影响bash,仍然可以在bash上输入指令去执行。客户端也能够很好的接收到数据,这就符合现实中服务端的逻辑。

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

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

相关文章

Java Collection接口详解

Collection 接口 Collection接口是Java集合框架的根接口。 Collection 接口是 List、Set 和 Queue 接口的父接口&#xff0c;通常情况下不被直接使用。 Collection 接口定义了一些通用的方法&#xff0c;通过这些方法可以实现对集合的基本操作。定义的方法既可用于操作 Set …

docker创建镜像并上传云端服务器

docker创建镜像并上传云端服务器 docker容器与镜像的关系1.基本镜像相关文件创建1.1 创建dockerfile文件1.2.创建do.sh文件1.3 创建upload_server_api.py文件1.4 创建upload_server_webui.py文件1.5 文件保存位置 2. 创建镜像操作2.1 创建镜像2.3 创建容器2.2 进入环境容器2.3 …

【2023年电赛国一必备】A题报告模板--可直接使用

任务 图1 任务内容 要求 图2 基本要求内容 图3 发挥部分内容 说明 图4 说明内容 评分标准 图5 评分内容 正文 &#xff08;部分&#xff09; 摘要 本实验旨在设计和制作一个由两个单相逆变器组成的并联系统&#xff0c;用于为电阻负载供电或并入220V电网。采用基于STM…

【react】react生命周期钩子函数:

文章目录 一、生命周期概念:二、生命周期:三、挂载阶段&#xff08;constructor > render > componentDidMount&#xff09;&#xff1a;四、更新阶段&#xff08;render > componentDidUpdate&#xff09;&#xff1a;五、卸载阶段&#xff08;componentWillUnmount …

il汇编整数相加

在这里尝试了IL汇编字符串连接&#xff1b; IL汇编字符串连接_bcbobo21cn的博客-CSDN博客 下面来看一下IL汇编整数相加&#xff1b; 大概的看一下一些资料&#xff0c;下面语句&#xff0c; ldc.i4 20 ldc.i4 30 add 看上去像是&#xff0c;装载整数20到一个类似于…

VK1056B 液晶LCD显示驱动IC/14x4com工作电压2.4-5.2V稳定测试

LCD液晶显示驱动芯片VK1056B 14x4位的显示RAM适用于各种LED应用产品 产品型号&#xff1a;VK1056B (兼容替代TM系列驱动) 产品品牌&#xff1a;VINKA永嘉微电 封装形式&#xff1a;SOP24 SSOP24 产品年份&#xff1a;新年份 提供专业工程服务&#xff0c…

Django Rest_Framework(三)

文章目录 1. 认证Authentication2. 权限Permissions使用提供的权限举例自定义权限 3. 限流Throttling基本使用可选限流类 4. 过滤Filtering5. 排序Ordering6. 分页Pagination可选分页器 7. 异常处理 ExceptionsREST framework定义的异常 8. 自动生成接口文档coreapi安装依赖设置…

Apache Kafka Learning

目录 一、Kafka 1、Message Queue是什么&#xff1f; 2、Kafka 基础架构 3、Kafka安装 二、Maven项目测试 1、Topic API 2、生产者&消费者 一、Kafka Kafka是由Apache软件基金会开发的一个开源流处理平台&#xff0c;由Scala和Java编写。Kafka是一种高吞吐量的分布式…

LCD驱动芯片VK1024B兼容HT系列驱动芯片,体积更小

产品型号&#xff1a;VK1024B 产品&#xff1a;VINKA/永嘉微电 封装形式&#xff1a;SOP16 产品年份&#xff1a;新年份 工程服务&#xff0c;技术支持&#xff0c;用芯服务 VK1024概述&#xff1a; VK1024B 是 24 点、 内存映象和多功能的 LCD 驱动&#xff0c; VK1024B …

【css】css隐藏元素

display:none&#xff1a;可以隐藏元素。该元素将被隐藏&#xff0c;并且页面将显示为好像该元素不在其中。visibility:hidden&#xff1a; 可以隐藏元素。但是&#xff0c;该元素仍将占用与之前相同的空间。元素将被隐藏&#xff0c;但仍会影响布局。 代码&#xff1a; <!…

C++ - 模板分离编译

模板分离编译 我们先来看一个问题&#xff0c;我们用 stack 容器的声明定义分离的例子来引出这个问题&#xff1a; // stack.h // stack.h #pragma once #include<deque>namespace My_stack {template<class T, class Container std::deque<T>>class stack…

完整模型的训练套路

从心所欲 不逾矩 天大地大 皆可去 一、官方模型的初使用 使用VGG16模型 VGG模型使用代码示例&#xff1a; import torchvision.models from torch import nndataset torchvision.datasets.CIFAR10(/cifar10, False, transformtorchvision.transforms.ToTensor())vgg16_true …

Electron 开发,报handshake failed; returned -1, SSL error code 1,错误

代码说明 在preload.js代码中&#xff0c;暴露参数给渲染线程renderer.js访问&#xff0c; renderer.js 报&#xff1a;ERROR:ssl_client_socket_impl.cc(978)] failed; returned -1, SSL error code 1,错误 问题原因 如题所说&#xff0c;跨进程传递消息&#xff0c;这意味…

[Docker实现测试部署CI/CD----自由风格的CI操作[最终架构](5)]

目录 11、自由风格的CI操作&#xff08;最终&#xff09;Jenkins容器化实现方案修改 docker.sock 权限修改 Jenkins 启动命令后重启 Jenkins构建镜像推送到Harbor修改 daemon.json 文件Jenkins 删除构建后操作Jenkins 添加 shell 命令重新构建 Jenkins通知目标服务器拉取镜像目…

redis的安装和配置

一、nosql 二、redis的安装和配置 redis的安装&#xff1a; redis常见配置&#xff1a; 配置文件redis.conf

【数据结构】这堆是什么

目录 1.二叉树的顺序结构 2.堆的概念及结构 3.堆的实现 3.1 向上调整算法与向下调整算法 3.2 堆的创建 3.3 建堆的空间复杂度 3.4 堆的插入 3.5 堆的删除 3.6 堆的代码的实现 4.堆的应用 4.1 堆排序 4.2 TOP-K问题 首先&#xff0c;堆是一种数据结构&#xff0c;一种特…

在SAP中使用苹果手机进行条码扫描

适用于iOS的Liquid UI支持使用内置摄像头或第三方设备&#xff08;如Linea Pro&#xff09;进行条形码扫描。它使您能够通过单击在任何 SAP 输入字段中填充数据。它支持&#xff1a;一维和二维条码扫描。此外&#xff0c;编辑扫描的数据或在扫描后对操作进行编程&#xff0c;以…

2023牛客暑期多校训练营6-C-idol!!

奇数的双阶乘等于小于等于本身的奇数的乘积&#xff0c;偶数的双阶乘等于小于等于本身的非零偶数的乘积。 思路&#xff1a;考虑末位0的个数&#xff0c;我们能想到的最小两数相乘有零的就是2*5&#xff0c;所以本题我们思路就是去找因子2的个数以及因子5的个数&#xff0c;2的…

kubernetes基于helm部署gitlab

kubernetes基于helm部署gitlab 这篇博文介绍如何在 Kubernetes 中使用helm部署 GitLab。 先决条件 已运行的 Kubernetes 集群负载均衡器&#xff0c;为ingress-nginx控制器提供EXTERNAL-IP&#xff0c;本示例使用metallb默认存储类&#xff0c;为gitlab pods提供持久化存储&…

mysql存储过程定时调度

假设我们要创建一个简单的数据库&#xff0c;其中包含两张表&#xff1a;students 表和 courses 表&#xff0c;以及一个存储过程用于插入学生数据。下面是完整的建表语句、插入语句和存储过程&#xff1a; 1】建表 -- 创建 courses 表 CREATE TABLE courses (course_id INT …