【Linux】来写一个tcp的服务端+客户端

news2025/1/14 20:34:20

本文首发于 慕雪的寒舍

今天让我们来写一个tcp的服务器/客户端代码。

完整代码见我的gitee 链接

阅读本文前,建议先阅读👉 udp服务器

由于本文采用自建图床,CSDN可能因带宽不够,出现外链图片缓存失败。

1.基本框架

tcp的服务器和udp服务器初始化接口是非常相似的,区别就在于要选择字节流进行初始化

但是到运行状态就不同了

  • tcp是需要连接的
  • udp不需要连接

所以就会出现分歧:udp可以用sendto和receve来发送/接收信息,服务端只需要监听特定端口收到了什么信息;

但tcp并不能这么做,在通信之前,tcp服务器必须要和客户端建立链接。

举个不恰当的例子,udp服务器好比一个水盆,等待水的注入;而tcp服务器是个水管,必须要两头连通了,才能开始注水

1.1 类成员

类的成员变量和udp很相似,都是需要服务器的ip、端口、sockfd这些信息。为了更容易区分,将tcp服务器的socket fd改为_listenSock,意为监听端口

class TcpServer{


private:
    // 服务器端口号
    uint16_t _port;
    // 服务器ip地址
    string _ip;
    // 服务器socket fd信息
    int _listenSock;
};

1.2 头文件

这里对头文件进行一定的说明,因为服务器代码中的头文件实在太多了

当你需要使用一个接口的时候,可以去采用man手册来获取该接口的头文件信息

#pragma once
// 头文件太多了,所以新起一个文件
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>//判断字符串大写小写接口需要的库
#include <unistd.h> 
#include <strings.h>// 忽略大小写比较strcasecmp
#include <sys/types.h>  //很多liunx系统接口都需要这个
#include <sys/socket.h> // 网络
#include <netinet/in.h> // 网络
#include <arpa/inet.h> // 网络
using namespace std;

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

#define BUFFER_SIZE 1024

2.初始化

接口的介绍就跟随实现一步一步来吧

2.1 构造sock

这里出现了tcp和udp第一个不同之处,tcp是面向字节流的,udp面向的是数据报

    TcpServer(uint16_t port,const string& ip="")
     :_port(port), _ip(ip), _listenSock(-1)
    {
        // 1.创建socket套接字,采用字节流(即tcp)
        _listenSock = socket(AF_INET, SOCK_STREAM, 0); //本质是打开了一个文件
        if (_listenSock < 0)
        {
            logging(FATAL, "socket:%s:%d", strerror(errno), _listenSock);
            exit(1);
        }
        logging(DEBUG, "socket create success: %d", _listenSock);
    }

2.2 初始化sockaddr_in

继续,初始化sockaddr_in的操作和udp是完全一致的

// 2. 绑定网络信息,指明ip+port
// 2.1 先填充基本信息到 struct sockaddr_in
struct sockaddr_in local;
memset(&local,0,sizeof(local));//初始化
// 协议家族,设置为ipv4
local.sin_family = AF_INET; 
// 端口,需要进行 本地->网络转换
local.sin_port = htons(_port);
// 配置ip
// 如果初始化时候的ip为空,则调用INADDR_ANY代表任意ip。否则对传入的ip进行转换后赋值
local.sin_addr.s_addr = _ip.empty() ? htonl(INADDR_ANY) : inet_addr(_ip.c_str());

2.3 bind

也是一样,绑定服务器的ip和端口

// 2.2 绑定ip端口
if (bind(_listenSock,(const struct sockaddr *)&local, sizeof(local)) == -1)
{
    logging(FATAL, "bind: %s:%d", strerror(errno), _listenSock);
    exit(2);
}
logging(DEBUG,"socket bind success: %d", _listenSock);

2.4 监听listen

对于tcp服务器来说,成员变量的_listenSock是用来监听的,即找个老哥一直盯着云服务器的这个端口,看看有没有需要连接它的客户端

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);

其中第一个参数是我们的_listenSock,第二个参数是用于限制在阻塞等待连接的数量

The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow.   If  a connection  request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt  at  con‐nection succeeds.

翻译过来就是,backlog参数限制了能被阻塞等待连接的数量。如果超过这个数量,则会返回一个ECONNREFUSED错误。亦或者如果协议支持重传,多余的请求会被忽略,后续可以重传

