【网络编程】实现一个简单多线程版本TCP服务器(附源码)

news2025/1/11 14:52:07

00

TCP多线程

  • 🌵预备知识
    • 🎄 Accept函数
    • 🌲字节序转换函数
    • 🌳listen函数
  • 🌴代码
    • 🌱Log.hpp
    • 🌿Makefile
    • ☘️TCPClient.cc
    • 🍀TCPServer.cc
    • 🎍 util.hpp

🌵预备知识

🎄 Accept函数

accept 函数是在服务器端用于接受客户端连接请求的函数,它在监听套接字上等待客户端的连接,并在有新的连接请求到来时创建一个新的套接字用于与该客户端通信。

  • 下面是 accept 函数的详细介绍以及各个参数的意义:
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd: 是服务器监听套接字的文件描述符,通常是使用 socket 函数创建的套接字。accept 函数在该套接字上等待连接请求。

addr: 是一个指向 struct sockaddr 类型的指针,用于存储客户端的地址信息。当新连接建立成功后,客户端的地址信息将会被填充到这个结构体中。

addrlen: 是一个指向 socklen_t 类型的指针,它指示 addr 结构体的长度。在调用 accept 函数之前,需要将其初始化为 addr 结构体的大小,函数执行后会更新为实际的客户端地址长度。

返回值:如果连接成功建立,accept 函数将返回一个新的文件描述符,该文件描述符用于与客户端进行通信。如果连接失败,函数将返回 -1,并设置 errno 以指示错误原因。

  • accept 函数的工作原理如下:

当服务器的监听套接字接收到一个新的连接请求时,accept 函数会创建一个新的套接字用于与该客户端通信。
新的套接字会继承监听套接字的监听属性,包括 IP 地址、端口等。
accept 函数会填充 addr 结构体,以便获取客户端的地址信息。
服务器可以使用返回的新套接字与客户端进行通信。

  • 注意事项:

accept 函数在没有连接请求时会阻塞,直到有新的连接请求到来。
如果希望设置非阻塞模式,可以使用 fcntl 函数设置 O_NONBLOCK 属性。
在多线程或多进程环境下,需要注意 accept 函数的线程安全性,可以使用互斥锁等机制来保护。
综上所述,accept 函数在构建服务器程序时非常重要,它使服务器能够接受客户端的连接请求并创建新的套接字与客户端进行通信。

🌲字节序转换函数

在网络编程中,字节序问题很重要,因为不同的计算机体系结构可能使用不同的字节序,这可能导致在通信过程中的数据解释错误。为了在不同体系结构之间正确传递数据,需要进行字节序的转换。

  • 以下是一些常用的字节序转换函数:

ntohl 和 htonl: 这些函数用于 32 位整数的字节序转换。ntohl 用于将网络字节序转换为主机字节序,htonl 则相反,将主机字节序转换为网络字节序。

ntohs 和 htons: 这些函数用于 16 位整数的字节序转换。ntohs 用于将网络字节序转换为主机字节序,htons 则相反,将主机字节序转换为网络字节序。

这些函数通常用于在网络编程中处理套接字通信中的数据转换,以确保在不同平台上的正确数据交换。

  • 示例
#include <arpa/inet.h>

int main() {
    uint32_t networkValue = 0x12345678;
    uint32_t hostValue = ntohl(networkValue); // 0x78563412 on a little-endian host
    uint32_t convertedValue = htonl(hostValue); // 0x12345678 on a little-endian host

    uint16_t networkPort = 0x1234;
    uint16_t hostPort = ntohs(networkPort); // 0x3412 on a little-endian host
    uint16_t convertedPort = htons(hostPort); // 0x1234 on a little-endian host

    return 0;
}

请注意,在使用这些函数时,需要包含 <arpa/inet.h> 头文件。这些函数通常在网络编程中用于正确处理字节序问题,以确保不同平台之间的数据传输正确。

🌳listen函数

在TCP通信中,服务端需要使用 listen 函数来监听连接请求。这是因为TCP是一种面向连接的协议,它采用客户端-服务端模型进行通信,通信双方需要先建立连接,然后进行数据的传输。监听的过程是为了等待客户端发起连接请求。

  • 具体原因如下:

