【探索Linux】—— 强大的命令行工具 P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)

news2024/11/15 4:17:00

在这里插入图片描述

阅读导航

  • 引言
  • 一、TCP协议
  • 二、TCP网络程序模拟实现
    • 1. 预备代码
      • ⭕ThreadPool.hpp(线程池)
      • ⭕makefile文件
      • ⭕打印日志文件
      • ⭕将当前进程转变为守护进程
    • 2. TCP 服务器端实现(TcpServer.hpp)
    • 3. TCP 客户端实现(main函数)
  • 温馨提示

引言

在前一篇文章中,我们详细介绍了UDP协议和TCP协议的特点以及它们之间的异同点。本文将延续上文内容,重点讨论简单的TCP网络程序模拟实现。通过本文的学习,读者将能够深入了解TCP协议的实际应用,并掌握如何编写简单的TCP网络程序。让我们一起深入探讨TCP网络程序的实现细节,为网络编程的学习之旅添上一份精彩的实践经验。

一、TCP协议

TCP(Transmission Control Protocol)是一种面向连接的通信协议,它要求在数据传输前先建立连接,以确保数据的可靠传输。TCP通过序号、确认和重传等机制来保证数据的完整性和可靠性,同时还实现了拥塞控制和流量控制,以适应不同网络环境下的数据传输需求。由于TCP的可靠性和稳定性,它被广泛应用于网络通信中,包括网页浏览、文件传输、电子邮件等各种应用场景,成为互联网协议套件中的重要组成部分。详介绍可以看上一篇文章:UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同

二、TCP网络程序模拟实现

接下来,我们打算运用线程池技术,模拟实现一个简单的TCP网络程序。通过充分利用线程池,我们能够更有效地管理并发连接,从而提高程序的性能和稳定性。这一实践将有助于加深我们对网络编程关键概念和技术的理解和掌握。在前文中已经提到了线程池,这里就不再赘述其原理和作用。详细可以点击传送门:🚩 线程池

1. 预备代码

⭕ThreadPool.hpp(线程池)

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

// 线程信息结构体
struct ThreadInfo
{
    pthread_t tid;  // 线程ID
    std::string name;  // 线程名称
};

static const int defalutnum = 10;  // 默认线程池大小为10

template <class T>
class ThreadPool
{
public:
    void Lock() // 加锁
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock() // 解锁
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup() // 唤醒等待中的线程
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep() // 线程休眠
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty() // 判断任务队列是否为空
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid) // 获取线程名称
    {
        for (const auto &ti : threads_)
        {
            if (ti.tid == tid)
                return ti.name;
        }
        return "None";
    }

