linux并发服务器 —— linux网络编程(七)

网络结构模式

C/S结构 - 客户机/服务器;采用两层结构,服务器负责数据的管理,客户机负责完成与用户的交互;C/S结构中,服务器 - 后台服务,客户机 - 前台功能;

优点

1. 充分发挥客户端PC处理能力,先在客户端处理再提交服务器,响应速度快;

2. 操作界面好看,满足个性化需求;

3. 安全性较高,面向固定用户群,程序更注重流程;

缺点

1. 需要安装专用的客户端软件;

2. 对客户端的操作系统有限制,不能跨平台;

B/S结构 - 浏览器/服务器;将系统功能实现的核心部分集中于服务器,简化系统开发,维护;

优点

总体成体低,维护方便,分布性强,开发简单;

缺点

1. 通信开销大,系统和数据的安全性较低;

2. 无法实现个性化的功能要求;

3. 协议固定;

4. 响应速度明显降低;

MAC地址、IP地址、端口

MAC地址

网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件,又称为网络透配器或网络接口卡NIC,其拥有 MAC 地址,属于 OS 模型的第 2层。

每个网卡都有一个被称为MAC地址的第一无二的48位串行号(以太网卡/无线网卡);

网卡的功能:

1. 数据封装与解封装

2. 链路管理

3. 数据编译与译码

MAC地址 - 媒体存取控制地址/局域网地址/以太网地址/物理地址/硬件地址

MAC地址是用来确认网络设备位置的地址,由网络设备制造商生产时烧录在网卡中;一台设备可以有多个网卡;

IP地址

IP地址是互联网的协议地址,是IP协议提供的一种统一的地址格式,为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此屏蔽物理地址的差异;

A类IP地址 - 一个字节网络地址,三个字节主机地址;1.0.0.1 - 126.255.255.254;子网掩码255.0.0.0;用于广域网

B类IP地址 - 两个字节网络地址,两个字节主机地址;128.0.0.1 - 191.255.255.254;子网掩码255.255.0.0;用于城际网络

C类IP地址 - 三个字节网络地址,一个字节主机地址;192.0.0.1 - 223.255.255.254;子网掩码255.255.255.0;用于局域网

0.0.0.0 - 当前主机

255.255.255.255 - 当前子网的广播地址

IP地址不能以127开头,127.0.0.1可以代表本机IP地址

子网掩码

子网掩码必须结合IP地址一起使用,用于屏蔽IP地址一部分,区分网络地址和主机地址;

192.168.100.10/24 - IP地址192.168.100.10 , 子网掩码24个1;

端口

设备与外界通讯交流的出口 - 虚拟端口/物理端口;

虚拟端口是逻辑意义上的端口,特指TCP/IP协议中的端口;一个IP地址可以由65536个端口,端口通过端口号标识 0 - 65535;一个计算机中不能出现同样端口号的进程,不用进程号的原因是因为进程号是变化的;0~1023是周知端口,紧密绑定一些特定服务,不能自己设置使用;

1024~49151为注册端口,用户选择安装的一些应用程序

49152~65535 动态分配

网络模型

七层参考模型/osi参考模型

1. 物理层 - 定义物理设备的标准(接口类型、传输速率)

2. 数据链路层 - 提供介质访问、链路管理

3. 网络层 - IP选址、路由选择

4. 传输层 - 建立、管理、维护端到端的连接

5. 会话层 - 建立、管理、维护会话

6. 表示层 - 数据格式转化、数据加密

7. 应用层 - 为应用程序提供服务,用户和网络服务的接口

TCP/IP四层模型

协议

通信双方必须共同遵从的一组约定;三要素:语法、语义、时序,最终体现为在网络上传输的数据包格式;各个层之间的协议不互相影响

应用层协议 - FTP(文件传输)/HTTP(超文本传输协议)/NFS(网络文件协议)

传输层协议 - TCP(传输控制协议)/UDP(用户数据包协议)