建立连接: 在TCP通信中,通信双方需要通过三次握手建立连接。客户端通过 connect 函数向服务器发起连接请求,而服务端则需要通过 listen 函数来准备接收连接请求。

处理并发连接: 服务端可能会同时接收多个客户端的连接请求,而每个连接都需要为其分配一个独立的套接字。通过监听连接请求,服务端可以在一个循环中接受多个连接,为每个连接创建对应的套接字,从而实现并发处理多个客户端。

连接队列: listen 函数将连接请求存储在一个队列中,等待服务端逐个接受。这个队列称为“未完成连接队列”(backlog queue)。如果连接请求过多,超出了队列的长度,那么新的连接请求可能会被拒绝或被丢弃。

连接参数: listen 函数还可以指定一个参数,表示在未完成连接队列中可以容纳的连接请求数量。这个参数可以影响服务端处理并发连接的能力。

总之,TCP监听是为了等待客户端发起连接请求,建立连接,然后实现双方的数据传输。这种机制允许服务器处理多个客户端连接,实现高并发的网络服务。

  • 函数原型:
int listen(int sockfd, int backlog);

  • 参数说明:

sockfd:要进行监听的套接字描述符。
backlog:表示在未完成连接队列中可以容纳的连接请求数量。这个参数可以影响服务器处理并发连接的能力。通常情况下,系统会为这个值设置一个默认的最大值,但你也可以根据你的需求进行适当调整。
返回值:
如果函数调用成功,返回 0。
如果出现错误,返回 -1,并设置全局变量 errno 来指示错误类型。

使用步骤:

创建套接字并绑定地址。
调用 listen 函数将套接字标记为被动套接字,开始监听连接请求。
使用 accept 函数接受客户端连接请求,建立实际的连接。

  • 示例用法
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    if (listen(listen_sock, 5) == -1) { // 开始监听,最多允许5个未完成连接
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 现在可以使用 accept 函数接受连接请求并建立连接

    close(listen_sock);
    return 0;
}

注意:listen 后的套接字仅能用于接受连接请求,不能用于读写数据。接收到的连接请求将在一个队列中等待,直到使用 accept 函数从队列中取出并建立连接。

🌴代码

🌱Log.hpp

#pragma once

#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};

// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);

    char *name = getenv("USER");

    char logInfo[1024];
    va_list ap; // ap -> char*
    va_start(ap, format);

    vsnprintf(logInfo, sizeof(logInfo)-1, format, ap);

    va_end(ap); // ap = NULL


    FILE *out = (level == FATAL) ? stderr:stdout;

    fprintf(out, "%s | %u | %s | %s\n", \
        log_level[level], \
        (unsigned int)time(nullptr),\
        name == nullptr ? "unknow":name,\
        logInfo);

    // char *s = format;
    // while(s){
    //     case '%':
    //         if(*(s+1) == 'd')  int x = va_arg(ap, int);
    //     break;
    // }
}

🌿Makefile

.PHONY:all
all:TCPClient TCPServer

TCPClient: TCPClient.cc
	g++ -o $@ $^ -std=c++11 -lpthread
TCPServer:TCPServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f TCPClient TCPServer

☘️TCPClient.cc

#include"util.hpp"
volatile bool quit=false;
static void Usage(std::string proc)
{
    std::cerr<<"Usage:\n\t"<<proc<<"serverip serverport "<<std::endl;
    std::cerr<<"Example:\n\t"<<proc<<"127.0.0.1 8080\n"<<std::endl;
}

