简单的TCP网络程序·多进程、多线程(后端服务器)

news2024/11/25 14:56:53

        前文链接 -- 简单的TCP网络程序·单进程

        上篇文章中,实现了TCP网络通信的的单进程版本,因为实现的是一个死循环的逻辑,是串行实运行的,显然这和实际中的TCP通信是不同的,为了解决这方面的问题,需要使用多进程、多线程的方式去解决,这里就直接写了

目录

版本二:多进程

接口1:fork

细节1:父进程直接等待吗?

解决方法1:利用孤儿进程特性

测试结果1

解决方法2:信号捕捉

细节2:父进程要关闭自己不用的文件描述符

测试结果2

多进程版本修改代码

函数1:start()

版本三:多线程

对象类型:pthread_t -- 需填写

接口3:pthread_detach()

接口4:pthread_self()

版本三:多线程版的修改区域

函数2:threadRoutine()

多线程传参对象:ThreadData

细节3:编译的时候记得带上 lpthread

测试3:多线程的测试

细节4:使用本地回环的时候操作符会多用一个

测试4:多线程的测试 -- 在监控脚本下观看链接状态

监控脚本:

全部代码

tcpClient.cc

tcpClient.hpp

tcpServer.cc

tcpServer.hpp

log.hpp

makefile


版本二:多进程

接口1:fork

        注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份,也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock

细节1:父进程直接等待吗?

        显然这里不能单纯的等待,这样又会回归串行运行了,因为等待的时候会阻塞式等待,且这里并不适用非阻塞式等待,因为当有一百个链接来了,就有一百个进程运行,因为非阻塞,就会回到开始,一但后面没有链接到来的话,那么accept这里就等不到了,再也不会返回,这些进程就不会再也不会被回收了

解决方法1:利用孤儿进程特性

我们可以直接让操作系统领养这里的子进程,直接退出,让孙子进程来做服务

测试结果1

解决方法2:信号捕捉

        上面的一种用法是一种巧妙的用法,不过我们这里正常来说使用信号捕捉就行了,这样可以避免一直反复在创建子进程

为SIGCHLD建立信号处理程序

        即无论一个子进程于何时终止,系统都会向其父进程发送 SIGCHLD 信号**。对该信号的默认处理是将其忽略,不过也可以按照信号处理程序来捕获它

细节2:父进程要关闭自己不用的文件描述符

        文件描述符会越来越大,因为老的文件描述符一直正在被使用,如图所示,文件描述符会一直增大,即使链接已经断开了

        一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1,这个文件描述符子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了,所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了

测试结果2

至此多进程版本完成

多进程版本修改代码

函数1:start()

注意头文件的引入 

void start()
        {
            //解决方法2:信号捕捉
            signal(SIGCHLD, SIG_IGN);   // SIG_IGN:忽略
            for (;;) // 一个死循环
            {
                // 4. server 获取新链接
                // sock 和client 进行通信的fd
                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"); // 这个不影响服务器的运行,用ERROR,就像张三不会因为没有把人招呼进来就不干了
                    continue;
                }
                logMessage(NORMAL, "accept a new link success");
                std::cout << "sock: " << sock << std::endl;

                // 5. 这里就是一个sock, 未来通信我们就用这个sock, 面向字节流的,后续全部都是文件操作!
                // 我们就可以直接使用read之类的面向字节流的操作都行

                /*
                // version 1 -- 单进程
                serviceIO(sock);
                close(sock); // 走到这里就说明客户端已经关闭
                             // 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符会越来越少,因为文件描述符本质就是一个数组下标
                             // 只要是数组下标就会有尽头,提供服务的上限 就等于文件描述符的上限
                             // 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
                */

                // version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,
                                            // 即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份
                                            // 也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock
                pid_t id = fork();
                if (id == 0) // 当id为 0 的时候就代表这里是子进程
                {
                    // 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符
                    // 即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好
                    close(_listensock);
                    //if(fork()>0) exit(0); 解决方法1:利用孤儿进程特性
                    serviceIO(sock);
                    close(sock);
                    exit(0);
                }
                // 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1
                // 子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了
                // 所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了
                close(sock);   
                
                //father
                // 那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待
                // 且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待
                // 一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了

                //不需要等待了
                /*pid_t ret = waitpid(id, nullptr, 0);
                if(ret > 0)
                {
                    std::cout<< "wait success: " << ret << std::endl;
                }
                */
            }
        }

