请添加图片描述


半桔:个人主页

 🔥 个人专栏: 《Linux手册》《手撕面试算法》《网络编程》

🔖很多人在喧嚣声中登场,也有少数人在静默中退出。 -张方宇-

文章目录

  • 前言
  • 套接字接口
  • TCP服务器
  • TCP + 多进程
  • TCP + 线程池
    • 重写Task任务
    • 放函数对象
  • 客户端重连
  • 进程组与守护进程
    • 进程组和会话
  • 守护进程

前言

在互联网技术蓬勃发展的今天,高并发、高可靠的网络服务已成为各类应用的核心诉求 —— 从支撑海量用户的 Web 服务器,到实时交互的分布式系统,甚至是物联网设备的通信底座,高效的网络通信设计进程生命周期管理,始终是保障服务稳定运行的基石。

本文将聚焦 Linux 网络编程与进程管理 的核心技术,以 “从基础到进阶,从实现到优化” 的脉络展开:

  • 从最基础的 套接字接口 出发,剖析网络通信的底层逻辑;
  • 通过 TCP 服务器 的搭建,掌握客户端 - 服务端交互的核心流程;
  • 针对高并发场景,探索 多进程、线程池 等并行模型的设计,突破服务吞吐量的瓶颈;
  • 最终深入 进程组与守护进程 的实践,解决服务 “脱离终端、长期稳定运行” 的生产级需求。

TCP通信是面向字节流的,而UDP是面向最字节报的,因此两者通信方式上有本质的差异。

TCP面向字节流也就意味着,接收方读取上来的数据可能是不完整的,因此TCP通信要进行协议定制,规定一个消息从哪到哪是一个整体部分。关于协议的定制我们在下一篇博客中详细讲解,本篇文章我们假设通过TCP通信对方就可以拿到一个完整的数据。

套接字接口

TCP的接口和UDP接口有类似的,当时也有一些不同之处。
UDP通信的步骤就是:创建套接字,绑定,接收和发送消息;而TCP与其是不一样的。

  • TCP通信时面向连接的,需要通信双方先建立连接,服务器一般是比较“被动”的,服务器一直处于等待外界连接的状态(监听状态)。

因此在进行绑定完成之后,服务器要先进入监听状态,与客户端建立连接后才能进行通信:

int listen(int sockfd , int backlog)

  1. 参数一:套接字;
  2. 参数二: backlog 表示未完成连接队列(处于三次握手过程中)和已完成连接队列(三次握手完成,等待 accept 处理)的最大长度之和。用来调节连接时的并发量;
  3. 返回值:成功返回0,失败-1;

第二个接口,将服务器设置为监听模式之后,要对客户端的连接请求做出响应,要接收客户端的请求:

int accept(int sockfd , struct sockaddr_in *addr , socklen_t *addrlen)

  1. 参数一:套接字;
  2. 参数二:输出型参数,一个结构体,存储着客户端的ip和端口号信息;
  3. 参数三:输出型参数,表示第二个结构体的大小;
  4. 返回值:返回一个文件描述符,通过该文件描述符可以让直接使用writeread接口进行通信,就像从文件中进行读写一样。

注意:accept中的sockfd也属于文件描述符,只不过该描述符主要负责将底层的连接请求来上来,而不负责进行IO交互;而accept返回的文件描述符是专门用来进行IO交互的。

随着客户端越来越多,accept返回的文件描述符也就也来越多,每一个都负责与一个客户端进行通信。

客户端要与服务端建立连接,所以需要先服务端发送连接请求:

int connet(int sockfd , struct sockaddr* addr , socklen_t addrlen)

  1. 参数一:套接字;
  2. 参数二:结构体,内部包含要进行连接的IP和端口号;
  3. 参数三:参数二结构体的大小;
  4. 返回值:0表示成功,-1表示失败。

TCP服务器

使用一个类来实现TCP服务器:

  • 内存成员需要有IP和端口号,来进行绑定;
  • 并且需要将套接字存储起来,否则后续在不到套接字就会导致无法关闭对应的网络文件位置。
  • 此处在设计一个bool类型的变量,让用户可以控制时候打开服务器。

初始化的时候需要外界将这些参数都传进行保存起来,但是并不在初始化时创建套接字,而是当用户运行时才进行创建。

const std::string defaultip = "0.0.0.0";class Server
{
public:Server(const uint16_t &port , const std::string &ip = defaultip):port_(port) , ip_(ip){}private:uint16_t port_;std::string ip_;int sockfd_;
};

