网络编程套接字 | TCP套接字

news2025/1/12 19:09:01

前面的文章中我们使用UDP套接字编写了四个版本,不同的UDP服务器,在本文中我们将要对TCP套接字进行几个不同的版本的代码的编写,首先测试一下TCP套接字的代码,然后是将这个版本进行修改成多进程版本的,再将代码修改成多线程版本的,最后在编写一个线程池版本的代码。
在编写TCP套接字之前我们会使用如下的一些API

socket() - int socket(int domain, int type, int protocol);

  • socket()打开一个网络通信端口,如果成功的话,就像open()一样返回一个文件描述符 ;
  • 应用程序可以像读写文件一样用read/write在网络上收发数据;
  • 如果socket()出错则返回-1;
  • 对于IPv4,第一个参数指定为AF_INET;
  • 对于TCP协议,type参数指定为SOCK_ATREAM,表示面向流的传输协议;
  • protocol参数指定为0即可

bind() - int bind(int socket, const struct sockaddr *address, socklen_t address_len);

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端的值服务器的地址与端口号后就可以向服务器发起连接,服务器需要调用bind一个固定的网络地址与端口号;
  • bind()成功返回0,失败返回-1;
  • bind()的作用是将参数sockfd和myaddr绑定在一起,是sockfd这个网络通信的文件描述符监听myaddr所描述的地址和端口号;
  • 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。
    对于myaddr参数是这样初始化的:
bzero(&servaddr, sizeof(servaddr));// 将整个结构体清零;
servaddr.sim_family = AF_INET;// 设置地址类型为AF_INET;
servaddr.sin_family.s_addr = htonl(INADDR_ANY);// 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
servaddr.sin_port = htons(SERV_PORT);// 端口号为SERV_PORT, 我们可以自己定义;

listen() - int listen(int socket, int backlog);

  • listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5);
  • listen()成功返回0,失败返回-1;

accept() - int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);

  • 三次握手完成后, 服务器调用accept()接受连接;
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
  • 如果给addr 参数传NULL,表示不关心客户端的地址;
  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
  • accept()的返回值是一个文件描述符,在传入参数中,他传入的是上面socket()返回的文件描述符,socket()返回的文件描述符可以想象是饭店门口拉客的,招呼客人进入饭店。等到客人进入饭店后,需要有一个新的店小二来招呼客人吃饭。

对于客户来说,不需要进行accept(),而需要调用connect()连接服务器

connect() - int connect(int socket, const struct sockaddr *address, socklen_t address_len);

  • connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
  • connect()成功返回0,出错返回-1

TCP_v1 测试版

在TCP中的错误码同样是我们自己定义在err.hpp头文件中的

// tcp_server.hpp
static const uint16_t defaultport = 8081;
static const int backlog = 32;
using func_t = std::function<std::string (std::string)>;
class TcpServer{
public:
    TcpServer(func_t func, uint16_t port = defaultport) : func_(func), port_(port), quit_(true) {}
    void initServer(){
        // 1. 创建socket,文件
        listensock_ = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字,这个套接字是用于监听的
        if(listensock_ < 0){
            std::cerr << "create socket error" << std::endl;
            exit(SOCKET_ERR);
        }
        // 2. bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_); // 收发消息的时候,没有做主机转网络,会自动做大小端转化
        local.sin_addr.s_addr = htonl(INADDR_ANY);
        if(bind(listensock_, (struct sockaddr*)&local, sizeof(local)) < 0){
            std::cerr << "bind socket error" << std::endl;
            exit(BIND_ERR);
        }
        // 3. 监听 客户端要链接服务器
        if(listen(listensock_, backlog) < 0){
            std::cerr << "listen socket error" << std::endl;
            exit(LISTEN_ERR);
        }
    }
    void start(){
        quit_ = false;
        while (!quit_){
            struct sockaddr_in client; // 获取客户端的信息
            socklen_t len = sizeof(client);
            // 4. 获取连接 accept
            int sock = accept(listensock_, (struct sockaddr*)&client, &len); // 返回值是一个文件描述符,一个描述符专门用于监听,另一个描述符专门用于套接字通信
            if (sock < 0){
                std::cerr << "accept error" << std::endl;
                continue; // 获取连接失败并不需要终止程序
            }
            // 提取client信息
            std::string clientip = inet_ntoa(client.sin_addr);
            uint16_t clientport = ntohs(client.sin_port);
            // 5. 获取新连接成功,开始业务处理
            std::cout << "获取新连接成功: " << sock << " from " << listensock_ << ", " << clientip << "-" << clientport << std::endl;
            service(sock, clientip, clientport); 
            // 这里存在的问题 - 只能支持一个客户端的运行,当给一个执行流进入了service中时就无法再去accept了,因此一次只能给一个客户进行通信
        }
    }
    