public:
    static void *HandlerTask(void *args) // 线程任务处理函数
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            tp->Lock();

            while (tp->IsQueueEmpty()) // 若任务队列为空,则线程等待
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop(); // 从任务队列中取出任务
            tp->Unlock();

            t(); // 执行任务
        }
    }
    void Start() // 启动线程池
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this); // 创建线程
        }
    }
    T Pop() // 从任务队列中取出任务
    {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }
    void Push(const T &t) // 将任务推入任务队列
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }
    static ThreadPool<T> *GetInstance() // 获取线程池实例
    {
        if (nullptr == tp_) // 若线程池实例为空
        {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_) // 双重检查锁
            {
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>(); // 创建线程池实例
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
    ThreadPool(int num = defalutnum) : threads_(num) // 构造函数
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool() // 析构函数
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &) = delete; // 禁用拷贝构造函数
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // 禁用赋值操作符,避免 a=b=c 的写法
private:
    std::vector<ThreadInfo> threads_; // 线程信息数组
    std::queue<T> tasks_; // 任务队列

    pthread_mutex_t mutex_; // 互斥锁
    pthread_cond_t cond_; // 条件变量

    static ThreadPool<T> *tp_; // 线程池实例指针
    static pthread_mutex_t lock_; // 静态互斥锁
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr; // 初始化线程池实例指针为nullptr

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER; // 初始化静态互斥锁

以上代码实现了一个简单的线程池模板类 ThreadPool,其中包含了线程池的基本功能和操作。

  1. 首先定义了一个线程信息结构体 ThreadInfo,用来保存线程的ID和名称。

  2. 然后定义了一个模板类 ThreadPool,其中包含了线程池的各种操作和属性:

    • Lock()Unlock() 分别用于加锁和解锁。
    • Wakeup() 用于唤醒等待中的线程。
    • ThreadSleep() 用于使线程进入休眠状态。
    • IsQueueEmpty() 判断任务队列是否为空。
    • GetThreadName() 根据线程ID获取线程名称。
  3. 定义了静态成员函数 HandlerTask,作为线程的任务处理函数。在该函数中,线程会不断地从任务队列中取出任务并执行。

  4. Start() 函数用于启动线程池,创建指定数量的线程,并将线程的任务处理函数设置为 HandlerTask

  5. Pop() 函数用于从任务队列中取出任务。

  6. Push() 函数用于将任务推入任务队列。

  7. GetInstance() 函数用于获取线程池的实例,采用了双重检查锁(Double-Checked Locking)实现单例模式。

  8. 线程池的构造函数和析构函数分别用于初始化和销毁互斥锁和条件变量。

  9. 最后使用静态成员变量初始化了线程池实例指针和静态互斥锁。

⭕makefile文件

.PHONY:all
all:tcpserverd tcpclient

tcpserverd:Main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.cc
	g++ -o $@ $^ -std=c++11


.PHONY:clean
clean:
	rm -f tcpserverd tcpclient

这段代码是一个简单的 Makefile 文件,用于编译生成两个可执行文件 tcpserverdtcpclient

  1. .PHONY: all:声明 all 为一个伪目标,表示 all 不是一个实际的文件名,而是一个指定的操作。

  2. all: tcpserverd tcpclient:定义了 all 目标,它依赖于 tcpserverdtcpclient 目标。当执行 make all 时,会先编译 tcpserverdtcpclient

  3. tcpserverd: Main.cc:定义了生成 tcpserverd 可执行文件的规则,依赖于 Main.cc 源文件。使用 g++ 编译器进行编译,指定输出文件名为 tcpserverd,使用 C++11 标准,并链接 pthread 库。

  4. tcpclient: TcpClient.cc:定义了生成 tcpclient 可执行文件的规则,依赖于 TcpClient.cc 源文件。同样使用 g++ 编译器进行编译,指定输出文件名为 tcpclient,使用 C++11 标准。

  5. .PHONY: clean:声明 clean 为一个伪目标。

  6. clean: rm -f tcpserverd tcpclient:定义了 clean 目标,用于清理生成的可执行文件。执行 make clean 时将删除 tcpserverdtcpclient 可执行文件。

⭕打印日志文件

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen; // 默认打印方式为屏幕输出
        path = "./log/"; // 默认日志文件路径为当前目录下的"log/"目录
    }

    void Enable(int method)
    {
        printMethod = method; // 设置打印方式
    }

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

    void printLog(int level, const std::string &logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl; // 在屏幕上输出日志信息
            break;
        case Onefile:
            printOneFile(LogFile, logtxt); // 将日志信息写入单个文件中
            break;
        case Classfile:
            printClassFile(level, logtxt); // 根据日志级别将日志信息写入不同的文件中
            break;
        default:
            break;
        }
    }

    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname; // 拼接日志文件路径
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // 打开或创建一个文件,以追加方式写入
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size()); // 将日志信息写入文件
        close(fd); // 关闭文件
    }

    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level); // 生成日志文件名,例如"log.txt.Debug/Warning/Fatal"
        printOneFile(filename, logtxt); // 将日志信息写入对应级别的文件中
    }

    ~Log()
    {
    }

    // 重载()运算符,用于输出日志信息
    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t); // 获取当前时间

        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // 格式化左侧部分,包括日志级别和时间信息

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); // 格式化右侧部分,即用户自定义的日志内容
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer); // 拼接左右两侧的日志内容
        printLog(level, logtxt); // 打印日志信息
    }

private:
    int printMethod; // 打印方式
    std::string path; // 日志文件路径
};

这段代码是一个简单的日志记录类 Log,它提供了几种不同的日志输出方式和日志级别。

  • #pragma once: 使用编译器指令,确保头文件只被编译一次。

  • 定义了一些常量:

    • SIZE: 缓冲区大小为 1024。
    • 日志级别常量:Info, Debug, Warning, Error, Fatal
    • 打印方式常量:Screen, Onefile, Classfile
    • 日志文件名常量:LogFile
  • Log 类包含以下成员函数和变量:

    • printMethod: 记录当前的打印方式,默认为屏幕输出。
    • path: 日志文件路径,默认为"./log/"。
  • 构造函数 Log() 初始化 printMethodpath

  • Enable(int method): 设置日志的打印方式。

  • levelToString(int level): 将日志级别转换为对应的字符串。

  • printLog(int level, const std::string &logtxt): 根据打印方式输出日志信息。

  • printOneFile(const std::string &logname, const std::string &logtxt): 将日志信息写入单个文件中。

  • printClassFile(int level, const std::string &logtxt): 根据日志级别将日志信息写入不同的文件中。

  • 析构函数 ~Log()

  • 重载的函数调用运算符 operator(): 接受日志级别和格式化字符串,格式化输出日志信息到不同的输出位置。