man手册下面的notes还有更多解释

    The behavior of the backlog argument on TCP sockets changed with Linux 2.2.  Now it specifies  the  queue  length  for completely  established  sockets waiting to be accepted, instead of the number of incomplete connection requests.  The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog.  When syn‐cookies are enabled there is no logical maximum length and this setting is ignored.  See tcp(7) for more information.

     If  the  backlog  argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128.  In kernels before 2.4.25, this  limit  was  a  hard  coded  value,SOMAXCONN, with the value 128.

如果backlog参数高于/proc/sys/net/core/somaxconn中的默认值128,则会被截断为128


在我们这里,将其设置为5即可,反正也是做测试嘛,问题不大

        // 3.监听
        // tcp服务器是需要连接的,连接之前要先监听有没有人来连
        if (listen(_listenSock, 5) < 0)
        {
            logging(FATAL, "listen: %s", strerror(errno));
            exit(LISTEN_ERR);
        }
        logging(DEBUG, "listen: %s, %d", strerror(errno), _listenSock);

3.运行

初始化到这就完毕了,下面就是开跑了

3.1 accept

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

这个接口的作用就相当于tcp中的recevefrom,传参是完全相同的;与之不同的是,该函数的返回值是一个全新的sockfd

  • tcp需要和客户端建立链接
  • 链接需要用socket fd 来管理
  • 所以accept必须返回新的socket fd,让服务端有办法管理新的链接和已有链接
  • 原有的socket fd不受影响
  • 如果没有客户端来连接,进程会在accept内阻塞等待

下为man手册中的描述

The  accept()  system  call  is  used  with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET).  It extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected  socket,  and  returns  a  new  file descriptor referring to that socket.  The newly created socket is not in the listening state.  The original socket sockfd is unaffected by this call.

举个例子,tcp服务器自身的socket fd只会用来监听端口上有没有消息,当监听到有消息并通过accept建立连接后,就会让另外一位服务员来对这个连接提供服务。

while(1)
{
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    // 获取连接
    int conet = accept(_listenSock,(struct sockaddr*)&peer,&len);
    if(conet<0)
    {
        logging(FATAL, "accept: %s", strerror(errno));
        exit(CONN_ERR);//连接错误
    }
    //。。。
}

注意这里len的参数是socklen_t,其本质上是一个无符号整形

typedef __socklen_t socklen_t;
__STD_TYPE __U32_TYPE __socklen_t;
#define __U32_TYPE		unsigned int

3.2 获取连接信息

这部分和udp是完全相同的,通过accept返回的socket fd,获取用户的ip和端口耨

// 获取连接信息
string senderIP = inet_ntoa(peer.sin_addr);// 来源ip
uint16_t senderPort = ntohs(peer.sin_port); // 来源端口
logging(DEBUG, "accept: %s | %s[%d], socket fd: %d", strerror(errno), senderIP.c_str(), senderPort, conet);

其实到这里,我们就可以运行服务器进行测试了。因为tcp的特性,我们不需要写客户端,直接用浏览器就能连上服务端

int main(int argc,char* argv[])
{
    //参数只有两个(端口/ip)所以参数个数应该是2-3
    if(argc!=2 && argc!=3)
    {
        cout << "Usage: " << argv[0] << " port [ip]" << endl;
        return 1;
    }
    
    string ip;
    // 3个参数,有ip
    if(argc==3)
    {
        ip = argv[2];
    }
    TcpServer t(atoi(argv[1]),ip);
    t.start();

    return 0;
}

先编译后执行代码,让tcp服务器运行起来

image-20230207081805916

随后在浏览器的地址栏输入公网ip:端口(先要开启防火墙内的端口)

image-20230207081754768

此时会发现什么都加载不出来,这是对的,因为我们并没有写前端,也没有提供任何服务。但是来到后台,可以看到出现了一个新的连接,并显示出了ip+端口

image-20230207082138291

3.3 提供服务(线程)

接下来要做的,就是写一个简单的服务了,这里我写的是字符串转ASCII码,会将发出去的字符串的ascii码加加起来后返回

3.3.1 问题1 如何通信

此时问题就来了,tcp服务器不能使用recevefrom和sendto,那么获取到socket之后要怎么进行通信呢?

答案是:用linux的文件读写接口,read和write。别忘了,socket fd本质上就是一个linux下的文件描述符!