网络层协议 - IP(因特网互联协议)/ICMP(因特网控制报文协议)/IGMP(因特网组管理协议)

网络接口层协议 - ARP(地址解析协议)/RARP(反向地址解析协议)

UDP协议

TCP协议

IP协议

以太网帧协议

ARP协议

网络通信的过程

封装 - 上层协议通过封装使用下层协议提供的服务;每层协议在上层数据的基础上加上自己的头部/尾部信息,实现该层的功能;

分用 - 帧到达主机,沿着协议栈自底向上依次传递,各层协议处理本层负责的头部数据,获取信息;

ARP协议 - 通过IP地址查找MAC地址; - 28个字节

RARP协议 - 通过MAC地址查找IP地址;

Socket介绍

套接字 - 对网络中不同主机上的应用进程之间进行双向通信的端点的抽象;一个套接字就是网络上进程通信的一段,提供了应用层进程利用网络协议交换数据的机制;上联应用程序,下联网路协议栈,是应用程序与网络协议进行交互的接口;

通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket通过与网络接口卡 (NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段言息。socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。

在linux环境下,用于表示进程间网络通信的特殊文件类型;本质为内核借助缓冲区形成的伪文件;

套接字通信分两部分

服务器端:被动接收连接,一般不主动 

客户端:主动向服务器发起连接

socket地址

socket地址是一个结构体 - 封装IP和端口号

sa_family是地址族类型的变量,地址族类型通常与协议族相对应;sa_data存放socket地址值

PF_UNIX - 文件路径名 - 108字节

PF_INET - 6个字节 , 16位端口号,32位IP

PFINET6 - 26个字节 , 16位端口号,32位流标识 ,128位IP,32bit范围ID

为了方便使用提出专用的SOCKET地址:

所有专用socket地址实际使用时都需要转换为通用的socket地址;

socket函数

#inc]ude <sys/types .h>
#incIude <sys/socket .h>
#incTude <arpa/inet .h> // 包含了该头文件上面两个可以省略
int socket(int domain, int type, int protoco1);功能:创建一个套接字参数:domain - 协议族 AF_INET - ipv4 AF_INET6 - ipv6AF_UNIX AF_LOCAL - 本地套接字通信(进程间)type - 通信过程中实现的协议类型SOCK_STREAM - 流式协议SOCK_DGRAM - 报式协议protocol - 具体的协议0 - SOCK_STREAM (TCP)- SOCK_DGRAM (UDP)返回值:成功 - 返回文件描述符,操作的就是内核缓冲区失败 - -1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);功能:绑定fd和本地的IP/端口参数:sockfd - socket函数得到的fdaddr - 需要绑定的socket地址addrlen - 第二个参数结构体的内存大小
int listen(int sockfd, int backlog);// /proc/sys/net/core/somaxconn功能:监听socket上的连接参数:sockfd - 文件描述符backlog - 未连接和已连接的和的最大值
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);功能:接收客户端连接,默认阻塞,等待客户端连接进来参数:sockfd - 文件描述符addr - 传出参数,记录了连接成功后客户端的地址信息addrlen - 第二个参数的内存大小返回值成功 - 用于通信的文件描述符失败 - -1
int connect(int sockfd, const struct sockaddr *addr , socklen_t addrlen) ;功能:客户端连接服务器参数:sockfd - 用于通信的文件描述符addr - 客户端要连接的服务器的地址信息addtrlen - 第二个参数的内存大小返回值:成功 - 0失败 - -1
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf,size_t count);

字节序

字节序,顾名思义宁节的顺序,就是大于一个宁节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)

字节序分为大端字节序和小端字节序;

大端小端的判断

/*字节序判断 - 大端/小端
*/
#include <iostream>
using namespace std;int main(){union{short value;char byte;}test;test.value = 0x0102;if(test.byte == 1){cout<<"大端存储"<<endl;}else if(test.byte == 2){cout<<"小端存储"<<endl;}else{cout<<"未知"<<endl;}return 0;
}