⭕将当前进程转变为守护进程

#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile = "/dev/null"; // 定义空设备文件路径

// 将当前进程变为守护进程的函数
void Daemon(const std::string &cwd = "")
{
    // 1. 忽略一些异常信号,以避免对守护进程造成影响
    signal(SIGCLD, SIG_IGN); // 忽略子进程结束信号
    signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信号
    signal(SIGSTOP, SIG_IGN); // 忽略终止信号

    // 2. 创建一个子进程并使父进程退出,确保守护进程不是进程组组长,创建一个新的会话
    if (fork() > 0)
        exit(0); // 父进程退出
    setsid(); // 创建新的会话,并成为该会话的首进程

    // 3. 更改当前调用进程的工作目录,如果指定了工作目录则切换到相应目录
    if (!cwd.empty())
        chdir(cwd.c_str()); // 切换工作目录到指定路径

    // 4. 将标准输入,标准输出,标准错误重定向至/dev/null,关闭不需要的文件描述符
    int fd = open(nullfile.c_str(), O_RDWR); // 打开空设备文件
    if (fd > 0)
    {
        dup2(fd, 0); // 标准输入重定向至空设备
        dup2(fd, 1); // 标准输出重定向至空设备
        dup2(fd, 2); // 标准错误重定向至空设备
        close(fd); // 关闭打开的文件描述符
    }
}

这段代码实现了将当前进程转变为守护进程的函数 Daemon

  1. 忽略一些异常信号,避免对守护进程产生影响。
  2. 创建一个子进程并使父进程退出,确保守护进程不是进程组组长,创建一个新的会话。
  3. 更改当前调用进程的工作目录,如果指定了工作目录,则切换到相应目录。
  4. 将标准输入、标准输出和标准错误重定向至 /dev/null,即空设备文件,关闭不需要的文件描述符,确保守护进程不产生输出和错误信息。

2. TCP 服务器端实现(TcpServer.hpp)

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"

const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 最大连接请求队列长度

extern Log lg;

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};

class TcpServer;

// 线程数据结构,用于传递给线程处理函数
class ThreadData
{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
    {}
public:
    int sockfd;
    std::string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};

// TCP服务器类
class TcpServer
{
public:
    // 构造函数,初始化端口和IP地址
    TcpServer(const uint16_t &port, const std::string &ip = defaultip) : listensock_(defaultfd), port_(port), ip_(ip)
    {
    }

    // 初始化服务器
    void InitServer()
    {
        // 创建套接字
        listensock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock_ < 0)
        {
            lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, listensock_: %d", listensock_);

        // 设置套接字选项,允许地址重用
        int opt = 1;
        setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));

        // 绑定本地地址
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &(local.sin_addr));

        if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }

        lg(Info, "bind socket success, listensock_: %d");

        // 监听套接字,开始接受连接请求
        if (listen(listensock_, backlog) < 0)
        {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }

        lg(Info, "listen socket success, listensock_: %d");
    }

    // 启动服务器
    void Start()
    {
        // 将当前进程变为守护进程
        Daemon();

        // 启动线程池
        ThreadPool<Task>::GetInstance()->Start();

        lg(Info, "tcpServer is running....");
        
        // 循环接受客户端连接并处理
        while(true)
        {
            // 获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); 
                continue;
            }

            // 获取客户端IP和端口
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

            // 打印客户端连接信息
            lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);

            // 创建任务对象并加入线程池处理
            Task t(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);
        }
    }

    // 析构函数
    ~TcpServer() {}

private:
    int listensock_;  // 监听套接字
    uint16_t port_;   // 端口号
    std::string ip_;  // IP地址
};

这段代码是一个简单的TCP服务器的实现,包括了创建套接字、绑定地址、监听连接、接受客户端连接等基本操作。