3.3.2 问题2 多客户端

tcp服务器要想给多个客户端提供服务,就必须采用多线程/多进程的方式来实现操作。否则会出现一个严重的问题,服务端因为提供服务而没有accept,无法链接上下一个客户端

while(1)
{
    //accept 获取到链接上的客户端
    
    while(1)
    {
         //如果在这里提供服务,则会其他连接会在listen里面阻塞
    	//只有当前服务终止了,其他客户端的其中之一才能连上服务器
    }
}

3.3.3 问题3 线程传参

既然需要采用多线程服务,那就需要设定好给线程传的参数。理论上来说,我们只需要传入accept的返回值socket fd即可进行read/write

但实际上,我们还需要打印debug消息,要知道当前是谁向你发送了这条消息,ip和端口是什么。

为了方便操作,这里封装一个结构体,将socket fd,客户端的ip+端口封装成一个参数进行传参(线程的函数只能传入一个参数

struct ClientData
{
    int _fd;
    uint16_t _port;
    string _ip;
    TcpServer* _this;

    ClientData(int fd,uint16_t port,const string& ip,TcpServer* this1)
        :_fd(fd),_port(port), _ip(ip),_this(this1)
    {}
};

你可能会想到另外一个办法,那就是在tcp服务器的class中新增一个map成员变量,用于映射socket fd和客户端信息的键值对,但是这无法实现。

因为在类中设计的多线程函数,为了去掉默认传入的this指针,必须要设置成static静态的,此时该静态函数无法访问类内成员!

    // 因为需要取消this指针,所以需要设置成静态的
    static void* threadRoutine(void*args)
    {   
        pthread_detach(pthread_self()); //设置线程分离
        ClientData* data=(ClientData*)args;
        // 通过预先设置的this指针来访问类内成员,并进行传参
        data->_this->transService(data->_fd,data->_ip,data->_port);
        delete data;
        return nullptr;
    }

3.3.4 服务代码

解决了上面的问题,就可以继续往下看看服务端的代码了

// 提供服务
pthread_t service;
// 因为这个成员使用范围极小,所以采用new/delete,避免占用太多空间
ClientData* data = new ClientData(conet,senderPort,senderIP,this);
pthread_create(&service,nullptr,threadRoutine,(void*)data);

在accept之后,通过线程操作用线程来提供服务

    static void* threadRoutine(void*args)
    {   
        pthread_detach(pthread_self()); //设置线程分离
        ClientData* data=(ClientData*)args;
        // 通过预先设置的this指针来访问类内成员,并进行传参
        data->_this->transService(data->_fd,data->_ip,data->_port);
        delete data;
        return nullptr;
    }

threadRoutine的作用就是把线程的单参数转为多参数,传给真正用来服务的函数。函数的操作很简单,就是Linux下文件操作的读写。

读写成功后,将客户端发来的信息转成ASCII码的和发回给客户端

void transService(int sockfd, const string &clientIp, uint16_t clientPort)
{
    assert(sockfd >= 0);
    assert(!clientIp.empty());
    assert(clientPort>0);
    // 开始服务
    char buf[BUFFER_SIZE];
    while(1)
    {
        // 读取客户端发来的信息,s是读取到的字节数
        ssize_t s = read(sockfd, buf, sizeof(buf)-1);
        if(s>0)
        {
            buf[s]='\0';//手动添加字符串终止符
            if(strcasecmp(buf,"quit")==0)
            {//客户端主动退出
                break;
            }
            // 服务
            string tmp = buf;
            int ret = str2ascii(tmp);//获取字符串的ascii总和
            cout << ret << endl;
            string retS =  to_string(ret);//转字符串
            cout << retS << endl;
            write(sockfd,retS.c_str(),retS.size());//写入
        }
        else if (s == 0)
        {//s == 0代表对方关闭,客户端退出
            logging(DEBUG, "client quit: %s[%d]", clientIp.c_str(), clientPort);
            break;
        }
        else
        {
            logging(DEBUG, "read err: %s[%d] -  %s", clientIp.c_str(), clientPort, strerror(errno));
            continue;
        }
    }
    close(sockfd);
    logging(DEBUG,"server quit %d",sockfd);
}

private:
    // 服务函数可以不暴露
    int str2ascii(const string& str)
    {
        int ret = 0;
        for(auto e:str)
        {
            ret += e;
        }
        return ret;
    }

3.4 提供服务(子进程)

上面的代码采用的是线程来提供服务,除了线程,我们还有父子进程的方式,也能避免阻塞

需要注意的是,父子进程都需要关闭掉对方使用的文件描述符,避免出现文件描述符在服务结束后还没有关闭的情况

pid_t id = fork();
if(id == 0)
{
    close(_listenSock);//因为子进程不需要监听,所以关闭掉监听socket
    //子进程
    transService(conet, senderIP, senderPort);
    exit(0);// 服务结束后,退出,子进程会进入僵尸状态等待父进程回收
}
// 父进程
close(conet); // 因为此时是子进程提供服务,conet会有拷贝,相当于有两个进程打开了该文件
// 如果父进程不关闭,即便子进程结束服务了,该文件描述符也会保持开启
pid_t ret = waitpid(id, nullptr, 0);

直接这样写有一个很大的问题,那就是父进程没有办法正常释放子进程的资源

  • 如果进行阻塞等待,那就违背了初衷,完全没有意义
  • 如果进行非阻塞等待,在waitpid结束之后,父进程直接去干其他事了,完全忘记了这里的这个子进程

所以我们要做的,就是在子进程退出,向父进程发送信号的时候回收子进程

3.4.1 信号回收

signal(SIGCHLD, FreeChild);//自定义捕捉
// 回收子进程
void FreeChild(int signo)
{
    assert(signo == SIGCHLD);
    while (true)
    {
        //如果没有子进程了,waitpid就会调用失败
        pid_t id = waitpid(-1, nullptr, WNOHANG); // 非阻塞等待
        if (id > 0)
        {
            cout << "父进程等待成功, child pid: " << id << endl;
        }
        else if(id == 0)
        {
            //还有子进程没有退出
            cout << "尚有未退出的子进程,父进程继续运行" << endl;
            break;//退出等待子进程
        }
        else
        {
            cout << "父进程等待所有子进程结束" << endl;
            break;
        }
    }
}

除了自定义捕捉,我们还可以设置成ignore不搭理子进程,这样子进程退出的时候就会被系统自动释放

signal(SIGCHLD, SIG_IGN);

3.4.2 爷爷进程

这里还有另外一个骚操作,那就是在创建子进程之后,再创建一个子进程

// 提供服务(孙子进程)-2
pid_t id = fork();
if(id == 0)
{
    close(_listenSock);//因为子进程不需要监听,所以关闭掉监听socket
    //又创建一个子进程,大于0代表是父进程,即创建完子进程后父进程直接退出
    if(fork()>0){
        exit(0);
    }

    // 孙子进程执行
    transService(conet, senderIP, senderPort);
    exit(0);// 服务结束后,退出,子进程会进入僵尸状态等待父进程回收
}
// 爷爷进程
close(conet); 
pid_t ret = waitpid(id, nullptr, 0); //此时就可以直接用阻塞式等待了
assert(ret > 0);//ret如果不大于0,则代表等待发生了错误

采用这种办法以后,由于父进程退出了,孙子进程会直接被操作系统接管。下图中能看到这些进程的父进程都是1,即操作系统。这时候我们也不需要担心子进程的释放问题了

image-20230207111205871

4.客户端

客户端部分的代码和udp也很相似,只不过将sendto改成了write

下方提供了客户端的代码,都写了注释😁

#include "utils.h"


// ./clientTcp serverIp serverPort
int main(int argc, char *argv[])
{
    if (argc != 3)//客户端必须要有3个参数
    {
        cerr << "Usage:\n\t" << argv[0] << " serverIp serverPort" << endl;
        cerr << "Example:\n\t" << argv[0] << " 127.0.0.1 8080\n"
                << endl;
        exit(USAGE_ERR);
    }
    // 解析服务端的ip和端口
    string serverIp = argv[1];
    uint16_t serverPort = atoi(argv[2]);

    // 1. 创建tcp的socket SOCK_STREAM
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        cerr << "socket: " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }

    // 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽
    // 2.1 先填充需要连接的远端主机的基本信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    inet_aton(serverIp.c_str(), &server.sin_addr);
    // 2.2 发起请求,connect 会自动bind
    if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
    {
        cerr << "connect: " << strerror(errno) << endl;
        exit(CONN_ERR);
    }
    cout << "connect success: " << sock << endl;

    // 客户端发现的消息
    string message;
    while (1)
    {
        message.clear();//每次循环开始,都清空一下msg
        cout << "请输入你的消息# ";
        getline(cin, message);//获取输入
        // 如果客户端输入了quit,则退出
        if (strcasecmp(message.c_str(), "quit") == 0)
            break;
        // 向服务端发送消息
        ssize_t s = write(sock, message.c_str(), message.size());
        if (s > 0) // 写入成功
        {
            message.clear();//清空输入的消息
            message.resize(BUFFER_SIZE);
            // 因为string的c_str本质上是返回地址,所以强转后是可以往里面写入的
            s = read(sock, (char *)(message.c_str()), BUFFER_SIZE);// 获取服务端的结果
            if (s > 0)// 读取成功
            {
                message[s] = '\0';//追加\0
            }
            // 打印返回值
            cout << "Server Echo# " << message << endl;
        }
        else if (s <= 0) // 写入失败
        {
            break;
        }
    }
    // 关闭文件描述符
    close(sock);
    return 0;
}