与UDP一样,为了保证服务器能够接收来自各个网卡上的数据,我们再对服务器进行绑定的时候使用ip为0。

在此之前我们需要思考以下接收到的信息如何进行处理?

如果我们直接让处理方法都在循环内完成,就会导致代码拓展性差,如果后续希望接入进程池就需要对代码进行重构,因此此处将对接收到的信息处理方法也单独封装一个类:

该类主要负责,将对信息进行处理,处理完后,向客户端返回数据,因此该类的成员必须有一个string用来存储待处理的信息,为了进行通信还需要拿到对应的文件描述符

我们可以在类中对调用运算符进行重载,在进行消息调用的时候更简单。
为了后续测试,我们先不进行太复杂的处理:

class Task
{
public:Task(const int & fd , const std::string message):fd_(fd) , message_(message){}bool operator()(){std::string ret = "I have got your message : " + message_;write(fd_ , ret.c_str() , ret.size()); return true;}
private:int fd_;std::string message_;
};

现在可以对服务器进行初始化了,初始化主要分为3步:

  1. 创建套接字;
  2. 绑定;
  3. 设置监听模式。
    void Init(){// 1. 创建套接字// 2. 绑定// 3. 设置监听模式sockfd_ = socket(AF_INET , SOCK_STREAM , 0);if(sockfd_ < 0){Log(Fatal) << "socket failed ";exit(Socket_Err);}struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(port_);char clientip[32];inet_aton(ip_.c_str() , &local.sin_addr);if(bind(sockfd_ , (const struct sockaddr*)&local , sizeof(local)) < 0){Log(Fatal) << "bind failed" ;exit(Bind_Err);}if(listen(sockfd_ , 10) < 0){Log(Fatal) << "listen failed" ;exit(Listen_Err);}}

运行服务器了,运行服务器:

  1. 先建立连接;
  2. 读取数据;
  3. 做出反应。
    void Service(int fd_){   char buffer[1024];while(1){int n = read(fd_ , buffer , sizeof(buffer) - 1);if(n > 0){buffer[n] = 0;Task task(fd_ , buffer);task();}else if(n == 0){close(fd_);break;}else {Log(Error) << "read error";close(fd_);break;}}}void Start(){// 1. 建立连接// 2. 读取消息// 3. 对消息进行处理,并返回struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(sockfd_ , (struct sockaddr*)&client , &len);if(fd < 0){Log(Warning) << "accept failed";}Service(fd);}

此处我们将服务单独进行了封装,方便后面接入多线程/多进程。

服务器的类编写完成,后面再进行拓展,当前先进行以下简单测试:
编写一个源文件来运行一下服务器:在执行的时候,必须给出端口号。

void Menu(char* argv[])
{std::cout << "\r" << argv[0] <<  "  [port] " << "\n";
}int main(int argc , char* argv[])
{if(argc != 2){Menu(argv);exit(1);}uint16_t port = std::stoi(argv[1]);Server server(port);server.Init();server.Start();return 0;
}

当前服务器编写完成了,但是客户端还没进行实现。如果想对服务端进行测试的话,可以先使用telnet工具,绑定本地环回地址127.0.0.1进行测试,但是只能起到本地通信的作用,不会将信息推送到网络中

下一步就是编写客户端了:

客户端的编写就比较简单了:

  1. 创建套接字;
  2. 发送连接请求;
  3. 连接成功,发送数据;
  4. 接收数据。

与服务端的编写类似,只不过要用到connect接口:

void Menu(char *argv[])
{std::cout << argv[0] << "  [ip] " << " [port] " << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Menu(argv);exit(1);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);// 1.创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << " socket failed ";exit(2);}// 2.发送连接请求struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(port);inet_aton(ip.c_str(), &server.sin_addr);int n = connect(sockfd, (sockaddr *)&server, sizeof(server));if (n < 0){std::cerr << " connect failed ";exit(2);}// 3.进行通信std::string message;char buffer[1024];while (1){std::cout << "Please Enter@";std::getline(std::cin, message);write(sockfd, message.c_str(), message.size());n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}if (message == "quit")break;}close(sockfd);return 0;
}

以上就是客户端和服务端的所有代码编写,只不过给服务端只能处理一个用户端。

为了能够同时处理多个用户端,此处我们需要使用多进程或多线程来实现。

TCP + 多进程

  • 父进程创建子进程,让子进程来与客户端进行交互;
  • 父进程只负责与子进程建立连接。

此处需要考虑子进程的回收问题,我们并不希望对子进程进行等待,因此有两种方案:

  1. 直接将SIGCHLD信号进行屏蔽;
  2. 使用孙子进程来完成与客户端通信,子进程直接回收;

此处我们采用孙子进程的方式直接回收子进程,让孙子进程被超卓系统领养。

此处我们仅需要对服务端类中得Start进行修改即可:

    void Start(){// 1. 建立连接// 2. 读取消息// 3. 对消息进行处理,并返回while (1){struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(sockfd_, (struct sockaddr *)&client, &len);if (fd < 0){Log(Warning) << "accept failed";}// 使用多进程来实现pid_t id = fork();if (id < 0){Log(Fatal) << "fork failed";}else if (id == 0){close(sockfd_);if (fork() == 0)   // 使用孙子进程进行通信{Service(fd);exit(0);}exit(0);}// 父进程直接将fd关闭,不允许父进程与客户端进行通信close(fd);pid_t rid = waitpid(id, nullptr, 0);  // 回收子进程}}

以上就是多进程服务端的修改,也很简单。

TCP + 线程池

  • 主线程先任务队列中添加任务,而线程池中的线程负责将任务取出来,执行。

引入线程池,向任务队列中放什么???

有两种方案:

  1. 对Task任务类进行从写;
  2. 向任务队列中放函数对象,让线程能够直接调用。

此处两种方法都实现一下:

重写Task任务

  • 我们希望主线程构建一个Task任务,加入到任务队列中,然后线程池中的线程拿出来执行。
  • 线程池中的线程如果想与用户端进行通信,就必须拿到文件描述符,因此Task类私有成员有一个文件描述符
  • task任务的调用运算符重载,应该变成原来的Service函数实现.

重写如下:

class Task
{
public:Task(const int &fd): fd_(fd){}void operator()(){char buffer[1024];while (1){memset(buffer, 0, sizeof(buffer));int n = read(fd_, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::string ret = "I have got your message : " + std::string(buffer);write(fd_, ret.c_str(), ret.size());if (strcmp(buffer, "quit") == 0)break;}else if (n == 0){close(fd_);break;}else{Log(Level::Error) << "read error";close(fd_);break;}}}private:int fd_;
};

下一步就是对服务端的Start的函数进行重写,主线程负责向线程池放入Task对象:

    void Start(){// 1. 建立连接// 2. 读取消息// 3. 对消息进行处理,并返回std::unique_ptr<thread_poll<Task>>& ptp = thread_poll<Task>::GetInstance();ptp->run();while (1){struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(sockfd_, (struct sockaddr *)&client, &len);if (fd < 0){Log(Level::Warning) << "accept failed";}// 父进程直接将fd关闭,不允许父进程与客户端进行通信ptp->push(Task(fd));}}

通过这种方式,就实现了主线程向任务队列中放数据,由线程池中的线程来与用户端进行沟通。

放函数对象

我们已经有现成的函数调用对象了,就是服务端中的Service函数,但是如果线程池中的线程并没有在该函数中,因此也就没有this指针了,所以我们在传函数对象的时候,可以使用std::bind进行绑定,将this指针绑定到函数对象中,这样线程池中的线程就可以直接进行调用了。

我们只需要对Service函数进行绑定,保证线程池中的线程在调用的时候,不需要传递任何参数,可以直接调用即可:

    void Start(){// 1. 建立连接// 2. 读取消息// 3. 对消息进行处理,并返回using  fun_t =  std::function<void()>;std::unique_ptr<thread_poll<fun_t>>& ptp = thread_poll<fun_t>::GetInstance();ptp->run();while (1){struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(sockfd_, (struct sockaddr *)&client, &len);if (fd < 0){Log(Level::Warning) << "accept failed";}// 父进程直接将fd关闭,不允许父进程与客户端进行通信fun_t func = std::bind(&Server::Service , this , fd);  // 绑定this指针和文件描述符ptp->push(func);}}

以上两种方法都比较常用,后一种方法实现上更简单一些。

客户端重连

当服务端挂掉或者读写出错时,我们上面的客户端会直接退出;当服务端出现问题的时候,我们并不应该将客户端直接退出,而是让客户端进行重连,即重新向服务端发送建立连接的请求

下面我们将进行模拟实现,客户端重连的机制:

  • 客户端重连,必定需要进行循环;当服务端挂掉时,让客户端重新进行connect尝试重新建立连接;
  • 我们也不能一直让客户端进行连接,当尝试连接的次数达到一定限制时,才让客户端退出。

下面时修改后的代码实现,我们的主循环内部有两个循环,一个用来控制重连的次数,另一个用来与服务端建立联系。

void Menu(char *argv[])
{std::cout << argv[0] << "  [ip] " << " [port] " << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Menu(argv);exit(1);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(port);inet_aton(ip.c_str(), &server.sin_addr);while (1){int cnt = 0, n = 0 , sockfd = -1;const int max_cnt = 6;do{// 1.创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << " socket failed ";exit(2);}// 2.connextn = connect(sockfd, (sockaddr *)&server, sizeof(server));if (n < 0){std::cout << "connet failed : " << cnt++ << std::endl;sleep(1);}elsebreak;} while (cnt < max_cnt);if (cnt == max_cnt){std::cout << "server error" << std::endl;return 0;}// 3.进行通信std::string message;char buffer[1024];while (1){std::cout << "Please Enter@";std::getline(std::cin, message);write(sockfd, message.c_str(), message.size());n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}elsebreak;if (message == "quit"){close(sockfd);return 0;}}}return 0;
}

客户端在直接进行连接的时候,会出现连接失败,因核心原因是 服务器重启时,原端口因 TCP TIME_WAIT 状态被占用,导致无法重新绑定端口(监听失败)

所以我们需要对服务器进行设置:在服务器的 socket 创建后、bind 前,添加 端口复用选项

int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
// 防止偶发性的服务器无法进行立即重启

进程组与守护进程

在操作系统中我们有前台进程和后台进程;

  • 通过jobs指令可以查看后台进程;
  • fg + 任务号:将后台进程拿到前台;

但前台进程被暂停后,如果向前台进程发送19号信息,即SIGSTOP时,前台进程会被自动移动到后
台进程,此时bash命令行解释器会被移动到前台。

  • bg + 任务号,将后台暂停的进程继续执行。

在设计服务器的时候,我们希望服务器是后台进程,并且不受到用户的登录和退出的影响
下面解释如何做到:

进程组和会话

  • 在操作系统中有一个进程组的概念,进程组是一个或多个进程的集合,进程组中有一个组长:PID==PGID就是组长;
  • 组长负责创建一个进程组或者在进程组中创建进程;该组长进程执行完毕,并不会影响组内其他进程的执行;

一个进程组中的进程协作来完成任务,最常见的就是通过管道执行命令,管道中的所有命令都属于一个进程组

可以通过ps aj来查看进程的相关ID信息:

![[Pasted image 20250827194606.png]]

  • 在操作系统中又定义了session会话的概念,session指的是一个或多个进程组。
  • 通常默认一个会话与一个终端进行关联,在操作系统中会有一个初始会话,该会话与终端直接建立联系,控制终端可以向初始会话中的进程发送信号,同时当控制终端退出的时候,内部的所有进程,进程组都会被退出,这就会导致我们的服务器也会退出。

但是好在,当我们创建一个新会话的时候,新会话默认没有控制终端,这也就保证了新会话不受终端的登录和退出的控制。

因此只要让服务端自成一个新会话,就可以保证服务端持续运行。该进程不再与键盘关联,不受到登录和注销的影响,这种进程就被称为守护进程。下面看看守护进程如何实现。

守护进程

  • 一个进程组的组长不能自成会话,也就不能当守护进程。

因此在自成会话的时候,需要时子进程,让父进程直接退出,子进程作为孤儿进程自成会话。
我们通过pid_t setsid(void)来让一个进程自成会话。

  • 一般我们会选择将守护进程的一些信号进行忽略,防止收到信号影响;
  • 并且一般会更改目录,以及输入输出,将输入输出定向到/dev/null中。

现在让我们来实现守护进程:

const std::string defaultdir = "/";
const std::string nullfile = "/dev/null";void Deamon(bool ischdir , bool isclose)
{// 1.忽略信号signal(SIGPIPE , SIG_IGN);signal(SIGPIPE , SIG_IGN);signal(SIGSTOP , SIG_IGN);// 2. 自成会话if(fork() > 0 ) exit(0);   // 父进程直接退出setsid();if(ischdir)chdir(defaultdir.c_str());if(isclose)    // 是否关闭文件{close(0);close(1);close(2);}else{int fd = open(nullfile.c_str() , O_RDWR);dup2(fd , 0);dup2(fd , 1);dup2(fd , 2);}
}

以上就是自己实现的守护进程接口。

实际上操作系统也提供了接口,让一个进程自成会话int daemon(int nochdir , int noclose),在这里就不再介绍了。

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

还停留在批处理时代吗?增量计算架构详解

在当今的数字化环境中&#xff0c;企业不再只是一味地囤积数据——他们痴迷于尽快把数据转化为可付诸行动的洞察。真正的优势来自于实时发现变化并立即做出反应&#xff0c;无论是调整推荐策略还是规避危机。 十年前&#xff0c;硬件与平台技术的进步让我们能够从容应对海量数…

DataSet-深度学习中的常见类

深度学习中Dataset类通用的架构思路 Dataset 类设计的必备部分 1. 初始化 __init__ 配置和路径管理&#xff1a;保存 config&#xff0c;区分 train/val/test 路径。加载原始数据&#xff1a;CSV、JSON、Numpy、Parquet 等。预处理器/归一化器&#xff1a;如 StandardScaler&am…

【VC】 error MSB8041: 此项目需要 MFC 库

▒ 目录 ▒&#x1f6eb; 导读问题背景环境1️⃣ 核心原因&#xff1a;MFC 组件缺失或配置不当2️⃣ 解决方案&#xff1a;安装 MFC 组件并验证配置2.1 步骤1&#xff1a;检查并安装 MFC 组件2.2 步骤2&#xff1a;检查并修正项目配置2.3 步骤3&#xff1a;针对特定场景的补充方…

Java零基础学习Day10——面向对象高级

一.认识final1.含义final关键字是最终的意思&#xff0c;可以修饰&#xff1a;类&#xff0c;方法&#xff0c;变量修饰类&#xff1a;该类被称为最终类&#xff0c;特点是不能被继承修饰方法&#xff1a;该方法被称为最终方法&#xff0c;特点是不能被重写了修饰变量&#xff…

Qt中解析JSON文件

Qt中解析JSON文件 在Qt中解析JSON字符串主要有两种方式&#xff1a;使用QJsonDocument类或使用QJsonDocument结合QVariant。以下是详细的解析方法&#xff1a; 使用QJsonDocument&#xff08;推荐&#xff09; 这种方式的主要相关类如下&#xff1a; QJsonDocument: QJsonDocum…

深度解析HTTPS:从加密原理到SSL/TLS的演进之路

在互联网时代,数据安全已成为不可忽视的基石。当我们在浏览器地址栏看到"https://"前缀和那把小小的绿色锁图标时,意味着正在进行一场受保护的通信。但这层保护究竟是如何实现的?HTTPS、SSL和TLS之间又存在着怎样的联系与区别?本文将深入剖析这些技术细节,带你全…

Flutter 官方 LLM 动态 UI 库 flutter_genui 发布,让 App UI 自己生成 UI

今日&#xff0c;Flutter 官方正式发布了它们关于 AI 大模型的 package 项目&#xff1a; genui &#xff0c;它是一个非常有趣和前沿的探索类型的项目&#xff0c;它的目标是帮助开发者构建由生成式 AI 模型驱动的动态、对话式用户界面&#xff1a; 也就是它与传统 App 中“写…

Redis常用数据结构及其底层实现

Redis常用数据结构主要有String List Set Zset Hash BitMap Hyperloglog Stream GeoString:Redis最常用的一种数据结构,Sting类型的数据存储结构有三种int、embstr、raw1.int:用来存储long以下的整形embstr raw 都是用来存字符串&#xff0c;其中小于44字节的字符串用embstr存 …

O3.4 opencv图形拼接+答题卡识别

一图形拼接逻辑导入必要的库pythonimport cv2 import numpy as np import sys导入cv2库用于图像处理&#xff0c;numpy库用于数值计算&#xff0c;sys库用于与 Python 解释器进行交互&#xff0c;例如退出程序。定义图像显示函数def cv_show(name, img):cv2.imshow(name, img)c…

SQL注入常见攻击点与防御详解

SQL注入是一种非常常见且危险的Web安全漏洞。攻击者通过将恶意的SQL代码插入到应用程序的输入参数中&#xff0c;欺骗后端数据库执行这些非预期的命令&#xff0c;从而可能窃取、篡改、删除数据或获得更高的系统权限。以下是详细、准确的SQL注入点分类、说明及举例&#xff1a;…

EKSPod 资源利用率配置修复:从占位符到完整资源分析系统

概述 在 Kubernetes 集群管理过程中,资源利用率的监控和优化是保证应用性能和成本效益的关键环节。近期,我们对 EKSPod 管理界面的资源利用率显示功能进行了全面修复,将原先简单的占位符文本升级为完整的资源分析系统。本文将详细介绍这次修复的背景、方案、实现细节和最终…

Linux内核(架构)

文章目录Linux内核架构概述核心子系统详解1、进程管理2、内存管理3、虚拟文件系统(VFS)4、设备驱动模型掌握Linux内核核心技术阶段1&#xff1a;基础准备阶段2&#xff1a;内核基础阶段3&#xff1a;深入子系统阶段4&#xff1a;高级主题&#xff08;持续学习&#xff09;调试和…

基于数据挖掘的单纯冠心病与冠心病合并糖尿病的证治规律对比研究

标题:基于数据挖掘的单纯冠心病与冠心病合并糖尿病的证治规律对比研究内容:1.摘要 背景&#xff1a;冠心病和冠心病合并糖尿病在临床上较为常见&#xff0c;且二者在证治方面可能存在差异&#xff0c;但目前相关系统研究较少。目的&#xff1a;对比基于数据挖掘的单纯冠心病与冠…

即梦AI快速P图

原图&#xff1a; 模型选择3.0效果比较好&#xff0c;提示词“根据提供图片&#xff0c;要求把两边脸变小&#xff0c;要求把脸变尖&#xff0c;要求眼妆变淡&#xff0c;眼睛更有神&#xff0c;要求提亮面部肤色要求面部均匀&#xff0c;面部要磨皮!鼻头高光和鼻翼两边阴影变淡…

【办公类-109-04】20250913圆牌卡片(接送卡被子卡床卡入园卡_word编辑单面)

背景需求: 为了发被子,我做了全校批量的圆形挂牌,可以绑在“被子包”提手上,便于再操场上发放被子时,很多老师可以协助根据学号发放。 https://blog.csdn.net/reasonsummer/article/details/149755556?spm=1011.2415.3001.5331https://blog.csdn.net/reasonsummer/arti…

Shoptnt 促销计算引擎详解:策略模式与责任链的完美融合

在电商系统中&#xff0c;促销计算是业务逻辑最复杂、变更最频繁的模块之一。它不仅需要处理多种促销类型&#xff08;满减、折扣、优惠券等&#xff09;&#xff0c;还要管理它们之间的优先级和互斥关系。 Shoptnt 设计了一套基于 策略模式 (Strategy Pattern) 和 责任链模式…

【HTTP 请求格式】从请求行 到 请求体

引言 在前后端开发中&#xff0c;前端和后端之间的交互主要依赖于 HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;。HTTP 是互联网通信的基础&#xff0c;它定义了客户端&#xff08;通常是浏览器或App&#xff09;和服务器之间如何交换数…

【自记】SQL 中 GROUPING 和 GROUPING SETS 语句的案例说明

我们用一个生活中的例子来理解&#xff0c;比如你开了家小超市&#xff0c;想统计「销售额」&#xff0c;但需要从多个角度看&#xff08;比如按 “日期 商品”、“仅日期”、“仅商品”、“整体总销售额”&#xff09;。假设你的销售数据长这样&#xff08;简化版&#xff09…

C语言第五课:if、else 、if else if else 控制语句

C语言第五课&#xff1a;if、else 、if else if else 控制语句if else 、if else if else 联合使用编程快速学习平台if else 、if else if else 联合使用 代码示列 #include <stdio.h> int main(){//设置中文编码输出到控制台system("chcp 65001");//今天星…

七彩喜智慧养老:用科技温暖晚年,让关爱永不掉线

“当银发潮遇见科技力&#xff0c;养老方式正在发生一场静悄悄的变革。”你有没有想过&#xff1a;当父母年迈独居时&#xff0c;如何确保他们的安全&#xff1f;当老人突然摔倒&#xff0c;如何第一时间获得救助&#xff1f;当慢性病需要长期管理&#xff0c;如何避免频繁奔波…