字节序转换函数

格式化的数据在两台不同字节序的主机之间传递会发生问题;所以需要发送端先转大端,接收端再根据自身情况进行转换;socket提供了封装好的转换函数

s - 转换端口;l - 转换IP

 网络通信时,需要将主机字节序转换成网络字节序(大端),另外一段获取到数据以后根据情况将网络字节序转换成主机字节

IP地址转换

将字符串IP转为整数/主机网络字节序转换

#include <arpa/inet.h>
// p - 点分十进制字符串 ; n - 网络字节序的整数
int inet_pton(int af, const char *src, void *dst);af - 地址族AF - INET IPV4AF - INET6 IPV6src - 需要转换的点分十进制的IP字符串dst - 转换后的结果保存在这儿
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);size - 第三个参数的大小(数组大小)返回值 - 转换后的数据地址,和dst一样
#include <iostream>
#include <arpa/inet.h>
#include<cstdio>
using namespace std;// int inet_pton(int af, const char *src, void *dst);// const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);int main(){//字符串转整数char buf[] = "192.168.1.4";unsigned int num = 0;inet_pton(AF_INET , buf , &num);unsigned char* p = (unsigned char*)&num;cout<<(int)*p<<" "<<(int)*(p+1)<<" "<<(int)*(p+2)<<" "<<(int)*(p+3)<<endl;// 整数转字符串char ip[16] = "";const char* str = inet_ntop(AF_INET , &num , ip , sizeof(ip));cout<<str<<endl;return 0;
}

TCP通信流程

UDPTCP
用户数据包协议传输控制协议
面向无连接面向连接
可以单播,多播,广播只能1v1(单播)
面向数据报基于字节流
不可靠的协议可靠的协议

TCP 通信流程

服务器

        - 创建有一个用于监听的套接字

         - 将监听的文件描述符和本地的IP、端口绑定(IP 端口就是服务器的地址信息)

        - 设置监听,监听的fd开始工作

        - 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端连接,得到和客户端通信的套接字

        - 通信(接受数据/发送数据)

        - 通信结束断开连接

客户端

        - 创建一个用于通信的套接字

        - 连接服务器,需要指定连接服务器的IP/端口

        - 连接成功了,发生通信

        - 通信结束断开连接

TCP通信实现(服务端/客户端)

// 实现TCP服务器端
#include <iostream>
#include <arpa/inet.h>
#include<cstdio>
#include <unistd.h>
#include <string.h>
using namespace std;int main(){// 1. 创建socket(用于监听)int lfd = socket(AF_INET , SOCK_STREAM , 0);// 2. 绑定struct sockaddr_in saddr;saddr.sin_family = PF_INET;inet_pton(AF_INET , "192.168.93.129" , &saddr.sin_addr.s_addr);saddr.sin_port = htons(9998);bind(lfd , (struct sockaddr*)&saddr , sizeof(saddr));// 3. 监听listen(lfd , 8);// 4. 接收客户端连接struct sockaddr_in caddr;socklen_t len = sizeof(caddr); int cfd = accept(lfd , (struct sockaddr*)&caddr , &len);// 输出客户端的信息char client_ip[16];inet_ntop(AF_INET , &caddr.sin_addr.s_addr , client_ip , sizeof(client_ip));unsigned short client_port = ntohs(caddr.sin_port);cout<<"IP:"<<client_ip<<" "<<"PORT: "<<client_port<<endl;// 获取客户端的数据char buf[1024] = {0};len = read(cfd , buf , sizeof(buf));cout<<"服务器读取数据:"<<buf<<endl;// 给客户端发数据const char *str = "hello 647";write(cfd , str , strlen(str));close(lfd);close(cfd);return 0;
}
// TCP通信客户端
#include <iostream>
#include <arpa/inet.h>
#include<cstdio>
#include <unistd.h>
#include <string.h>
using namespace std;int main(){// 1. 创建套接字int fd = socket(AF_INET , SOCK_STREAM , 0);// 2. 连接服务器端struct sockaddr_in caddr;caddr.sin_family = AF_INET;inet_pton(AF_INET , "192.168.93.129" , &caddr.sin_addr.s_addr);caddr.sin_port = htons(9998);connect(fd , (struct sockaddr*)&caddr , sizeof(caddr));// 3. 读写数据const char *str = "hello zry";write(fd , str , strlen(str));char buf[1024] = {0};int len = read(fd , buf , sizeof(buf));cout<<"客户端读取数据:"<<buf<<endl;close(fd);return 0;
}