4.1 运行测试

先运行服务端,再运行客户端,客户端输入后,服务短会返回字符串的ascii码总和

image-20230207095713615

而客户端输入quit后,在服务端可以看到客户端退出,但服务端并没有推出,正在等待下一个连接

image-20230207095848863

5.线程池

在上面的操作中,每次提供服务都需要当场新建一个线程。对于tcp这种要求高性能的网络服务器而言,其实是不太合适的。理论上来说,我们希望越早给客户建立联系越好,而不是食客都来了老板才去买菜。

这时候,就可以把我们写的线程池和tcp服务器给联系起来!

线程池的代码见我的gitee,此处只说明task类的编写

5.1 task

先前编写线程池的时候,将线程池要处理的任务写成了一个task类,并规定所有task类都需要提供一个()操作符重载,即仿函数。这样线程池就可以一视同仁的处理这些工作,我们只需要将新增的工作添加到线程池的任务队列里面

class Task
{
    using callback_t = std::function<void (int, std::string, uint16_t)>;//相当于typedef
public:
    Task() = default;
    // 将需要调用的函数传入,相当于通用
    Task(int sockfd, const std::string &clientIP, uint16_t clientPort,callback_t func)
        : _sockfd(sockfd), _ip(clientIP), _port(clientPort),_func(func)
    {
    }
    // 仿函数
    void operator()()
    {
        logging(DEBUG, "TID[%p] = %s:%d START",\
            pthread_self(), _ip.c_str(), _port);

        _func(_sockfd, _ip, _port);

        logging(DEBUG, "TID[%p] = %s:%d END  ",\
            pthread_self(), _ip.c_str(), _port);
    }

private:
    int _sockfd;
    std::string _ip;
    uint16_t _port;
    callback_t _func;
};