int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    std::string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);
    //1.创建socket SOCK_STREAM
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        std::cerr<<"socket :"<<strerror(errno)<<std::endl;
        exit(SOCKET_ERR);
    }
    //2.链接 
    //向服务器发起链接请求
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(server.sin_port);
    inet_aton(serverip.c_str(),&server.sin_addr);

    //2.2发起请求 connect自动会进行bind
    if(connect(sock,(const struct sockaddr*)&server,sizeof(server))!=0)
    {
        //链接失败
        std::cerr<<"connect :"<<strerror(errno)<<std::endl;
        exit(CONN_ERR);
    }
    //链接成功
    std::cout<<" info :connect success :"<<sock<<std::endl;

    std::string message;


    while(!quit)
    {
        message.clear();
        std::cout<<"请输入您的消息>>>>"<<std::endl;
        std::getline(std::cin,message);
        if(strcasecmp(message.c_str(),"quit")==0)
        {
            //如果输入的是quit 直接退出程序
            quit=true; //设置成true 会把当前信息先执行发送到服务器 再进入while循环时条件不满直接退出

        }

        //从服务器接收到的消息
        ssize_t s=write(sock,message.c_str(),message.size());
        
        if(s>0)
        {
            message.resize(1024);
            ssize_t s=read(sock,(char *)(message.c_str()),1024);
            if(s>0)
                message[s]=0;
            std::cout<<"Server Echo>>>"<<"message"<<std::endl;
        }
         else if (s <= 0)
        {
            break;
        }
    }
    close(sock);
    
    return 0;
}

🍀TCPServer.cc

#include "util.hpp"
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
class ServerTcp;//先声明

class ThreadData
{
    public:
    uint16_t clientPort_;//客户端端口号
    std::string clientip_;//客户端ip
    int sock_;
    ServerTcp *this_;

     ThreadData(uint16_t port, std::string ip, int sock,  ServerTcp *ts)
        : clientPort_(port), clientip_(ip), sock_(sock),this_(ts)
    {}

};


class ServerTcp
{

    public:
    //构造和和析构函数
    ServerTcp(uint16_t port,const std::string &ip=""):port_(port),ip_(ip),listenSock_(-1)
    {}
    ~ServerTcp()
    {}
    