TCP三次握手

三次握手发生在客户端丽连接,调用connect(),底层会通过TCP协议进行三次握手;

注意:第三次握手可以携带数据

滑动窗口

滑动窗口的大小意味着接收方还有多大的缓冲区可用于接收数据;

滑动窗口的大小会随着发送数据/接收数据而变化;

通信双方都有发送缓冲区和接收缓冲区

mss: 一条数据最大的数据量;
win: 滑动窗口;

TCP四次挥手

发生在断开连接的时候,程序调用close()会使用TCP协议进行四次挥手;

客户端/服务端都可以主动发起/断开连接,谁调用close()就是谁发起的;

注意:发起方FIN请求可以携带数据!!

多进程实现并发服务器

实现TCP通信服务器处理并发的任务,使用多线程或者多进程解决;

1. 一个父进程,多个子进程

2. 父进程负责等待并接收客户端的连接

3. 子进程:完成通信,接收一个客户端请求就创建一个子进程

#include <iostream>
#include <arpa/inet.h>
#include<cstdio>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
using namespace std;void fun(int arg){while(1){int ret = waitpid(-1 , NULL , WNOHANG);if(ret == - 1){break;}else if(ret == 0){break;}else{cout<<"回收到了子进程: "<<ret<<endl;}}
}int main(){// 注册信号捕捉struct sigaction act;act.sa_flags = 0;sigemptyset(&act.sa_mask);act.sa_handler = fun;sigaction(SIGCHLD , &act , NULL);// 创建int lfd = socket(AF_INET , SOCK_STREAM , 0);if(lfd == -1){perror("socket");exit(0);}// 绑定struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(9999);saddr.sin_addr.s_addr = INADDR_ANY;int ret = bind(lfd , (struct sockaddr*)&saddr , sizeof(saddr));if(ret == -1){perror("bind");exit(0);}// 监听ret = listen(lfd , 5);if(ret == -1){perror("listen");exit(0);}// 不断循环等待客户端连接while(1){struct sockaddr_in caddr;socklen_t len = sizeof(caddr);int cfd = accept(lfd , (struct sockaddr*)&caddr , &len);if(cfd == -1){if(errno == EINTR){continue;}perror("accept");exit(0);}// 每一个连接进来,创建子进程与客户端通信pid_t pid = fork();if(pid == 0){// 获取客户端信息char ip[16];inet_ntop(AF_INET , &caddr.sin_addr.s_addr , ip , sizeof(ip));unsigned short port = ntohs(caddr.sin_port);cout<<"IP: "<<ip<<" "<<"port: "<<port<<endl;// 接收客户端发来的数据char buf[1024] = {0};while(1){int len = read(cfd , &buf , sizeof(buf));if(len == -1){perror("read");exit(0);}else if(len > 0){cout<<"读到了数据:"<<buf<<endl;}else{cout<<"已经断开连接了.....";}write(cfd , buf , strlen(buf));}}close(cfd);}close(lfd);return 0;
}
// TCP通信的客户端
#include <iostream>
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
using namespace std;
int main() {// 1.创建套接字int fd = socket(AF_INET, SOCK_STREAM, 0);if(fd == -1) {perror("socket");exit(-1);}// 2.连接服务器端struct sockaddr_in caddr;caddr.sin_family = AF_INET;inet_pton(AF_INET , "192.168.93.129" , &caddr.sin_addr.s_addr);caddr.sin_port = htons(9999);int ret = connect(fd, (struct sockaddr *)&caddr, sizeof(caddr));if(ret == -1) {perror("connect");exit(-1);}// 3. 通信char recvBuf[1024];int i = 0;while(1) {sprintf(recvBuf, "data : %d\n", i++);// 给服务器端发送数据write(fd, recvBuf, strlen(recvBuf)+1);int len = read(fd, recvBuf, sizeof(recvBuf));if(len == -1) {perror("read");exit(-1);} else if(len > 0) {printf("recv server : %s\n", recvBuf);} else if(len == 0) {// 表示服务器端断开连接printf("server closed...");break;}sleep(1);}// 关闭连接close(fd);return 0;
}