这里,我又将task队列给封装成了一个可以接收函数指针的方式。这样一来,只要我们任务的函数参数为(SOCKET,IP,PROT),就能传入给这个task类,让线程池来运行

5.2 tcpServer的处理

因为需要线程池,我们在tcpserver中添加一个线程池的指针,通过线程池的类名来获取单例,赋值给成员变量。并让获取到的单例线程池开始运行

        // 4.获取线程池 单例
        _tpool = ThreadPool<Task>::getInstance(4);
        _tpool->start();//开始运行

start函数中,则将之前的任务函数实例化为一个task,并将其push到线程池的任务队列中

// 提供服务(通过线程池)
Task t(conet,senderIP,senderPort,transService);
_tpool->push(t);

这样,就能通过线程池来提供服务了!

5.3 运行测试

可以看到,我们的线程池正确运行了任务,给客户端提供了ascii返回值

image-20230209094304860

使用ps -aL命令查看轻量级进程,可以看到有4个线程在为我们服务

  PID   LWP TTY          TIME CMD
 7288  7288 pts/8    00:00:00 tcpServer
 7288  7289 pts/8    00:00:00 handler
 7288  7290 pts/8    00:00:00 handler
 7288  7291 pts/8    00:00:00 handler
 7288  7292 pts/8    00:00:00 handler
 7441  7441 pts/9    00:00:00 tcpClient
 7632  7632 pts/7    00:00:00 ps

此时,即便我们多开几个终端,tcp服务器也能正常提供服务

image-20230209094632613