    public:
    //初始化函数
    void init()
    {

        //第一步:创建套接字
        listenSock_=socket(PF_INET,SOCK_STREAM,0);

        if(listenSock_<0)
        {
            //创建失败
            logMessage(FATAL,"socket:%s",strerror(errno)); //用日志打印错误信息
            exit(SOCKET_ERR);
        }
        //创建成功
        logMessage(DEBUG,"sockt:%s,%d",strerror(errno),listenSock_);

        //第二步 bind绑定
        //2.1填充服务器信息

        struct sockaddr_in local;
        memset(&local,0,sizeof(local));//设置0?
        /*可以确保将所有这些字段初始化为零,以避免在实际使用过程中出现未定义行为或不可预测的结果。*/
        local.sin_family=AF_INET;   
        /*如果 ip_ 为空,服务器将绑定到任意可用的本地IP地址。如果 ip_ 不为空,服务器将绑定到 ip_ 所代表的具体IP地址。*/
        ip_.empty()?(local.sin_addr.s_addr)=htons(INADDR_ANY):(inet_aton(ip_.c_str(),&local.sin_addr));
        
        //2.2
        if(bind(listenSock_,(const struct sockaddr*)&local,sizeof local)<0)//
        {
            //bind绑定失败
            logMessage(FATAL,"bind:%s",strerror(errno));
            exit(BIND_ERR);
        }

        //绑定成功
        logMessage(DEBUG,"bind:%S,%d",strerror(errno),listenSock_);
        //3.监听socket
        if(listen(listenSock_,5)<0)
        {
            logMessage(FATAL,"listen:%s",strerror(errno));
            exit(LISTEN_ERR);
        }
        //监听成功
        logMessage(DEBUG,"listen:%S,%d",strerror(errno),listenSock_);
        //到这一步就等待运行 等待客户端链接
 
    }

    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self()); //设置线程分离
        ThreadData *td = static_cast<ThreadData*>(args);
        td->this_->tranService(td->sock_, td->clientip_, td->clientPort_);
        delete td;
        return nullptr;
    }
    
    //加载
    void loop()
    {
        while(true)
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            //获取链接 accept返回值??
            int serviceSock=accept(listenSock_,(struct sockaddr*)&peer,&len);
            if(serviceSock<0)
            {
                //获取连接失败
                logMessage(WARINING,"Accept :%S[%d]",strerror(errno),serviceSock);
                continue;//获取失败 继续接收....
            }

            //获取客户端的基本信息 存储起来
             uint16_t peerPort=ntohs(peer.sin_port);
             std::string peerip=inet_ntoa(peer.sin_addr);
            //打印一下获取的客户端信息
             logMessage(DEBUG,"Aceept :%s|%s[%d],socket fd :%d",strerror(errno),peerip.c_str(),peerPort,serviceSock);


               // 5 提供服务, echo -> 小写 -> 大写
            // 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept
            // transService(serviceSock, peerIp, peerPort);
            
            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0)
            // {
            //     close(listenSock_); //建议
            //     //子进程
            //     transService(serviceSock, peerIp, peerPort);
            //     exit(0); // 进入僵尸
            // }
            // // 父进程
            // close(serviceSock); //这一步是一定要做的!

            // 5.1 v1.1 版本 -- 多进程版本  -- 也是可以的
            // 爷爷进程
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     // 爸爸进程
            //     close(listenSock_);//建议
            //     // 又进行了一次fork,让 爸爸进程
            //     if(fork() > 0) exit(0);
            //     // 孙子进程 -- 就没有爸爸 -- 孤儿进程 -- 被系统领养 -- 回收问题就交给了系统来回收
            //     transService(serviceSock, peerIp, peerPort);
            //     exit(0);
            // }
            // // 父进程
            // close(serviceSock); //这一步是一定要做的!
            // // 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态
            // pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式
            // assert(ret > 0);
            // (void)ret;

            // 5.2 v2 版本 -- 多线程
            // 这里不需要进行关闭文件描述符吗??不需要啦
            // 多线程是会共享文件描述符表的!
            ThreadData *td = new ThreadData(peerPort, peerip, serviceSock, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, threadRoutine, (void*)td);

            // waitpid(); 默认是阻塞等待!WNOHANG
            // 方案1

            // logMessage(DEBUG, "server 提供 service start ...");
            // sleep(1);
        }

    }


     //提供服务函数 -----> 大小写转换
     void tranService(int sock,const std::string &clientip,uint16_t clientPort)
     {
            assert(sock>=0);
            assert(!clientip.empty());
            assert(clientPort>=1024); //1~~1024端口为系统端口 不可轻易更改
            char inbuffer[BUFFER_SIZE];
            while(true)
            {
                ssize_t s=read(sock,inbuffer,sizeof(inbuffer)-1); //-1是给\0留出一个位置
                if(s>0)
                {
                    inbuffer[s]='0';
                    if(strcasecmp(inbuffer,"quit")==0)
                    {
                        logMessage(DEBUG,"client quit----------%s[%d]",clientip.c_str(),clientPort);
                        break;
                    }
                    logMessage(DEBUG,"Treans Before:%s[%d]>>>%s",clientip.c_str(),clientPort,inbuffer);
                    //进行大小写转换
                    for(int i=0;i<s;i++)
                    {
                        if(isalpha(inbuffer[i])&&islower(inbuffer[i]))
                        {
                            inbuffer[i]=toupper(inbuffer[i]);
                        }
                    }
                        logMessage(DEBUG,"Trans after:%s[%d]>>>>%s",clientip.c_str(),clientPort,inbuffer);
                
                        write(sock,inbuffer,strlen(inbuffer));//给客户端发送回去
                }
                    else if(s==0)
                    {
                    // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
                     // s == 0: 代表对方关闭,client 退出
                    logMessage(DEBUG, "client quit -- %s[%d]", clientip.c_str(), clientPort);
                    break;
                    }
                    else
                     {
                    logMessage(DEBUG, "%s[%d] - read: %s", clientip.c_str(), clientPort, strerror(errno));
                    break;
                     }
 
                }
                // 只要走到这里,一定是client退出了,服务到此结束
        close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
        logMessage(DEBUG, "server close %d done", sock);
            }
          
    private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
};



static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n" << std::endl;

}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if(argc != 2 && argc != 3 )
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    std::string ip;
    if(argc == 3) ip = argv[2];

    ServerTcp svr(port, ip);
    svr.init();
    svr.loop();
    return 0;
}

🎍 util.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

#define SOCKET_ERR 1
#define BIND_ERR   2
#define LISTEN_ERR 3
#define USAGE_ERR  4
#define CONN_ERR   5