3. TCP 客户端实现(main函数)

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void Usage(const std::string &proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    // 检查命令行参数是否正确
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 设置服务器地址信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    while (true)
    {
        int cnt = 5; // 连接重试次数
        int isreconnect = false; // 是否需要重连
        int sockfd = 0;

        // 创建套接字
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return 1;
        }

        do
        {
            // 尝试连接服务器
            int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                isreconnect = true;
                cnt--;
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
                sleep(2); // 等待一段时间后重连
            }
            else
            {
                break;
            }
        } while (cnt && isreconnect);

        if (cnt == 0)
        {
            std::cerr << "user offline..." << std::endl;
            break;
        }

        // 与服务器建立连接后进行通信
        while (true)
        {
            std::string message;
            std::cout << "Please Enter# ";
            std::getline(std::cin, message);

            // 向服务器发送消息
            int n = write(sockfd, message.c_str(), message.size());
            if (n < 0)
            {
                std::cerr << "write error..." << std::endl;
            }

            // 从服务器接收消息并显示
            char inbuffer[4096];
            n = read(sockfd, inbuffer, sizeof(inbuffer));
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cout << inbuffer << std::endl;
            }
        }

        // 关闭套接字
        close(sockfd);
    }

    return 0;
}

温馨提示

感谢您对博主文章的关注与支持!如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C++编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索Linux、C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
在这里插入图片描述

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

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

相关文章

Doris实战——天眼查Doris实时数仓构建

目录 前言 一、业务背景 二、原有架构及痛点 三、理想架构 四、技术选型 五、新数仓架构 六、应用场景优化 6.1 人群圈选 6.2 C端分析数据及精准营销线索场景 七、优化经验 八、规模和收益 九、未来规划 原文大佬的这篇实时数仓构建有借鉴意义的&#xff0c;这些摘…

JavaEE 初阶篇-深入了解进程与线程(常见的面试题:进程与线程的区别)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 进程概述 2.0 线程概述 2.1 多线程概述 3.0 常见的面试题&#xff1a;谈谈进程与线程的区别 4.0 Java 实现多线程的常见方法 4.1 实现多线程方法 - 继承 Thread 类…

从先序与中序遍历序列构造二叉树

从先序与中序遍历序列构造二叉树 描述&#xff1a; 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 递归法 解题思路&#xff1a; 通过先序遍历我…

山西生态与智慧水利科技展会之华锐VRAR元宇宙水利应用展位恭候大驾

2024黄河流域生态与智慧水利科技博览会于今日在山西太原隆重开启&#xff0c;本次展会持续至3月24日共三天时间&#xff0c;相比之前的水展&#xff0c;这次展会区域之广辐射了中部六省&#xff0c;展会内容之多包括黄河流域生态保护的现状与挑战、水资源管理与水污染防治、智慧…

鸿蒙开发-UI-动画-页面间动画

鸿蒙开发-UI-组件导航-Navigation 鸿蒙开发-UI-组件导航-Tabs 鸿蒙开发-UI-图形-图片 鸿蒙开发-UI-图形-绘制几何图形 鸿蒙开发-UI-图形-绘制自定义图形 鸿蒙开发-UI-图形-页面内动画 鸿蒙开发-UI-图形-组件内转场动画 鸿蒙开发-UI-图形-弹簧曲线动画 文章目录 前言 一、放大缩…

今天简单聊聊容器化

什么是容器化 容器化&#xff08;Containerization&#xff09;是一种软件开发和部署的方法&#xff0c;其核心思想是将应用程序及其所有依赖项打包到一个独立的运行环境中&#xff0c;这个环境被称为容器。容器化技术使得应用程序可以在不同的计算环境中以一致的方式运行&…

ES集群不识别节点SSL证书的问题处理

问题描述 在启动ES服务并试图加入其他节点上已启动的集群时&#xff0c;出现报错(原文是一大段话&#xff0c;我按语义拆成了几段)&#xff1a; [2024-03-19T16:32:02,844][WARN ][o.e.c.s.DiagnosticTrustManager] [node-2-master] failed to establish trust with server a…

宏璇物流邀您参观2024快递物流供应链与技术装备展览会

展会介绍 ESYE CHINA 2024快递物流展是亚洲范围内超大规模的快递物流业展示平台&#xff0c;由于展会的需求及扩大市场的影响力&#xff0c;ESYE 2024转战杭州&#xff0c;凭借着先进的经济发展水平、优越的地理位置、成熟的产业环境以及巨大的电商、微商、零售业、消费的市场…

云手机为电商提供五大出海优势