但是!如果出现了一个尴尬的情况,线程池中的线程<当前需要连接的客户端数量,会发生什么呢?

5.4 task等待问题

为了测试,我们将线程池单例中的线程个数初始化为2个

image-20230209094741651

此时,我们发现第三个客户端会进入阻塞状态,但实际上它已经成功链接上了服务器,task也被插入到了任务队列里面,只不过当前没有空闲的线程来运行它

image-20230209095249694

如果我们把左侧其中一个客户端退出,最右侧的客户端就能正常收到服务器返回的结果了

image-20230209095429982

要解决这个问题,我们就需要让线程池有能力判断是否出现了阻塞问题,并扩容线程来解决阻塞。

可是这又引出了另外一个问题:如果空闲了很久都没有任务过来,多出来的线程不就是在白白消耗资源吗?

实际上,线程池适合处理的,应该是短小的任务,而不是一个while(1)循环;

但是我还没有学习到如何将其修改为服务于短小任务的线程池,仍待后续的精进

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

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

相关文章

walt 调度算法

Walt 算法 WALT负载统计原理_walt算法_森森浅浅笙笙的博客-CSDN博客 CPU负载均衡之WALT学习【转】_mb5fdcad0be2e90的技术博客_51CTO博客 1、A task’s demand is the maximum of its contribution to the most recently completed window and its average demand over the p…

webgl-attribute、uniform、varying三者的区别

通用js: let canvas document.getElementById(webgl) canvas.width window.innerWidth canvas.height window.innerHeight let ctx canvas.getContext(webgl) attribute&#xff1a; 范围: 只适用于vertexShader&#xff0c;将js代码中的数据传递给vertexShader。 使用方…

知识图谱扩充|蜕变测试|蜕变关系

目录 前言&#xff1a;概念定义 什么是蜕变测试&#xff1f; 那么&#xff0c;怎么进行蜕变测试呢&#xff1f; 1. 生成蜕变关系 a 等价关系 b 混排关系 c 交集关系 d 并集关系 2. 生成蜕变用例 3. 执行蜕变用例 4. 校验蜕变关系 学术报告 一、蜕变测试MT 二、蜕变…

【id:32】【20分】B. Date(类与构造)

题目描述 下面是一个日期类的定义&#xff0c;请在类外实现其所有的方法&#xff0c;并在主函数中生成对象测试之。 注意&#xff0c;在判断明天日期时&#xff0c;要加入跨月、跨年、闰年的判断 例如9.月30日的明天是10月1日&#xff0c;12月31日的明天是第二年的1月1日 2月…

vue2路由(下)

编程式路由导航 通过点击按钮实现push和replace俩种模式的跳转 实现&#xff1a;就是通过$router原型里面的方法 也能实现路由的跳转和后退&#xff0c;分别采用的是$router里面的black和forward方法 感觉就是BOM对象中的history对象里面的方法 正是前进&#xff0c;后是后…

快鲸scrm | 三个步骤,快速打造企业长效私域营销阵地

快鲸scrm对企业微信私域流量玩法进行系统梳理&#xff0c;把企业微信用户运营拆解为“获客”、“转化”和“服务”这三个关键步骤&#xff0c;从点到面&#xff0c;为企业私域运营提供可行性的解决方案。 一、构建私域流量管理机制 用户是企业发展的基础&#xff0c;用户增长对…

三电技术之电控技术

三电技术之电控技术 1 基本功能 整车控制系统能够实现对汽车动力、舒适度、安全性以及能耗等多方面进行调整优化&#xff0c;配合大数据让电动汽车拥有更好的操作性和可靠性&#xff0c;具体来讲整车控制器对电动汽车主要有以下功能&#xff1a; 数据交互管理&#xff1a;整…

mybatis-plus-join MPJ连表查询 这样写太香了!

mybatis-plus作为mybatis的增强工具&#xff0c;它的出现极大的简化了开发中的数据库操作&#xff0c;但是长久以来&#xff0c;它的联表查询能力一直被大家所诟病。一旦遇到left join或right join的左右连接&#xff0c;你还是得老老实实的打开xml文件&#xff0c;手写上一大段…

【unity实战】用对象池设计制作Dash冲锋残影的效果