版本三:多线程

由于创建进程的消耗比较大,所以这里再提供一个多线程版本的

对象类型:pthread_t -- 需填写

 参考文献 -- pthread_create()函数:创建线程

接口3:pthread_detach()

int pthread_detach(pthread_t thread);    成功:0;失败:错误号

作用:从状态上实现线程分离,注意不是指该线程独自占用地址空间。

线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源)。网络、多线程服务器常用。

接口4:pthread_self()

作用:获取自身id

版本三:多线程版的修改区域

下图有个小错误,应该先close(), 再delete

函数2:threadRoutine()

        // 注意因为是静态的所以,不能访问类内成员,所以我们把this指针当成参数传进来
        static void *threadRoutine(void *args)
        {
            pthread_detach(pthread_self()); // 线程分离,这样就不需要等待了,自己结束后自己会释放
            ThreadData *td = static_cast<ThreadData *>(args);

            td->_self->serviceIO(td->_sock);
            delete td;
            close(td->_sock);
            return nullptr;
        }

多线程传参对象:ThreadData

在TcpServer类内,所以类内类

    // 用以线程传参
    class ThreadData
    {
    public:
        ThreadData(TcpServer *self, int sock) : _self(self), _sock(sock)
        {}
    public:
        TcpServer *_self;
        int _sock;
    };

细节3:编译的时候记得带上 lpthread

测试3:多线程的测试

        我们会发现这里的文件操作符会增加,因为随着用户的链接,这里的操作符会被使用,只有当用户断开链接的时候才会被释放

细节4:使用本地回环的时候操作符会多用一个

测试4:多线程的测试 -- 在监控脚本下观看链接状态

监控脚本:

while :; do ps -aL | head -1 && ps -aL | grep tcpserver; sleep 1; echo "#######################"; done

全部代码

tcpClient.cc

#include "tcpClient.hpp"
#include <memory>

using namespace std;

static void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " serverip serverport\n\n"; // 命令提示符
}

// ./tcpclient serverip serverport  调用逻辑
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    unique_ptr<TcpClient> tcli(new TcpClient(serverip, serverport));
    tcli->initClient();
    tcli->start();

    return 0;
}

tcpClient.hpp

#pragma once

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

#define NUM 1024

class TcpClient
{
public:
    TcpClient(const std::string &serverip, const uint16_t &port)
        : _sock(1), _serverip(serverip), _serverport(port)
    {
    }
    void initClient()
    {
        // 1. 创建socket
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            // 客户端也可以有日志,不过这里就不再实现了,直接打印错误
            std::cout << "socket create error" << std::endl;
            exit(2);
        }

        // 2. tcp的客户端要不要bind? 要的! 但是不需要显示bind,这里的client port要让OS自定!
        // 3. 要不要listen? -- 不需要!客户端不需要建立链接
        // 4. 要不要accept? -- 不要!
        // 5. 要什么? 要发起链接!
    }

    void start()
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);
        server.sin_addr.s_addr = inet_addr(_serverip.c_str());

        if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
        {
            std::cerr << "socket connect error" << std::endl;
        }
        else
        {
            std::string msg;
            while (true)
            {
                std::cout << "Enter# ";
                std::getline(std::cin, msg);
                write(_sock, msg.c_str(), msg.size());

                char buffer[NUM];
                int n = read(_sock, buffer, sizeof(buffer) - 1);
                if (n > 0)
                {
                    // 目前我们把读到的数据当成字符串, 截至目前
                    buffer[n] = 0;
                    std::cout << "Server回显# " << buffer << std::endl;
                }
                else
                {
                    break;
                }
            }
        }
    }
    ~TcpClient()
    {
        if(_sock >= 0) close(_sock);    //不写也行,因为文件描述符的生命周期随进程,所以进程退了,自然也就会自动回收了
    }

private:
    int _sock;
    std::string _serverip;
    uint16_t _serverport;
};

tcpServer.cc

#include "tcpServer.hpp"
#include <memory>

using namespace server;
using namespace std;

static void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_port\n\n"; // 命令提示符
}

// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{ 
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<TcpServer> tsvr(new TcpServer(port));
    tsvr->initServer();
    tsvr->start();

    return 0;
}

tcpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>

#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; // 10、20、50都可以,但是不要太大比如5千,5万
    class TcpServer;               // 声明

    // 用以线程传参
    class ThreadData
    {
    public:
        ThreadData(TcpServer *self, int sock) : _self(self), _sock(sock)
        {}
    public:
        TcpServer *_self;
        int _sock;
    };

    class TcpServer
    {
    public:
        TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
        {
        }
        void initServer()
        {
            // 1. 创建socket文件套接字对象 -- 流式套接字
            _listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0
            if (_listensock < 0)
            {
                logMessage(FATAL, "create socket error");
                exit(SOCKET_ERR);
            }
            logMessage(NORMAL, "create socket success");

            // 2.bind绑定自己的网路信息 -- 注意包含头文件
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);      // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到
            local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                logMessage(FATAL, "bind socket error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "bind socket success");

            // 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程
            if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑
            {
                logMessage(FATAL, "listen socket error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "listen socket success");
        }

        void start()
        {
            // 解决方法2:信号捕捉 版本二
            // signal(SIGCHLD, SIG_IGN);   // SIG_IGN:忽略
            for (;;) // 一个死循环
            {
                // 4. server 获取新链接
                // sock 和client 进行通信的fd
                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"); // 这个不影响服务器的运行,用ERROR,就像张三不会因为没有把人招呼进来就不干了
                    continue;
                }
                logMessage(NORMAL, "accept a new link success");
                std::cout << "sock: " << sock << std::endl;

                // 5. 这里就是一个sock, 未来通信我们就用这个sock, 面向字节流的,后续全部都是文件操作!
                // 我们就可以直接使用read之类的面向字节流的操作都行

                /*
                // version 1 -- 单进程
                serviceIO(sock);
                close(sock); // 走到这里就说明客户端已经关闭
                             // 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符会越来越少,因为文件描述符本质就是一个数组下标
                             // 只要是数组下标就会有尽头,提供服务的上限 就等于文件描述符的上限
                             // 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
                */

                // version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,
                // 即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份
                // 也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock
                /*pid_t id = fork();
                if (id == 0) // 当id为 0 的时候就代表这里是子进程
                {
                    // 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符
                    // 即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好
                    close(_listensock);
                    //if(fork()>0) exit(0); 解决方法1:利用孤儿进程特性
                    serviceIO(sock);
                    close(sock);
                    exit(0);
                } */
                // 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1
                // 子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了
                // 所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了
                // close(sock);

                // father
                //  那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待
                //  且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待
                //  一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了

                // 不需要等待了 version 2 
                /*pid_t ret = waitpid(id, nullptr, 0);
                if(ret > 0)
                {
                    std::cout<< "wait success: " << ret << std::endl;
                }
                */

                // version 3 多线程
                // 线程不需要关闭文件操作符,且看的到进程的共享资源
                pthread_t tid;
                ThreadData *td = new ThreadData(this, sock);
                pthread_create(&tid, nullptr, threadRoutine, td); // 把this传过去

                // pthread_join(tid, nullptr);  不能阻塞式等待 -- 这样又会会到之前的串行运行了
                // 所以我们直接线程分离, 这样就不需要等待了
            }
        }

        // 注意因为是静态的所以,不能访问类内成员,所以我们把this指针当成参数传进来
        static void *threadRoutine(void *args)
        {
            pthread_detach(pthread_self()); // 线程分离,这样就不需要等待了,自己结束后自己会释放
            ThreadData *td = static_cast<ThreadData *>(args);

            td->_self->serviceIO(td->_sock);
            close(td->_sock);
            delete td;
            return nullptr;
        }

        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()); // 在多路转接的时候再详谈write的返回值
                }
                else if (n == 0)
                {
                    // 代表client退出 -- 把它想象成一个建立好的管道,客户端不写了,并且把它的文件描述符关了,读端就会像管道一样读到 0 TCP同理
                    logMessage(NORMAL, "client quit, me too!");
                    break;
                }
            }
        }

        ~TcpServer() {}

    private:
        int _listensock; // 修改二:改为listensock 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!
        uint16_t _port;
    };

} // namespace server

log.hpp

#pragma once

#include <iostream>
#include <string>