    // 流式操作可以使用read进行读取数据,UDP不是流式的是面向数据报的
    void service(int sock, const std::string &clientip, const uint16_t &clientport)    {
        std::string who = clientip + "-" + std::to_string(clientport);
        char buffer[1024];
        while (true){
            ssize_t s = read(sock, buffer, sizeof(buffer)-1);
            if (s > 0){
                buffer[s] = 0;
                std::string res = func_(buffer); // 进行回调 这里我们使用的就是在前面UDP套接字中使用过的function将业务与网络IO进行分离
                std::cout << who << " >>> " << res << std::endl;
                write(sock, res.c_str(), res.size());
            }
             else if (s == 0){
                // 对方将连接关闭了
                close(sock);
                std::cout << who << " quit, me too" << std::endl;
                break;
            }
            else{
                close(sock);
                std::cerr << "read error: " << strerror(errno) << std::endl;
                break;
            }
        }
    }
    ~TcpServer(){}
private:
    uint16_t port_;
    int listensock_;
    bool quit_;
    func_t func_;
};
// tcp_client.cc
int main(int argc, char* argv[]){
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    int sock = socket(AF_INET, SOCK_STREAM, 0);// 1. creatr socket
    if (sock < 0){
        cerr << "socket error : " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }
    // 要不要bind?要
    // 要不要自己bind?不要, 因为client要让OS自动给用户进行bind
    // 要不要listen?不要 要不要accept?不需要
    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);
    int cnt = 5;
    while (connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0){// 2. connect 
        sleep(1);
        cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;
        if(cnt <= 0) break;
    }
    if (cnt <= 0){
        cerr << "连接失败..." << endl;
        exit(CONNECT_ERR);
    }
    char buffer[1024];// 3. 连接成功
    while(true){
        string line;
        cout << "Enter>>> ";
        getline(cin, line);
        write(sock, line.c_str(), line.size());
        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0){
            buffer[s] = 0;
            cout << "server echo >>>" << buffer << endl;
        }else if(s == 0){
            cerr << "server quit" << endl;
            break;
        }else{
            cerr << "read error: " << strerror(errno) << endl;
            break;
        }
    }
    close(sock);
    return 0;
}

运行的结果就如下所示,可以发现当一个客户端连接上服务器之后,另外一个客户端就无法连接,只有当前连接上服务器的客户端退出后,另一个客户端才能够连接上。
在这里插入图片描述
为了解决上述的服务器只能够同时连接一个客户端的问题,我们在v2版本中使用多进程的形式:

TCP_v2 多进程版