#define BUFFER_SIZE 1024

大家可以拉下来自行测试…

🎋 🍃 🍂 🍁 🍄 🐚 💐 🌷 🌹 🥀 🌺 🌸 🌼 🌻

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

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

相关文章

未来C#上位机软件发展趋势

C#上位机软件迎来新的发展机遇。随着工业自动化的快速发展&#xff0c;C#作为一种流行的编程语言在上位机软件领域发挥着重要作用。未来&#xff0c;C#上位机软件可能会朝着以下几个方向发展&#xff1a; 1.智能化&#xff1a;随着人工智能技术的不断演进&#xff0c;C#上位机…

数据结构--最小生成树

数据结构–最小生成树 连通图 \color{red}连通图 连通图的生成树是 包含图中全部顶点的一个极小连通子图 \color{red}包含图中全部顶点的一个极小连通子图 包含图中全部顶点的一个极小连通子图。 若图中顶点数为n&#xff0c;则它的生成树含有 n-1 条边。对生成树而言&#xff…

RFID技术助力半导体制造行业自动化生产

由于芯片短缺问题和近2年海运拥堵和成本上升等因素&#xff0c;致使全球资本对于芯片制造工厂的投入增大&#xff0c;而中兴、华为的例子已经凸显出国产半导体供应链的重要性&#xff0c;除去地缘政治上的意义&#xff0c;发展半导体其实是中国经济的转型的必走之路。 半导体生…

Vue2(生命周期,列表排序,计算属性和监听器)

目录 前言一&#xff0c;生命周期1.1&#xff0c;生命周期函数简介1.2&#xff0c;Vue的初始化流程1.3,Vue的更新流程1.4&#xff0c; Vue的销毁流程1.5&#xff0c; 回顾生命周期1.,6&#xff0c;代码演示1.6-1&#xff0c;beforeCreate1.6-2&#xff0c;created1.6-3&#xf…

雨水旋流过滤器、旋流雨水过滤器、水力旋流雨水过滤器、旋流分离器、旋流沉砂一体机、旋流沉砂井

产品组成 主要材料&#xff1a;PE304不锈钢 组成&#xff1a;过滤器筒体、桶盖、截止阀&#xff08;选配&#xff09;、不锈钢滤网 工作原理 雨水由过滤器进水口进入时&#xff0c;水流被引导沿过滤器内壁切线方向进入筒体。在水力、重力等作用下&#xff0c;形成雨水紧贴过…

【基础类】—前端算法类

一、排序 1. 排序方法列表 2. 常见排序方法 快速排序选择排序希尔排序 二、堆栈、队列、链表 堆栈、队列、链表 三、递归 递归 四、波兰式和逆波兰式 理论源码

Unity之ShaderGraph 节点介绍 Utility节点

Utility 逻辑All&#xff08;所有分量都不为零&#xff0c;返回 true&#xff09;Any&#xff08;任何分量不为零&#xff0c;返回 true&#xff09;And&#xff08;A 和 B 均为 true&#xff09;Branch&#xff08;动态分支&#xff09;Comparison&#xff08;两个输入值 A 和…

wm8960没有声音

最近在imx6ull上调试这个声卡&#xff0c;用官方的镜像是能发声的&#xff0c;换到自己做的镜像上&#xff0c;就没有声音。 记录一下过程&#xff1a; 内核和设备树。只要有下面的显示&#xff0c;就说明加载成功。 再看一下aplay的显示 到此&#xff0c;驱动都是正常的。但…

每天一道leetcode:剑指 Offer 32 - II. 从上到下打印二叉树 II(适合初学者)

今日份题目&#xff1a; 从上到下按层打印二叉树&#xff0c;同一层的节点按从左到右的顺序打印&#xff0c;每一层打印到一行。 示例 例如: 给定二叉树: [3,9,20,null,null,15,7], 3 / \ 9 20 / \ 15 7 返回其层次遍历结果&#xff1a; [ [3], […

selenium常见等待机制及其特点和使用方法