出海电商行业中&#xff0c;各大电商平台的账号安全是每一个电商运营者的重中之重&#xff0c;账号安全是第一生产力&#xff0c;也是店铺运营的基础。因此多平台多账号的防关联管理工具成了所有电商大卖家的必备工具。云手机最核心的优势就是账户安全体系&#xff0c;本文将对…

ky10.aarch64安装Jenkins

参考地址&#xff1a;《安装部署 Jenkins》 前言 有war包和rpm两种安装方式&#xff0c;如果是长期使用更加推荐rpm的安装方式&#xff0c;可以更好的管理Jenkins&#xff1b; 我此次安装jenkins主要用于测试和简单的个人使用&#xff0c;所以选择更轻便的war安装。 1 下载J…

【C#】C#窗体应用修改窗体的标题和图标

修改窗体顶部的标题和图表&#xff0c;如果不修改则会使用默认的图标&#xff0c;标题默认为Form1&#xff0c;如第一张图&#xff0c;这时候如果想换成和系统有关的内容&#xff0c;如第二张图&#xff0c;可以使用下面的方法进行修改&#xff0c;修改后打开该软件任务栏显示的…

【yaml包如何安装】

【yaml包如何安装】 安装yaml包&#xff08;也称为pyyaml&#xff09; &#xff0c;可以直接使用如下述命令 pip install pyyaml

LeetCode - 存在重复元素

219. 存在重复元素 II 这道题可以用两个方法解决。 哈希表 从左到右遍历数组&#xff0c;并将数组的下标存到hash中&#xff0c;在遍历数字的过程中&#xff0c;如果hash中不存在nums[i]&#xff0c;将nums[i]加入到hash当中&#xff0c;若存在&#xff0c;则判断下标之间的关…

【算法】数组-移除元素

给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须仅使用 O(1) 额外空间并原地修改输入数组。 元素的顺序可以改变。你不需要考虑数组中超出新长度后面的…

Java的IO之BIO

Java IO流是用于处理输入和输出的机制&#xff0c;用于读取和写入数据。Java提供了丰富的IO类和接口&#xff0c;用于处理不同类型的数据和操作。Java中的IO模型主要分为BIO和NIO两种&#xff0c;他们可以分别被视为IO编程的不同风格或模式&#xff0c;并非IO流具体的类型&…

云计算2主从数据库

设置主从数据库的目的是将数据库1和数据库2分别建在两个虚拟机上&#xff0c;并实现数据互通访问 首先准备两个虚拟机&#xff0c;这里示例ip分别为&#xff1a; 192.168.200.10&#xff1b;192.168.200.20 修改主机名&#xff0c;一个是mysql1&#xff0c;一个是mysql2&#x…

除了Confluence,还有哪些好用的知识库平台

大家都知道&#xff0c;Confluence作为知识库平台界的佼佼者&#xff0c;确实给我们带来了不少便利。但好东西总是层出不穷&#xff0c;除了Confluence&#xff0c;市面上还有其他好用的知识库平台也值得我们去尝试。今天&#xff0c;我就给大家聊聊我个人用过并且觉得挺不错的…

Vue+Element UI 开发PC端页面,出现页面抖动原因及解决办法

问题描述&#xff1a; 页面 拖动 放大放小&#xff0c;出现页面抖动&#xff0c;屏幕不停闪动 解决方案&#xff1a; 1.找到相对应的页面的div 加上样式&#xff1a; .app-container {height: 100%;overflow: auto;margin: 0; //加上这个把滚动条隐藏掉&#xff0c;否则就…

PHP页面如何实现设置独立访问密码

PHP网页如果需要查看信息必须输入密码&#xff0c;验证后才可显示出内容的代码如何实现&#xff1f; 对某些php页面设置单独的访问密码,如果密码不正确则无法查看内容,相当于对页面进行了一个加密。 如何实现这个效果&#xff0c;详细教程可以参考&#xff1a;PHP页面如何实现…

香港科技大学(广州)先进材料学域可持续能源与环境学域智能制造学域博士招生宣讲会——北京专场(暨全额奖学金政策)

三个学域代表教授亲临现场&#xff0c;面对面答疑解惑助攻申请&#xff01;可带简历现场咨询和面试&#xff01; &#x1f4b0;一经录取&#xff0c;享全额奖学金1.5万/月&#xff01; 报名链接&#xff1a; https://www.wjx.top/vm/wF2Mant.aspx# 地点&#xff1a;中关村皇冠…