// 之前的问题就存在于service(sock, clientip, clientport); 这句处理网络服务的代码与服务器的accept函数处于同一个进程中,下面就对其进行修改
void start(){
	// signal(SIGCHLD, SIG_IGN);
	pid_t id = fork();// 创建子进程
	if (id < 0){
    	close(sock);
    	continue;
	}        
	else if (id == 0){ // child,父进程fd会被子进程继承吗?会。 父子会用同一张文件描述符表吗?不会。子进程会拷贝父进程的fd table
    	// 子进程,建议关闭掉不需要的fd
    	close(listensock_);
	    // if (fork() > 0) exit(0); // 这一行代码会让child进程退出,孙子进程关闭其父进程之后->孤儿进程,由1号进程领养
	    service(sock, clientip, clientport);
	    exit(0);
	}    
	// 父进程,一定要关闭掉不需要的fd,不关闭 - 文件描述符泄露
	close(sock);
	pid_t ret = waitpid(id, nullptr, 0); // 父进程默认阻塞 waitpid(id, nullptr, WOHANG)
	if (ret == id)
	    std::cout << "wait child " << id << " success" << std::endl;
}

上述多进程程序运行时,当运行到waitpid的时候会被阻塞,如果阻塞了,那么就还是与v1版本的一样,在这里我们有两个比较推荐的方案,第一种就是在start函数开始的时候将子进程的信号忽略signal(SIGCHLD, SIG_IGN);或者我们也可以if (fork() > 0) exit(0);这句话就是创建孙子进程,让子进程退出,此时孙子进程就变为了孤儿进程有1号进程领养。

TCP_v3 多线程版

同样的我们也可以对v1版本进行修改,将其修改为多进程的版本,与v2的版本相同都是对service(sock, clientip, clientport);处进行修改

class ThreadData{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer *ts)
        : sock(fd), clientip(ip), clientport(port), current(ts) {}
public:
    int sock;
    std::string clientip;
    uint16_t clientport;
    TcpServer *current;
};
static void* threadRoutine(void* args){
    pthread_detach(pthread_self()); // 为了不用join将线程自己进行分离
    ThreadData* td = static_cast<ThreadData*>(args);
    td->current->service(td->sock, td->clientip, td->clientport);
    delete td;
    return nullptr;
}
void start(){
	// ...
	// 要不要关闭不要的socket??不能,多线程将进程的文件描述符表共享
	pthread_t tid;
	ThreadData* td = new ThreadData(sock, clientip, clientport, this);
	pthread_create(&tid, nullptr, threadRoutine, td); // 要给线程传入sock...
}

TCP_v4 线程池版

最后,我们可以再次对其进行修改,最后让其成为一个线程池版本的TCP服务器。
在线程池版本的TCP服务器中我们构建了日志输出模块、线程池模块、任务模块、守护进程模块、Tcpserver模块、Tcpclient模块等

日志输出模块

// log.hpp
#pragma once

#include <cstdio>
#include <cstring>
#include <cstdarg>
#include <string>
#include <sys/types.h>
#include <unistd.h>

const std::string filename = "log/tcpserver.log";

// 日志是有日志等级的
enum{ // 使用枚举类型来定义不同的日志等级
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal,
    Uknown
};
static std::string toLevelString(int level){ // 将数字日志等级转换为字符串
    switch (level){
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Uknown";
    }
}
static std::string getTime(){ 
    time_t curr = time(nullptr); // 获取当前时间离1970.1.1的差值
    struct tm *tmp = localtime(&curr); // ANSI C标准称使用tm结构的这种时间表示为分解时间(broken-down time)。
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon+1, tmp->tm_mday, tmp->tm_hour, tmp->tm_min, tmp->tm_sec); // 格式化输出字符串,并将结果写入到指定的缓冲区
    return buffer;
}

// ...可变参数
// 日志格式:日志等级 时间 pid 消息体
// logMessage(DEBUG, "%d, %s", 12, s.c_str())
void logMessage(int level, const char* format, ...) // 传入的可能是字符串常量
{
    char logLeft[1024];

    std::string level_string = toLevelString(level);
    std::string cur_time = getTime();

    snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), cur_time.c_str(), getpid());

    char logRight[1024];
	
	// 可变参数可以使用vsnprintf直接进行获取
    va_list p;
    va_start(p, format);
    vsnprintf(logRight, sizeof(logRight), format, p);
    va_end(p);

    // 打印
    // printf("%s %s\n", logLeft, logRight);

    // 保存到文件中
    FILE *fp = fopen(filename.c_str(), "a");
    if(fp == nullptr)return;
    fprintf(fp,"%s%s\n", logLeft, logRight);
    fflush(fp); //可写也可以不写
    fclose(fp);

    // va_list p; // char* 类型
    // int a = va_arg(p, int); // 根据类型提取参数
    // va_start(p, format); // 将指针指向可变参数部分的起始地址
    // va_end(p); // p = NULL
} 