多线程实现并发服务器 - client同多进程并发

#include <iostream>
#include <arpa/inet.h>
#include <cstdio>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
using namespace std;struct sockInfo{int fd;pthread_t tid;struct sockaddr_in addr;
};struct sockInfo sockinfos[128];void *work(void * arg){// 子线程和客户端通信 cfd/客户端信息/线程号// 获取客户端信息struct sockInfo *pinfo = (struct sockInfo *)arg;char ip[16];inet_ntop(AF_INET , &pinfo->addr.sin_addr.s_addr , ip , sizeof(ip));unsigned short port = ntohs(pinfo->addr.sin_port);cout<<"IP: "<<ip<<" "<<"port: "<<port<<endl;// 接收客户端发来的数据char buf[1024] = {0};while(1){int len = read(pinfo->fd , &buf , sizeof(buf));if(len == -1){perror("read");exit(0);}else if(len > 0){cout<<"读到了数据:"<<buf<<endl;}else{cout<<"已经断开连接了.....";break;}write(pinfo->fd , buf , strlen(buf)+1);}close(pinfo->fd);return NULL;
}int main(){// 创建int lfd = socket(AF_INET , SOCK_STREAM , 0);if(lfd == -1){perror("socket");exit(0);}cout<<1;// 绑定struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(9999);saddr.sin_addr.s_addr = INADDR_ANY;int ret = bind(lfd , (struct sockaddr*)&saddr , sizeof(saddr));if(ret == -1){perror("bind");exit(0);}// 监听ret = listen(lfd , 5);if(ret == -1){perror("listen");exit(0);}// 初始化数据int max = sizeof(sockinfos)/sizeof(sockinfos[0]);for(int i = 0 ; i<max ; i++){bzero(&sockinfos[i] , sizeof(sockinfos[i]));sockinfos[i].fd = -1;sockinfos[i].tid = -1;}// 不断循环等待客户端连接(子线程创建)while(1){struct sockaddr_in caddr;socklen_t len = sizeof(caddr);int cfd = accept(lfd , (struct sockaddr*)&caddr , &len);struct sockInfo *pinfo;for(int i = 0 ; i<max ; i++){// 从数组中找到可用的sockInfoif(sockinfos[i].fd == -1){pinfo = &sockinfos[i];break;}if(i == max - 1){sleep(1);i--;}}pinfo->fd = cfd;memcpy(&pinfo->addr , &caddr , len);// 每一个连接进来,创建子线程与客户端通信pthread_t tid;pthread_create(&pinfo->tid , NULL , work , pinfo);pthread_detach(pinfo->tid);}close(lfd);return 0;
}

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

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

相关文章

【二等奖方案】大规模金融图数据中异常风险行为模式挖掘赛题「冀科数字」解题思路

第十届CCF大数据与计算智能大赛&#xff08;2022 CCF BDCI&#xff09;已圆满结束&#xff0c;大赛官方竞赛平台DataFountain&#xff08;简称DF平台&#xff09;正在陆续释出各赛题获奖队伍的方案思路&#xff0c;欢迎广大数据科学家交流讨论。 本方案为【大规模金融图数据中…

机器学习笔记之最优化理论与方法(六)无约束优化问题——最优性条件