什么是对象池? 在Unity中,对象池是一种重复使用游戏对象的技术。使用对象池的好处是可以减少游戏对象的创建和销毁,从而提高游戏的性能。如果不使用对象池,每次需要创建游戏对象时,都需要调用Unity的Instantiate函数,这会导致内存分配和垃圾回收的开销。而使用对象池,可…

李宏毅 深度学习【持续更新】

目录pytorch快速入门csdn快速入门OS包PIL包Opencv包Dataset类Tensorboard的使用torchvision.transforms 的使用torchvision中数据集的使用DataLoader的使用(torch.utils.data)神经网络的搭建nn.Module深度学习 李宏毅Chatgpt1 研究方向2 Chatgpt学习的步骤3 Fine tune vs. Prom…

【Linux内网穿透】使用SFTP工具快速实现内网穿透

文章目录内网穿透简介1. 查看地址2.局域网测试连接3.创建tcp隧道3.1. 安装cpolar4.远程访问5.固定TCP地址内网穿透简介 是一种通过公网将内网服务暴露出来的技术&#xff0c;可以使得内网服务可以被外网访问。以下是内网穿透的一些应用&#xff1a; 远程控制&#xff1a;通过内…

九【springboot】

Springboot一 Spring Boot是什么二 SpringBoot的特点1.独立运行的spring项目三 配置开发环境四 配置开发环境五 创建 Spring Boot 项目1.在 IntelliJ IDEA 欢迎页面左侧选择 Project &#xff0c;然后在右侧选择 New Project&#xff0c;如下图2.在新建工程界面左侧&#xff0c…

深入理解PyTorch中的train()、eval()和no_grad()

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️&#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

【数据结构】栈的实现

&#x1f61b;作者&#xff1a;日出等日落 &#x1f4d8; 专栏&#xff1a;数据结构 &#x1f339; 如果说&#xff0c;读书是在奠定人生的基石&#xff0c;在梳理人生的羽毛&#xff0c;那么&#xff0c;实践&#xff0c;就是在构建人生的厅堂&#xff0c;历练人生的翅膀。是不…

阿里P7晒工资条,看完好扎心了……

前几天&#xff0c;有位老粉私信我&#xff0c;说看到某95后学弟晒出阿里P7的工资单&#xff0c;他是真酸了…想狠补下技术&#xff0c;努力冲一把大厂。 为了帮到他&#xff0c;也为了大家能在最短的时间内做面试复习&#xff0c;我把软件测试面试系列都汇总在这一篇文章了。 …

自然语言处理: 知识图谱的十年

动动发财的小手&#xff0c;点个赞吧&#xff01; NLP 中结合结构化和非结构化知识的研究概况 自 2012 年谷歌推出知识图谱 (KG) 以来&#xff0c;知识图谱 (KGs) 在学术界和工业界都引起了广泛关注 (Singhal, 2012)。作为实体之间语义关系的表示&#xff0c;知识图谱已被证明与…

ECharts 横向柱状图自动滚动

核心代码 const seriesList [120, 200, 150, 80, 70, 110, 130, 120, 200, 150, 120, 200]; const xAxisList [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; const dataZoomEndValue 6; // 数据窗口范围的结束数值(一次性展示几个) dataZoom: [{show: false, // 是否显示滑动…

Java面向对象高级【类加载器】

目录 Java程序是怎样被运行的 类加载器的作用 加载类文件 链接类 定位类 类加载器间的委派 实现类的隔离 类加载器的类型 启动类加载器&#xff08;Bootstrap Class Loader&#xff09; 扩展类加载器&#xff08;Extension Class Loader&#xff09; 应用程序类加载器…

数据结构和算法学习记录——二叉树的非递归遍历(中序遍历、先序遍历、后序遍历)

目录 中序遍历 代码实现 思路图解 先序遍历 代码实现 后序遍历 思路图解 二叉树的非递归遍历运用到堆栈 中序遍历 循环的思路是 遇到一个节点&#xff0c;就把它压栈&#xff0c;并去遍历它的左子树。当左子树遍历结束之后&#xff0c;从栈顶弹出这个节点并访问…

MybatisPlus主键策略

Mybatis默认主键策略是TableId(type IdType.ASSIGN_ID) 这是默认策略雪花算法 此时主键类型可以是String 数据表字段类型可以是bigint int varchar 无需数据表主键自增 TableId(type IdType.ASSIGN_AUTO) 是主键自增策略:该策略为跟随数据库表的主键递增策略&…