线程池模块就是我们之前写过的带有单例模式的线程池模块。
下面是服务器类实现:

namespace ns_server
{
    static const uint16_t defaultport = 8081;
    static const int backlog = 32;

    using func_t = std::function<std::string(std::string)>;

    class TcpServer;
    class ThreadData
    {
    public:
        ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer *ts)
            : sock(fd), clientip(ip), clientport(port), current(ts)
        {}

    public:
        int sock;
        std::string clientip;
        uint16_t clientport;
        TcpServer *current;
    };

    class TcpServer
    {
    public:
        TcpServer(func_t func, uint16_t port = defaultport) : func_(func), port_(port), quit_(true)
        {}
        void initServer()
        {
			//...
        }
        void start()
        {
            // ...
            // 线程池版
            // 使用线程池的时候,一定是有限的,一定是要处理短任务,不一定在这里接入线程池
            Task t(sock, clientip, clientport, bind(&TcpServer::service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); // 构建任务,在类中的函数有隐含的this指针需要使用bind进行处理
            ThreadPool<Task>::getinstance()->pushTask(t); // 将任务推送至线程池中
            }
        }

        // 流式操作可以使用read进行读取数据,UDP不是流式的是面向数据报的
        void service(int sock, const std::string &clientip, const uint16_t &clientport){
            std::string who = clientip + "-" + std::to_string(clientport);
            char buffer[1024];
            ssize_t s = read(sock, buffer, sizeof(buffer) - 1); // 读取从网络中收到的数据根据不同的情况返回不同的结果
            while(true)
            {
	            if (s > 0){
	                buffer[s] = 0;
	                std::string res = func_(buffer); // 进行回调
	                logMessage(Debug, "%s# %s", who.c_str(), res.c_str());
	                write(sock, res.c_str(), res.size());
	            }
            } 
            close(sock);
        }
        ~TcpServer(){}
    private:
        uint16_t port_;
        int listensock_;
        bool quit_;
        func_t func_; // 回调函数处理读取之后的数据信息
    };
}

守护进程模块

#pragma once

// 进程组 前台任务 后台任务
// 如果后台任务提到前台,老的前台任务无法运行
// 在会话中只能有一个前台任务在运行 --- 我们在命令行中启动一个进程的时候,bash就无法运行
// 如果登录就是创建一个会话,bash任务,启动我们的进程,就是在当前会话中创建新的前后台任务

// 1. setsid();
// 2. setsid(), 调用进程,不能是组长!我们怎么保证自己不是组长呢?
// 3. 守护进程a. 忽略异常信号 b. 0,1,2要做特殊处理 c. 进程的工作路径可能要更改 /

//守护进程的本质:是孤儿进程的一种!
void Daemon()
{
    // 1. 忽略信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);
    // 2. 让自己不要成为组长
    if (fork() > 0) exit(0);
    // 3. 新建会话,自己成为会话的话首进程
    pid_t ret = setsid();
    if ((int)ret == -1)
    {
        logMessage(Fatal, "deamon error, code: %d, string: %s", errno, strerror(errno));
        exit(SETSID_ERR);
    }
    // 4. 可选:可以更改守护进程的工作路径
    // chdir("/")
    // 5. 处理后续的对于0,1,2的问题
    int fd = open("/dev/null", O_RDWR);// 在 Linux 系统中,/dev/null 是一个特殊的文件,它被称为“空设备”。它没有任何数据,读取它永远不会产生任何输出,写入它永远不会导致任何数据被存储。/dev/null 起着丢弃数据的作用,可以用于一些需要忽略输出或者输入的场合。
    if (fd < 0)
    {
        logMessage(Fatal, "open error, code: %d, string: %s", errno, strerror(errno));
        exit(OPEN_ERR);
    }
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    close(fd);
}