机器学习笔记之最优化理论与方法——无约束优化问题[最优性条件] 引言无约束优化问题无约束优化问题最优解的定义 无约束优化问题的最优性条件无约束优化问题的充要条件无约束优化问题的必要条件无约束优化问题的充分条件 引言 本节将介绍无约束优化问题&#xff0c;主要介绍无…

Spring Boot 整合 Shiro(后端)

1 Shiro 什么是 Shiro 官网&#xff1a; http://shiro.apache.org/ 是一款主流的 Java 安全框架&#xff0c;不依赖任何容器&#xff0c;可以运行在 Java SE 和 Java EE 项目中&#xff0c;它的主要作用是对访问系统的用户进行身份认证、 授权、会话管理、加密等操作。 …

avue实现用户本地保存自定义配置字段属性及注意事项(基于tj-vue2-tools)

avue实现用户本地保存自定义配置字段属性及注意事项&#xff08;基于tj-vue2-tools&#xff09; tj-vue2-tools项目地址&#xff1a;https://www.npmjs.com/package/tj-vue2-tools 文档请看项目官方 依赖js-base64 安装依赖 npm install js-base64安装 npm install tj-vue2-t…

Python综合案例(基本地图使用)

一、基本地图的使用 基本代码&#xff1a; """ 演示地图可视化的基本使用 """ from pyecharts.charts import Map from pyecharts.options import VisualMapOpts# 准备地图对象 map Map() # 准备数据 data [("北京", 99),("…

02-Flask-对象初始化参数

对象初始化参数 前言对象初始化参数import_namestatic_url_pathstatic_foldertemplate_floder 前言 本篇来学习Flask中对象初始化参数 对象初始化参数 import_name Flask程序所在的包(模块)&#xff0c;传__name__就可以 _name_ 是一个标识 Python 模块的名字的变量&#x…

网络协议从入门到底层原理学习(一)—— 简介及基本概念

文章目录 网络协议从入门到底层原理学习&#xff08;一&#xff09;—— 简介及基本概念一、简介1、网络协议的定义2、网络协议组成要素3、广泛的网络协议类型网络通信协议网络安全协议网络管理协议 4、网络协议模型对比图 二、基本概念1、网络互连模型2、计算机之间的通信基础…

配置本地maven

安装maven安装包 修改环境变量 vim ~/.bash_profile export JMETER_HOME/Users/yyyyjinying/apache-jmeter-5.4.1 export GOROOT/usr/local/go export GOPATH/Users/yyyyjinying/demo-file/git/backend/go export GROOVY_HOME/Users/yyyyjinying/sortware/groovy-4.0.14 exp…

百度低质量站点怎么办?解决百度低质量站点的方法和工具

百度低质量站点怎么恢复&#xff1f;这是许多网站主和运营人员在SEO优化过程中经常面临的一个问题。百度作为中国最大的搜索引擎&#xff0c;对于网站收录和排名具有至关重要的影响。然而&#xff0c;由于各种原因&#xff0c;有些网站可能面临被百度降权或收录减少的情况。那么…

IDea寻找冲突的依赖包

场景&#xff1a;boot项目运行时&#xff0c;提示log4j2依赖包冲突。 SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/D:/maven/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/Stati…

AIGC专栏5——EasyPhoto AI写真照片生成器 sd-webui插件介绍、安装与使用

AIGC专栏5——EasyPhoto AI写真照片生成器 插件安装与使用 学习前言源码下载地址技术原理储备&#xff08;SD/Control/Lora&#xff09;StableDiffusionControlNetLora EasyPhoto插件简介EasyPhoto插件安装安装方式一&#xff1a;Webui界面安装 &#xff08;需要良好的网络&…

stable diffusion实践操作-大模型介绍-SDXL1大模型

系列文章目录 大家移步下面链接中&#xff0c;里面详细介绍了stable diffusion的原理&#xff0c;操作等&#xff08;本文只是下面系列文章的一个写作模板&#xff09;。 stable diffusion实践操作 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生…