// 定义五种不同的信息
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3     //一种不影响服务器的错误
#define FATAL 4     //致命错误

void logMessage(int level, const std::string message)
{
    // 格式如下
    // [日志等级] [时间戳/时间] [pid] [message]
    // [FATAL0] [2023-06-11 16:46:07] [123] [创建套接字失败]

    // 暂定
    std::cout << message << std::endl;
}

makefile

cc=g++
.PHONY:all
all:tcpserver tcpclient

tcpclient:tcpClient.cc
	$(cc) -o $@ $^ -std=c++11

tcpserver:tcpServer.cc
	$(cc) -o $@ $^ -std=c++11 -lpthread
 
.PHONY:clean
clean:
	rm -f tcpserver tcpclient

 线程池版本转下篇 -- 简单的TCP网络程序·线程池(后端服务器)

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

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

相关文章

B+树:MySQL数据库索引的实现

作为一个软件开发工程师&#xff0c;你对数据库肯定再熟悉不过了。作为主流的数据存储系统&#xff0c;它在我们的业务开发中&#xff0c;有着举足轻重的地位。在工作中&#xff0c;为了加速数据库中数据的查找速度&#xff0c;我们常用的处理思路是&#xff0c;对表中数据创建…

chatgpt赋能python:Python怎么横着输出?

Python怎么横着输出&#xff1f; 如果你是一名有10年Python编程经验的工程师&#xff0c;你一定已经经历过许多项目&#xff0c;很可能你曾经需要对代码进行一些横向格式化或输出。Python作为一种高级编程语言&#xff0c;有各种各样的技巧和技能。在本文中&#xff0c;我们将…

node安装后的全局环境变量配置

安装node时&#xff0c;位置最好不要装在c盘&#xff0c;这里&#xff0c;我在D盘下创建了文件夹"node"&#xff0c;安装地址选择在该文件夹下 一直next&#xff0c;直到安装结束&#xff0c;打开"node"文件夹&#xff0c;安装完后&#xff0c;里面的配置…

C++【STL】之vector的使用

文章目录&#xff1a; vector介绍vector使用1. 默认成员函数1.1 默认构造1.2 拷贝构造1.3 析构函数1.4 赋值重载 2. 迭代器2.1 正向迭代器2.2 反向迭代器 3. 容量操作3.1 获取空间数据3.2 空间扩容3.3 大小调整3.4 空间缩容 4. 数据访问4.1 下标随机访问4.2 获取首尾元素 5. 数…

chatgpt赋能python:Python怎么横向键盘输入?

Python怎么横向键盘输入&#xff1f; 如果你是一位使用Python进行编程的工程师&#xff0c;你肯定明白快速而准确地输入代码的重要性。现在&#xff0c;许多程序员都找到了一个方法来更快地输入代码-横向键盘输入。 什么是横向键盘输入&#xff1f; 横向键盘输入是一种方法&…

基于Java校园驿站管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

力扣题库刷题笔记5--最长回文子串

1、题目如下&#xff1a; 2、个人Python代码实现&#xff1a; 首先想到的是通过类似冒泡排序的方式进行切片&#xff0c;然后判断切片的子字符串是否为回文字符串&#xff0c;然后记录出最长的回文字符串&#xff0c;代码如下&#xff1a; 可以看到&#xff0c;通过切片的方式&…

合宙Air724UG Cat.1模块硬件设计指南--电源供电

电源供电 简介 在模块应用设计中&#xff0c;电源设计是很重要的一部分&#xff0c;供电部分的电路设计不当会造成模块出现工作异常、指标恶化等现象&#xff0c;而良好的电源设计方案能够给模块提供稳定的工作状态。 特性 模块主供电VBAT&#xff1a;3.3V~4.3V&#xff0c;推…

设计模式—模板方法模式

模板方法模式&#xff1a; 定义一个操作的流程框架&#xff0c;而将流程中一些步骤延迟到子类中实现。使得子类在不改变流程结构的情况下&#xff0c;重新定义流程中的特定步骤。 主要角色&#xff1a; 抽象类: 负责给出操作流程的轮廓或框架&#xff0c;由模板方法和若干基…

用户模块的增删改查接口设计