服务器cpp文件

#include "tcpServer.hpp"
#include "daemon.hpp"
#include <memory>
using namespace std;
using namespace ns_server;
static void usage(string proc){
    std::cout << "Usage:\n\t" << proc << " port\n" << std::endl;
}

std::string echo(const std::string& message){
    return message;
}
// ./tcp_server 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(echo, port));
    tsvr->initServer();
    // 将服务器守护进程化
    Daemon();
    tsvr->start();
    return 0;
}

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

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

相关文章

R语言RSTAN MCMC:NUTS采样算法用LASSO 构建贝叶斯线性回归模型分析职业声望数据...

全文链接&#xff1a;http://tecdat.cn/?p24456 如果你正在进行统计分析&#xff1a;想要加一些先验信息&#xff0c;最终你想要的是预测。所以你决定使用贝叶斯&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。 相关视频 但是&#xff0c;你没有共轭先验。你…

【算法训练-二叉树 五】【最近公共祖先】二叉树的最近公共祖先

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是【二叉树的节点查找】&#xff0c;使用【二叉树】这个基本的数据结构来实现&#xff0c;这个高频题的站点是&#xff1a;CodeTop&#xff0c;筛选条…

基于微信小程序的高校就业招聘系统设计与实现(源码+lw+部署文档+讲解等)

前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb;…

悬崖边:企业如何应对网络安全漏洞趋势

在本文中&#xff0c;我们将讨论企业在处理漏洞时面临的挑战&#xff0c;解释安全漏洞是如何引发网络攻击的&#xff0c;以及为什么它会导致不可接受的事件。我们还将分享我们在识别趋势性漏洞方面的经验。 现代信息安全方法正在成为企业的工作流程。例如&#xff0c;不久前&a…

Visio 无边框保存png和pdf文件

1 无边框保存为png 1 点击【开发工具】- 【显示ShapeSheet】- 【页】 选择【Print Properties】 更改【PageLeftMargin、PageRightMargin、PageTopMargin、PageBottomMargin】的数值&#xff0c;如果改成0&#xff0c;那么就完全没有空白。更改数值后一定要回车&#xff01; …

Uni-app 调用微信地图导航功能【有图】

前言 我们在使用uni-app时&#xff0c;有时候会遇到需要开发地图和导航的功能&#xff0c;这些方法其实微信小程序的API已经帮我们封装好了 详见&#xff1a;微信小程序开发文档 接下来我们就演示如何用uni-app来使用他们 使用 <template><view><button type…

什么是Vue的自定义指令(custom directives)?如何自定义指令?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ Vue.js 的自定义指令⭐ 示例⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友…

蓝桥杯 题库 简单 每日十题 day7

01 啤酒和饮料 题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。啤酒每罐2.3元&#xff0c;饮料每罐1.9元。小明买了若干啤酒和饮料&#xff0c;一共花了82.3元。我们还知道他买的啤酒比饮料的数量少&#xff0c;请你…

时间任务管理软件OmniFocus 3 mac中文版软件特色

OmniFocus Standard mac是一款高效的任务管理软件&#xff0c;具有任务管理功能、自定义功能、简洁直观的界面以及强大的提醒和通知功能。 OmniFocus Standard mac软件特色 ​任务管理功能&#xff1a;OmniFocus Standard支持多种视图以适应不同的需求和偏好&#xff0c;比如项…

vue3【echarts 做的词云图】

效果图 安装 安装echarts npm install echarts安装词云图 npm install echarts-wordcloudecharts-wordcloud的git仓库地址 echarts官网地址 引用 import * as ECharts from "echarts"; //引用eacharts import echarts-wordcloud;//引用云词这里的echarts 是自己简…

驱动开发 linux内核GPIO子系统、及其新版API的概念和使用,linux内核定时器