目录 1、强制等待 2、隐式等待 3、显式等待 1、强制等待 强制等待是在程序中直接调用Thread.sleep(timeout) ,来完成的&#xff0c;该用法的优点是使用起来方便&#xff0c;语法也比较简单&#xff0c;缺点就是需要强制等待固定的时间&#xff0c;可能会造成测试的时间过…

APP外包开发的android开发模式

开发 Android 应用有多种方法&#xff0c;每种方法都有其优势和适用场景。综合考虑各自的特点&#xff0c;你可以根据项目的需求和团队的技能选择最合适的开发方法。今天和大家分享几种常见的开发方法以及它们之间的对比&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公…

探索泛型与数据结构:解锁高效编程之道

文章目录 引言第一部分&#xff1a;了解泛型1.1 为什么使用泛型1.2 使用泛型的好处 第二部分&#xff1a;泛型的使用场景2.1 类的泛型2.2 方法的泛型2.3 接口的泛型 第三部分&#xff1a;泛型通配符3.1 通配符3.2 通配符的受限泛型 第四部分&#xff1a;数据结构和泛型的应用4.…

边缘计算框架 Baetyl v2.4.3 正式发布

导读Baetyl v2.4.3 版本已经发布&#xff0c;对 v2.3.0 版本的部分功能进行了升级优化。公告称&#xff0c;这些新功能继续遵循云原生理念&#xff0c;构建了一个开放、安全、可扩展、可控制的智能边缘计算平台。 Baetyl 项目由百度发起&#xff0c;基于百度天工 AIoT 智能边缘…

【LeetCode】数据结构题解(9)[复制带随机指针的链表]

复制带随机指针的链表 &#x1f609; 1.题目来源&#x1f440;2.题目描述&#x1f914;3.解题思路&#x1f973;4.代码展示 所属专栏&#xff1a;玩转数据结构题型❤️ &#x1f680; >博主首页&#xff1a;初阳785❤️ &#x1f680; >代码托管&#xff1a;chuyang785❤…

使用Pytest集成Allure生成漂亮的图形测试报告

目录 前言 依赖包安装 Pytest Allure Pytest Adaptor 改造基于Pytest的测试用例 生成测试报告 运行测试 生成测试报告 打开测试报告 资料获取方法 前言 之前写过一篇生成测试报告的博客&#xff0c;但是其实Allure首先是一个可以独立运行的测试报告生成框架&#xff…

(杭电多校)2023“钉耙编程”中国大学生算法设计超级联赛(6)

1001 Count 当k在区间(1n)/2的左边时,如图,[1,k]和[n-k1,n]完全相同,所以就m^(n-k) 当k在区间(1n)/2的右边时,如图,[1,n-k1]和[k,n]完全相同,所以也是m^(n-k) 别忘了特判,当k等于n时,n-k为0,然后a1a1,a2a2,..anan,所以没什么限制,那么就是m^n AC代码&#xff1a; #includ…

Vue3 大屏数字滚动效果

父组件&#xff1a; <template> <div class"homePage"> <NumRoll v-for"(v, i) in numberList" :key"i" :number"v"></NumRoll> </div> </template> <script setup> import { onMounted, r…

【Vue3】动态组件

动态组件的基本使用 动态组件&#xff08;Dynamic Components&#xff09;是一种在 Vue 中根据条件或用户输入来动态渲染不同组件的技术。 在 Vue 中使用动态组件&#xff0c;可以使用 元素&#xff0c;并通过 is 特性绑定一个组件的名称或组件对象。通过在父组件中改变 is 特…

C++ 虚析构函数

在C中&#xff0c;不能声明虚构造函数&#xff0c;但是可以声明虚析构函数。 析构函数没有类型&#xff0c;也没有参数&#xff0c;和普通成员函数相比&#xff0c;虚析构函数情况很简单。 虚析构函数的声明语法&#xff1a; virtual ~类名();如果一个类的析构函数是虚函数&…

Python自动化测试用例:如何优雅的完成Json格式数据断言

目录 前言 直接使用 优化 封装 小结 进阶 总结 资料获取方法 前言 记录Json断言在工作中的应用进阶。 直接使用 很早以前写过一篇博客&#xff0c;记录当时获取一个多级json中指定key的数据&#xff1a; #! /usr/bin/python # coding:utf-8 """ aut…