MongoDB 数据库常用操作 MongoDB数据库中常用的操作包括&#xff1a; 插入数据&#xff1a;使用insertOne()或insertMany()方法向集合中插入数据。查询数据&#xff1a;使用find()方法查询满足条件的数据。更新数据&#xff1a;使用updateOne()或updateMany()方法更新满足条件…

【力扣刷题 | 第九天】150 逆波兰 239滑动窗口最大值

目录 前言&#xff1a; 150. 逆波兰表达式求值 - 力扣&#xff08;LeetCode&#xff09; 239. 滑动窗口最大值 - 力扣&#xff08;LeetCode&#xff09; 总结&#xff1a; 前言&#xff1a; 本片仍然是利用栈与队列的思想来解决实际问题&#xff0c;希望各位小伙伴可以和我…

chatgpt赋能python:Python断言之等于两个值其中一个

Python断言之等于两个值其中一个 在Python编程中&#xff0c;我们经常需要对程序进行断言&#xff0c;以判断程序是否正确地运行。其中一种常见的断言方式是判断某个方法的结果是否等于两个值中的其中一个。本文将介绍如何在Python中实现这种断言&#xff0c;并探讨其在实际应…

⑨电子产品拆解分析-触摸化妆镜

⑨电子产品拆解分析-触摸化妆镜 一、功能介绍二、电路分析以及器件作用1、电源部分2、触摸部分3、灯光控制部分三、数据手册以及其它资料1、注意点2、数据手册汇总一、功能介绍 ①短按白光、暖光、冷光三档色温切换;②长按支持无极调光;③三档调亮度关机记忆当前亮度功能;二…

chatgpt赋能python:Python模块更新技巧详解

Python模块更新技巧详解 为什么需要更新Python模块&#xff1f; Python语言自问世以来一直在得到广泛的应用&#xff0c;其中最大的原因在于它的灵活性和可扩展性。Python拥有丰富的模块库&#xff0c;覆盖了各种不同的应用场景。然而&#xff0c;由于软件环境不断发展&#…

服务器配置远程vscode

1 使用sftp同步远程代码 打开vscode&#xff0c;在扩展种搜索sftp&#xff0c;点击安装。   按住快捷键shiftctrlp&#xff0c;可以打开界面顶部的命令行&#xff0c;输入sftp&#xff0c;点击如下图的config选项&#xff1a;   会自动在.vscode目录下创建一个名为sftp.j…

photoscan(metashape)跑GPS辅助的无人机影像SfM(空三)教程

刚打开的photoscan界面如下图所示&#xff1a;   然后&#xff0c;点击工作区左上角的添加堆块选项&#xff1a;   可以看到新增了一个名为“Chunk 1”的堆块&#xff0c;然后&#xff0c;右击“Chunk 1”&#xff0c;依次选择add、添加照片&#xff1a;   即可弹出照…

踩坑系列 Spring websocket并发发送消息异常

文章目录 示例代码WebSocketConfig配置代码握手拦截器代码业务处理器代码 问题复现原因分析解决方案方案一 加锁同步发送方案二 使用ConcurrentWebSocketSessionDecorator方案三 自研事件驱动队列&#xff08;借鉴 Tomcat&#xff09; 总结 今天刚刚经历了一个坑&#xff0c;非…

云原生之使用Docker部署wordpress网站

云原生之使用Docker部署wordpress网站 一、wordpress介绍二、检查本地docker环境1.检查docker状态2.检查docker版本 三、下载wordpress镜像四、创建数据库1.创建数据目录2.创建mysql数据库容器3.查看mysql容器状态4.远程客户端测试连接数据库 五、部署wordpress1.创建wordpress…

java开发——程序性能的优化方法

java开发——程序性能的优化方法 1、算法逻辑优化2、redis缓存优化3、异步编排4、MQ削峰填谷5、预加载6、sql调优7、jvm调优8、集群搭建 后端开发必须学习的优化思想&#xff01; 1、算法逻辑优化 (1) 什么算法优化&#xff1f; 算法优化的思想就是&#xff0c;对于同一个问题…

ll 内容详解

linux的数据存储是以block&#xff08;块&#xff09;为单位的 &#xff1a; 1个block 4 KB 4096 字节 1KB 1024 字节[rootCTF-RHCSA-2 ~]# ll -sh total 76K &#xff08;列表中 所有文本文件 总共占用磁盘空间的KB大小 &#xff09;&#xff08;root用户家目录中…