1、GPIO子系统 概述&#xff1a;每一个芯片厂商生产出芯片后会给linux提供一个当前芯片中gpio外设的驱动&#xff0c;我们只需要调用对应的厂商驱动就可以完成硬件的控制。而linux内核源码中的gpio厂商驱动有很多&#xff0c;linux内核个会对厂商驱动做一些封装&#xff0c;会…

MyBatis 缓存模块

文章目录 前言缓存的实现Cache接口PerpetualCache 缓存的应用缓存对应的初始化一级缓存二级缓存第三方缓存 前言 MyBatis作为一个强大的持久层框架&#xff0c;缓存是其必不可少的功能之一&#xff0c;Mybatis中的缓存分为一级缓存和二级缓存。但本质上是一样的&#xff0c;都…

第七天:gec6818开发板QT和Ubuntu中QT安装连接sqlite3数据库驱动环境保姆教程

sqlite3数据库简介 帮助文档 SQL Programming 大多数关系型数的操作步骤&#xff1a;1&#xff09;连接数据库 多数关系型数据库都是C/S模型 (Client/Server)sqlite3是一个本地的单文件关系型数据库&#xff0c;同样也有“连接”的过程 2&#xff09;操作数据库 作为程序员&am…

windows下gvim的配置

一、vim配置文件 "查看自己的vimrc所在的目录 "在命令模式下 :echo $MYVIMRC"打开自己的vimrc文件 "在命令模式下 :e $MYVIMRC 二、排版 "查看自己当前的字体及大小 "在命令模式下 :set guifont?"设置默认的字体为仿宋_GB2312&#xff…

蓝桥杯打卡Day12

文章目录 接龙数列冶炼金属 一、接龙数列OJ链接 本题思路:本题是一道经典的dp问题&#xff0c;设第i个数的首位数字是first&#xff0c; 末位数字是last。因为第i个数只可能加到一个以first结尾的接龙数列中使得这个接龙数列长度加1并且结尾数字变成last.所以状态转移方程为d…

让Mac菜单栏变得更加美观整洁——Bartender 5

Bartender 5是一款Mac电脑上的菜单栏图标管理软件&#xff0c;能够帮助您把菜单栏上的图标整理得更加美观、整洁和易于使用。如果您的菜单栏上充斥着许多图标&#xff0c;导致视觉上很不舒适和疲劳&#xff0c;那么Bartender 5就是解决这一问题的最佳选择&#xff01; Bartend…

智能热水器丨打造智能家居新体验

随着科学技术的不断发展&#xff0c;智能电器越来越被大众所采纳&#xff0c;如智能扫地机&#xff0c;智能洗衣机&#xff0c;智能微波炉等等&#xff0c;越来越智能的电器为人们的生活带来了许多便利。以往的热水器一般都是只有按键/机械的控制方式&#xff0c;没有其他无线控…

Python绘制X-bar图和R图 | 统计过程控制SPC

X-bar图和R图是用于统计过程控制&#xff08;SPC&#xff09;的两种常用工具&#xff0c;用于监测过程的平均值和范围&#xff08;变异性&#xff09;。这些图有助于识别过程中的变化和异常&#xff0c;以便及时采取纠正措施。 **X-bar图&#xff08;平均值控制图&#xff09;…

Gitee使用用户名密码登录一直错误/IDEA连接gitee仓库密码错误

天坑,注册的时候名字带了大写,用户名自动给你变成小写 真正的用户名在个人主页里面看,是后面的字符,才是真正的用户名.排查了一个小时密码问题,真的坑

使用扩展运算符(...)合并数组

在项目开发过程中&#xff0c;有一个需求&#xff0c;需要制作一个带有标题的表格&#xff0c;如下所示&#xff1a; 和后端开发沟通时&#xff0c;后端计划返回三个数组&#xff0c;标题写死。所以我需要做的就是把数组合并&#xff0c;然后在三个数组之前增加标题。这里我采用…