跟着小潘学后端
【C++】C++程序设计入门二
前言在上一节中,我们选了一个问题,演示了如何根据问题编写c++程序,并在过程中讲解了部分知识。本节内容旨在上一节基础上继续讲解程序设计入门的基础知识。本篇有的知识难度比较高,可以先不学,后续对C++其他知识比较熟练后再学习。一、变量1.导入在初等数学中,变量是表示数字的字母字符,具有任意性和未知性。上图中,我们使用N表示了购买苹果的数量。当我们需要算总价的时候,直用N*2就能得到。假如老鼠发现了一堆糖果,糖果有N颗。它一次可以背走a颗糖果。你能完成下方的表格吗?运走糖果的次数1次2次8次X次剩余的糖果数N-a???如果你能完成上述问题,我们就可以学习C++中的变量了。2.变量C++为什么需要变量?实际上和我们冬天一样的,都是因为真的离不开。比如,我们去超市买东西。结账的时候,收银员需要知道购买的物品和价格,这些信息可以用变量来存储。我们可以用一个变量来存储购买的物品名称,另一个变量来存储价格,这样就可以方便地进行计算和管理。如果没有变量,超市收银员就需要记住所有的物品和价格,我觉得超市收银员还没上岗就疯了。通过例子我们可以看到为了社会和平,需要变量。2.1 变量的作用C++ 变量是程序中用于存储数据值的一种占位符,可以理解为用来存储东西的箱子。我们在使用变量的时候,使用的不是这个箱子,而是箱子里面的东西。当然有了变量以后,为了防止它丢,C++还会记下它的地址,当我们想用它的时候能马上找到它。2.2 变量的定义那我们如何拥有一个属于我们自己的变量呢?其实这件事就和养宠物是一样的。养宠物之前我们是不是得先想一下养个什么种类的宠物,是养猫呢?还是养小狗呢?选好养什么宠物后,是不是还得给宠物取个名字,旺财、来福、咪咪等等。好吧,我不擅长取名字QAQ。取好名字以后,就可以把宠物买回家了。在创建变量的时候有几个点需要注意:1. 变量没有定义之前不能使用。感觉说了一句废话,宠物都没有买回家怎么摸?但实际上是初学者容易出现的错误。比如没有区分大小写或者中途改了变量的名字又忘记改其他地方就有可能出现找不到变量的情况。2. 变量名具有唯一性。这个也很简单,一个宠物对应一个名字要区分开,总不能养了5只猫全叫“咪咪”吧。虽然不能5只猫不能同时叫咪咪,但是可以把咪咪这个名字从原来的猫给其他猫。这就是变量的重新赋值,具体操作后面会讲。3. 多个变量名之间用逗号“,”隔开,结尾用分号“;”结束。你可以试试不隔开,然后你就会隔开了。3. 变量名应该见名知意,不要随意乱取。比如不要给你家咪咪,取个旺财或者来福这种一听就是小狗的名字。也不要jahsfadshsdajdsis 这样去叫你家的宠物,别人来你家做客想和你的宠物互动都跟唐僧过81难一样。4. 变量名不要用数字开头,可以用数字结尾。会报错,别试了。5. 呃,先到这吧,多了你们也记不住。在 C++中变量定义后如果不赋值,那么他的值是随机的,因此变量定义后,在使用之前一定要赋值(多定义几个变量,不赋值试试看)二、常量常量和变量一听名字就知道两个差不多,不同的是常量在程序执行期间其值不会发生改变,也就是不能重新赋值。常量可以分为字面常量和符号常量两种类型。C++中的字面常量不需要定义。字面常量是指在程序中直接使用的常量值,例如整型常量、字符型常量、字符串常量等。这些常量的值在程序编译时就已经确定了,因此不需要进行定义。例如:int num = 10; // 定义一个整型变量num并初始化为10
int result = num + 20; // 使用字面常量20进行计算在上面的代码中,20就是一个整型字面常量,它直接出现在程序中进行计算,而不需要进行定义。如果要在程序中多次使用同一个常量值,可以使用const关键字定义一个符号常量。比如计算圆的面积、周长、体积,我们需要多次用到圆周率。圆周率又是一个确定的值,我们就可以把圆周率定义为符号常量,这样可以提高程序的可读性和可维护性。例如:#include <iostream>
using namespace std;
const double PI = 3.14159265358979323846; // 定义圆周率常量
int main() {
double r, c, s, v;
cout << "请输入圆的半径:";
cin >> r;
c = 2 * PI * r; // 计算圆的周长
s = PI * r * r; // 计算圆的面积
v = 4.0 / 3.0 * PI * r * r * r; // 计算圆的体积
cout << "圆的周长为:" << c << endl;
cout << "圆的面积为:" << s << endl;
cout << "圆的体积为:" << v << endl;
return 0;
}C++中定义符号常量的方法有两种:使用const关键字和使用宏定义。使用const关键字定义符号常量的语法格式如下:const 数据类型 常量名 = 常量值;只是在变量的定义基础上加了const关键字,需要注意的是常量名全部大写,养成良好的代码习惯。关于宏定义方式,感兴趣的同学可以自行百度,更推荐使用const。三、标识符其实标识符就是我们前面讲到的变量名。准确的说变量名则是标识符的一种。数据类型 变量名 = 值;标识符是用来标识变量、函数、类、结构体等各种程序实体的名称。标识符必须以字母、下划线或者汉字开头,后面可以跟字母、下划线、数字或者汉字。C++标识符是区分大小写的,长度没有限制。以下是一些C++标识符的命名规则:标识符不能是C++关键字,例如if、else、while等。标识符不能包含空格。标识符不能以数字开头。标识符不能包含特殊字符,例如@、#、$等。标识符应该具有描述性,能够清晰地表达其含义。以下是一个C++标识符的例子:int myVariable = 10;在这个例子中,myVariable是一个标识符,用来标识一个整型变量。四、关键字C++关键字是指在C++编程语言中具有特殊含义的保留字,这些关键字不能作为标识符使用。C++中的关键字包括:auto、bool、break、case、catch、char、class、const、constexpr、continue、decltype、default、delete、do、double、dynamic_cast、else、enum、explicit、export、extern、false、float、for、friend、goto、if、inline、int、long、mutable、namespace、new、nullptr、operator、private、protected、public、register、reinterpret_cast、return、short、signed、sizeof、static、static_assert、static_cast、struct、switch、template、this、thread_local、throw、true、try、typedef、typeid、typename、union、unsigned、using、virtual、void、volatile、wchar_t、while等。并不需要将关键字全部记住,在Dev C++中关键字都会被不同的颜色和字体粗细区别出来。五、字符串(难度较高)C++中的字符串是由一系列字符组成的。字符串可以使用字符数组或string类来表示。5.1 使用字符数组表示字符串5.1.1 什么是数组数组,就是一个集合,里面存放了相同类型的数据元素。大家都去买过电池吧,一个包装里面有很多块电池,电池都是5号属于相同类型的,并且连续排列在一起。我们就可以把数组和这种包装好的电池结合在一起理解。特点1:数组中的每个数据元素都是相同的数据类型特点2:数组是由连续的内存位置组成的5.1.2 字符数组使用方法字符数组:存储字符的数组1. 定义字符数组定义字符数组的语法如下:char array_name[array_size];其中,array_name是字符数组的名称,array_size是字符数组的大小,即字符序列的长度。例如,定义一个长度为10的字符数组可以这样写:char str[10];2. 初始化字符数组字符数组可以通过以下方式进行初始化:2.1. 直接赋值char str[] = "hello";2.2. 逐个赋值char str[6];
str[0] = 'h';
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';
str[5] = '\0'; // 字符串以'\0'结尾2.3. 使用字符串常量初始化char str[] = {'h', 'e', 'l', 'l', 'o', '\0'};3.访问字符数组可以使用下标运算符[]来访问字符数组中的元素。例如,访问字符数组str中的第一个字符可以这样写:char ch = str[0];4.输出字符数组char str[] = "Hello World"; // 定义一个字符数组
cout << str << endl; // 输出字符串5.字符数组作为函数参数字符数组可以作为函数的参数传递。例如,以下函数将打印出传入的字符串:void printString(char str[]) {
cout << str << endl;
}
5.2 使用string类表示字符串C++中的string类是一个非常常用的字符串处理类。string类提供了一系列的成员函数,可以方便地进行字符串的操作,例如查找、替换、插入、删除等等。1. 字符串的定义和初始化#include <string>
using namespace std;
string str1; // 定义一个空字符串
string str2 = "hello"; // 定义并初始化一个字符串
string str3("world"); // 使用构造函数定义并初始化一个字符串2. 字符串的输入和输出#include <iostream>
#include <string>
using namespace std;
string str;
cin >> str; // 从标准输入读入一个字符串
cout << str << endl; // 输出字符串到标准输出3. 保留空格的字符串输入#include <iostream>
#include <string>
using namespace std;
string str;
getline(cin, str); // 从标准输入读入一个字符串,保留空格
cout << str << endl; // 输出字符串到标准输出4. 字符串的拼接#include <string>
using namespace std;
string str1 = "hello";
string str2 = "world";
string str3 = str1 + " " + str2; // 字符串拼接5. 字符串的查找和替换#include <string>
using namespace std;
string str = "hello world";
int pos = str.find("world"); // 查找子串的位置
str.replace(pos, 5, "you"); // 替换子串6. 字符串的反转#include <algorithm>
#include <string>
using namespace std;
string str = "hello";
reverse(str.begin(), str.end()); // 反转字符串7. 字符串的截取#include <string>
using namespace std;
string str = "hello world";
string sub = str.substr(6, 5); // 截取子串六、表达式C++表达式是由运算符和操作数组成的式子,可以用来进行各种数学和逻辑运算。6.1 算术表达式(必须掌握)C++算术表达式是由运算符和操作数组成的表达式,可以进行加法、减法、乘法、除法和求模等基本算术运算。以下是一些C++算术表达式的例子:1.加法运算int num1 = 6;int num2 = 8;
int sum = num1 + num2;
cout << "Sum: " << sum << endl; // 输出:Sum: 142.减法运算int num1 = 8;
int num2 = 6;
int diff = num1 - num2;
cout << "Difference: " << diff << endl; // 输出:Difference: 23.乘法运算int num1 = 6;
int num2 = 8;
int product = num1 * num2;
cout << "Product: " << product << endl; // 输出:Product: 484.除法运算int num1 = 8;int num2 = 6;
int quotient = num1 / num2;
cout << "Quotient: " << quotient << endl; // 输出:Quotient: 1
5.求模运算int num1 = 8;
int num2 = 6;
int remainder = num1 % num2;
cout << "Remainder: " << remainder << endl; // 输出:Remainder: 26.2 关系表达式(必须掌握)C++中的关系表达式用于比较两个值之间的关系,返回一个布尔值(true或false)。常见的关系表达式包括:相等关系(==):判断两个值是否相等,如果相等则返回true,否则返回false。不等关系(!=):判断两个值是否不相等,如果不相等则返回true,否则返回false。大于关系(>):判断左侧的值是否大于右侧的值,如果是则返回true,否则返回false。小于关系(<):判断左侧的值是否小于右侧的值,如果是则返回true,否则返回false。大于等于关系(>=):判断左侧的值是否大于或等于右侧的值,如果是则返回true,否则返回false。小于等于关系(<=):判断左侧的值是否小于或等于右侧的值,如果是则返回true,否则返回false。例如,下面的代码演示了如何使用关系表达式判断一个数的奇偶性:#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
if (n % 2 == 0) {
cout << "even" << endl;
} else {
cout << "odd" << endl;
}
return 0;
}6.3 赋值表达式(必须掌握)C++赋值表达式返回的是一个被修改后的左操作数,即一个左值。这意味着,C++中的赋值表达式可以作为另一个表达式的一部分,并且可以在表达式中多次使用。例如:int x = 5;int y = (x = 3); // y的值为3,x的值也为3在这个例子中,赋值表达式(x = 3)将3赋值给了变量x,并返回了x的新值3,这个值被赋给了变量y。因此,y的值为3,x的值也为3。另外,需要注意的是,在C++中,赋值表达式的优先级比大多数运算符都要低,因此在使用时需要注意加上括号。6.4 逻辑表达式(必须掌握)C++中的逻辑表达式用于执行布尔逻辑运算。常用的逻辑运算符包括:逻辑与(&&):当且仅当两个操作数都为true时,结果才为true。逻辑或(||):当两个操作数中至少有一个为true时,结果为true。逻辑非(!):对操作数取反,如果操作数为true,则结果为false;如果操作数为false,则结果为true。例如:int a = 5, b = 3;
bool c = (a < b) && (a > 0); // 结果为false
bool d = (a > b) || (b < 0); // 结果为true
bool e = !(a == b); // 结果为true需要注意的是,逻辑运算符的优先级低于算术运算符和关系运算符,但高于赋值运算符。因此,在使用逻辑运算符时,建议使用括号来明确优先级。6.5 三目运算表达式(难度较高)C++中的三目运算符是一种简洁的条件表达式,它可以根据条件的真假来返回两个不同的值。其语法格式如下:条件表达式 ? 表达式1 : 表达式2其中,条件表达式的值为真时返回表达式1的值,否则返回表达式2的值。例如:int a = 10, b = 20;
int max = (a > b) ? a : b; // 如果a > b,则返回a的值,否则返回b的值在上面的例子中,如果a > b,则max的值为a,否则max的值为b。三目运算符可以嵌套使用,例如:int a = 10, b = 20, c = 30;
int max = (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c);在上面的例子中,如果a > b,则判断a是否大于c,如果是,则max的值为a,否则max的值为c;否则判断b是否大于c,如果是,则max的值为b,否则max的值为c。6.6 位运算表达式(难度较高)C++提供了6种位运算符,分别是按位与(&)、按位或(| )、按位异或(^)、取反(~)、左移(<<)、右移(>>)。这些运算符只能用于整型操作数,即只能用于带符号或无符号的char、short、int与long类型。下面是一些位运算的例子:1.按位与(&):两个操作数的每一位进行与运算,结果为1则该位为1,否则为0。int a = 5; // 二进制表示为101
int b = 3; // 二进制表示为011
int c = a & b; // 二进制表示为001,即12.按位或(|):两个操作数的每一位进行或运算,结果为0则该位为0,否则为1。int a = 5; // 二进制表示为101
int b = 3; // 二进制表示为011
int c = a | b; // 二进制表示为111,即73.按位异或(^):两个操作数的每一位进行异或运算,结果为相同则为0,不同则为1。int a = 5; // 二进制表示为101
int b = 3; // 二进制表示为011
int c = a ^ b; // 二进制表示为110,即64.取反(~):对操作数的每一位进行取反运算,即0变为1,1变为0。int a = 5; // 二进制表示为101
int b = ~a; // 二进制表示为11111111111111111111111111111010,即-65.左移(<<):将操作数的二进制表示向左移动指定的位数,右侧用0填充。int a = 5; // 二进制表示为101
int b = a << 2; // 二进制表示为10100,即20
6.右移(>>):将操作数的二进制表示向右移动指定的位数,左侧用0或1填充(取决于操作数的符号位)。int a = 5; // 二进制表示为101
int b = a >> 2; // 二进制表示为1,即1七、课后作业八、总结以上就是今天要讲的内容,本文在上一节的基础上继续讲解C++入门的基础知识。
跟着小潘学后端
认识Netty
前言上文介绍了几种IO模型以及Java NIO,了解了在网络编程时使用哪种模型可以提高系统性能及效率。即使Java NIO可以帮助开发人员编写和维护网络应用程序,但由于其复杂性以及bug问题,还是诞生很多强大和流行的网络编程框架,比如Netty、Undertow、Grizzly,在平时的开发中大家更倾向于选择这些框架进行开发,而在我们学习和理解网络编程的底层原理时,使用Java NIO可以更加直接和深入地了解底层操作。本文对 Netty 进行简单介绍,并通过 Netty 实现一个HTTP服务器。Netty介绍Netty是一个基于Java的高性能网络应用框架,它封装了Java NIO的复杂性,提供了简单而强大的网络编程API,使得开发者能够更方便地构建高性能、可伸缩的网络应用程序。所以说学习Netty前先理解一下Java NIO是很有必要的,不然云里雾里的。使用Netty能有多强大呢?包括但不限于以下几点:高性能的IO处理:如果基于Java NIO开发一个成熟的应用,要非常注意如ByteBuffer内存泄漏、Channel注册连接、线程管理等问题。而Netty能够更好地处理连接管理、线程模型和内存管理等方面的问题,从而提供更高的吞吐量和更低的延迟。强大的功能扩展:如果基于Java NIO写一个HTTP协议、Websocket协议,那我们需要考虑格式、编解码问题,而Netty提供提供了丰富的扩展点,比如编解码器、处理器和拦截器等,开发人员可以通过不同的配置搭建HTTP、WebSocket、TCP和UDP等协议,也可以轻松地添加编解码器,实现自定义协议。可靠性和稳定性:Netty具有良好的容错能力和稳定性,能够处理各种网络故障和异常情况,并提供了多种容错和恢复机制,如断线重连和心跳机制等。总的来说,在开发网络应用程序时使用Netty能够更专注于业务逻辑。下图为Netty所支持的功能 Netty发展历程为了进一步了解Netty,这里介绍一下Netty的前世今生。2004年:Netty的前身Jboss Netty项目在JBoss公司内部启动,目标是提供一个可扩展的、易用的网络编程框架。2008年:Netty项目在JBoss公司内部开源,并发布了第一个公开版本Netty 3.0。该版本主要针对TCP和HTTP协议进行了支持。2011年:Netty 3.2发布,引入了更多的特性和改进,包括更好的性能和更灵活的API设计。2012年:Netty 4.0发布,这是一个重大的里程碑版本。在这个版本中,Netty进行了全面的重构和优化,引入了新的API设计和更高级的特性。2013年:Netty 4.0获得了广泛的认可和采用,并成为了许多大型互联网公司和项目的首选网络编程框架。同年底发布了5.0.0.Alpha1,目标是对Netty 4改进和优化。2015年:Netty 5在开发过程中遇到了一些挑战和技术难题,决定暂停Netty 5的开发,并将重心转移到Netty 4的改进和维护上。2016年:Netty 4.1发布,基于4.0版本进一步改进和优化,提供了更好的性能和更多的功能。目前有很多知名的项目都选用了Netty作为网络通信的基础,比如知名的RPC框架Dubbo、gRPC,消息队列Kafka、RocketMQ,搜索引擎Elasticsearch等,所以当学习了解这些项目时,Netty会作为一个加分项。Netty核心组件因为Netty是基于Java NIO封装的,更加的抽象,要使用Netty进行开发,必须要熟悉Netty中的几个核心组件,下面一一介绍:Channel(通道):与Java NIO中的SocketChannel一样,可以进行数据的读取和写入。EventLoop(事件循环):EventLoop是Netty的事件处理机制,它负责处理各种事件,包括连接的建立与关闭、数据的读取与写入等。可以理解成Java NIO中的Selector监听socket得事件,只不过Netty时多线程处理,后面代码中有体现。ChannelHandler(通道处理器):用来处理Channel中的事件和数据的组件,例如对数据编解码、业务逻辑处理等。Netty提供了许多内置的ChannelHandler,用于处理网络连接和I/O操作。以下是一些常用的ChannelHandler:ChannelPipeline(通道管道):ChannelPipeline是一个事件处理器链,用于管理和执行ChannelHandler,每个Channel都有一个对应的Pipeline,当数据进入或离开Channel时,会经过Pipeline中的一系列ChannelHandler进行处理。ByteBuf(字节缓冲区):ByteBuf是Netty中的字节容器,用于高效地存储和传输字节数据。与Java NIO的ByteBuffer相比,ByteBuf提供了更灵活的API和更高效的内存管理。Future(异步操作结果):Netty中的操作都是异步的,Future用来获取操作的状态和结果。Bootstrap(引导类):Bootstrap是启动客户端的类,负责配置和启动客户端的相关组件。ServerBootstrap(服务器引导类):ServerBootstrap是创建和启动服务器的类,用于配置和管理服务器的各个组件。实现HTTP服务器下面以HTTP协议为例,用Netty编写一个HTTP服务器。在这之前,我们先用上篇文章的NIOServer接收一下浏览器的请求,大概是这样的:GET / HTTP/1.1
Host: localhost:8888
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
可以看到接收到了一个HTTP请求的报文数据,有请求行、请求头和请求主体,这时候也能看到浏览器返回的响应是:ERR_INVALID_HTTP_RESPONSE,发送的响应无效。为什么?这是因为NIOServer中的输出格式HTTP协议不认识。 所以如果使用Java NIO实现一个HTTP服务器,需要处理很多的工作,但是如果用Netty实现一个HTTP服务器非常简单,直接上代码:import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.CharsetUtil;
import java.util.List;
import java.util.Map;
public class HttpServer {
private final int port;
public HttpServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
//将原始的HTTP请求和响应数据进行编解码
p.addLast(new HttpServerCodec());
//将HTTP请求或响应的多个部分合并成一个完整的消息
p.addLast(new HttpObjectAggregator(65536));
p.addLast(new SimpleChannelInboundHandler<FullHttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
//查询URI中的参数:
QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
Map<String, List<String>> params = decoder.parameters();
System.out.println(params);
ByteBuf content = request.content();
//请求主体中的参数
String requestBody = content.toString(CharsetUtil.UTF_8);
System.out.println(requestBody);
String responseContent = "你好, Netty!";
FullHttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.OK, Unpooled.wrappedBuffer(responseContent.getBytes()));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_HTML + ";charset=utf-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
});
}
});
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
HttpServer server = new HttpServer(port);
System.out.println("Server started on port " + port);
server.start();
}
}
运行这个示例后,你可以使用浏览器或者其他工具发送HTTP请求到 http://localhost:8080 ,一个HTTP服务器就诞生了,非常简单。接下来对代码进行讲解:代码中的b.group(bossGroup, workerGroup)意思是有两个线程组会去处理服务器中的IO事件,bossGroup只用一个线程来专门负责监听服务端的端口,接收客户端连接请求,并将连接分配给 workerGroup 中的 EventLoop 进行处理;workerGroup负责处理已接收的连接的 I/O 事件,将请求解码、处理业务逻辑以及发送响应等操作都交给 EventLoop 来处理。这个是典型的主从Reactor模式,通过将连接的接收和处理分离到不同的线程池中,可以提高网络应用程序的性能,模型如下。 NioServerSocketChannel是指定服务器的Channel类型,还有NioDatagramChannel等类型,取决于应用场景。.handler(new LoggingHandler(LogLevel.INFO))是为bossGroup指定一个通道处理器,记录进出 Channel 的数据流,将相关信息打印到日志中,便于排查。.childHandler()则是为workerGroup中的EventLoop配置处理器,比如请求解码、处理业务逻辑以及发送响应。ChannelPipeline就是添加具体的通道处理器,代码中的HttpServerCodec、HttpObjectAggregator处理器都是用来处理HTTP请求的编解码,SimpleChannelInboundHandler则是拿到经过多个处理器的数据流后进行业务逻辑及响应。总结Netty是一个非常优秀的、强大的、高性能的网络通信框架,在这个互联网飞速发展的时代,我们需要了解并且使用像这样的优秀的框架,帮助我们快速开发应用,在使用它的同时要知其原理,也可以在业务中进行创新,就像Dubbo、gRPC、Zookeeper一样采用Netty成为与其一样优秀的框架。
跟着小潘学后端
【C++】C++程序结构入门之循环结构一
一、导入史蒂芬·斯特兰奇是漫威世界中最强大的魔法师之一,原本是一位优秀的神经外科医生,凭借高超的医术被外界誉为上帝对手术界的恩赐,但好景不长,在一场意外的车祸断送了他的从医生涯,赖以生存的双手粉碎性骨折,再也无法拿起手术刀。他走遍世界,只为了治好双手,最终,他在喜马拉雅山遇见了古一,被古一收为弟子,他开始学习神秘的咒术与魔法,成为一代至尊魔法师—奇异博士。那么在奇异博士的诸多魔法中,你最想学习哪种呢?路人:那还用问吗?当然是传送门啊,这样早上就可以多睡几分钟再起床了o( ̄▽ ̄)ブ吃瓜群众:Σ(っ °Д °;)っ路人:而且传送门还可以困住敌人😁吃瓜群众:(⊙o⊙)?路人:一个传送门放在他的脚下,另外一个传送门放在他的头上。大致是这样的:吃瓜群众:他落下来不还是可以捅你吗?🙄路人:那如果我会循环魔法呢?🤪吃瓜群众:循环?什么是循环?😲路人:循环就是可以让任何东西重复地做同一件事情。比如你早上要很早的起床然后去上学,然后疲惫的回到家里还要做该死的作业,好不容易做完又得洗漱睡觉。第二天很早的起床去上学 …吃瓜群众:求求你,别说了。路人:怎么样循环魔法厉害吧😋吃瓜群众:听你这么一说,好像是挺厉害(~ ̄▽ ̄)~,我要学循环魔法!!!!路人:二、while循环while循环是一种常用的循环结构,它会重复执行一段代码,直到指定的条件不再满足为止。它的语法格式如下:while (condition) {
// 循环体语句
}其中,condition是循环条件,当condition为真时,执行循环体语句,执行完循环体语句后再次判断condition是否为真,如果为真则继续执行循环体语句,否则跳出循环。路人:上面就是循环魔法的咒语。吃瓜群众:路人:我就以上面的坏人举例,看我施展魔法。while (它是坏人!) {
// 循环体语句
加载传送门。
}吃瓜群众:他不能停下来吗?路人:可以,等我蓝条用完(计算机死机)。吃瓜群众:路人:咳咳~~,这样肯定是不行。while循环会重复执行一段代码,直到指定的条件不再满足为止。所以我们在每一次循环体运行以后要对条件进行更新。while (它是坏人!) {
// 循环体语句
加载传送门。
//条件更新
你是好人吗?
}路人:如果他是好人,那么就不满足循环的条件,魔法(程序)就会终止。路人:怎么样?你学会了吗?吃瓜群众:路人:ok,你出师了。三、例题讲解问题一:1004 - 编程求123*…*n编程求 1×2×3×⋯×n 。1.分析问题已知:整数 n未知:整数 n 的阶乘p关系:p=n!=1×2×3×…×(n-1)×n2.定义变量根据分析的已知,未知按需要定义变量。i 作为条件循环的变量。 //二、数据定义
int n,i=1,p=1;3.输入数据从键盘读入整数 n。 //三、数据输入
cin>>n;4.数据计算第一次学习循环结构可以看看循坏体是如何运行的。 while(i<=n){ //i=1 n=5
//循环体
cout<<i<<endl;
//条件更新
++i;
}初始值i=1,假如n=5。那么i<=n条件成立,程序进入循环体内部。执行输出i语句后,程序向下执行++i;对条件变量进行更新。 while(i<=n){ //i=1 n=5
//循环体
cout<<i<<endl;//输出1
//条件更新
++i; //i=2
}此时i = 2,n=5;依旧满足i<=n。条件成立,程序再次进入循环体内部。 while(i<=n){ //i=2 n=5
//循环体
cout<<i<<endl;//输出2
//条件更新
++i; //i=3
}此时i = 3,n=5;依旧满足i<=n。条件成立,程序再次进入循环体内部。 while(i<=n){ //i=3 n=5
//循环体
cout<<i<<endl;//输出3
//条件更新
++i; //i=4
}直到…i = 6,n=5;不满足i<=n的条件,循环终止,程序跳过循环部分向下执行。本题根据阶乘的数学模型设计代码:p=p*i; //四、数据计算
while(i<=n){
p*=i;
//条件更新
++i;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:整数 n
//未知:整数 n 的阶乘p
//关系:p=n!=1×2×3×...×(n-1)×n
//二、数据定义
int n,i=1,p=1;
//三、数据输入
cin>>n;
//四、数据计算
while(i<=n){
p*=i;
//条件更新
++i;
}
//五、输出结果
cout<<p;
return 0;
}问题二:1056 - 所有不超过1000的数中含有数字3的自然数编程求出所有不超过1000 的数中,含有数字 3 的自然数,并统计总数。1.分析问题已知:不超过 1000 的数未知:含有数字3的自然数的总数count关系:个位 ==3?十位 ==3?百位 ==3?2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int i=1,count=0;3.输入数据本题不需要输入。4.数据计算拆位求解属于基本操作,在 C++程序结构入门之顺序结构二 中介绍过,忘记的可以自行回去复习一下。这里要使用 “或” ,因为满足其中任意一个条件就可以了,即个位、十位、百位有数字3。//四、数据计算
while(i<1000){
if(i%10==3||i/10%10==3||i/100==3){
++count;
}
//条件更新
++i;
}5.输出结果#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:不超过 1000 的数
//未知:含有数字3的自然数的总数count
//关系:个位 ==3?十位 ==3?百位 ==3?
//二、数据定义
int i=1,count=0;
//三、数据输入
//四、数据计算
while(i<1000){
if(i%10==3||i/10%10==3||i/100==3){
++count;
}
//条件更新
++i;
}
//五、输出结果
cout<<count;
return 0;
}问题三:1059 - 求数输出 1∼999 中有因数 3 ,且至少有一位数字是 5 的数。1.分析问题已知:1-999的数未知:有因数3,且至少有一位数字是5的数关系:数%3==0;个位 ==5?十位 ==5?百位 ==5?2.定义变量根据分析的已知,未知按需要定义变量。为什么将i赋值为3呢?其实是为了好找有因数3的数。如果i每次都加上3,那么必定满足有因数3的条件,还可以少进行一次if判断。 //二、数据定义
int i=3; 3.输入数据本题不需要输入。4.数据计算如上所述,i的累加从1变成3。拆位和上问题二一样的操作。 //四、数据计算
while(i<1000){
if(i%10==5||i/10%10==5||i/100==5){
}
//条件更新
i+=3;
}5.输出结果#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:1-999的数
//未知:有因数3,且至少有一位数字是5的数
//关系:数%3==0;个位 ==5?十位 ==5?百位 ==5?
//二、数据定义
int i=3;
//三、数据输入
//四、数据计算
while(i<1000){
if(i%10==5||i/10%10==5||i/100==5){
//五、输出结果
cout<<i<<endl;
}
//条件更新
i+=3;
}
return 0;
}问题四:1055 - 求满足条件的整数个数在1∼n 中,找出能同时满足用 3 除余 2 ,用 5 除余 3 ,用 7 除余 2 的所有整数的个数,如果没有请输出 0 。1.分析问题已知:1-n的整数未知:能同时满足用3除余2,用5除余3,用7除余2的所有整数的个数sum关系:%3== 2,%5== 3,%7== 22.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int i=1,n,sum=0;3.输入数据 //三、数据输入
cin>>n;4.数据计算注意是同时满足,所以要使用逻辑与(&&)。//四、数据计算
while(i<=n){
if(i%3==2&&i%5==3&&i%7==2){
++sum;
}
//条件更新
++i;
} 5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:1-n的整数
//未知:能同时满足用3除余2,用5除余3,用7除余2的所有整数的个数sum
//关系:%3==2,%5==3,%7==2
//二、数据定义
int i=1,n,sum=0;
//三、数据输入
cin>>n;
//四、数据计算
while(i<=n){
if(i%3==2&&i%5==3&&i%7==2){
++sum;
}
//条件更新
++i;
}
//五、输出结果
cout<<sum;
return 0;
}四、练习问题: 1014 - 编程求1+1/2+1/3+…+1/n问题: 1058 - 求出100至999范围内的所有水仙花数。问题: 1054 - 编程求1平方+2平方+…+n平方问题: 1003 - 编程求1+3+5+…+n问题: 1002 - 编程求解1+2+3+…+n问题: 1053 - 求100+97+……+4+1的值。问题: 1004 - 编程求123*…*n问题: 1021 - 求数II问题: 1085 - 寻找雷劈数五、总结以上就是关于循环结构中while语句的学习内容,难度不高,多多练习即可掌握。喜欢的朋友可以关注本专栏C++从零基础入门到NOI竞赛,一起学习一起进步。
跟着小潘学后端
【C++】c++入门之数组基础一
一、数据结构概念在《C++程序设计入门三》中我们曾提到过数据类型这个概念,而今天我们要学习数据结构。首先回顾以下数据类型。数据类型是指计算机处理的对象的类型,它定义了数据的存储方式和可进行的操作。C++中的数据类型包括整型、浮点型、字符型等。数据类型用于定义变量的类型,以便在程序中存储和操作数据。可以理解为计算机为了区分不同的变量,我们给变量穿了一个叫数据类型的马甲,而不同的数据类型将获得不同的“服务”。路人:哦,那什么是数据结构呢?数据结构是指数据的组织形式,它描述了数据之间的关系和如何存储数据。数据结构用于组织和管理大量数据,以便更高效地进行操作和处理。比如说我们以前可以用一个变量记录一个学生的一门成绩,然后可以多建立几个变量去记录他的所有成绩,通过运算可以得到他的平均分等等。但是现在要求你算出班上所有同学的平均成绩和找出优秀学科成绩呢?有 20 位同学的成绩要计算,我们需要定义 20 个变量吗?如果有100 位同学的成绩要计算,我们要定义 100 个变量吗?显然这种做法是不行的,聪明的程序员也想到了这个问题,所以数据结构被发明了出来。简而言之,数据类型是描述数据的属性,而数据结构是描述数据之间的关系和组织方式。数据结构可以分为两类:线性数据结构和非线性数据结构。而我们今天学习的数组属于线性数据结构。线性数据结构是一种按照顺序排列的数据结构,其中数据元素之间存在一对一的关系。C++数组是一种用于存储多个相同类型的元素的数据结构。我们可以想象有一个运动会,有一群选手,他们穿着相同的马甲,然后按着顺序在排队,他们有个响亮的名号叫做“江南皮革厂”。二、数组的创建C++数组的创建可以通过以下两种方式实现:静态数组:静态数组是在编译时期确定大小的数组,其大小在声明时就已经确定,并且不能改变。C++中创建静态数组的方法如下:声明数组类型和数组名:dataType arrayName[arraySize];其中,dataType表示数组中元素的数据类型,arrayName是数组的名称,arraySize是数组的大小。例如:int arr[5];这将创建一个包含5个整数的静态数组。初始化数组元素:int arr[5] = {1, 2, 3, 4, 5};这将创建一个包含5个整数的静态数组,并将其初始化为给定的值。提示:静态数组的声明和初始化可以在同一行完成,也可以分开进行。动态数组:动态数组是在运行时期确定大小的数组,其大小可以根据需要进行动态分配和释放。动态数组的创建需要使用new关键字来分配内存空间,并使用delete关键字来释放内存空间。C++中创建一维动态数组的方法如下:使用new关键字来分配内存空间,指定数组的大小。将返回的指针赋值给一个指针变量,以便后续使用。关闭一维动态数组的步骤如下:使用delete关键字释放之前分配的内存空间。将指针变量设置为nullptr,以避免悬空指针的问题。演示了如何创建和关闭一维动态数组:#include <iostream>
using namespace std;
int main() {
int size;
cout << "请输入数组的大小:";
cin >> size;
// 创建一维动态数组
int* arr = new int[size];
// 为数组赋值
for (int i = 0; i < size; i++) {
cout << "请输入第 " << i+1 << " 个元素的值:";
cin >> arr[i];
}
// 输出数组的值
cout << "数组的值为:";
for (int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
// 关闭一维动态数组
delete[] arr;
arr = nullptr;
return 0;
}三、数组的使用要想学会如何使用数组,那么就要知道什么是数组下标?C++数组下标是指通过索引值来访问数组中的元素。在C++中,数组的索引从0开始,依次递增。通过使用数组名和索引值,可以访问和修改数组中的特定元素。访问数组元素:int x = arr[2];这将访问数组中索引为2的元素,并将其赋值给变量x。修改数组元素:arr[3] = 10;这将修改数组中索引为3的元素的值为10。遍历数组:length=7;
for (int i = 0; i < length; i++) {
cout << arr[i] << " ";
}这将遍历数组并打印每个元素。注意:长度为 n 的数组,下标从 0~n-1,不能超过这个范围访问数组元素,如果超过就是错的,语法上叫“越界”了!四、样题讲解问题一:1423 - 考试成绩的简单统计期末考试结束,王老师想知道这次考试中成绩优秀的同学有多少人(考试成绩大于或等于 90 表示成绩优秀),请你编程帮助王老师来计算出成绩优秀的人数。1.分析问题已知:n个人的成绩。未知:成绩优秀的同学有多少人。关系:大于或等于 90 表示成绩优秀。2.定义变量根据分析的已知,未知按需要定义变量。n:n个人的成绩。a[100]:数组,存储具体成绩数值。count:成绩优秀的同学人数。 //二、数据定义
int n,a[100],count=0; 3.输入数据通过遍历的方式,循环录入学生成绩。//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}4.数据计算大于或等于 90 表示成绩优秀,统计个数即可。//四、数据计算
if(a[i]>=90){
count++;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:n个人的成绩
//未知:成绩优秀的同学有多少人
//二、数据定义
int n,a[100],count=0;
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
//四、数据计算
if(a[i]>=90){
count++;
}
}
//五、输出结果
cout<<count<<endl;
return 0;
}问题二:1153 - 查找“支撑数”在已知一组整数中,有这样一种数非常怪,它们不在第一个,也不在最后一个,而且刚好都比左边和右边相邻的数大,你能找到它们吗?1.分析问题已知:一组整数未知:支撑数关系:比左边和右边相邻的数大的数2.定义变量根据分析的已知,未知按需要定义变量。n:n个整数a[100]:用来存储n个整数数值。 //二、数据定义
int n,a[100]; 3.输入数据从键盘读入。 //三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}4.数据计算同时满足比左边和右边相邻的数大,注意第一个数(索引为0)左边没有数,所以应该从第二个数开始判断(索引为1)。//四、数据计算
for(int i=1;i<n-1;i++){
if(a[i-1]<a[i]&&a[i]>a[i+1]){
}
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一组整数
//未知:刚好都比左边和右边相邻的数大的数
//二、数据定义
int n,a[100];
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
//四、数据计算
for(int i=1;i<n-1;i++){
if(a[i-1]<a[i]&&a[i]>a[i+1]){
//五、输出结果
cout<<a[i]<<endl;
}
}
return 0;
}问题三:1156 - 排除异形基因神舟号飞船在完成宇宙探险任务回到地球后,宇航员张三感觉身体不太舒服,去了医院检查,医生诊断结果:张三体内基因已被改变,原有人体基因序列中已经被渗入外星球不明异形生物基因,但可喜的是,这些异形基因都有一个共同的特征,就是该基因序号的平方除以 7 的余数都是 1,要赶快清除掉,否则会危害整个人类。赶快行动吧。1.分析问题已知:人体基因序列未知:异形基因关系:基因序号的平方除以 7 的余数是否等于 12.定义变量根据分析的已知,未知按需要定义变量。//二、数据定义
int n,a[200];3.输入数据从键盘读入。//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
4.数据计算基因序号的平方除以 7 的余数等于 1是异形基因。//四、数据计算
if(a[i]*a[i]%7!=1){
}5.输出结果去除异形基因后的正常序列,空格隔开。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:人体基因序列
//未知:该基因序号的平方除以 7 的余数是否等于 1
//二、数据定义
int n,a[200];
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
//四、数据计算
if(a[i]*a[i]%7!=1){
//五、输出结果
cout<<a[i]<<"\t";
}
}
return 0;
}问题四:1155 - 找找谁的身高超过全家的平均身高找找谁的身高超过全家的平均身高。全家 n 口人,输入输出数据如下: (平均身高保留一位小数)。1.分析问题已知:全家的身高。未知:谁的身高超过全家的平均身高。关系:平均身高=全家的身高/人数。2.定义变量根据分析的已知,未知按需要定义变量。ave:平均身高。 //二、数据定义
int n,a[100];
double ave=0;3.输入数据从键盘读入。先将全家的身高进行累加。//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
ave+=a[i];
}4.数据计算第一行为全家的平均身高(保留一位小数);并判断谁的身高超过全家的平均身高。//四、数据计算
ave/=n;
printf("AVE=%.1f\n",+ave);
for(int i=0;i<n;i++){
if(a[i]*1.0 > ave){
}
}5.输出结果第二行有若干个数,为超过平均身高的人的身高厘米数。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:全家的身高
//未知:谁的身高超过全家的平均身高
//二、数据定义
int n,a[100];
double ave=0;
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
ave+=a[i];
}
//四、数据计算
ave/=n;
printf("AVE=%.1f\n",+ave);
for(int i=0;i<n;i++){
if(a[i]*1.0 > ave){
//五、输出结果
cout<<i+1<<":"<<a[i]<<" ";
}
}
return 0;
}五、练习问题一:1231 - 考试成绩的分布情况期末考试结束,小明的语文老师想知道,这次考试的成绩分布情况,主要计算如下几个数据:平均分、≥ 平均分的总人数、 < 平均分的总人数,请你写程序帮助小明的语文老师来计算一下!1231 - 考试成绩的分布情况 题解问题二:1160 - 打折优惠商场周末大优惠,规定凡购物超过 100 元时,超过 100 元那部分便可打9折。小雄同妈妈一起购买了一大批物品,你能帮他算出最终的应付款吗?1160 - 打折优惠 题解问题三:1316 - 橘子称重学校买回来一大箱橘子,有 m 个( 100≤m≤1000 ),橘子大小比较均匀,学校想称一下总共有多重,发现大称坏掉了还没有修好,只有一个小的弹簧秤。学校又不想分开称,那样太慢了。小明想了一个办法,由于橘子大小比较均匀,可以从中拿n个出来(5≤n≤20),这 n 个橘子的重量弹簧秤是可以称出来的,有了这 n 个橘子的重量,就可以计算出平均一个橘子有多重,这样就能知道整箱大约有多重了。请编写程序,从键盘读入橘子总数 m ,小明称的橘子的个数 n 以及这 n 个橘子的重量,计算出这箱橘子总共约有多重(结果保留1位小数)。1316 - 橘子称重 题解问题四:1388 - 陶陶摘苹果陶陶家的院子里有一棵苹果树,每到秋天树上就会结出 10 个苹果。苹果成熟的时候,陶陶就会跑去摘苹果。陶陶有个 30 厘米高的板凳,当她不能直接用手摘到苹果的时候,就会踩到板凳上再试试。现在已知 10个苹果到地面的高度,以及陶陶把手伸直的时候能够达到的最大高度,请帮陶陶算一下她能够摘到的苹果的数目。假设她碰到苹果,苹果就会掉下来。1388 - 陶陶摘苹果 题解问题五:1174 - 求和输入 n ( 1≤n≤5000 )个正整数,每个数都在 1到20000 之间;要求对这 n 个数中的奇数和偶数分别求和。1174 - 求和 题解问题六:1397 - 完美的偶数?完美偶数指的是,如果一个数本身是偶数,且这个数是偶数位的数,且这个数的各个位也是偶数,那么这个数就可以称为完美偶数;比如: 28 就是完美偶数,而 246 就不是,因为 246 是一个 3 位数。请你编程求出,从键盘读入的 n 个数中,哪些数是完美的偶数,输出他们。1397 - 完美的偶数? 题解问题七:1158 - 输出奇数和偶数输入 n 个整数,将其中的奇数和偶数分别显示出来(1<n<30)。1158 - 输出奇数和偶数 题解问题八:1354 - 拿到某个数的概率是多少?老师在一个不透明的纸袋里放入一些乒乓球,每个乒乓球上都有一个数字,当球放入之后,让小明从中随机拿一个。在球放入之后,小明抽之前,老师想让您帮忙编程先计算一下,拿到某个数字x的概率是多少?比如:老师向袋子里面放入了 5 个球,对应的数字分别是 2 3 2 4 2 ,那么拿到数字 2 的概率为 3/5 = 0.6 ,拿到数字3的概率为 1/5 = 0.2。1354 - 拿到某个数的概率是多少? 题解问题九:1357 - 哪个厂家的零件更标准?在统计描述中,方差用来计算每一个变量(观察值)与总体均数之间的差异。比如:甲乙 2 个厂商生产某零件,一批零件要求在尺寸合格的情况下,大小越一致越好,由于生产工艺的问题,零件生产厂商生产的零件不可能一模一样。1357 - 哪个厂家的零件更标准? 题解六、总结以上就是本节的全部内容,数组在实际运用中的次数还是非常多,但是学习难度并不大,静态数组的创建、数组下标的使用,数组的遍历都是必须要掌握的,动态数组可以了解学习。
跟着小潘学后端
【C++】C++程序结构入门之分支结构一
一、分支结构是什么?1. 导入《震惊!某男子为了凉快竟然干出这种事》盛夏的中午,火辣辣的太阳毫不留情地炙烤着眼前的一切。马路两旁大树叶子被晒得无精打采地耷拉下了脑袋有气无力地挨着这难熬的时光,不时地又抬起头,好像在看远处的天空能否有乌云飘过。小狗躺在阴凉的地上乘凉,舌头伸在嘴巴外面喘气。此时的小明,只盼望能来一场酣畅淋漓的大雨。“哎,热得我受不了啦!”说着就把手里的扇子一扔,朝着躺在旁边凉席上的人喊道:”妈,给我点钱呗,我去买点雪糕!“躺在凉席上的人似乎睡着了,并没有什么动作,偶尔还传来几声呼噜声。小明沉默了一会儿,决定先拿自己的零花钱。…经过一阵跋涉,小明穿过林荫小道,顶着火辣的阳光,终于来到了超市门口。此时的小明已经满头大汗,踮了踮脚,身体极力拔高,把头越过柜台朝着里面的一个穿着背心躺在藤椅上正嘿嘿嘿笑个不停的大叔说道:“老板!来个雪糕!”。只见老板把头一歪,把手机顺手放在大腹便便的肚子上,从旁边的冰柜中随意拿出一个。“50,谢谢惠顾。””什么?“小明大吃一惊,不敢相信的问道:”多少?“老板:”50!“”老板再见!“然后小明头也不会的就走掉,嘴里不停骂骂咧咧。”这么贵,吃了要成仙啊“。站在小区的绿道上,小明望了望天上的太阳。因为出来了很久,嘴里已是干渴的不行。但是想买便宜的雪糕就得去更远的地方,但是这太阳不禁让小明有点打退堂鼓。“出都出来了,总不能空着手回去吧“,说罢便头也不回的走向小区大门。走了很远的路小明终于看到了一个冰淇淋批发店,门上写着购买20个及以上7折,少于20个原价。小明点了点头,“就它了”。于是便走进了小店。欲知后事如何,请听下回分解o( ̄▽ ̄)ブ。2. 单分支结构咱们用程序思维分析分析《小明买雪糕的故事》,从小明出门到小区超市说起,此时的小明因为天气太热打算去买雪糕。来到超市的小明,此时的他面临了一个选择,买雪糕吗?可以看到,买雪糕是一件选择事件,选择买就可以吃上雪糕。因为只有一个选择事件,因此买雪糕是一个单分支结构。代入到C++中,C++的单分支结构是if语句,它的语法格式如下:if (condition) {
// 如果条件为真,执行这里的代码
}其中,condition是一个表达式,如果它的值为真(非零),则执行花括号中的代码块。既如果小明花钱买了雪糕,那么小明就可以吃雪糕。并且从上面可以看到,小明有没有买雪糕都并不影响回家。因此如果condition的值为假(0),则跳过整个if语句,继续执行后面的代码。下面是一个简单的例子,演示了如何使用if语句判断一个数是否为正数:#include <iostream>
using namespace std;
int main() {
int num;
cout << "请输入一个整数:";
cin >> num;
if (num > 0) {
cout << num << "是正数" << endl;
}
return 0;
}在上面的例子中,如果用户输入的数大于0,则输出该数是正数。如果用户输入的数小于等于0,则if语句不执行,直接跳过。关于表达式,已经在C++程序设计入门二 中讲解过。3. 双分支结构咱们接着分析小明的故事,小明决定不买雪糕从超市出来以后,来到了冰淇淋批发店。在这里小明决定买点雪糕,但是批发店是有优惠活动,20个及以上7折,少于20个原价。此时的小明面临两个选择。因为可以选择买20个以上,或者20个以下。因此此时是一个双分支的结构。代入到C++中,C++中的双分支结构是if-else语句,它的语法如下:if (condition) {
// 如果条件成立执行的代码块
} else {
// 如果条件不成立执行的代码块
}其中,condition是需要判断的条件,如果条件成立则执行if后面的代码块,否则执行else后面的代码块。假如我们设置的条件是雪糕数大于等于20,那么小明买的雪糕超过20就能享受折扣优惠,当然没有超过20就进入否则语句,原价购买。而且程序运行到双分支结构是必须选其一执行,不能两个语句块都跳过或都执行。下面是一个简单的例子,演示了如何使用if-else语句实现输入两个整数,将这两个数按照从小到大的顺序输出的功能:#include <iostream>
using namespace std;
int main() {
int a, b;
cin >> a >> b;
if (a > b) {
int t = a;
a = b;
b = t;
}
cout << a << " " << b << endl;
return 0;
}在这个例子中,我们首先使用cin语句输入两个整数a和b,然后使用if-else语句判断a和b的大小关系,如果a大于b,则交换它们的值,最后使用cout语句输出结果。二、例题讲解问题一:1303 - 冷饮的价格1小明去冷饮店买冰激凌,如果买 10个以上或者 10 个,2 元 / 个, 10个以下, 2.2元 / 个。请从键盘读入小明的购买数量,计算小明应付的价格!1.分析问题已知:购买数量 n,未知:应付的金额2.定义变量根据分析的已知,未知按需要定义变量。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:购买数量 n,
//未知:应付的金额
//二、数据定义
int n;
}3.输入数据从键盘读入购买的数量。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:购买数量 n,
//未知:应付的金额
//二、数据定义
int n;
//三、数据输入
cin>>n;
}4.数据计算买冰激凌,如果买 10个以上或者 10 个,2 元 / 个, 10个以下, 2.2元 / 个。因此需要使用双分支结构。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:购买数量 n,
//未知:应付的金额
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
if(n<10){
}else{
}
}5.输出结果#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:购买数量 n,
//未知:应付的金额
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
if(n<10){
//五、输出结果
//printf("%.1f",n*2.2);
cout<<fixed<<setprecision(1)<<n*2.2;
}else{
//五、输出结果
//printf("%.1f",n*2);
cout<<fixed<<setprecision(1)<<n*2.0;
}
}问题二:1033 - 判断奇偶数输入一个整数,判断是否为偶数。是输出 y e s ,否则输出n o。1.分析问题已知:一个整数未知:是否为偶数2.定义变量根据分析的已知,未知按需要定义变量。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个整数
//未知:是否为偶数
//二、数据定义
int n;
return 0;
}3.输入数据从键盘读入需要判断的整数。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个整数
//未知:是否为偶数
//二、数据定义
int n;
//三、数据输入
cin>>n;
return 0;
}4.数据计算判断是否为偶数。是输出 y e s ,否则输出n o。因此还是要使用双分支结构。判断偶数的方法:如果这个数能整除2,那么它就是偶数。即 % 2 == 0;#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个整数
//未知:是否为偶数
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
if(n%2==0){
}else{
}
return 0;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个整数
//未知:是否为偶数
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
if(n%2==0){
//五、输出结果
cout<<"y e s";
}else{
//五、输出结果
cout<<"n o";
}
return 0;
}问题三:1302 - 是否适合晨练?夏天到了,气温太高,小明的爷爷每天有晨练的习惯,但有时候温度不适合晨练;小明想编写一个程序,帮助爷爷判断温度是否适合晨练。输入温度 t 的值,判断其是否适合晨练,适合晨练输出 OK ,不适合输出 NO 。( 20≤t≤30 ,则适合晨练 OK ,否则不适合 NO )。1.分析问题已知:温度t未知:是否适合晨练2.定义变量根据分析的已知,未知按需要定义变量。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:温度t
//未知:是否适合晨练
//二、数据定义
int t;
return 0;
}3.输入数据从键盘读入需要判断的温度。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:温度t
//未知:是否适合晨练
//二、数据定义
int t;
//三、数据输入
cin>>t;
return 0;
}4.数据计算20 ≤ t ≤ 30 ,则适合晨练 OK ,否则不适合 NO 。还是使用双分支结构。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:温度t
//未知:是否适合晨练
//二、数据定义
int t;
//三、数据输入
cin>>t;
//四、数据计算
if(t>=20&&t<=30){
}else{
}
return 0;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:温度t
//未知:是否适合晨练
//二、数据定义
int t;
//三、数据输入
cin>>t;
//四、数据计算
if(t>=20&&t<=30){
//五、输出结果
cout<<"OK";
}else{
//五、输出结果
cout<<"NO";
}
return 0;
}问题四:1030 - 心系南方灾区2008年年初我国南方正在承受百年不遇的大雪、冻雨灾害。北京市已经开始了面向全体市民的捐款捐物活动,并组织运力,以最快速度将这些救灾物资运送到灾区人民的手中。已知救灾物资中有 m 件大衣(10000≤m≤2000000 ),一辆卡车一次最多可以运走 n 件(2000≤n≤10000 )。请你编写程序计算一下,要将所有的大衣运走,北京市政府最少需要调动多少辆卡车参与运送。1.分析问题已知:有m件大衣未知:需要调动多少辆卡车2.定义变量根据分析的已知,未知按需要定义变量。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:有m件大衣
//未知:需要调动多少辆卡车
//二、数据定义
int m,n,result;
return 0;
}3.输入数据从键盘读入大衣的数量。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:有m件大衣
//未知:需要调动多少辆卡车
//二、数据定义
int m,n,result;
//三、数据输入
cin>>m>>n;
return 0;
}4.数据计算这个问题不仔细看好像是一个顺序结构的问题,直接用大衣的数量/卡车运输的数量=卡车的数量就完成了,但是因为int类型是向下取整,而且大衣也存在不能刚好运输完的概率,因此需要判断m % n == 0。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:有m件大衣
//未知:需要调动多少辆卡车
//二、数据定义
int m,n,result;
//三、数据输入
cin>>m>>n;
//四、数据计算
if(m%n==0){
result=m/n;
}else{
result=m/n+1;
}
return 0;
}5.输出结果#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:有m件大衣
//未知:需要调动多少辆卡车
//二、数据定义
int m,n,result;
//三、数据输入
cin>>m>>n;
//四、数据计算
if(m%n==0){
result=m/n;
}else{
result=m/n+1;
}
//五、输出结果
cout<<result;
return 0;
}问题五:1043 - 行李托运价格某车站行李托运收费标准是: 10 公斤或 10 公斤以下,收费2.5 元,超过 10 公斤的行李,按每超过 1 公斤增加 1.5元进行收费。试编一程序,输入行李的重量,算出托运费。1.分析问题已知:收费标准、 行李的重量未知:托运费2.定义变量根据分析的已知,未知按需要定义变量。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:收费标准、 行李的重量
//未知:托运费
//二、数据定义
int n;
double price;
return 0;
}3.输入数据从键盘读入行李的重量。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:收费标准、 行李的重量
//未知:托运费
//二、数据定义
int n;
double price;
//三、数据输入
cin>>n;
return 0;
}4.数据计算因为有两种收费标准,那么就会有两种结果。因此使用双分支结构。10 公斤或 10 公斤以下,收费2.5 元,超过 10 公斤的行李,按每超过 1 公斤增加 1.5元进行收费。注意是超过的部分加收1.5元,price = 2.5 + (n - 10 ) * 1.5 。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:收费标准、 行李的重量
//未知:托运费
//二、数据定义
int n;
double price;
//三、数据输入
cin>>n;
//四、数据计算
if(n<=10){
price=2.5;
}else{
price=2.5+(n-10)*1.5;
}
return 0;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:收费标准、 行李的重量
//未知:托运费
//二、数据定义
int n;
double price;
//三、数据输入
cin>>n;
//四、数据计算
if(n<=10){
price=2.5;
}else{
price=2.5+(n-10)*1.5;
}
//五、输出结果
printf("%.2f",price);
return 0;
}三、作业问题一:1037 - 恐龙园买门票问题二:1045 - 判断能否构成三角形问题三:1324 - 扩建鱼塘问题问题四:1309 - 最多能倒多少杯水问题五:1034 - 两数比大小问题六:1304 - 冷饮的价格(2)四、总结以上就是单,双分支结构的全部内容。单,双分支结构的难点在于对表达式的判断理解以及单,双分支结构在什么时候区分使用。我的建议是就是多练o( ̄▽ ̄)o。
跟着小潘学后端
Python模块与包(八)
一.模块(1) 什么是模块一个Python文件,以.py 结尾,能定义函数,类和变量,也能包含可执行的代码作用:我们可以认为不同的模块就是不同工具包,每一个工具包中都有各种不同的工具(如函数)供我们使用进而实现各种不同的功能.(2) 模块的导入模块在使用之前需要先导入正在开发的文件导入语法:[from 模块名] import [模块|类|变量|函数|*] [as 别名]
# *表示导入所有常用的组合形式如:import 模块名from 模块名 import 类、变量、方法等from 模块名 import *import 模块名 as 别名from 模块名 import 功能名 as 别名(2.1) 用法一基本语法:# 导入
import 模块名
import 模块名1,模块名2
# 使用
模块名.功能名()使用示例:# 导入时间模块
import time
print("开始") # 打印 开始
# 使用time模块中睡眠功能(其中还有众多其他功能)
# 可以让程序睡眠10秒后再继续执行
time.sleep(10)
print("结束") #十秒后打印 结束 (2.2) 用法二基本语法# 导入
from 模块名 import 功能名
# 使用
功能名()使用示例:# 导入时间模块中的sleep方法
# 只能使用time模块中导入的sleep的方法
from time import sleep
print("开始") # 打印 开始
# 让程序睡眠10秒后再继续执行
sleep(10)
print("结束") #十秒后打印 结束 效果图与(2.1)一致(2.3) 用法三基本语法# 导入一:模块定义别名
import 模块名 as 别名
# 使用一:
别名.功能名()
# 导入二:功能定义别名
from 模块名 import 功能 as 别名
# 使用二:
别名()使用示例一:# 本名time将不可用
import time as tt
print("开始") # 打印 开始
# 让程序睡眠10秒后再继续执行
# 通过别名调用
tt.sleep(10)
print("结束") #十秒后打印 结束 使用示例二:# 本名sleep将不可用
from time import sleep as sl
print("开始") # 打印 开始
# 让程序睡眠10秒后再继续执行
sl(10)
print("结束") # 十秒后打印 结束 效果图与(2.1)一致(2.4) 用法四基本语法# 导入
from 模块名 import *
# 使用
功能名()使用示例:# 导入时间模块中的全部功能
# 导入效果与(2.1)一致,使用与(2.2)一致
from time import *
print("开始") # 打印 开始
# 让程序睡眠10秒后再继续执行
sleep(10)
print("结束") #十秒后打印 结束 效果图与(2.1)一致(2.5) 小结from可以省略,直接importas别名可以省略通过”.”来调用模块提供的功能模块的导入一般写在代码文件的开头位置(3) 自定义模块Python中已经帮我们实现了很多的模块,直接导入即可使用有时候我们需要一些个性化(满足自己特定需求)的模块, 就可以通过自定义模块实现,即自己制作一个模块上述提到:每个Python文件都可以作为一个模块,模块的名字就是文件的名字.也就是说将自己编写的文件导入另一个文件即可当作模块使用。使用示例:运行test_04.py打印结果:(3.1) 注意一在实际开发中,当一个开发人员编写完一个模块后,为了让模块能够在项目中达到想要的效果,开发人员可能会在在py文件中添加一些测试信息此时,无论是当前文件,还是其他已经导入了该模块的文件,在运行的时候都会自动执行测试信息例如:测试完后未删除测试信息并未使用其他措施,打印结果:解决:测试完后直接删除(繁琐,容易忘,不推荐)在if条件判断中使用使用示例,此时只有运行test_03才会执行测试代码,运行test_04时不再执行测试代码:使用解释:# 只在当前文件中运行条件才为True,导入其他文件时均为False
if __name__ == '__main__':
# __main__ 运行时程序的名称
# __name__ 系统自动赋值,不用管
# 在Run时为 __main__
# 未Run时为 文件名称(3.2) 注意二当导入多个模块的时候,如果模块内有同名功能,且未使用用法一导入形式,后面导入的模块将会覆盖前面模块内同名的功能。例如:结果:(3.3) 注意三如果一个模块文件中有__all__变量,当使用from xxx import *导入时,只能导入这个列表中的元素其他导入方式不受限制二.包当Python的模块太多了,就可能造成一定的混乱,此时可以通过Python包的功能来管理。(1) 什么是包从物理上看,包就是一个文件夹,在该文件夹下包含了一个 __init__.py 文件,该文件夹可用于包含多个模块文件从逻辑上看,包的本质依然是模块(2) 自定义包当我们的模块文件越来越多时,包可以帮助我们分类管理这些模块,包的作用就是包含多个模块,但本质依然是模块(2.1) 创建包新建包后,包内部会自动创建__init__.py文件,这个文件控制着包的导入行为(2.2) 导入包导入包与导入模块几种方式类似,例如:import 包名.模块名
包名.模块名.功能名()(2.3) 限制导入可以在__init__.py文件中添加__all__ = ['模块名'],控制允许导入的模块列表与导入模块类似__all__只针对from 包名.模块名 import *而对其他方式无效(3) 安装第三方包第三方(其他人)开发的,Python没有内置,需要先安装才可以导入使用在Python程序的生态中,有非常多的第三方包(非Python官方),可以极大的帮助我们提高开发效率,如:科学计算中常用的:numpy包数据分析中常用的:pandas包大数据计算中常用的:pyspark、apache-flink包图形可视化常用的:matplotlib、pyecharts人工智能常用的:tensorflow等(3.1) 如何安装只需要使用Python内置的pip程序即可在终端中输入如下指令即可通过网络快速安装第三方包:pip install 包名称例如安装科学计算中常用的:numpy包,安装成功后即可导入使用:(3.2) 提高安装速度由于pip是连接的国外的网站进行包的下载,下载速度经常很慢。我们可以通过如下命令,让其连接国内的网站进行包的安装:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 包名称
# 网站为清华大学提供的一个网站,可供pip程序下载第三方包如果经常使用上述方法过于麻烦,可直接配置成镜像源之后就不需要加连接python -m pip install --upgrade pip
# 升级pip版本,防止版本过低无法配置
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# 配置为全局镜像源(3.3) 在Pycharm中安装三.全文概览
跟着小潘学后端
【C++】C++程序设计入门三
一、数据类型1.1 什么是数据类型?在上一节中我们讲到变量是计算机程序中用于存储和表示数据的一种命名方式。我们可以把变量和生活中的箱子一起理解。并且为了计算机能够方便快速找到它,还会为变量绑定一个地址。现在我们就可以把东西放在变量里面了,可是有一个问题我们需要装的东西可能大小是不一样的。这个时候我们应该造一个苹果大小的箱子还是楼房大小的箱子呢?简单,我们直接全部造大箱子不就好了吗?但是你们考虑过苹果的感受吗?那我们给每一个东西都设计一个箱子不就好了。我们可以对它们进行分类,再根据分类后的结果造箱子。比如苹果属于水果,那我们可以造个水果类的箱子,按照水果里面最大的尺寸去设计,是不是所有的水果都可以用。所以数据类型决定了变量或表达式可以存储的数据的种类和范围。二、基本数据类型2.1 整型(short、int、long、longlong)C++中的整型变量是一种用来存储整数值的数据类型。整数就是没有小数部分的数字。以下是一些常见的C++整数变量类型:2.1.1 short:短整型占用2个字节,范围为-32768到32767。声明一个short类型的变量a,并将其初始化为-123:short a = -123;2.1.2 int:整型它通常占用4个字节(32位),可以表示从-2147483648到2147483647之间的整数。声明一个int类型的变量a,并将其初始化为-123:int a = -123;2.1.3 long:长整型占用4个字节或8个字节,可以表示的最大值为2147483647,最小值为-2147483648。当处理边界情况时,需要注意long变量可能会发生溢出的情况,例如long a = 2147483647; a++的操作,a的值会变成-2147483648。#include <iostream>
using namespace std;
int main() {
long a = 2147483647;
a++;
cout << "a = " << a << endl; // 输出:a = -2147483648
return 0;
}2.1.4 long long:长长整型long long是一种整数类型,它可以存储更大的整数值。long long类型的变量至少占用8个字节(64位),可以存储的最大值为9223372036854775807,最小值为-9223372036854775808。#include <iostream>
using namespace std;
int main() {
long long b = 9223372036854775807;
b++;
cout << "b = " << b << endl; // 输出:b = -9223372036854775808
return 0;
}short、int、long和long long在运行速度上没有明显的区别,它们的区别在于占用的空间大小和能够表示的整数范围。2.2 浮点型(float和double)浮点数是小数点在逻辑上是不固定的数,并不一定等于小数。2.2.1 floatfloat用于存储单精度浮点数,通常占用4个字节(32位),可以表示的有效位数为7位。声明一个float类型的变量num,并将其初始化为3.14159:#include <iostream>
using namespace std;
int main() {
float num = 3.14159;
cout << num << endl;//输出:3.14159
return 0;
}2.2.2 doubledouble是一种双精度浮点数类型,通常占用8个字节(64位),提供大约15到16位十进制数的精度。相较于float类型具有更高的精度。由于double类型具有更多的位数来表示数字,它能够在计算过程中保留更多的有效数字,减少舍入误差的累积,因此在复杂计算中通常提供更高的运算精度。以下是一个C++中使用double类型进行计算和int比较的例子:#include <iostream>
using namespace std;
int main() {
double a = 1.0 / 3.0;
double b = 2.0 / 3.0;
int c = 1 / 3;
int d = 2 / 3;
double e = a + b;
int f = c + d;
cout<<e<<" "<<f;//输出: 1 0
return 0;
}2.3 布尔型(bool)C++中的布尔型它只有两个取值:true和false。在C++中,true被定义为1,false被定义为0。可以使用bool类型的变量来存储和操作布尔值。以下是一个简单的例子:#include <iostream>
using namespace std;
int main() {
bool isTrue = true;
bool isFalse = false;
cout << "isTrue = " << isTrue << endl; // 输出:isTrue = 1
cout << "isFalse = " << isFalse << endl; // 输出:isFalse = 0
return 0;
}在C++中,可以使用逻辑运算符(&&、||、!)来操作布尔值。例如:#include <iostream>
using namespace std;
int main() {
bool isTrue = true;
bool isFalse = false;
cout << "(isTrue && isFalse) = " << (isTrue && isFalse) << endl; // 输出:(isTrue && isFalse) = 0
cout << "(isTrue || isFalse) = " << (isTrue || isFalse) << endl; // 输出:(isTrue || isFalse) = 1
cout << "(!isTrue) = " << (!isTrue) << endl; // 输出:(!isTrue) = 0
return 0;
}2.4 字符型(char)C++中的char类型用于表示单个字符。char类型的变量只能存储一个字符,可以用单引号括起来表示。例如,'a’表示字符a,'1’表示数字1。在C++中,char类型的变量占用1个字节的内存空间,可以存储ASCII码表中的任何一个字符。下面是一个使用char表示一个字符的例子:#include <iostream>
using namespace std;
int main() {
char a='a';
int b=a+1;
cout<<a<<endl;//输出:a
cout<<b;//输出:98
return 0;
}C++中的字符串是由一系列字符组成的,可以使用char类型的数组来表示。字符串以空字符’\0’结尾,因此char类型的数组必须足够大,以便存储字符串中的所有字符和空字符。可以使用C++标准库中的string类来处理字符串,string类提供了许多方便的方法来操作字符串。下面是一个使用char类型的数组表示字符串的例子:#include <iostream>
using namespace std;
int main() {
char str1[] = "Hello";
char str2[] = "World";
cout << str1<<","<<str2 << endl;//输出:Hello,World
return 0;
}三、 总结以上就是今天要讲的内容,本文简单介绍了C++中常见的基本数据类型及其用法。根据不同的需求,选择合适的数据类型可以提高程序的效率和可读性。
跟着小潘学后端
Python面向对象(九)
一.什么是面向对象万物皆对象现实世界的事物都有属性和行为,可在程序中抽离为类来描述现实世界的事物属性和行为。使用类充当程序内现实事物的“设计图纸”,基于图纸(类)生产实体(对象),由对象做具体的工作,称之为:面向对象编程在现实世界中,生产事物:先设计图纸,完成功能属性分析,再批量制造在程序中,通过类作为事物的设计图纸,记录事物的属性和行为基于类(设计图纸)构建(生产)闹钟对象二.类与对象使用类封装属性,基于类创建出一个个的对象来使用(1) 基本语法# 创建类
class 类名称:
类的属性(成员变量)
类的行为(成员方法)
# 基于类创建对象
对象名 = 类名称()
# 调用
对象名.成员变量
对象名.成员方法()class:关键字,表示要定义类了类的属性:定义在类中的变量(成员变量) -> 事物的属性类的行为:定义在类中的函数(成员方法) -> 事物的行为(2) 使用示例设计表格即设计类(class):class Student:
name = None # 姓名
sex = None # 性别
country = None # 国籍
native_place = None # 籍贯
age = None # 年龄打印表格即创建对象:stu_1 = Student() # 一张
stu_2 = Student() # 两张
stu_3 = Student() # 三张填写表格即使用对象(为属性赋值):stu_1.name = "李白"
stu_2.name = "观止"
stu_3.name = "罗辑"概览:(3) 成员变量和成员方法(3.1) 成员变量定义在类内部的变量称之为成员变量,用法与正常变量一致。(3.2) 成员方法定义在类内部的函数称之为方法,与函数存在细微区别。# 函数
# 形参可以为0-N个
def 函数名(形参1,形参2,..,形参N):
函数体
# 方法
# 形参可以为0-N个
# self关键字必须填写
def 方法名(self,形参1,形参2,..,形参N):
方法体self关键字在成员方法定义的时候必须填写,表示类对象自身在方法内部,想要访问类的成员变量,必须使用selfclass Student:
name = None
# 调用say_hi1时正常打印
def say_hi1(self):
print(f"大家好,我叫{self.name}")
# 调用say_hi2时报错,'name' is not defined
def say_hi2(self):
print(f"大家好,我叫{name}") 当我们使用对象调用方法的时,self会自动被python传入,尽管在参数列表中,但是传参的时候可以忽略它。# 定义
class Student:
name = None
def say_hi(self, msg):
print(f"大家好,我是{msg}")
# 创建
stu_1 = Student()
# 通过对象名调用
stu_1.say_hi("练习两年半的偶像实习生")
# 打印 大家好,我是练习两年半的偶像实习生(4) 构造方法通过传参的形式快速对属性赋值正常情况下,为对象的属性赋值需要依次进行# 定义类
class Student:
name = None # 姓名
sex = None # 性别
age = None # 年龄
# 创建对象
stu_1 = Student()
# 为对象赋值
stu_1.name = "李白"
stu_1.sex = "男"
stu_1.age = 1000在类可以使用:__init__()方法,即构造方法,快速为对象赋值。# 定义类
class Student:
name = None # 姓名
sex = None # 性别
age = None # 年龄
def __init__(self, name, sex, age):
self.name = name
self.sex = sex
self.age = age
# 创建对象并赋值
stu_1 = Student("李白", "男", 1000)
# 简化形式:可以省略成员属性定义,但仍可调用
class Student:
def __init__(self, name, sex, age):
self.name = name # 姓名
self.sex = sex # 性别
self.age = age # 年龄
# 创建对象并赋值
stu_1 = Student("李白", "男", 1000)在创建类对象(构造类)的时候,会自动执行,将传入参数自动传递给__init__方法使用。构造方法也是成员方法,定义时也需要在参数列表中提供:self变量定义在构造方法内部,如果要成为成员变量,需要用self来表示,例如self.name使用了构造方法,创建对象时必须传参否则会报错(5) 魔术方法Python类内置的类方法,各自有各自特殊的功能魔术方法非常多,我们学习几个常用的即可。方法功能__init__构造方法,可用于创建类对象的时候设置初始化行为__str__字符串方法,用于实现类对象转字符串的行为__lt__用于2个类对象进行小于(<)或大于(>)比较__le__用于2个类对象进行小于等于(<=)或大于等于(>=)比较__eq__用于2个类对象进行相等(==)比较(5.1)__str__方法当直接打印类对象时,打印的是对象的内存地址,用处不大。class Student:
def __init__(self, name, age):
self.name = name # 姓名
self.age = age # 年龄
stu_1 = Student("李白", 1000)
print(stu_1)
# 打印 <__main__.Student object at 0x0000024D8C6195D0>我们可以通过__str__方法,自定义控制打印类对象时输出的内容。class Student:
def __init__(self, name, age):
self.name = name # 姓名
self.age = age # 年龄
# 自定义打印输出内容
def __str__(self):
return f"Student对象,name={self.name},age={self.age}"
stu_1 = Student("李白", 1000)
print(stu_1)
# 打印 Student对象,name=李白,age=1000(5.2)__lt__方法直接对2个对象进行比较是不可以的,会报错。class Student:
def __init__(self, name, age):
self.name = name # 姓名
self.age = age # 年龄
stu_1 = Student("李白", 1000)
stu_2 = Student("罗辑", 300)
print(stu_1 > stu_2) # 报错在类中实现__lt__方法即可完成:小于符号 和 大于符号 2种比较class Student:
def __init__(self, name, age):
self.name = name # 姓名
self.age = age # 年龄
def __lt__(self, other):
return self.age < other.age
stu_1 = Student("李白", 1000)
stu_2 = Student("罗辑", 300)
print(stu_1 > stu_2) # 打印 True(5.3) __le__方法在类中实现__le__方法即可完成:小于等于符号 和 大于等于符号 2种比较,否则会报错class Student:
def __init__(self, name, age):
self.name = name # 姓名
self.age = age # 年龄
def __le__(self, other):
return self.age <= other.age
stu_1 = Student("李白", 1000)
stu_2 = Student("罗辑", 1000)
print(stu_1 <= stu_2) # True
print(stu_1 >= stu_2) # True(5.4) __eq__方法不实现__eq__方法,对象之间可以比较,但是是比较内存地址,但是不同对象==比较一定是False结果。class Student:
def __init__(self, name, age):
self.name = name # 姓名
self.age = age # 年龄
stu_1 = Student("李白", 1000)
stu_2 = Student("李白", 666)
print(stu_1 == stu_2) # False实现了__eq__方法,就可以按照自己的想法来决定2个对象是否相等了。class Student:
def __init__(self, name, age):
self.name = name # 姓名
self.age = age # 年龄
# 自定义比较规则
def __eq__(self, other):
return self.name == self.name
stu_1 = Student("李白", 1000)
stu_2 = Student("李白", 666)
print(stu_1 == stu_2) # True三.三大特性面向对象包含3大主要特性:封装,继承,多态(1) 封装将现实世界事物的属性和行为在类中描述为成员变量和成员方法,完成程序对现实世界事物的描述现实世界中的事物,有属性和行为。但是不代表这些属性和行为都是开放给用户使用的(1.1) 私有成员在类中提供仅供内部使用的属性和方法,无法被对象调用基本语法:class Student:
name = None # 普通成员变量
__age = None # 私有成员变量
# 普通成员方法
def say_hi(self):
print("你好")
# 私有成员方法
def __DNA(self):
print("DNA数量")仅在成员内部可以使用class Student:
name = None # 普通成员变量
__age = 16 # 私有成员变量
# 普通成员方法
def show(self):
self.__check() # 在类中使用私有成员变量
if self.__age > 18: # 在类中使用私有成员变量
print("成年人")
else:
print("未成年")
# 私有成员方法
def __check(self):
print("自检")(2) 继承一个类继承另外一个类的所有成员变量和成员方法(不含私有)(2.1) 单继承基本语法:class 类名(父类名):
类内容体基本使用:# 待继承的类
class Phone:
producer = "apple" # 厂商
def call_by_4g(self):
print("4g通话")
# 继承Phone
class Phone2022(Phone):
face_id = True # 面部识别
def call_by_5g(self):
print("2022最新5G通话")
# 创建对象
phone = Phone2022()
# 使用
print(phone.producer) # 可调用 继承自Phone的成员变量
print(phone.face_id) # 可调用 自身的成员变量
phone.call_by_4g() # 可调用 继承自Phone的成员方法
phone.call_by_5g() # 可调用 自身的成员方法
(2.2) 多继承一个类也可以继承多个类多个父类中,如果有同名的成员,默认以继承顺序(从左到右)为优先级。即:后继承的被先继承的覆盖基本语法:class 类名(父类1,父类2,...,父类N):
类内容体使用示例:class Phone:
producer = "apple" # 厂商
class Camera:
producer = "suoni" # 厂商
class Phone2022(Phone, Camera):
face_id = True # 面部识别
def call_by_5g(self):
print("2022最新5G通话")
phone = Phone2022()
print(phone.producer) # 打印结果为apple而非suoni(2.3) 复写子类继承父类的成员属性和成员方法后,如果对其“不满意”,那么可以进行复写。即:在子类中重新定义同名的属性或方法。一旦复写父类成员,那么类对象调用成员的时候,就会调用复写后的新成员class Phone2021:
producer = "apple" # 厂商
def call_by_5g(self):
print("2021版5G通话")
class Phone2022(Phone2021):
face_id = True # 面部识别
def call_by_5g(self):
print("2022升级版5G通话")
phone = Phone2022()
phone.call_by_5g() # 打印 2022升级版5G通话如果需要使用被复写的父类的成员,只能在子类内通过如下方式调用父类的同名成员:方式一:使用父类名调用使用成员变量:父类名.成员变量
使用成员方法:父类名.成员方法(self)
方式二:使用super()调用使用成员变量:super().成员变量
使用成员方法:super().成员方法()使用示例:class Phone2021:
producer = "apple" # 厂商
def call_by_5g(self):
print("2021版5G通话")
class Phone2022(Phone2021):
face_id = True # 面部识别
def call_by_5g(self):
# 方式一调用
print(Phone2021.producer) # 打印 apple
Phone2021.call_by_5g(self)# 打印 2021版5G通话
# 方式二调用
print(super().producer)# 打印 apple
super().call_by_5g()# 打印 2021版5G通话
(3) 多态多种状态,即完成某个行为时,使用不同的对象会得到不同的状态多态常作用在继承关系上,函数(方法)形参声明接收父类对象,实际传入父类的子类对象进行工作,即以父类做定义声明以子类做实际工作用以获得同一行为, 不同状态(3.1) 抽象类(接口)抽象类就好比定义一个标准,包含了一些抽象的方法,要求子类必须实现抽象类:包含抽象方法的类抽象方法:没有具体实现的方法(只含pass)称之为抽象方法pass是占位语句,用来保证函数(方法)或类定义的完整性,表示无内容,空的意思多用于做顶层设计(设计标准),以便子类做具体实现。是对子类的一种软性约束,要求子类必须复写(实现)父类的一些方法并配合多态使用,获得不同的工作状态。四.全文概览
跟着小潘学后端
【C++】C++程序结构入门之顺序结构二
一、两数交换1.1 问题描述输入两个数a,b;要求程序运行后a,b的值发生了互换。1.2 分析问题已知:a,b的值。未知:a,b交换以后的值。1.3 定义变量根据分析后的已知,未知来确定变量。#include<iostream>
using namespace std;
int main(){
int a,b;
}1.4 数据输入根据定义的变量,哪些需要从键盘读取,哪些不需要。#include<iostream>
using namespace std;
int main(){
//1.分析问题: 已知:a,b的值。 未知:a,b交换以后的值。
//2.定义变量
int a,b;
//3.输入数据
cin>>a>>b;
}1.5 数据计算有人会想既然是交换两个数,那么直接a=b,b=a不就可以了吗。简单,下一题。真的是这样吗?假如,小明和小刚两个人。小明有一个苹果,小刚有一个香蕉。他们两个人都想吃对方手里的水果,但是现在他们的一只手在做其他的事情,只有一只手拿着水果。这个时候他们能直接交换水果吗?老师,这样不就行了!( •̀ ω •́ )✧根本难不倒你,是吧!(╯▔皿▔)╯可以看到从现实生活出发也是办不到的,计算机会怎么处理呢?假如a=3,b=4。如果通过a=b,b=a的方式去交换两个数,可以看到当a=b时,a的值已经发生变化,不再是之前的值;所以在b=a时,b不能得到a=3这个值。错误方法:#include<iostream>
using namespace std;
int main(){
//1.分析问题: 已知:a,b的值。 未知:a,b交换以后的值。
//2.定义变量
int a,b;
//3.输入数据
cin>>a>>b;//a=3,b=4
//4.数据计算
a=b;//a=b=4
b=a;//b=a=4
//5.结果输出
cout<<a<<" "<<b;//4 4
}那应该怎么去交换呢?聪明的人肯定已经想到了,找个人帮忙啊。方法一:请个临时工还是假如a=3,b=4。但是在b=a之前,我们通过temp=b这个操作,把b的值给了temp;然后当a需要b的值时,就去找temp要,如此就能实现两个数的交换。(temp 临时工)程序如下:#include<iostream>
using namespace std;
int main(){
//1.分析问题: 已知:a,b的值。 未知:a,b交换以后的值。
//2.定义变量
int a,b,temp;
//3.输入数据
cin>>a>>b;//a=3,b=4
//4.数据计算
temp=b;//temp=b=4
b=a;//b=a=3
a=temp;//a=temp=4
//5.结果输出
cout<<a<<" "<<b;//4 3
}那能不能不请临时工呢?方法二:加法交换律加法交换律:指两个加数相加,交换加数的位置,和不变假如a = 3,b = 4;那么a + b = 3 + 4 = 7,b + a = 4 + 3 = 7。7- b = a,那么刚好就可以把7- b的值给b,此时完成b=a的交换;想要a获得原来的b值,那么直接用和减去现在的b值(原来的a值)就能得到。程序如下:#include<iostream>
using namespace std;
int main(){
//1.分析问题: 已知:a,b的值。 未知:a,b交换以后的值。
//2.定义变量
int a,b;
//3.输入数据
cin>>a>>b;
//4.数据计算
a=a+b;
b=a-b;
a=a-b;
//5.结果输出
cout<<a<<" "<<b;
}方法三:swap函数在C++中,swap函数用于交换两个变量的值。实际上就是方法一,但是不需要自己去写交换逻辑。程序如下:#include<iostream>
using namespace std;
int main(){
//1.分析问题: 已知:a,b的值。 未知:a,b交换以后的值。
//2.定义变量
int a,b,temp;
//3.输入数据
cin>>a>>b;
//4.数据计算
swap(a,b);
//5.结果输出
cout<<a<<" "<<b;
}1.6 结果输出输出如上。1.7 小结两数交换还是使用的非常多的,比如排序,找最大,三角形的验证等等。难度不高,多练习即可掌握。二、拆位求解2.1 问题描述1027 - 求任意三位数各个数位上数字的和2.2 分析问题已知:x的值。未知:x各数位相加后的和s。2.3 定义变量根据分析后的已知,未知来确定变量 x,s。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//1.分析问题:已知:x的值。 未知:x各数位相加后的和s。
//2.定义变量
int x,s;
return 0;
}2.4 数据输入根据定义的变量,x需要输入,其它不需要。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//1.分析问题:已知:x的值。 未知:x各数位相加后的和s。
//2.定义变量
int x,s;
//3.输入数据
cin>>x;
return 0;
}2.5 数据计算在解决问题之前,我们先来了解什么是数位。数位是指一个数中的每一位的数字,比如x = 123,那么x的百位=1,十位=2,个位=3;即x = 1 * 100 + 2 * 10 +3。ok,是不是很简单。那怎么去得到每个数位的数呢?这里我们复习一下数学中的除法。有一个数x = 123。从第二张图可以看到,当123除以10以后产生了余数3,而3刚好是我们想要的个数位。那我们可以通过 %10 ( 取余 )得到3。但是不能通过 %100 得到2,因为得到的数是23。如果这个数是12,那么 % 10 = 2。如何将123变成12呢?要知道int整数类型在除法时,是向下取正,不管余数是1还是9都不会进位。所以想得到12可以用123 / 10。想要的得到最高位,直接除以最高位数的0即可。比如123想要得到1,那么直接123 / 100。因此拆位的公式就是:个位:%10;中间位:/数位%10;最高位 :/数位。那么根据拆位公式和本题要求,需要求出个位,十位,百位;再将个位,十位,百位相加。程序如下:#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//1.分析问题:已知:x的值。 未知:x各数位相加后的和s。
//2.定义变量
int x,s;
//3.输入数据
cin>>x;
//4.数据计算
int ge=x%10;
int shi=x/10%10;
int bai=x/100;
s=ge+shi+bai;
return 0;
}2.6 结果输出将结果输出即可。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//1.分析问题:已知:x的值。 未知:x各数位相加后的和s。
//2.定义变量
int x,s;
//3.输入数据
cin>>x;
//4.数据计算
int ge=x%10;
int shi=x/10%10;
int bai=x/100;
s=ge+shi+bai;
//5.输出数据
cout<<s;
return 0;
}2.7 小结以上就是关于拆位求解的内容,主要记住拆位公式。关于短除法会在循环结构入门介绍。三、课后作业1028 - 输入一个三位数,把个位和百位对调后输出1390 - 四位数的和1109 - 加密四位数1020 - 算算和是多少1029 - 倒序输出一个四位整数1418 - 求一个5位数的各个位之和四、总结以上的两个内容是以后学习中使用的次数非常多的,因此一定要掌握
跟着小潘学后端
【C++】C++程序结构入门之循环结构二
一、导入路人:怎么样?上次教你的while魔法学会了没有。吃瓜群众:练习了几个题,都能做出来。师父!师父!我听说循环魔法好像不只一种呢,是不是啊😍路人:确实是还有两种。分别是:for循环和do-while循环。这样吧,我再教你一个for循环。吃瓜群众:二、for循环路人:我要先考考你,还记得while的咒语(语法)吗?吃瓜群众:是这样的。初始化表达式
while (循环条件) {
// 循环体语句
更新表达式
}路人:下面是for的咒语(语法),你能看出什么不同吗?C++中的for循环是一种常用的循环结构,它可以重复执行一段代码,直到满足某个条件为止。for循环的语法如下:for (初始化表达式; 循环条件; 更新表达式) {
// 循环体语句
}其中,初始化表达式只会在循环开始时执行一次,循环条件会在每次循环开始前被判断,如果为真则执行循环体语句,否则跳出循环,更新表达式会在每次循环结束后执行。吃瓜群众:好像两个人。路人:没错,for循环和while循环的语法结构是很像的,并且执行的顺序也是一致的,只是放的位置不太一样。而且for循环也可以把初始化表达式和更新表达式放到和while循环一样的位置,只是一定 一定 一定要写 “;”这个是固定不变的。三、例题讲解问题一:1264 - 4位反序数设 N 是一个四位数,它的 9 倍恰好是其反序数,求 N 。反序数就是将整数的数字倒过来形成的整数。例如:1234 的反序数是 4321 。1.分析问题已知:N的9倍恰好是其反序数temp未知:N关系:N*9=temp2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int i=1000,temp;3.输入数据无输入。4.数据计算可以看到,我们在定义变量的时候就已经初始化了for循环语法中的初始化表达式,所以初始化表达式的位置是可以直接空着的。 //四、数据计算
for(;i<10000;i++){
temp=i%10*1000+i/10%10*100+i/100%10*10+i/1000;
if(i*9==temp){
}
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:N的9倍恰好是其反序数temp
//未知:N
//关系:N*9=temp
//二、数据定义
int i=1000,temp;
//三、数据输入
//四、数据计算
for(;i<10000;i++){
temp=i%10*1000+i/10%10*100+i/100%10*10+i/1000;
if(i*9==temp){
//五、输出结果
cout<<i<<endl;
}
}
return 0;
}问题二:1085 - 寻找雷劈数把整数 3025 从中剪开分为 30 和 25 两个数,此时再将这两数之和平方,计算结果又等于原数。(30+25)×(30+25)=55×55=3025 ,这样的数叫“雷劈数”。求所有符合这样条件的四位数。(ab+cd)×(ab+cd)=abcd1.分析问题已知:四位数未知:所有符合雷劈数的四位数关系:(ab+cd)×(ab+cd)=abcd2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int ab,cd;3.输入数据无输入。4.数据计算//四、数据计算
for(int i=1000;i<10000;i++){
ab=i/100;
cd=i%100;
if(i==(ab+cd)*(ab+cd)){
}
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:四位数
//未知:所有符合雷劈数的四位数
//关系:(ab+cd)×(ab+cd)=abcd
//二、数据定义
int ab,cd;
//三、数据输入
//四、数据计算
for(int i=1000;i<10000;i++){
ab=i/100;
cd=i%100;
if(i==(ab+cd)*(ab+cd)){
//五、输出结果
cout<<i<<endl;
}
}
return 0;
}问题三:1057 - 能被5整除且至少有一位数字是5的所有整数的个数1.分析问题已知:1-N的整数未知:能被5整除且至少有一位数字是5的所有整数的个数sum关系:%5==0 个位=5,十位=5,百位=5,千位=52.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n,sum=0;3.输入数据从键盘读入整数n。 //三、数据输入
cin>>n;4.数据计算可以参照 问题二:1056 - 所有不超过1000的数中含有数字3的自然数。 //四、数据计算
for(int i=5;i<=n;i+=5){
if(i%10==5||i/10%10==5||i/100%10==5||i/1000%10==5){
sum++;
}
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:1-N的整数
//未知:能被5整除且至少有一位数字是5的所有整数的个数sum
//关系:%5==0 个位=5,十位=5,百位=5,千位=5
//二、数据定义
int n,sum=0;
//三、数据输入
cin>>n;
//四、数据计算
for(int i=5;i<=n;i+=5){
if(i%10==5||i/10%10==5||i/100%10==5||i/1000%10==5){
sum++;
}
}
//五、输出结果
cout<<sum;
return 0;
}四、练习将C++程序结构入门之循环结构一中的例题和练习用for循环完成。将本文中的例题用while循环完成。五、总结以上就是关于循环结构中for语句的学习内容,难度不高,多多练习即可掌握。喜欢的朋友可以关注本专栏C++从零基础入门到NOI竞赛,一起学习一起进步。
跟着小潘学后端
Python函数使用(四)
一.函数基础定义:组织好的,可重复使用的,用来实现特定功能的代码段(1) 完整格式# 定义
def 函数名(传入参数):
函数体
return 返回值
# 使用
函数名(传入参数)传入参数的数量是不受限制的:可以不使用参数,省略->(2)也可以使用任意N个参数可以不使用参数,省略->(2)也可以使用任意N个参数返回值如不需要,可以省略->(3)(2) 简单格式# 定义
def 函数名():
函数体
# 使用
函数名()函数必须先定义后使用使用示例:# 定义
def say():
print("hello world")
# 使用
say()
# 输出 hello world(3) 带参格式在函数进行计算的时候,可接受外部(调用时)提供的数据# 定义
def 函数名(传入参数):
函数体
# 使用
函数名(传入参数)传入的时候,按照顺序传入数据,参数之间使用逗号进行分隔函数定义中,提供的x和y,称之为:形式参数(形参),表示函数声明将要使用2个参数(占位)函数调用中,提供的10和6,称之为:实际参数(实参),表示函数执行时真正使用的参数值使用示例# 定义
def add(x, y):
res = x + y
print(res)
# 使用
add(10, 6)
# 输出 16
# 定义
def show(x):
print(x)
# 使用
show(10)
# 输出 10(4) 带返回值格式所谓“返回值”,就是程序中函数完成事情后,最后给调用者的结果# 定义
def 函数名(传入参数):
函数体
return 返回值
# 或
def 函数名():
函数体
return 返回值
# 使用
变量 = 函数名(传入参数)
变量 = 函数名()函数体在遇到return后就结束了,所以写在return后的代码不会执行使用示例:# 定义
def add(x, y):
return x + y
print("end not print") # 在return后不再执行输出
# 使用
res = add(10, 6)
print(res)
# 只输出 16(5) None类型如果函数没有使用return语句返回数据,函数返回值为None,其类型是:<class ‘NoneType’>None表示:空的、无实际意义的意思函数返回的None,就表示,这个函数没有返回什么有意义的内容,也就是返回了空的意思。使用示例:# 定义
def say():
print("hello world")
# 使用
mes = say()
# 输出 hello world
print(mes)
print(type(mes))
# 输出
# hello world
# None
# <class 'NoneType'>其他应用:用在if判断上在if判断中,None等同于False一般用于在函数中主动返回None,配合if判断做相关处理# 定义
def check_age(age):
if age > 18:
return "enter"
return None
# 使用
res = check_age(5)
# not表示取布尔类型相反值
# not False = True
# not True = False
if not res:
print("未成年禁止入内")
# 输出 未成年禁止入内
用于声明无内容的变量上# 暂不赋予变量具体值
name = None(6) 函数的嵌套调用一个函数里面又调用了另外一个函数使用示例:def fun_1():
fun_2()
print("----fun_1-----")
def fun_2():
fun_3()`
print("----fun_2-----")
def fun_3():
print("----fun_3----")
fun_1()
# 输出
# ----fun_3----
# ----fun_2-----
# ----fun_1-----在函数1中,调用了另外一个函数2,函数2中,调用了另外一个函数3。执行顺序为: fun_1-> fun_2->fun_3->fun_2->fun_1在一个函数中先把当前函数中的任务都执行完毕之后才会回到上次调用位置继续执行二.变量的作用域定义:变量的作用范围(变量在哪里可用,在哪里不可用)(1) 局部变量定义在函数体内部的变量,即只在函数体内部生效def test():
num = 100
print(num)
test() # 输出 100
print(num) # 报错 name 'num' is not defined变量num是定义在test函数内部的变量,在函数外部访问则立即报错.在函数体内部,临时保存数据,即当函数调用完成后,则销毁局部变量(2) 全局变量在函数体内、外都能生效的变量num = 100
def test():
print(num)
test() # 输出 100
print(num) # 输出 100(3) global关键字一般情况下,在函数内无法修改全局变量的值num = 100
def test():
# 声明一个值为200的局部变量num
num = 200
print(num)
test() # 输出 200
print(num) # 输出 100使用 global关键字可以在函数内部声明变量为全局变量, 如下所示num = 100
def test():
# 声明num为全局变量
global num
num = 200
print(num)
test() # 输出 200
print(num) # 输出 200三.函数进阶(1) 多返回值按照返回值的顺序,写对应顺序的多个变量接收即可变量之间用逗号隔开使用示例:def test():
return 6, 9, 16
x, y, z= test()
print(f"第一个值为{x},第二个值为{y},第三个值为{z}")
# 输出 第一个值为6,第二个值为9,第三个值为16支持return不同类型的数据def test():
return "观止", True, 16
x, y, z = test()
print(f"第一个值为{x},第二个值为{y},第三个值为{z}")
# 输出 第一个值为观止,第二个值为True,第三个值为16(2) 多种传参方式(2.1) 位置参数调用函数时根据函数定义的参数位置来传递参数传递的参数和定义的参数的顺序及个数必须一致def user_info(name, age, gender):
print(f"您的名字是{name},年龄是{age},性别是{gender}")
user_info('TOM', 20, '男')
# 输出 您的名字是TOM,年龄是20,性别是男(2.2) 关键字参数函数调用时通过“键=值”形式传递参数.可以让函数更加清晰、容易使用,同时也清除了参数的顺序需求.def user_info(name, age, gender):
print(f"您的名字是{name},年龄是{age},性别是{gender}")
user_info(name='TOM', age=20, gender='男')
# 输出 您的名字是TOM,年龄是20,性别是男
user_info(gender='男', age=20, name='TOM')
# 输出 您的名字是TOM,年龄是20,性别是男函数调用时,如果有位置参数时,位置参数必须在关键字参数的前面,但关键字参数之间不存在先后顺序def user_info(name, age, gender):
print(f"您的名字是{name},年龄是{age},性别是{gender}")
user_info('TOM', gender='男, age=20')
# 输出 您的名字是TOM,年龄是20,性别是男(2.3) 缺省参数也叫默认参数,用于定义函数,为参数提供默认值,调用函数时可不传该默认参数的值所有位置参数必须出现在默认参数前,包括函数定义和调用当调用函数时没有传递参数, 就会使用默认是用缺省参数对应的值.函数调用时,如果为缺省参数传值则修改默认参数值, 否则使用这个默认值def user_info(name, age, gender='男'):
print(f"您的名字是{name},年龄是{age},性别是{gender}")
user_info('TOM', 20)
# 输出 您的名字是TOM,年龄是20,性别是男
user_info('TOM', 20, '女')
# 输出 您的名字是TOM,年龄是20,性别是女(2.4) 不定长参数也叫可变参数. 用于不确定调用的时候会传递多少个参数(不传参也可以)的场景.(2.4.1) 位置传递以*号标记一个形式参数,以元组的形式接受参数传进的所有参数都会被args变量收集,它会根据传进参数的位置合并为一个元组(tuple),args是元组类型使用示例:def user_info(*args):
print(args)
user_info('TOM')
# 输出 ('TOM',)
user_info('TOM', 20, '女')
# 输出 ('TOM', 20, '女')(2.4.2) 关键字传递关键字不定长传递以**号标记一个形式参数,以字典的形式接受参数参数是“键=值”形式的形式的情况下, 所有的“键=值”都会被kwargs接受, 同时会根据“键=值”组成字典.def user_info(**kwargs):
print(kwargs)
user_info(name='TOM', age=20)
# 输出 {'name': 'TOM', 'age': 20}(3) 函数作为参数传递函数本身也可以像普通变量一样作为参数传递使用。函数名存放的是函数所在空间的地址def func():
print("hello world~")
print(func) # 打印 <function func at 0x0000022E77983EB0>通过 函数名() 的形式可执行所存放空间中的代码(执行函数)def func():
print("hello world~")
func() # 打印 hello world~函数名可以像普通变量一样赋值,func1 = func2def func1():
print("hello world~")
func = func1
func() # 打印 hello world~函数本身也可以像普通变量一样作为参数传递使用def add(x, y):
return x + y
def compute(add):
result = add(6, 3)
print(result)
compute(add)
# 输出 9(4) lambda匿名函数无名称的函数def关键字,可以定义带有名称的函数有名称的函数,可以基于名称重复使用。lambda关键字,可以定义匿名函数(无名称)无名称的匿名函数,只可临时使用一次。定义格式:lambda 传入参数:函数体(一行代码)
# lambda 是关键字,表示定义匿名函数
# 传入参数表示匿名函数的形式参数,如:x, y 表示接收2个形式参数
# 函数体,就是函数的执行逻辑,要注意:只能写一行,无法写多行代码使用示例;def compute(add):
result = add(6, 3)
print(result)
# 输出 9
compute(lambda x, y: x + y)
# 等效于
def add(x, y):
return x + y
def compute(add):
result = add(6, 3)
print(result)
# 输出 9
compute(add)四.全文概览
跟着小潘学后端
Python文件基础操作(六)
一.文件编码编码就是一种规则集合,记录了内容和二进制间进行相互转换的逻辑。思考:计算机只能识别0和1,那么我们丰富的文本文件是如何被计算机识别,并存储在硬盘中呢?答案:使用编码技术(密码本)将内容翻译成0和1存入。计算机中有许多可用编码:UTF-8,GBK,Big5等不同的编码,将内容翻译成二进制也是不同的对内容的编码与解码必须使用同一套编码,否则会导致错误的结果UTF-8是目前全球通用的编码格式,除非有特殊需求,否则,一律以UTF-8格式进行文件编码即可。二.文件操作在日常生活中,文件操作主要包括打开、关闭、读、写等操作操作过程中请务必注意文件路径的书写只有操作文件与python文件在同一目录才能直接写文件名。新手建议都写文件的绝对路径,不易导致错误发生。(1) 文件的打开(1.1) 基本格式在Python,使用open函数,可以打开一个已经存在的文件,或者创建一个新文件基本语法:open(name, mode, encoding)
# name:是要打开的目标文件名的字符串(可以包含文件所在的具体路径)。
# mode:设置打开文件的模式(访问模式):只读、写入、追加等。
# encoding:编码格式(推荐使用UTF-8)示例代码:f = open("C:/code/bill.txt", "r", encoding="UTF-8")
# encoding的顺序不是第三位,所以不能用位置参数,用关键字参数直接指定
# f是open函数的文件对象,可以使用对象.属性或对象.方法对其进行访问(1.2) 打开模式文件常用的三种基础访问模式,可通过mode指定。r->read(读取),w->write(写入),a->append(追加)模式描述r以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。w打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,原有内容会被删除。如果该文件不存在,创建新文件。a打开一个文件用于追加。如果该文件已存在,新的内容将会被写入到已有内容之后。 如果该文件不存在,创建新文件进行写入。(2) 文件的读取操作功能文件对象.read(num)读取指定长度字节 不指定num读取文件全部文件对象.readline()读取一行文件对象.readlines()读取全部行,返回列表for line in 文件对象for循环文件行,一次循环得到一行数据文件对象.close()关闭文件对象with open() as f通过with open语法打开文件,可以自动关闭每次读取会从上一次读取结束的位置开始每次open()中的内容只能被读取一次(2.1) read方法num表示要从文件中读取的数据的长度(单位是字节),如果没有传入num,那么就表示读取文件中所有的数据。语法:文件对象.read(num)使用示例:f = open("C:/code/test.txt", "r", encoding="UTF-8")
content = f.read() # 不传入num,读取文件中所有的数据。
print(content)
# 打印
# 观止
# study
f = open("C:/code/test.txt", "r", encoding="UTF-8")
content = f.read(2) # 传入num,读取2字节长度数据。
print(content)
# 打印
# 观止(2.2) readline()方法一次读取一行内容语法:文件对象.readline()使用示例:f = open("C:/code/test.txt", "r", encoding="UTF-8")
content = f.readline()
print(f"第一行内容:{content}") # 打印 第一行内容:观止
content = f.readline()
print(f"第二行内容:{content}") # 打印 第二行内容:study(2.3) readlines方法按照行的方式把整个文件中的内容进行一次性读取,并且返回的是一个列表,其中每一行的数据为一个元素。语法:文件对象.readlines()使用示例:f = open("C:/code/test.txt", "r", encoding="UTF-8")
content = f.readlines()
print(content) # 打印 ['观止\n', 'study']
print(type(content)) # 打印 <class 'list'>(2.4) for循环读取for循环读取每一行数据使用示例:# 每一个line临时变量,就记录了文件的一行数据
for line in open("C:/code/test.txt", "r", encoding="UTF-8"):
print(line)
# 打印
# 观止
#
# study(2.5) close关闭文件对象如果不调用close,同时程序没有停止运行,那么这个文件将一直被Python程序占用,无法操作使用示例:f = open("C:/code/test.txt", "r", encoding="UTF-8")
# 需要执行代码
f.close()代码中不关闭文件对象,且python程序未停止运行,无法对文件删除重命名等操作:(2.6) 自动close通过在with open的语句块中对文件进行操作,可以在操作完成后自动关闭close文件即使出现异常也会自动调用关闭文件操作语法:with open() as f使用示例:with open("C:/code/test.txt", "r", encoding="UTF-8") as f:
f.readlines()(3) 文件的写入使用示例:f = open("C:/code/test.txt", "w")
# 文件如果不存在,使用”w”模式,会创建新文件
# 文件如果存在,使用”w”模式,会将原有内容清空
# 2.文件写入
f.write('hello world')
# 3. 内容刷新
f.flush()直接调用write,内容并未真正写入文件,而是会积攒在程序的内存中,称之为缓冲区当调用flush的时候,内容会真正写入文件这样做是避免频繁的操作硬盘,导致效率下降(攒一堆,一次性写磁盘)(4) 文件的追加使用w模式,每次写入会将原有内容清空,写入新内容使用a模式,文件不存在会创建文件,文件存在会在最后追加内容写入文件使用示例:f = open("C:/code/test.txt", "a")
# 2.文件写入
f.write('study')
# 3. 内容刷新
f.flush()三.全文概览
跟着小潘学后端
【C++】c++排序算法入门之选择排序
一、选择排序1.1 基本思想选择排序算法的主要思想是通过逐步寻找并交换当前未排序部分的最小(或最大)元素来构建有序序列。选择排序的详细步骤:初始化:从待排序序列的第一个元素开始。查找最小(或最大)值:在剩余未排序的所有元素中,找到一个最小(或按降序排列时的最大)的元素。交换位置:将找到的最小(或最大)元素与未排序部分的第一个元素交换位置。这样,该元素就被放置到了正确的位置上——已排序序列的末尾。更新范围:接下来的过程只针对剩余未排序的元素进行,不再考虑已经排序的部分。重复过程:重复上述步骤,即在新的未排序部分中继续寻找剩余元素中的最小(或最大)值,并将其移动到已排序序列的末尾。终止条件:当整个序列都被遍历过一遍且所有元素都经过了这样的比较和交换后,排序完成。选择排序的时间复杂度分析:最好情况:无论输入数据如何,选择排序的时间复杂度都是固定的。即使数组已经完全有序,它仍然需要遍历所有元素进行比较和可能的交换,因此最好、最坏和平均时间复杂度均为O(n²)。空间复杂度:由于选择排序是原地排序算法,只需要常数级的额外空间,所以空间复杂度为 O(1)。尽管选择排序易于理解并实现,但由于其较高的时间复杂度,在处理大量数据时效率较低,不适用于对性能要求较高的场景。但在实际应用中,对于小规模数据或者特定场合(如内存非常有限),选择排序仍有一定的价值。1.2 排序过程假设你正在整理一叠扑克牌,你想按照点数从小到大进行排列。1.初始状态:桌上有一堆乱序的扑克牌。2.第一步:你从这堆牌中任意拿起一张,然后查看剩下的所有牌,从中找出点数最小的那张。假设我们都先拿未排序的第一张,然后用一个变量记录下它在的位置,注意这个变量始终会记住最小牌的下标。用Q与后面的牌进行比较大小。如果出现一张牌比Q小,那么将新的牌位置去替换旧牌的位置,注意这里并不是要将他们交换,而是将minIndex的值换成4的下标。当然后续比较大小时,牌就变成了4。我们是要找到最小数,因此对于大于4的牌都不用理会。重复上面的过程,我们会找到最小数2。交换位置:将找到的最小点数的牌放在手边已排序部分的顶部,即你现在只有一张排好序的牌。交换以后的牌序如下图所示:4.重复上述过程,每次都在剩余的未排序扑克牌中找出最小点数的牌并放置到已排序部分的末尾。第二轮:在剩余未排序的七张牌中找到最小点数的牌,这里是黑桃3。第三轮:在剩余未排序的六张牌中找到最小点数的牌,这里是黑桃4。第四轮:在剩余未排序的五张牌中找到最小点数的牌,这里是方片5。第五轮:在剩余未排序的四张牌中找到最小点数的牌,这里是梅花7。第六轮:在剩余未排序的三张牌中找到最小点数的牌,这里是梅花J。第七轮:在剩余未排序的两张牌中找到最小点数的牌,这里是红桃Q。至此,经过七轮选择排序后,我们得到按照点数从小到大排列的扑克牌序列。二、例题讲解问题一:1010 - 数组元素的排序对数组的元素按从小到大进行排序。1.分析问题已知:n个整数的数组未知:更新后的数组关系:从小到大进行排序-冒泡排序2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n,a[10];3.输入数据从键盘读入。//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}4.数据计算选择排序。 //排序1:选择
for (int i = 0; i < n - 1; i++) { // 迭代所有元素,除了最后一个(因为最后一个是已排序部分的末尾)
int minIndex = i; // 假设当前元素为最小值
// 寻找剩余未排序部分中的最小值索引
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j; // 更新找到的最小值索引
}
}
// 将找到的最小值与当前位置的元素交换
if (minIndex != i) {
swap(arr[i], arr[minIndex]);
}
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:n个整数的数组
//未知:更新后的数组
//关系:从小到大进行排序-冒泡排序
//二、数据定义
int n,arr[10];
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>arr[i];
}
//四、数据计算
//排序1:选择
for (int i = 0; i < n - 1; i++) { // 迭代所有元素,除了最后一个(因为最后一个是已排序部分的末尾)
int minIndex = i; // 假设当前元素为最小值
// 寻找剩余未排序部分中的最小值索引
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j; // 更新找到的最小值索引
}
}
// 将找到的最小值与当前位置的元素交换
if (minIndex != i) {
swap(arr[i], arr[minIndex]);
}
}
//五、输出结果
for(int i=0;i<n;i++){
cout<<arr[i]<<" ";
}
return 0;
}问题二:1166 - 数的排序输入 n 个不超过 30000 的整数(n≤10 )。然后求出每个数的数字和,再按每个数的数字和由小到大排列输出。1.分析问题已知:n个不超过30000的整数的数组未知:更新后的数组关系:从小到大排列的每个数的数位和2.定义变量根据分析的已知,未知按需要定义变量。sum:和。 //二、数据定义
int n,a[20],sum; 3.输入数据从键盘读入。每个数字的数位和,因此使用短除法获得各位余数。//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
sum=0;
while(a[i]>0){
sum+=a[i]%10;
a[i]/=10;
}
a[i]=sum;
}4.数据计算选择排序。//四、数据计算
for (int i = 0; i < n - 1; i++) { // 迭代所有元素,除了最后一个(因为最后一个是已排序部分的末尾)
int minIndex = i; // 假设当前元素为最小值
// 寻找剩余未排序部分中的最小值索引
for (int j = i + 1; j < n; j++) {
if (a[j] < a[minIndex]) {
minIndex = j; // 更新找到的最小值索引
}
}
// 将找到的最小值与当前位置的元素交换
if (minIndex != i) {
swap(a[i], a[minIndex]);
}
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:n个不超过30000的整数的数组
//未知:更新后的数组
//关系:从小到大排列的每个数的数位和
//二、数据定义
int n,a[20],sum;
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
sum=0;
while(a[i]>0){
sum+=a[i]%10;
a[i]/=10;
}
a[i]=sum;
}
//四、数据计算
for (int i = 0; i < n - 1; i++) { // 迭代所有元素,除了最后一个(因为最后一个是已排序部分的末尾)
int minIndex = i; // 假设当前元素为最小值
// 寻找剩余未排序部分中的最小值索引
for (int j = i + 1; j < n; j++) {
if (a[j] < a[minIndex]) {
minIndex = j; // 更新找到的最小值索引
}
}
// 将找到的最小值与当前位置的元素交换
if (minIndex != i) {
swap(a[i], a[minIndex]);
}
}
//五、输出结果
for(int i=0;i<n;i++){
cout<<a[i]<<" ";
}
return 0;
}问题三:1175 - 语文成绩给出 N (5≤N≤150 )个人的语文成绩,求 N 个人的语文总分和平均分,并按成绩高低排序后输出。1.分析问题已知:N个人的语文成绩未知:总分sum 平均分avg 排名顺序关系:顺序从高到低2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n,a[200],sum=0;
double avg=0; 3.输入数据从键盘读入。 //三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
sum+=a[i];
}4.数据计算选择排序,注意是从高到低排列。//四、数据计算
avg=sum*1.0/n;
for (int i = 0; i < n - 1; i++) { // 迭代所有元素,除了最后一个(因为最后一个是已排序部分的末尾)
int maxIndex = i; // 假设当前元素为最小值
// 寻找剩余未排序部分中的最小值索引
for (int j = i + 1; j < n; j++) {
if (a[j] > a[maxIndex]) {
maxIndex = j; // 更新找到的最小值索引
}
}
// 将找到的最小值与当前位置的元素交换
if (maxIndex != i) {
swap(a[i], a[maxIndex]);
}
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:N个人的语文成绩
//未知:总分sum 平均分avg 排名顺序
//关系:顺序从高到低
//二、数据定义
int n,a[200],sum=0;
double avg=0;
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
sum+=a[i];
}
//四、数据计算
avg=sum*1.0/n;
for (int i = 0; i < n - 1; i++) { // 迭代所有元素,除了最后一个(因为最后一个是已排序部分的末尾)
int maxIndex = i; // 假设当前元素为最小值
// 寻找剩余未排序部分中的最小值索引
for (int j = i + 1; j < n; j++) {
if (a[j] > a[maxIndex]) {
maxIndex = j; // 更新找到的最小值索引
}
}
// 将找到的最小值与当前位置的元素交换
if (maxIndex != i) {
swap(a[i], a[maxIndex]);
}
}
//五、输出结果
cout<<sum<<endl;
printf("%.2f\n",avg);
for(int i=0;i<n;i++){
cout<<a[i]<<" ";
}
return 0;
}问题四:1233 - 求中位数中位数指的是一组数,如果按照大小排序排好后最中间的那个数的值,如果有偶数个元素,那么就是最中间两个数的平均数!比如:2 5 8 1 6 ,排序后的结果为 1 2 5 6 8 ,那么这组数的中位数就是 5 !再比如: 8 9 1 2 3 0 ,排序后的结果为0 1 2 3 8 9 ,那么这组数的中位数就是 (2+3)/2=2.5 。1.分析问题已知:一组数未知:中位数 median关系:最中间的那个数的值,有偶数个元素,那么就是最中间两个数的平均数2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n,a[100];
double median;3.输入数据从键盘读入。 //三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}4.数据计算想要求出一堆数中的中位数,必然先对它们进行排序。选择排序。//四、数据计算
for(int i=0;i<n-1;i++){
int minIndex=i;
for(int j=i+1;j<n;j++){
if(a[j]<a[minIndex]){
minIndex=j;
}
}
if(i!=minIndex){
swap(a[i], a[minIndex]);
}
}
if(n%2==0){
median=(a[n/2]+a[n/2-1])*1.0/2;
}else{
median=a[n/2]*1.0;
}
5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一组数
//未知:中位数 median
//关系:最中间的那个数的值,有偶数个元素,那么就是最中间两个数的平均数
//二、数据定义
int n,a[100];
double median;
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
//四、数据计算
for(int i=0;i<n-1;i++){
int minIndex=i;
for(int j=i+1;j<n;j++){
if(a[j]<a[minIndex]){
minIndex=j;
}
}
if(i!=minIndex){
swap(a[i], a[minIndex]);
}
}
if(n%2==0){
median=(a[n/2]+a[n/2-1])*1.0/2;
}else{
median=a[n/2]*1.0;
}
//五、输出结果
printf("%.1f",median);
return 0;
}三、练习请用选择排序。问题一:1172 - 寻找第K大数问题二:1221 - 优秀成绩的平均分问题三:1242 - 第K大与第K小数问题四:1399 - 学员的名次?四、总结以上就是本节关于选择排序的全部内容。
跟着小潘学后端
【C++】C++程序结构入门之循环结构四
一、常见的数值规律假设i是1~5的连续自然数,学会通过循环变量i得到常见数列。1.1 输出1 2 3 4 5的数列。输出结果为:1 2 3 4 5。for(int i=1;i<=5;i++){
cout<<i<<" ";
}1.2 输出2 4 6 8 10的数列。输出结果为:2 4 6 8 10。for(int i=1;i<=5;i++){
cout<<i*2<<" ";
}1.3 输出1 3 5 7 9的数列。输出结果为:1 3 5 7 9。for(int i=1;i<=5;i++){
cout<<i*2-1<<" ";
}1.4 输出5 4 3 2 1的数列。输出结果为:5 4 3 2 1。for(int i=1;i<=5;i++){
cout<<5-i+1<<" ";
}1.5 输出9 7 5 3 1的数列。输出结果为:9 7 5 3 1。for(int i=1;i<=5;i++){
cout<<(5-i+1)*2-1<<" ";
}总结:假设i是1-5的连续自然数(1)连续自然数:cout<<i<<" “;(2) 连续的偶数:cout<<i * 2<<” “;(3)连续的奇数:cout<<i * 2-1<<” “;(4)倒序的自然数:cout<<5-i+1<<” “;(5)倒序的奇数:cout<<(5-i+1) * 2-1<<” ";二、嵌套循环图形题问题一:1068 - 字符图形4-星号正三角输入一个整数打印字符图形。1.分析问题已知:一个整数未知:图形关系:数列规律2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n;3.输入数据从键盘读入整数n。 //三、数据输入
cin>>n;4.数据计算根据现有图形分析规律。首先观察图形,虽然只输出 * 图形,实际上图形中还包含空格元素,所以需要用一个循环控制空格,一个循环控制星号。空格:假如输入的整数n=3,那么会有两行出现空格,并且空格依次递减,第一行2个即n-1,第二行1个即n-2。已知i递增,所以,空格的数量刚好为n-i。for(int j=1;j<=n-i;j++){
//五、输出结果
cout<<" ";
}星号:假如输入的整数n=3,星号出现的数量为1,3,5,是一个奇数数列。在上面总结的规律中,奇数数列i * 2-1,因此星号的数量为i * 2-1。for(int k=1;k<=2*i-1;k++){
//五、输出结果
cout<<"*";
}然后我们将每一行的内容循环n次即可,注意换行。//四、数据计算
for(int i=1;i<=n;i++){
cout<<endl;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个整数
//未知:图形
//关系:数列规律
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
for(int i=1;i<=n;i++){
for(int j=1;j<=n-i;j++){
//五、输出结果
cout<<" ";
}
for(int k=1;k<=2*i-1;k++){
//五、输出结果
cout<<"*";
}
cout<<endl;
}
return 0;
}问题二:1070 - 字符图形6-星号倒三角输入一个整数打印字符图形。1.分析问题已知:一个整数未知:图形关系:数列规律2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n;3.输入数据从键盘读入整数n。 //三、数据输入
cin>>n;4.数据计算根据现有图形分析规律。首先观察图形,虽然只输出 * 图形,实际上图形中还包含空格元素,所以需要用一个循环控制空格,一个循环控制星号。空格:假如输入的整数n=3,那么会有两行出现空格,并且空格依次递增,第一行0个,第二行1个,第三行2个。每行的空格刚好受到行数控制。已知i递增,所以,空格的数量刚好为就 j < i。for(int j=1;j<i;j++){
//五、输出结果
cout<<" ";
}星号:假如输入的整数n=3,星号出现的数量为5,3,1,是一个倒序的奇数数列。在上面总结的规律中,倒序奇数数列(3-i+1) * 2-1,因此星号的数量为(n-i+1) * 2-1。for(int k=(n-i+1)*2-1;k>0;k--){
//五、输出结果
cout<<"*";
}然后我们将每一行的内容循环n次即可,注意换行。//四、数据计算
for(int i=1;i<=n;i++){
cout<<endl;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个整数
//未知:图形
//关系:数列规律
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
//五、输出结果
cout<<" ";
}
for(int k=(n-i+1)*2-1;k>0;k--){
//五、输出结果
cout<<"*";
}
cout<<endl;
}
return 0;
}问题三:1071 - 字符图形7-星号菱形输入一个整数 n ,请打印出 n ∗ 2+1 行的字符图形。1.分析问题已知:一个整数未知:图形关系:数列规律2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n;3.输入数据从键盘读入整数n。 //三、数据输入
cin>>n;4.数据计算本题属于上下对称类图形,这种类型的题目都应采取分而治之的方法,即将图形按规律分成几部分打印。上半部分:将菱形分成两部分,上半部分即为正三角形。根据上题《1068 - 字符图形4-星号正三角》可以轻松得到以下代码。//四、数据计算
for(int i=1;i<=n+1;i++){
for(int j=n-i;j>=0;j--){
//五、输出结果
cout<<" ";
}
for(int k=1;k<=2*i-1;k++){
//五、输出结果
cout<<"*";
}
cout<<endl;
}下半部分:将菱形分成两部分,下半部分即为倒三角形。根据上题《1070 - 字符图形6-星号倒三角》可以轻松得到以下代码。for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
//五、输出结果
cout<<" ";
}
for(int k=(n-i+1)*2-1;k>0;k--){
//五、输出结果
cout<<"*";
}
cout<<endl;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个整数
//未知:图形
//关系:数列规律
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
for(int i=1;i<=n+1;i++){
for(int j=n-i;j>=0;j--){
//五、输出结果
cout<<" ";
}
for(int k=1;k<=2*i-1;k++){
//五、输出结果
cout<<"*";
}
cout<<endl;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
//五、输出结果
cout<<" ";
}
for(int k=(n-i+1)*2-1;k>0;k--){
//五、输出结果
cout<<"*";
}
cout<<endl;
}
return 0;
}问题四:1219 - 放大的箭头请打印n行的放大的箭头( n 一定是一个奇数)。1.分析问题已知:一个整数未知:图形关系:数列规律2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n;3.输入数据从键盘读入整数n。 //三、数据输入
cin>>n;4.数据计算本题属于上下对称类图形,这种类型的题目都应采取分而治之的方法,即将图形按规律分成几部分打印。上半部分:空格:上半部分的空格是依次递增的。可以参考星号三角写法。for(int k=1;k<i;k++){
//五、输出结果
cout<<" ";
}星号:每行输出的星号数量都是确定,即n个。for(int j=1;j<=n;j++){
//五、输出结果
cout<<"*";
}因为是奇数列,所以要么上半部分多输出一行,要么在下半部分多输出一行。for(int i=1;i<=n/2+1;i++){
cout<<endl;
}下半部分:空格:下半部分的空格是依次递减的。for(int k=1;k<=i-1;k++){
//五、输出结果
cout<<" ";
}星号:同理,下半部分的每行输出的星号数量也是确定,即n个。for(int j=1;j<=n;j++){
//五、输出结果
cout<<"*";
}循环n/2次,做减法,是方便倒序输出空格。for(int i=n/2;i>=1;i--){
cout<<endl;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个整数
//未知:图形
//关系:数列规律
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
for(int i=1;i<=n/2+1;i++){
for(int k=1;k<i;k++){
//五、输出结果
cout<<" ";
}
for(int j=1;j<=n;j++){
//五、输出结果
cout<<"*";
}
cout<<endl;
}
for(int i=n/2;i>=1;i--){
for(int k=1;k<=i-1;k++){
//五、输出结果
cout<<" ";
}
for(int j=1;j<=n;j++){
//五、输出结果
cout<<"*";
}
cout<<endl;
}
return 0;
}三、程序调错使用DEV C++进行调试可以让您更好地了解程序的执行流程,并且可以在运行时查看变量的值。帮助你发现程序的逻辑错误。以下是使用DEV C++进行调试的一般步骤:3.1打开您的项目或源文件。打开或者新建项目。3.2 在菜单栏中选择“调试”>“开始调试”或使用快捷键F8来启动调试会话。3.3在调试会话中,您可以使用以下调试功能:3.3.1 设置断点:在您认为可能出错的代码行上设置断点。断点将使程序在执行到该行时暂停。在有数字的地方点击一次,看见与其他行出现不同变化即可。3.3.2 单步执行:使用F7键逐行执行程序。这将允许您逐步查看程序的执行流程。可以在调试栏中选择下一步,跳过,下一条语句等操作。3.3.3 查看变量:在调试会话中,您可以查看变量的当前值。您可以将鼠标悬停在变量上或使用“监视”窗口来查看变量的值。鼠标选择变量后,点击调试栏中的添加查看,即可在左边“监视”窗口看到变量变化的情况。3.3.4 监视表达式:您可以在“监视”窗口中添加表达式,以便在调试会话中查看其值。和查看变量是一样的操作。四、练习问题一:1225 - 打印空心等腰三角形从键盘读入一个整数 n ,代表等腰三角形的边长,请输出一个边长为 n 的等腰三角形!问题二:1069 - 字符图形5-星号梯形问题三:1073 - 沙漏赵老师最近在编一个操作系统,正好编到鼠标的繁忙状态,需要一个沙漏符号,正好大家都在学C++ ,你的任务就是帮赵老师编一个程序打印一个沙漏符号。问题四:1230 - 蝴蝶结请输出 n 行的蝴蝶结的形状,n 一定是一个奇数!问题五:1247 - 打印n行的完整的蝴蝶结请从键盘读入一个整数 n(1∼10 的范围内的奇数),打印出如下图所示的n行的完整的蝴蝶结!问题六:1246 - 请输出n行的9*9乘法表请从键盘读入一个整数 n ,代表有 n 行,输出 n 行的9*9乘法表。问题七:1008 - 字符图形9-数字正三角输入一个整数打印字符图形。问题八:1006 - 打印星号三角形打印星号三角形。问题九:1239 - 挑战赛第二题——放大的X请你编程画一个放大的 X (大写字母)。问题十:1353 - 轴对称三角形在数学上,我们发现有一类图形是对称图形。我们对于左右一样的图形叫做沿 y 轴对称,对于上下一样的图形叫做沿 x 轴对称。五、总结以上就是利用嵌套循环打印出各种图形的内容,关于这种类型的题目都要从图形入手,根据图形的变化规律按需将图形分割或者不分割。再从中找到数列的规律。
跟着小潘学后端
Python异常处理(七)
一.什么是异常程序运行的过程中出现了错误定义:在程序运行中,检测到一个错误,程序中止运行并且出现了一些错误的提示,也称作BUG例如:读取一个不存在的文件f = open("C:/code/观止.txt", "r")二.为什么要捕获异常避免程序中止,提前准备处理可能出现的异常在真实工作中, 我们肯定不能因为一个小的BUG就让整个程序全部奔溃,而是对BUG进行提醒, 整个程序继续运行三.如何捕获异常在可能出现异常的地方,做好提前准备,当真的出现异常的时候,可以有后续手段。(1) 捕获常规异常基本语法:try:
可能发生错误的代码
except:
如果出现异常执行的代码
# 未发生错误try全部代码都会执行
# 未发生错误不会执行except中的代码
# 发生错误try中只会执行到报错行为止的代码
# 发生错误会执行except中的代码使用示例:首次执行,文件不存在,程序未报错中止,而是转而执行except中代码,创建文件try:
print("r模式打开") # 执行
f = open("C:/code/观止.txt", "r") # 报错
print("r模式打开") # 不执行
except:
print("w模式打开") # 执行
f = open("C:/code/观止.txt", "w") # 执行
print("w模式打开") # 执行第二次执行,文件存在,程序无异常,只执行try中代码try:
print("r模式打开") # 执行
f = open("C:/code/观止.txt", "r") # 执行
print("r模式打开") # 执行
except:
print("w模式打开") # 不执行
f = open("C:/code/观止.txt", "w") # 不执行
print("w模式打开") # 不执行
(2) 捕获特定异常如果尝试执行的代码的异常类型和要捕获的异常类型不一致,则无法捕获异常。基本语法:try:
可能发生错误的代码
except 待捕获异常名 as 别名:
如果出现异常执行的代码例如:捕获未定义变量产生的错误try:
print(name) # 未定义变量,报错
except NameError as e:
print('name变量名称未定义错误')同样的代码却无法捕获处理找不到文件异常try:
f = open("C:/code/study.txt", "r") # 文件不存在,报错
except NameError as e:
print('文件不存在') (3) 捕获多个异常格式一:当待捕获异常名为Exception可以捕获所有类型异常,作用与(1)一致例如try:
f = open("C:/code/study.txt", "r")
except Exception as e:
print('文件不存在')格式二:把要捕获的异常类型的名字,放到except 后,并使用元组的方式进行书写。基本格式:try:
可能发生错误的代码
except (异常名1,异常名2) as 别名:
如果出现异常执行的代码使用示例:# 示例一:
try:
f = open("C:/code/study.txt", "r")
except (FileNotFoundError, NameError) as e:
print('文件不存在')
# 示例二:
try:
print(name)
except (FileNotFoundError, NameError) as e:
print('名称未定义')指定的两种异常都能捕获,未指定的无法捕获到(4) 其他用法(4.1) 打印异常信息异常描述信息存贮在别名中,可以通过打印别名获取使用示例:try:
print(num) # 未定义,报错
except (NameError, ZeroDivisionError) as e:
print(e) # 打印 name 'num' is not defined(4.2) 异常elseelse表示的是如果没有异常要执行的代码。使用示例:出现异常,打印结果与(4.2)一致try:
print(num) # 未定义,报错
except (NameError, ZeroDivisionError) as e:
print(e) # 打印 name 'num' is not defined
else:
print("无异常") # 有异常,不执行无异常try:
print("正常") # 不报错
except (NameError, ZeroDivisionError) as e:
print(e) # 不执行
else:
print("无异常") # 执行(4.3) 异常finallyfinally表示的是无论是否异常都要执行的代码使用示例:之前提过,如果open文件却一直未close且程序未中止,将一直占用文件无法操作如果打开文件后发生异常,未close也将导致一直占用,因此可选择在finally中closeglobal f
try:
f = open("C:/code/aaa.txt", "r")
except Exception as e:
print(e)
finally:
f.close() # 一定会执行close操作四.异常的传递异常是具有传递性的(向上一级抛出)当函数调用链中出现异常,如果所有函数都没有捕获异常的时候, 程序就会报错利用异常具有传递性的特点, 当我们想要保证程序不会因为异常崩溃的时候, 就可以在主函数中设置异常捕获, 由于无论在整个程序哪里发生异常, 最终都会传递到主函数中, 这样就可以确保所有的异常都会被统一捕获五.全文概览
跟着小潘学后端
【C++】C++程序结构入门之循环结构三
一、while循环和for循环1.1 循环的打破与跳过1.1.1 break 打破当循环执行到break语句时,程序会跳出循环,不再执行循环体内剩余的语句,继续执行循环体外的语句。演示如何使用break语句跳出循环:#include <iostream>
using namespace std;
int main() {
int i = 0;
while (i < 10) {
if (i == 5) {
break; // 当i等于5时跳出循环
}
cout << i << endl;
i++;
}
return 0;
}该程序使用while循环打印0到10的数字,但是当i等于5时使用break语句跳出循环。程序输出如下:0
1
2
3
41.1.2 continue 跳过当循环执行到continue语句时,程序会跳过循环体内剩余的语句,直接进入下一次循环。演示如何使用continue语句跳过循环体内的某些语句,直接进入下一次循环。#include <iostream>
using namespace std;
int main() {
int i = 0;
while (i < 10) {
i++;
if (i == 5) {
continue; // 当i等于5时跳过循环体内剩余语句,直接进入下一次循环
}
cout << i << endl;
}
return 0;
}该程序使用while循环打印0到10的数字,但当i等于5时,使用continue语句跳过循环体内剩余语句,直接进入下一次循环。以下是这个程序的输出结果:1
2
3
4
6
7
8
9
101.1.3 总结C++中break语句和continue语句都可以用于控制循环语句的执行流程,但它们的作用不同。break语句用于完全终止循环语句的执行,即跳出循环体。当程序执行到break语句时,循环语句会立即终止,程序将跳出循环体,继续执行循环语句之后的代码。break语句通常用于在满足某些条件时提前结束循环,以节省时间和资源。continue语句用于跳过当前循环中的某些语句,即跳过本次循环的剩余语句,直接进入下一次循环。当程序执行到continue语句时,循环语句会立即跳过本次循环的剩余语句,直接进入下一次循环。continue语句通常用于在满足某些条件时跳过本次循环,以提高循环效率。1.2 循环使用场景1.2.1 while循环C++中的while循环通常用于在不知道循环次数的情况下进行遍历,或者在满足某个条件时重复执行某个代码块。比如说常用于求余的短除法。问题 1119 - 求各位数字之和输入一个正整数 N(0≤N≤2147483647) ,求它的各位数字之和。首先从题目获取可用信息,这是一个整数各数位求和的问题,所以关键点在于如何找出各数位;根据之前的拆位公式可以很轻松完成,但是这个数可能很大,也可能很小;如果按照最大数去设计变量再编写代码是一件相当繁琐的事。这里就可以利用循环将每次求出来的余数直接累加即可,但是这个数是不确定的,所以我们不知道循环次数,所以考虑使用while循环。以下是代码展示:#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:正整数N
//未知:各位数字之和sum
//关系:拆位得到各数位再相加
//二、数据定义
int n,sum=0;
//三、数据输入
cin>>n;
//四、数据计算
while(n>0){
sum+=n%10;
n/=10;
}
//五、输出结果
cout<<sum;
return 0;
}1.2.2 for循环C++中的for循环通常用于已知循环次数的情况下进行遍历或者重复执行某个代码块。1065 - 字符图形1-星号矩形打印字符图形。输出 n 行 n 列 *首先从题目获取可用信息,打印出 n 行 n 列 * 的图形,n不确定肯定需要使用循环完成。 * 的每行数量从0开始到n结束,一共输出n行。所以循环次数是非常明显的,使用for循环。以下是代码展示:#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:整数n
//未知:字符图形
//关系:n行n列*
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
//五、输出结果
cout<<"*";
}
cout<<endl;
}
return 0;
}二、嵌套循环概念嵌套循环是指在一个循环结构中嵌套另一个循环结构,内层循环结构的循环次数由外层循环结构控制。嵌套循环通常用于需要对多维数据进行遍历或处理的情况。C++中的嵌套循环结构通常采用for循环嵌套的方式实现,其基本语法如下:for (循环变量1的初始化; 循环变量1的条件; 循环变量1的更新) {
for (循环变量2的初始化; 循环变量2的条件; 循环变量2的更新) {
// 执行语句
}
}其中,外层循环控制循环次数,内层循环在外层循环的每一次迭代中都会执行一次,直到内层循环的条件不满足为止。下面是一个简单的嵌套循环的例子,用于输出九九乘法表:for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
cout << j << "*" << i << "=" << i * j << " ";
}
cout << endl;
}这段代码中,外层循环变量i从1到9遍历,内层循环变量j从1到i遍历,输出乘法表的每一行。当然在C++中,循环嵌套不只使用for,还可以使用while循环。比如:while循环嵌套while循环。while()
{
while()
{
}
}
比如:while循环嵌套for循环。
while() {
for(;;){
}
}
搭配的格式并不固定,主要还是要考虑到使用场景,while循环适合不确定次数或者满足某个条件下使用,for循环更适合确定次数。问题一:1422 - 数字矩形(1)从键盘读入一个整数 n,输出如下图形。1.分析问题已知:整数 n未知:图形关系:n行n列的数字,数字为行的值。2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n;3.输入数据从键盘读入整数n。 //三、数据输入
cin>>n;4.数据计算既然想要打印出图形,就要思考是如何输出的?首先可以肯定的是按照行输出,所以我们一次性要将整行的内容输出完。例如n = 5,那么每行就是5个数字,所以循环次数就是5;如果n = 6,那么循环6次;因此可以推出,循环的次数就是n。for(int j=1;j<=n;j++){
}输出完每行的内容,就要考虑输出几行。如果n = 3,就要输出3行的数字;如果n=5,就要输出5行的数字;因此可以推出需要输出n行。 for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
}
}5.输出结果输出的数字是行的值,所以刚好可以使用i的值。注意i为行的循环,j为整行的内容循环。不要输出j,不然就会得到12345的结果。在完整输出一行内容后注意需要进行换行。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:整数 n
//未知:图形
//关系:n行n列的数字,数字为行的值。
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
//五、输出结果
cout<<i;
}
cout<<endl;
}
return 0;
}问题二:1363 - 数字矩形(2)从键盘读入一个整数 n,输出如下图形。1.分析问题已知:整数n未知:输出图形关系:n行n列,每行数值从1递增到n2.定义变量根据分析的已知,未知按需要定义变量。//二、数据定义
int n;3.输入数据从键盘读入整数n。//三、数据输入
cin>>n;4.数据计算本题和1422 - 数字矩形(1)题目基本相同,相同点是输出的格式一致,不同点在于输出的内容不一致。因此直接套用上一题的循环结构。//四、数据计算
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
}
}5.输出结果在1422 - 数字矩形(1)题目中,我们提到i为行数的循环值,j为行内容的循环值。这题的输出内容为n行n列,每行数值从1递增到n ;因此需要输出j的值。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:整数n
//未知:输出图形
//关系:n行n列,每行数值从1递增到n
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
//五、输出结果
cout<<j;
}
cout<<endl;
}
return 0;
}问题三:1066 - 字符图形2-星号直角从键盘读入一个整数n,输出如下图形。1.分析问题已知:整数n未知:输出图形关系:n行,每行*数量从1递增到每行n2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n;3.输入数据从键盘读入整数n。 //三、数据输入
cin>>n;4.数据计算在1422 - 数字矩形(1)和1363 - 数字矩形(2)题目中,输出的内容都是矩形形状。而本题要求输出三角形状。那么就不能直接套用循环结构的代码。首先还是要分析每行的内容,每行的数量不一致,是从1递增的。数量刚好和行数相同,比如第一行就只有1个*,第二行有2个*…依次类推第n行有n个*。那么j和i必然有某种联系(i为行数,j为行内容) ,即j的最大值应该等于当前i的值。//四、数据计算
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
}
}5.输出结果按题目要求输出*即可。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:整数n
//未知:输出图形
//关系:n行,每行*数量从1递增到每行n
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
//五、输出结果
cout<<"*";
}
cout<<endl;
}
return 0;
}三、练习问题:1392 - 回文偶数?小明发现有一类数非常有趣,他们正过来读和反过来读是一样的,比如:121、202、383 等,小明给这类数起了一个名字,叫做回文数。请你写程序帮助小明找出所有 3 位的既是回文数,又是偶数的数,比如: 202 就是满足条件的数,而 121 虽然是回文数但不是偶数。题解:1392 - 回文偶数?问题:1090 - 同因查找求出 10 至 1000 之内能同时被 2、3、7 整除的数,并输出。每行一个。题解:1090 - 同因查找问题:1393 - 与7无关的数?一个整数,如果这个数能够被 7 整除,或者其中有一位是7,我们称为这个数是与 7 有关的数。比如: 14 能被 7 整除,17 有一位为 7 ,这两个数都是与 7有关的数。请你编程求出1∼n(n≤999) 中,与 7 无关的数的总和是多少?比如 1∼10 中与 7 无关的数的和为:1+2+3+4+5+6+8+9+10=48 。题解:1393 - 与7无关的数?问题:1091 - 奇数及偶数和给出一个正整数 n(1≤n≤1000 ),求出 1,2,…n 中全部奇数和以及全部偶数的和。 例如:n=9 ;奇数和 1+3+5+7+9=25 ;偶数和 2+4+6+8=20 。题解:1091 - 奇数及偶数和问题:1395 - 小丽找数?小丽同学想在1~n中找出这样的数,这个数的各个位的和不能被2整除也不能被5整除,比如3、12、25、30、100。这些数都满足各个位的和不能被2和5整除。请你编程找出1~n中这些数有多少个?题解:1395 - 小丽找数?问题:1241 - 角谷猜想本一位中学生发现一个奇妙的定理,请角谷教授证明,而教授无能为力,于是产生了角谷猜想。猜想的内容:任给一个自然数,若为偶数则除以 2 ,若为奇数则乘 3 加 1 ,得到一个新的自然数后按上面的法则继续演算。若干次后得到的结果必为 1 。请编写代码验证该猜想:求经过多少次运算可得到自然数 1 。题解:1241 - 角谷猜想问题:1265 - 爱因斯坦的数学题爱因斯坦出了一道这样的数学题:有一条长阶梯,若每步跨 2 阶,则最最后剩一阶,若每步跨 3 阶,则最后剩 2 阶,若每步跨 5阶,则最后剩 4阶,若每步跨 6 阶则最后剩 5 阶。只有每次跨 7阶,最后才正好一阶不剩。请问这条阶梯最少共有多少阶?题解:1265 - 爱因斯坦的数学题四、总结以上就是今天的全部内容,结合循环一、循环二里面讲解的知识,那么关于循环结构部分我们已经介绍的相对详细。想要掌握牢固,还需要多多练习。
跟着小潘学后端
Netty为什么高效,为什么这么受欢迎?
前言上篇文章通过 Java NIO 的处理流程与 Netty 的总体流程比较,并结合 Netty 的源码,可以更加清晰地理解Netty。本文将结合源码详细解析Netty的高效和强大功能的设计原理,学习 Netty 是如何实现其卓越的性能和功能特性,也希望可以在日后工作中利用到 Netty 的设计思想。Netty 解决的问题我们先看看使用 Netty 在网络编程中帮助我们解决了什么问题简化网络编程首先基于 Netty 初次编码的直观体验来讲,开发者不用手动处理网络通信细节,包括线程管理、I/O 处理、协议解析等,可以专注于业务逻辑的实现。也正是因为如此,在学习 Netty 时比较抽象难懂 。下面对比一下 Java NIO 实现的简单的服务端和 Netty 实现的 HTTP 服务端,如下图,可以看到 Java NIO 的代码大概有 80 行,而且还没有实现 HTTP 协议,并且还是单线程,没有复杂的线程管理,更不用说性能什么的,而 Netty 实现的代码只有 30 多行,其中的差别一目了然。 粘包和拆包我们一般说粘包和拆包都是说 TCP 协议的问题,因为当用户消息通过 UDP 协议传输时,操作系统不会对消息进行拆分,所以发送出去的一条 UDP 报文就是完整的用户消息,也就是每个 UDP 报文就是用户消息的边界。而当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文进行传输,这个时候接收方收到多个报文后,由于不知道消息的边界,也就无法读出一个有效的用户消息。举个例子,当发送方准备发送 「Hi」和「I am Erdan」这两个消息,由于MTU限制、缓冲区的大小等条件,可能会出现几种情况:第一种情况,两条消息分到一个报文中,像这样:第二种情况,「I am Erdan」中的部分消息随「Hi」被分到一个报文中,像这样:还可能会有第三、四...种情况,而当接收方接收到第一种情况时我们称之为粘包,第二种情况称之为拆包。并且上面的种种情况可以表明不能认为一个用户消息对应一个 TCP 报文,正因为这样,所以 TCP 是面向字节流的协议。解决粘包和拆包的根本手段就是找出消息的边界,有几种方式:固定消息长度,这种方式灵活性不高,实际中很少用。特殊字符作为边界,HTTP 是一个非常好的例子,通过设置回车符、换行符作为 HTTP 报文协议的边界。自定义消息结构:消息头消息体,可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。而 Netty 提供了固定长度解码器(FixedLengthFrameDecoder)、行分隔符解码器(LineBasedFrameDecoder)、分隔符解码器(DelimiterBasedFrameDecoder)、**基于长度字段的解码器(LengthFieldBasedFrameDecoder)**几种方式来解决粘包问题,可以结合 Netty 的 ChannelPipeline 来使用。除此之外 Netty 也提供了 HTTP、WebSocket、TCP、UDP几种协议的编解码器,这也是 Netty 灵活扩展强大之处。高性能的设计Netty 除了帮助开发人员解决了一些问题,还提高了网络编程性能,体现如下:多线程调度在网络编程中如果使用单线程来处理,即便是IO多路复用,吞吐和性能也是会有局限的。而 Netty 中通过 EventLoopGroup 管理线程池,每个线程就是一个 EventLoop,而 EventLoop 内部有一个 Selector 负责处理一个或多个 Channel 的注册、读写和其他事件。所以 Netty 通过 EventLoopGroup、EventLoop 和 Selector 的配合工作,实现了高效的并发处理能力。 既然是多线程处理,肯定要去考虑线程安全以确保程序的正确性。而 Netty 是如何保障线程安全的?Netty 通过使用管道(ChannelPipeline)和处理器(ChannelHandler)的方式来实现数据的处理和流转,而 ChannelHandler 会被分配给一个 EventLoop 处理,而 EventLoop 内部的数据结构和状态都是线程封闭的,不会被其他线程访问或修改。所以 Netty 通过合理地设计组件之间的关系,通过单线程执行、无锁设计等方式保证了在高并发情况下的线程安全性。零拷贝在传统的网络编程中,数据在进行网络传输之前需要从应用层缓冲区复制到操作系统内核的缓冲区,然后再从内核的缓冲区复制到网络设备的缓冲区。这种复制操作会增加 CPU 的负载和内存的开销,如下图 而 Netty 利用零拷贝技术来减少数据复制的次数,提高了数据传输的效率。零拷贝通过将数据从内核空间直接传输到网络适配器,避免了数据在内核空间和用户空间之间的复制,从而减少了CPU的负担。 具体体现在以下几个方面:零拷贝文件传输:Netty 的 FileRegion 接口提供了直接在文件系统和网络之间传输数据的功能。通过使用零拷贝技术,数据可以直接从磁盘读取并发送到网络设备,避免了中间的缓冲区拷贝,提高了文件传输的性能。零拷贝内存传输:Netty 的 ByteBuf 类型支持零拷贝的内存传输。当数据在应用程序和内核之间传输时,Netty 使用直接内存缓冲区(Direct ByteBuffer)来避免额外的数据拷贝操作,提高了内存传输的效率。通过以上方式,Netty 实现了零拷贝技术在网络编程中的应用,提高了数据传输的效率和性能。这使得 Netty 在处理大量数据传输和高并发场景下具有更好的性能表现。总结总的来说,Netty 不论在功能、性能以及稳定性来讲都是一款很nice的网络编程框架,很多知名的项目都将 Netty 作为其网络通信的底层框架,比如Apache Kafka、Elasticsearch、gRPC、Dubbo等。熟悉这些框架的开发者通常都具备高并发开发经验,并且掌握 Netty 是理解这些框架的重要基础之一。
跟着小潘学后端
Python操作MySQL(十一)
一.安装第三方库在Python中,通过使用第三方库:pymysql,完成对MySQL数据库的操作。安装指令:pip install pymysql二.在Python中使用(1) 基本使用导包-》建立连接-》进行xx操作-》关闭连接:# 1.导入操作包
from pymysql import Connection
# 2.获取到MySQL数据库的连接对象
conn = Connection(
host='localhost', # 主机名或IP地址
port=3306, # 端口号,默认3306
user='root', # MySQL账号
password='root' # MySQL密码
)
# 打印MySQL版本信息
print(conn.get_server_info())
# 3.关闭到数据库的连接
conn.close()(2) 执行建表SQL导包-》建立连接-》获取游标对象-》选择数据库-》执行相应sql-》关闭连接:from pymysql import Connection
# 获取到MySQL数据库的连接对象
conn = Connection(
host='localhost', # 主机名或IP地址
port=3306, # 端口号,默认3306
user='root', # MySQL账号
password='root' # MySQL密码
)
"""
执行非查询性质SQL
"""
# 获取游标对象(用于操作数据库)
cursor = conn.cursor()
# 选择要操作的数据库
conn.select_db("db1")
# 使用游标对象,执行建表sql语句
cursor.execute("CREATE TABLE tb_user(id INT,name VARCHAR(8),age int)")
# 关闭到数据库的连接
conn.close()(3) 执行查询SQL导包-》建立连接-》获取游标对象-》选择数据库-》执行相应sql-》获取查询数据,执行xx操作-》关闭连接:from pymysql import Connection
# 获取到MySQL数据库的连接对象
conn = Connection(
host='localhost', # 主机名或IP地址
port=3306, # 端口号,默认3306
user='root', # MySQL账号
password='root' # MySQL密码
)
"""
执行查询性质SQL
"""
# 获取游标对象(用于操作数据库)
cursor = conn.cursor()
# 选择要操作的数据库
conn.select_db("db1")
# 使用游标对象,执行sql语句
cursor.execute("SELECT * FROM tb_user")
# 获取查询结果,返回元组对象
results: tuple = cursor.fetchall()
for result in results:
print(result)
# 关闭到数据库的连接
conn.close()(4) 执行插入SQL导包-》建立连接-》获取游标对象-》选择数据库-》执行相应sql-》提交行为-》关闭连接:from pymysql import Connection
# 获取到MySQL数据库的连接对象
conn = Connection(
host='localhost', # 主机名或IP地址
port=3306, # 端口号,默认3306
user='root', # MySQL账号
password='root', # MySQL密码
autocommit=True # 设置自动提交(commit)
)
"""
执行插入SQL
"""
# 获取游标对象(用于操作数据库)
cursor = conn.cursor()
# 选择要操作的数据库
conn.select_db("db1")
# 使用游标对象,执行sql语句
cursor.execute("Insert into tb_user values(1,'hhy','250')")
# 确认插入行为
# 如果在获取连接对象时设置自动提交可以不用再写。
conn.commit()
# 关闭到数据库的连接
conn.close()(5) 执行修改SQL导包-》建立连接-》获取游标对象-》选择数据库-》执行相应sql-》提交行为-》关闭连接:from pymysql import Connection
# 获取到MySQL数据库的连接对象
conn = Connection(
host='localhost', # 主机名或IP地址
port=3306, # 端口号,默认3306
user='root', # MySQL账号
password='root', # MySQL密码
autocommit=True # 设置自动提交(commit)
)
"""
执行修改SQL
"""
# 获取游标对象(用于操作数据库)
cursor = conn.cursor()
# 选择要操作的数据库
conn.select_db("db1")
# 使用游标对象,执行sql语句
cursor.execute("UPDATE tb_user set username='hhy' where username = 'fsp'")
# 确认修改行为
# 如果在获取连接对象时设置自动提交可以不用再写。
conn.commit()
# 关闭到数据库的连接
conn.close()(6) 执行删除SQL导包-》建立连接-》获取游标对象-》选择数据库-》执行相应sql-》提交行为-》关闭连接:from pymysql import Connection
# 获取到MySQL数据库的连接对象
conn = Connection(
host='localhost', # 主机名或IP地址
port=3306, # 端口号,默认3306
user='root', # MySQL账号
password='root', # MySQL密码
autocommit=True # 设置自动提交(commit)
)
"""
执行删除SQL
"""
# 获取游标对象(用于操作数据库)
cursor = conn.cursor()
# 选择要操作的数据库
conn.select_db("db1")
# 使用游标对象,执行sql语句
cursor.execute("DELETE from tb_user WHERE username = 'hhy'")
# 确认删除行为
# 如果在获取连接对象时设置自动提交可以不用再写。
conn.commit()
# 关闭到数据库的连接
conn.close()(7) 小结pymysql在执行数据插入或其它产生数据更改的SQL语句时,默认是需要提交更改的,即,需要通过代码“确认”这种更改行为。如果不想手动commit确认,可以在构建连接对象的时候,设置自动commit的属性。查询后,使用游标对象.fetchall()可得到全部的查询结果封装入嵌套元组内可使用游标对象.execute()执行SQL语句三.全文概览
跟着小潘学后端
认识网络编程
前言一个网络请求、服务之间的调用都需要进行网络通讯,在日常开发时我们可能并不会关心我们的服务端是怎么接收到请求的、调用别的服务是怎么调用的,都是直接使用现成的框架或工具,比如,Tomcat、Dubbo、OkHttp等提供网络服务的框架。作为程序员,我们还是要知其然知其所以然。本文将介绍在 Java 中如何进行网络编程以及网络编程的基础知识。什么是网络编程网络编程是指利用网络协议和技术实现计算机应用程序之间的通信、数据传输、交换,如TCP/IP协议、HTTP协议、Socket编程等,像 Java、C、C++、Python 这些语言都提供了网络编程的API和库函数,可以便捷地进行网络应用程序的开发。网络编程基础知识网络通讯流程网络通讯过程通常包括以下几个步骤:建立连接:通讯双方在网络中建立连接,即在物理层和链路层上进行握手,确认通讯协议和传输参数。数据传输:建立连接后,数据可以在通讯双方之间进行传输。数据传输过程中,需要进行分段、封装、逐层封装、加密和校验等。数据接收:数据接收方需要先解析、解封装和验证传输数据的正确性,然后对数据进行处理,包括存储和响应等。断开连接:在数据传输完成后,通讯双方需要在网络中断开连接,释放资源,并进行必要的后续操作。长连接和短连接长连接和短连接是指客户端和服务器端网络连接的不同方式。长连接指在客户端和服务器端之间建立一条长期保持的连接。一旦建立连接后,客户端和服务器端就可以持续交换数据,而不需要每次发送请求都重新建立连接。长连接通常用于需要频繁交换数据的场合,如在线游戏、聊天室和实时视频等。短连接指客户端和服务器端之间在完成一次请求后立即断开连接。每次发送请求都需要重新建立连接。短连接通常用于只需要偶尔交换数据的场合,如HTTP请求、电子邮件和浏览网页等。长连接可以减少连接建立的开销,提高数据传输效率,但可能会浪费一定的网络资源。短连接则可以节省网络资源,但会增加连接建立的开销和数据传输的延迟。Socket所谓 Socket (套接字),就是对网络中不同主机上的应用进程之间进行双向通信的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。Socket 接口把复杂的 TCP/IP 协议族隐藏在后面,对开发人员来讲,通过调用一组接口就可完成网络通信。Java 网络编程Java提供了一个强大的网络编程模型和丰富的API来实现网络应用程序,主要基于Socket编程,提供了 ServerSocket 和 Socket 两种Socket,分别用于实现服务器端和客户端。ServerSocket负责绑定IP地址,监听端口,Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。代码如下:public class Server {
public static void main(String[] args) throws IOException {
/*服务器必备*/
ServerSocket serverSocket = new ServerSocket();
/*绑定监听端口*/
serverSocket.bind(new InetSocketAddress(10001));
System.out.println("Server start.......");
while(true){
//serverSocket.accept()时阻塞,直到接收到客户端的请求
new Thread(new ServerTask(serverSocket.accept())).start();
}
}
private static class ServerTask implements Runnable{
private Socket socket = null;
public ServerTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
/*socket.getInputStream() 为客户端的输出流*/
try(
ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())
){
String msg = inputStream.readUTF();
System.out.println("Accept clinet message:"+msg);
/*服务器的响应*/
outputStream.writeUTF("Hello,"+msg);
outputStream.flush();
}catch (Exception e){
e.printStackTrace();
}
finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = null;
//与服务端通信的输入输出流
ObjectOutputStream output = null;
ObjectInputStream input = null;
//服务器的通信地址
InetSocketAddress addr = new InetSocketAddress("127.0.0.1",10001);
try{
socket = new Socket();
/*连接服务器*/
socket.connect(addr);
output = new ObjectOutputStream(socket.getOutputStream());
input = new ObjectInputStream(socket.getInputStream());
/*向服务器输出请求*/
output.writeUTF("i‘m Client");
output.flush();
//接收服务器的输出
System.out.println(input.readUTF());
}finally{
if (socket!=null) socket.close();
if (output!=null) output.close();
if (input!=null) input.close();
}
}
}
以上代码是传统BIO通信模型:采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接、处理、响应,典型的一请求一应答模型。该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,线程数量快速膨胀,系统的性能将急剧下降,随着访问量的继续增大,系统最终就会宕机。总结对网络编程有所了解后,也就大概知道Tomcat、Dubbo这样的框架最基本的实现原理,拿 Tomcat 举例,无非就是作为服务端监听一个端口,当这个端口有请求时对参数进行接收处理,处理后响应给客户端。当然这种成熟的框架是非常复杂的,有很多细节要考虑,比如数据的序列化问题、性能问题、安全问题、稳定性、扩展性、可靠性等。
跟着小潘学后端
Python基础语法(一)
一.字面量代码中被写下来的固定的值Python中常用的有6种值(数据)的类型:目前仅需了解如下三种(1)整数类似于数学上的整数,例如 10 , -10print(10) # 输出 10(1) 浮点数类似于数学上的小数,例如 13.14 , 5.21print(13.14) # 输出 13.14(2) 字符串定义: 字符串(string),又称文本,是由任意数量的字符如中文、英文、各类符号、数字等组成。所以叫做字符的串。“观止blog”“!@#$%^&”注:需要用 双引号" " 或者 单引号’ ’ 或者 三引号"“” “”" 包围起来。print("观止blog") # 输出 观止blog
print('观止blog') # 输出 观止blog
print("""观止blog""") # 输出 观止blog二.注释定义:在程序代码中对程序代码进行解释说明的文字。作用:注释不是程序,不能被执行,只是对程序代码进行解释说明,让别人可以看懂程序代码的作用。单行注释:以 #开头,#右边 的所有文字当作说明,起辅助说明作用。#号和注释内容一般建议以一个空格隔开.多行注释: 以 一对三个双引号 引起来 (“”" “”")来解释说明一段代码的作用使用方法# 我是单行注释
"""
我是
多行注释
"""注:三引号即能用做 多行注释 又能用做 字符串定义三.变量在程序运行时,能储存计算结果或能表示值的抽象概念 (记录数据)(1) 变量的定义格式"""
变量名 = 变量值
变量名 :每个变量自己的名称,即变量本身
= :表示将等号右侧的值,赋予左侧的变量
变量值 :每个变量储存的值(内容)
"""
name = "观止" # 示例(2) 变量的赋值"""
- 每个变量可以重复赋值
- 每次赋值将覆盖原有值
"""
name = "观止"
print(name) # 输出 观止
name = "study"
print(name) # 输出 study四.数据类型查看变量无类型而数据有类型可使用 type(变量) 查看数据的类型# 1.使用print直接输出类型信息
print(type(13.14)) # 输出 <class 'float'>
# 2.使用变量存储type()语句的结果(调用type()有返回值)
int_type = type(521)
print(int_type) # 输出 <class 'int'>
# 3.查看变量中存储的数据类型
name = "观止"
print(type(name)) # 输出 <class 'str'> str为string简写五.数据类型转换在特定的场景下,数据类型之间是可以相互转换的(1) 转为整数使用 int(x) ,将x转换为一个整数# 字符串转为整数
num = "666"
print(int(num)) # 输出 666
print("初始值类型:", type(num), ",转换后类型:", type(int(num)))
# 输出 初始值类型: <class 'str'> ,转换后类型: <class 'int'>
# 将浮点数转为小数
# 会导致精度丢失,即小数点后面的部分
print(int(13.14)) # 输出 13(2) 转为浮点数使用 float(x) ,将x转换为一个浮点数# 字符串转浮点数
num = "5.21"
print(float(num)) # 输出 5.21
print("初始值类型:", type(num), ",转换后类型:", type(float(num)))
# 输出 初始值类型: <class 'str'> ,转换后类型: <class 'float'>
# 整数转浮点数
# 进行补.0
print(float(5)) # 输出 5.0(3) 转为字符串使用 str(x) ,将x转换为一个字符串num = 13.14
print(str(num)) # 输出 13.14
print("初始值类型:", type(num), ",转换后类型:", type(str(num)))
# 输出 初始值类型: <class 'float'> ,转换后类型: <class 'str'>(4) 小结同前面学习的type()语句一样,这三个语句,都是带有结果的(返回值),可以用print直接输出或用变量存储结果值输出。任何类型,都可以通过str(),转换成字符串。字符串内必须真的是数字,才可以将字符串转换为数字,否则会报错。六.标识符用户在编程的时候所使用的一系列用于给变量、类、方法等命名的名字(1) 命名规则1. 内容限制标识符命名中,只允许出现这四类元素,其余任何内容都不被允许。英文中文 (不推荐使用)数字 (不可以作为开头)下划线(_)√ a × 1
√ a_b × 1_
√ _a × 1_a
√ _a_b × &a
√ a2 × )a
√ a_b_2 × !a2. 大小写敏感字母a的大写和小写,是完全能够区分的。guan = "study1"
Guan = "study2"
print(guan) # 输出 study1
print(Guan) # 输出 study2
3. 不可使用关键字在Python内部有特定用途,不可以使用它们作为标识符(不需要记,误用时编译器会报错)(2) 命名规范1. 变量的命名规范见名知意(尽量做到,看到名字,就知道是什么意思)name = "观止"下划线命名法(多个单词组合变量名,要使用下划线做分隔)student_name = "观止"英文字母全小写(竟然不推荐使用驼峰)√ name = "观止"
× Name = "观止"(3) 小结不遵守规则:会出现问题不遵守规范:不太高级七.运算符(1) 算数(数学)运算符运算符描述实例+加两个对象相加 a + b 输出结果 30-减得到负数或是一个数减去另一个数 a - b 输出结果 -10*乘两个数相乘或是返回一个被重复若干次的字符串 a * b 输出结果 200/除b / a 输出结果 2//取整除返回商的整数部分 9//2 输出结果 4 , 9.0//2.0 输出结果 4.0%取余返回除法的余数 b % a 输出结果 0**指数a**b 为10的20次方, 输出结果 100000000000000000000print("加:", 1 + 1) # 输出 加:2
print("减:", 10 - 1) # 输出 减:9
print("乘:", 2 * 5) # 输出 乘:10
print("除:", 5 / 4) # 输出 除:1.25
print("整数除:", 5 // 4) # 输出 整数除:1
print("取余:", 5 % 4) # 输出 取余:1
print("指数:", 2 ** 3) # 输出 指数:8(2) 赋值运算符运算符描述实例=赋值运算符把 = 号右边的结果 赋给 左边的变量,如 num = 1 + 2 * 3,结果num的值为7(3) 复合运算符运算符描述实例+=加法赋值运算符c += a 等效于 c = c + a-=减法赋值运算符c -= a 等效于 c = c - a*=乘法赋值运算符c *= a 等效于 c = c * a/=除法赋值运算符c /= a 等效于 c = c / a%=取模赋值运算符c %= a 等效于 c = c % a**=幂赋值运算符c **= a 等效于 c = c ** a//=取整除赋值运算符c //= a 等效于 c = c // anum = 2
num += 1 # 等效于 num = num + 1
print(num) # 输出 3八.字符串扩展知识(1) 三种定义方式单引号定义法:name = '观止blog'
双引号定义法:name = "观止blog"
三引号定义法:name = """观止blog"""(2) 字符串引号嵌套如果想要定义的字符串本身是包含单引号、双引号可通过单引号定义法,可以内含双引号name = '观"study"止'
print(name) # 输出 观"study"止 双引号定义法,可以内含单引号name = "观'study'止"
print(name) # 输出 观'study'止可以使用转义字符(\)来将引号解除效用,变成普通字符串name = '观\'study\'止'
print(name) # 输出 观'study'止(3) 字符串的拼接可以将两个字符串通过+号将其拼接成一个字符串 或者 将字面量和变量或变量和变量之间进行使用拼接print("观止" + "study") # 输出 观止study
name = "study"
print("观止" + name) # 输出 观止study缺点:(4) 字符串格式化v1完成字符串和变量的快速拼接% 右边变量的值替换 %s (“%占位符” % 变量)name = "study"
message = "观止 %s" % name
print(message) # 输出 观止 study
- % 表示:我要占位
- s 表示:将变量变成字符串放入占位的地方多个变量占位变量要用括号括起来并按照占位的顺序填入hobby = "study"
name = "观止"
message = "爱好 %s ,姓名 %s" % (hobby, name)
print(message) # 输出 爱好 study ,姓名 19可以完成字符串、整数、浮点数,三种不同类型变量的占位格式符号转化%s将内容转换成字符串,放入占位位置%d将内容转换成整数,放入占位位置%f将内容转换成浮点型,放入占位位置name = "study"
age = 19
money = 1.00
message = "姓名:%s,年龄:%d,家当:%f" % (name, age, money)
print(message) # 输出 姓名:study,年龄:19,家当:1.000000(5) 格式化的精度控制我们可以使用辅助符号"m.n"来控制数据的宽度和精度m,控制宽度,要求是数字(很少使用),设置的宽度小于数字自身,不生效.n,控制小数点精度,要求是数字,会进行小数的四舍五入age = 18 # %5d 表示将整数的宽度控制在5位,用三个空格补足宽度
money = 1.00 # %.2f 将小数点精度设置为2位
message = "观止%5d,身价:%.2f" % (age, money)
print(message) # 输出 观止 18,身价:1.00(6) 字符串格式化v2(优雅)通过语法:f"内容{变量}"的格式来快速格式化age = 18
money = 1.00
message = f"观止:{age},身价:{money}"
print(message) # 输出 观止:18,身价:1.0适用于快速格式化字符串,缺点:无法做做精度控制不理会数据类型(7) 对表达式进行格式化表达式:一条具有明确执行结果的代码语句1 + 1、5 * 2,就是表达式,因为有具体的结果,结果是一个数字name = “张三” age = 11 + 11上式等号右侧的都是表达式,因为它们有具体的结果,结果赋值给了等号左侧的变量。在无需使用变量进行数据存储的时候,可以直接格式化表达式,简化代码print(f"观止:{2022 - 2003}") # 输出 观止:18
print("观止:%d" % (2022 - 2003)) # 输出 观止:18九.数据输入使用input()语句可以从键盘获取输入数据输出:print :可以完成将内容(字面量、变量等)输出到屏幕上。数据输入:input :可以用来获取键盘输入name = input() # 输入 观止 用name变量来接收输入的数值
print(name) # 输出name储存的数值 观止可在input()中输入提示信息,将打印在控制台name = input(”tell me your name?“)
# 会在控制台打印tell me your name? 然后可输入 观止
print(name) # 输出name储存的数值 观止输入的数值都将转为字符串类型,可通过数据类型转换获取需要的数据类型name = input() # 输入 5
print(type(name)) # 输出 <class 'str'>
print(type(int(name))) # 输出 <class 'int'>十.全文概览
跟着小潘学后端
【Python】学习汇总(附思维导图)
思维导图涵盖了各篇章的大致内容,可以通过目录快速跳转查看对应章节思维导图,可针对尚不清楚的地方点击标题跳转文章详细复习一番~一.基础语法二.判断语句三.循环语句四.函数使用五.数据容器\六.文件基础操作七.异常处理八.模块与包九.面向对象十.类型注解十一.高阶技巧第三方库学习与使用一. 操作MySQL系列概览(终极完整版思维导图)
跟着小潘学后端
Python类型注解(十)
一.为什么需要类型注解在代码中提供数据类型的注解(显式的说明),使用时能获得相关提示帮助第三方IDE工具(如PyCharm)对代码进行类型推断,协助做代码提示显示声明时,pycharm确定这个对象是list类型,使用时能有对应提示没有声明具体类型时,使用不会有任何相关提示帮助开发者自身对变量进行类型注释(备注),后面调用不易出错二.变量的类型注解提示变量的数据类型(1) 语法格式变量名: 数据类型 = 数值注:Python中类型注解仅仅起到提示作用,没有其他语言那么严格Python解释器不会根据类型注解对数值做验证和判断,无法对应上也不会导致错误(2) 基础类型整数类型注解var_1: int = 1314 浮点数类型注解var_2: float = 5.21 布尔类型注解var_3: bool = True 字符串类型注解var_4: str = "hhybd" (3) 类对象# 定义学生类
class Student:
pass
stu: Student = Student() # 学生类类型注解(4) 数据容器列表类型注解方式一:my_list: list = [1, 2, 3]
方式二,list[基础类型]:my_list: list[int] = [1, 2, 3]
元组类型注解方式一:my_tuple: tuple = (1, 2, 3)方式二,元组类型需要将每一个元素都标记出来:my_tuple: tuple[str, int, bool] = ("bd", 521, True)集合类型注解方式一:my_set: set = {1, 2, 3}方式二,set[基础类型]:my_set: set[int] = {1, 2, 3}字典类型注解方式一:my_dict: dict = {"hhbdy": 250}方式二,dict[键类型,值类型]:my_dict: dict[str, int] = {"hhbdy": 250}字符串类型注解my_str: str = "hhybd"(5) 其他语法格式在注释中进行类型注解语法格式:# type:类型使用示例:stu = Student() # type:Student
var_1 = 123 # type:int
my_list = [1, 2, 3] # type:list
my_set = {1, 2, 3} # type:set[int]三.函数(方法)的类型注解标注形参和返回值数据类型类型注解仅仅起到提示作用(1) 形参注解语法格式:def 函数方法名(形参名1:类型,形参名2:类型):
函数体使用示例:(2) 返回值注解语法格式:def 函数方法名(形参名1:类型,形参名2:类型) -> 返回值类型:
函数体使用示例:def add(x: int, y: int) -> int:
return x + y四.Union类型联合类型注解,在变量注解、函数(方法)形参和返回值注解中均可使用需要导包使用当数据类型不唯一时基本格式无法满足要求,此时便可使用Union类型使用示例,Union[类型,类型]:在变量中:from typing import Union
# 数据为字符串和整数
my_list: list[Union[str, int]] = [2, "hhy", 5, "bd", 0]
# 键为字符串,值为字符串和整数
my_dict: dict[str, Union[str, int]] = {"name": "hhy", "QS": 250}在函数中:from typing import Union
# 接收字符串或整数,返回字符串或整数
def func(data: Union[int, str]) -> Union[int, str]:
pass五.全文概览
跟着小潘学后端
【C++】C++程序结构入门之分支结构二
一、多分支在上一文中,我们讲了一个小明买雪糕的故事。假如小明去的冰淇淋店重新制定了优惠活动。因为有原价、9折、8折、6折四种情况,此时我们使用双分支结构是没有办法处理的。就不得不想一种新的方法。C++中的多分支语句包括if语句和switch语句。1. if多分支语句它的语法如下:if (condition1) {
// 如果condition1为真,则执行这里的代码
} else if (condition2) {
// 如果condition1为假,但condition2为真,则执行这里的代码
} else {
// 如果condition1和condition2都为假,则执行这里的代码
}其中,condition1、condition2等都是条件表达式,可以是任何返回值为布尔类型的表达式。如果condition1为真,则执行if语句块中的代码;否则,判断condition2是否为真,如果为真,则执行else if语句块中的代码;否则,执行else语句块中的代码。以下是C++中if多分支语句的例子:if (a == b){
cout << "hello";
} else if (a == c){
cout << "bye";
} else if (a == d){
cout << "good morning";
} else{
cout << "hi";
}在这个例子中,如果a等于b,则输出"hello";如果a等于c,则输出"bye";如果a等于d,则输出"good morning";否则输出"hi"。2. switch语句switch语句也是一种多分支语句,它的语法如下:switch (expression) {
case value1:
// 如果expression的值等于value1,则执行这里的代码
break;
case value2:
// 如果expression的值等于value2,则执行这里的代码
break;
...
default:
// 如果expression的值不等于任何一个case后面的值,则执行这里的代码
break;
}其中,expression是一个表达式,可以是任何类型的表达式;value1、value2等是常量表达式,可以是整型、字符型、枚举型等类型的常量表达式。如果expression的值等于某个case后面的值,则执行该case语句块中的代码;如果expression的值不等于任何一个case后面的值,则执行default语句块中的代码。下面是一个简单的示例:#include <iostream>
using namespace std;
int main() {
int num = 2;
switch(num) {
case 1:
cout << "num is 1" << endl;
break;
case 2:
cout << "num is 2" << endl;
break;
default:
cout << "num is not 1 or 2" << endl;
break;
}
return 0;
}上述代码中,我们定义了一个整型变量num,并使用switch语句根据num的值执行不同的代码块。在这个例子中,num的值为2,因此会执行第二个case语句,输出"num is 2"。二、多个if在C++中,多个if语句是不必选其一执行的。如果多个if语句都满足条件,那么它们都会被执行。if (x > 0) {
cout << "x is positive" << endl;
}
if (x % 2 == 0) {
cout << "x is even" endl;
}如果x的值为6,则上述代码将输出:x is positive
x is even因为x既是正数又是偶数。三、多个if与多分支语句比较如图所示:四、例题讲解问题一:1304 - 冷饮的价格(2)小明夏天去买冰棍,老板说买 30 个及以上 1 元 / 个, 20∼29 个 1.2 元 / 个,10∼19 个 1.5 元 / 个, 10个以下 1.8 元 / 个!请从键盘读入小明买冰棍的数量,计算小明应该付的价格(价格保留 11 位小数)!1.分析问题已知:冰棍的数量、单价未知:应该付的价格2.定义变量根据分析的已知,未知按需要定义变量。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:冰棍的数量n、单价
//未知:应该付的价格
//二、数据定义
int n;
、
return 0;
}3.输入数据从键盘读入购买的数量。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:冰棍的数量n、单价
//未知:应该付的价格
//二、数据定义
int n;
//三、数据输入
cin>>n;
return 0;
}4.数据计算30 个及以上 1 元 / 个, 20∼29 个 1.2 元 / 个,10∼19 个 1.5 元 / 个, 10个以下 1.8 元 / 个。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:冰棍的数量n、单价
//未知:应该付的价格
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
if(n>=30){
}else if(n>=20){
}else if(n>=10){
}else {
}
return 0;
}5.输出结果复习一下输出小数点有两种方式。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:冰棍的数量n、单价
//未知:应该付的价格
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
if(n>=30){
//五、输出结果
cout<<fixed<<setprecision(1)<<n*1.0;
}else if(n>=20){
//五、输出结果
printf("%.1f",n*1.2);
}else if(n>=10){
//五、输出结果
printf("%.1f",n*1.5);
}else {
//五、输出结果
printf("%.1f",n*1.8);
}
return 0;
}问题二:1044 - 找出最经济型的包装箱型号已知有 A,B,C,D,E 五种包装箱,为了不浪费材料,小于 10 公斤的用 A 型,大于等于 10公斤小于 20公斤的用 B 型,大于等于 20公斤小于40 公斤的用 C 型,大于等于 40 公斤的小于 50公斤的用 D 型,大于等于 50 公斤小于 80 公斤的用 E 型。现在输入一货物的重量(小于 80 公斤),找出最经济型的包装箱型号。1.分析问题2.定义变量根据分析的已知,未知按需要定义变量。已知:货物的重量未知:最经济型的包装箱型号3.输入数据从键盘读入货物的重量。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:货物的重量
//未知:最经济型的包装箱型号
//二、数据定义
int weight;
//三、数据输入
cin>>weight;
return 0;
}4.数据计算根据题目要求设计表达式。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:货物的重量
//未知:最经济型的包装箱型号
//二、数据定义
int weight;
//三、数据输入
cin>>weight;
//四、数据计算
if(weight>=50&&weight<=80){
}else if(weight>=40){
}else if(weight>=20){
}else if(weight>=10){
}else{
}
return 0;
}5.输出结果#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:货物的重量
//未知:最经济型的包装箱型号
//二、数据定义
int weight;
//三、数据输入
cin>>weight;
//四、数据计算
if(weight>=50&&weight<=80){
//五、输出结果
cout<<"E";
}else if(weight>=40){
//五、输出结果
cout<<"D";
}else if(weight>=20){
//五、输出结果
cout<<"C";
}else if(weight>=10){
//五、输出结果
cout<<"B";
}else{
//五、输出结果
cout<<"A";
}
return 0;
}问题三:1035 - 判断成绩等级输入某学生成绩,如果 86 分以上(包括 86 分)则输出 VERY GOOD ,如果在 60 到 85 之间的则输出 GOOD (包括 60 和 85 ),小于 60 的则输出 BAD。1.分析问题已知:学生成绩n未知:学生成绩等级2.定义变量根据分析的已知,未知按需要定义变量。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:学生成绩n
//未知:学生成绩等级
//二、数据定义
int n;
return 0;
}3.输入数据从键盘读入学生成绩。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:学生成绩n
//未知:学生成绩等级
//二、数据定义
int n;
//三、数据输入
cin>>n;
return 0;
}4.数据计算<60、60<=n<=85、>=86三个等级。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:学生成绩n
//未知:学生成绩等级
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
if(n<60){
}else if(60<=n&&n<=85){
}else{
}
return 0;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:学生成绩n
//未知:学生成绩等级
//二、数据定义
int n;
//三、数据输入
cin>>n;
//四、数据计算
if(n<60){
//五、输出结果
cout<<"BAD";
}else if(60<=n&&n<=85){
//五、输出结果
cout<<"GOOD";
}else{
//五、输出结果
cout<<"VERY GOOD";
}
return 0;
}问题四:1039 - 求三个数的最大数已知有三个不等的数,将其中的最大数找出来。1.分析问题已知:三个数未知:谁最大2.定义变量根据分析的已知,未知按需要定义变量。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:三个数
//未知:谁最大
//二、数据定义
int a,b,c;
return 0;
}3.输入数据从键盘读入三个整数。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:三个数
//未知:谁最大
//二、数据定义
int a,b,c;
//三、数据输入
cin>>a>>b>>c;
return 0;
}4.数据计算既然是最大的数,必然是大于其它两个数。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:三个数
//未知:谁最大
//二、数据定义
int a,b,c;
//三、数据输入
cin>>a>>b>>c;
//四、数据计算
if(a>b&&a>c){
}
if(b>a&&b>c){
}
if(c>a&&c>b){
}
return 0;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:三个数
//未知:谁最大
//二、数据定义
int a,b,c;
//三、数据输入
cin>>a>>b>>c;
//四、数据计算
if(a>b&&a>c){
//五、输出结果
cout<<a;
}
if(b>a&&b>c){
//五、输出结果
cout<<b;
}
if(c>a&&c>b){
//五、输出结果
cout<<c;
}
return 0;
}问题五:1040 - 求三个数的大小顺序输入三个数,按由大到小顺序打印出来。1.分析问题已知:三个整数未知:大小顺序2.定义变量根据分析的已知,未知按需要定义变量。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:三个整数
//未知:大小顺序
//二、数据定义
int a,b,c,temp;
return 0;
}3.输入数据从键盘读入三个整数。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:三个整数
//未知:大小顺序
//二、数据定义
int a,b,c,temp;
//三、数据输入
cin>>a>>b>>c;
return 0;
}4.数据计算典型的两数交换问题,在C++程序结构入门之顺序结构二中介绍了三种方法,用其中一种即可。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:三个整数
//未知:大小顺序
//二、数据定义
int a,b,c,temp;
//三、数据输入
cin>>a>>b>>c;
//四、数据计算
if(a>b){
temp=a;
a=b;
b=temp;
}
if(a>c){
temp=a;
a=c;
c=temp;
}
if(b>c){
temp=b;
b=c;
c=temp;
}
return 0;
}5.输出结果#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//已知:三个整数
//未知:大小顺序
//二、数据定义
int a,b,c,temp;
//三、数据输入
cin>>a>>b>>c;
//四、数据计算
if(a>b){
temp=a;
a=b;
b=temp;
}
if(a>c){
temp=a;
a=c;
c=temp;
}
if(b>c){
temp=b;
b=c;
c=temp;
}
//五、输出结果
cout<<c<<" "<<b<<" "<<a;
return 0;
}五、练习问题:1049 - 汉译英问题:1391 - 公交卡充值问题?问题:1322 - 求数的量级?问题:1305 - 求四个数的最大数问题:1300 - 小明暑假的零花钱问题:1340 - 做纸箱最少需要多少面积的硬纸板。问题:1047 - 能否构成直角三角形问题:1041 - 判断三个整数是否相邻问题:1018 - 三角形类别六、总结以上就是多分支结构的内容,关于分支结构的内容就介绍到这里,下一节将对循环结构进行讨论。
跟着小潘学后端
python循环语句(三)
一.while 循环条件满足无限执行(1) 定义格式while 条件:
条件为True时重复执行
# 写法要求与if语句类似使用示例:i = 0
while i < 100:
print("观止study")
i += 1 # 等效于 i = i + 1
# 需要设置循环终止的条件,如i += 1配合 i < 100,就能确保执行100次后停止,否则将无限循环
# 控制台输出 100次观止study二.for循环对一批内容进行逐个处理(1) 定义格式for 临时变量 in 待处理数据集(可迭代对象):
循环满足条件时执行的代码
# 从待处理数据集中:逐个取出数据赋值给临时变量待处理数据集,也称之为:可迭代类型可迭代类型指,其内容可以一个个依次取出的一种类型,包括:字符串列表元组等字符串列表元组等for循环语句,本质上是遍历:可迭代对象。使用示例# 定义字符串name
name = "study"
# for循环处理字符串
for x in name:
print(x)
# 将字符串的内容:依次取出
# 输出
# s
# t
# u
# d
# y(2) 区别whilefor循环是无法定义循环条件的。只能从被处理的数据集中,依次取出内容进行处理.理论上讲,for循环无法构建无限循环(被处理的数据集不可能无限大)(3) range语句用于获得一个简单的数字序列(可迭代类型的一种)。语法一: range(num)# 获取一个从0开始,到num结束的数字序列(不含num本身)
# 如range(5)取得的数据是:[0, 1, 2, 3, 4]
for x in range(5):
print(x)
# 输出
# 0
# 1
# 2
# 3
# 4语法二: range(num1,num2)# 获得一个从num1开始,到num2结束的数字序列(不含num2本身)
# 如,range(5, 10)取得的数据是:[5, 6, 7, 8, 9]
for x in range(5, 10):
print(x)
# 输出
# 5
# 6
# 7
# 8
# 9语法三: range(num1, num2, step)# 获得一个从num1开始,到num2结束的数字序列(不含num2本身)
# 数字之间的步长,以step为准(step默认为1)
# 如,range(5, 10, 2)取得的数据是:[5, 7, 9]
for x in range(5, 10, 2):
print(x)
# 输出
# 5
# 7
# 9三.循环中断(1) continue关键字临时跳过: 暂时跳过本次循环,直接进行下一次中断本次循环,直接进入下一次循环可用于for循环和while循环,效果一致在嵌套循环中只对所在层循环生效使用示例:for num in range(5):
if num == 3:
continue # 当num=3时跳过后面语句,进行下次循环
print(num)
# 输出
# 0
# 1
# 2
# 4(2) break关键字直接结束: 提前退出循环,不再继续直接结束所在循环可以用for循环和while循环,效果一致在嵌套循环中只对所在层循环生效使用示例:for num in range(5):
if num == 3:
break # 当num=3时提前退出循环,不再继续
print(num)
# 输出
# 0
# 1
# 2四.全文概览
跟着小潘学后端
Python判断语句(二)
学习判断语句之前需要先了解布尔类型一. 布尔类型用来表达现实生活中的逻辑,即真与假(1) 定义# 定义布尔类型的字面量:
True 表示真(是、肯定)
False 表示假 (否、否定)
# True本质上是一个数字记作1,False记作0(2) 获取可自行定义# 变量名称 = 布尔类型字面量
name = True
name = False可以通过使用比较运算符进行比较运算得到布尔类型的结果运算符描述示例==判断内容是否相等,满足为True,不满足为False如a=3,b=3,则(a == b) 为 True!=判断内容是否不相等,满足为True,不满足为False如a=1,b=3,则(a != b) 为 True>判断运算符左侧内容是否大于右侧 满足为True,不满足为False如a=7,b=3,则(a > b) 为 True<判断运算符左侧内容是否小于右侧 满足为True,不满足为False如a=3,b=7,则(a < b) 为 True>=判断运算符左侧内容是否大于等于右侧 满足为True,不满足为False如a=3,b=3,则(a >= b) 为 True<=判断运算符左侧内容是否小于等于右侧 满足为True,不满足为False如a=3,b=3,则(a <= b) 为 True注:bool_name = "观止" == "观止"
bool_age = 19 <= 18
print(f"年龄{bool_age}") # 输出 年龄 False
print(f"姓名{bool_name}") # 输出 姓名 True
print(f"数据类型{type(bool_age)}") # 输出 数据类型<class 'bool'>
二.逻辑运算符逻辑运算符含义使用示例说明and逻辑与运算,等价于数学中的“且”a and b当 a 和 b 两个表达式都为真时,a and b 的结果才为真,否则为假。or逻辑或运算,等价于数学中的“或”a or b当 a 和 b 两个表达式都为假时,a or b 的结果才是假,否则为真。not逻辑非运算,等价于数学中的“非”not a如果 a 为真,那么 not a 的结果为假;如果 a 为假,那么 not a 的结果为真。相当于对 a 取反。在python当中,以下变量都会被当成False:任何数值类型的0、""或’'空字符串、空元组()、空列表[]、空字典{}等。and和or运算符会将其中一个表达式的值作为最终结果,而不是将 True 或者 False 作为最终结果当遇到一个语句当中有多个逻辑运算符时,按照优先级not>and>or顺序来运算(1) and运算符两边都是表达式:and两边的表达式都为真时,才为真,否则为假。print(15 > 10 and 15 > 6) # 打印 True
print(15 > 10 and 15 < 6) # 打印 False不全是表达式:左边表达式的值为假,左边表达式的值作为最终结果。左边表达式的值为真,右边表达式的值作为最终结果。print({} and 15) # 打印 {}
print(6 and 15) # 打印 15(2) or运算符两边都是表达式:or两边的表达式只要有一个真即为真,否则为假print(15 > 10 or 15 > 6) # 打印 True
print(15 > 10 or 15 < 6) # 打印 True
print(15 < 10 or 15 < 6) # 打印 False
不全是表达式左边表达式的值为假,右边表达式的值作为最终结果。左边表达式的值为真,左边表达式的值作为最终结果。print({} or 15) # 打印 15
print(6 or 15) # 打印 6
(3) not运算符当表达式为真时,运算结果就为假;当表达式为假时,运算结果为真。not可以理解为取反的意思print(not 16 > 9) # 打印 False
print(not 16 < 9) # 打印 True三. if判断语句条件为True 执行,条件为False跳过(1) 基本格式if 要判断的条件:
条件为True时执行该语句
# if与判断条件之间至少保留一个空格
# 判断条件后方需要加冒号:
# 执行语句前必须有四个空格示例代码:# 判断条件为 True,输出 您已成年
age = 19
if age > 18:
print("您已成年")
# 判断条件为 False 无输出
if age > 30:
print("观止")
print("study")(2) if-else 格式if 要判断的条件:
条件为True时执行该处语句
else:
条件为False时执行该处语句
# if部分写法不变
# else 后不需要再写条件,但需要冒号:
# else 需与if对齐示例代码:age = 19
if age > 18:
print("您已成年")
else:
print("您未成年")
# 条件为True,执行if下方的 输出 您已成年(3) if-elif-else 格式if 判断的条件:
条件为True时执行
elif 判断的条件:
条件为True时执行
elif 判断的条件:
条件为True时执行
else:
上述条件都为False时执行
# elif语法与if类似
# if/else之间可以包含多个elif
# else语句可以不写示例代码# age都不满足,执行else分支 输出 welcome
age = 1
if age > 10:
print("观止")
elif age < 0:
print("study")
elif age > 2:
print("blog")
else:
print("welcome")
# 判断是互斥且有序的,上面条件满足后面的就不会判断执行了(4) 判断语句的嵌套当满足前置条件时进行二次判断if 判断的条件:
条件为True时执行
if 判断的条件:
条件为True时执行
else:
条件为False时执行
else:
条件为False时执行
if 判断的条件:
条件为True时执行
else:
条件为False时执行
# 嵌套的关键点在于:空格缩进
# 通过空格缩进,来决定语句之间的:层次关系
示例代码:age = 20
money = 10
if age < 18:
print("第一处if")
if money > 9:
print("第二处if")
else:
print("第二处else")
else:
print("第一处else")
if money > 9:
print("第二处if")
else:
print("第二处else")
# 输出
"""
第一处else
第二处if
"""
# age > 18 进入第一层else
# money > 9,进入第一层第二处if四.全文概览
跟着小潘学后端
认识网络编程中的IO模型
前言上文介绍了网络编程的基础知识,并基于 Java 编写了 BIO 的网络编程。我们知道 BIO 模型是存在巨大问题的,比如 C10K 问题,其本质就是因其阻塞原因,导致如果想要承受更多的请求就必须有足够多的线程,但是足够多的线程会带来内存占用问题、CPU上下文切换带来的性能问题,从而造成服务端崩溃的现象。怎么解决这一问题呢?优化呗,所以后面就有了NIO、AIO、IO多路复用。本文将对这几个模型详细说明并基于 Java 编写 NIO。基本概念I/O阻塞是哪里阻塞、怎么阻塞?先简单了解一些基本概念用户空间:被分配给用户进程的虚拟地址空间,用来存储用户进程的代码、数据和堆栈等。内核空间:操作系统的基础,负责管理计算机的硬件资源和提供系统调用接口,同时也是用户空间和硬件之间的桥梁。为了保证操作系统的安全性和稳定性,用户进程和操作系统内核是隔离的,用户进程不能直接访问内核空间,而是需要通过系统调用等方式向内核发起请求,由内核代表用户进程执行操作。也就是说我们的应用程序在向硬件设备,比如网卡、磁盘等读取或写入数据时需要经过内核。下面对BIO、NIO、IO多路复用模型逐一介绍,详细了解各模型IO过程。BIO过程首先明确一下,我们所说的IO阻塞是用户进程也就是用户空间中的程序在向硬件设备读取的这个过程,在还没有数据时给用户的反映是需要一直等待的,这个我们叫阻塞IO。过程如下图: 我们可以看到,在进程向内核发起调用后直到数据返回,整个过程都是阻塞的,结合Java BIO 编程,也就是说在 inputStream.read() 这个过程是阻塞的,就存在几个问题:由于阻塞会占用当前线程,使之不能进行其他操作,当有新的请求时只能新建线程。在 Linux 系统中,每个线程的默认栈大小为 8MB,在不考虑其他因素的情况下,一个 8G 的服务器最多也就承载1000个请求量。由于线程数会随着请求量增大而增大,当有大量的线程阻塞唤醒,CPU 频繁的切换上下文会导致性能的下降。这个问题也就是C10K问题的本质,看上去很直观,使用少的线程就处理多个IO是不是就可以解决呢?继续看NIO过程。NIO过程NIO我们说的是非阻塞,通过对BIO的说明,NIO的非阻塞体现在:无论有无数据都直接响应给用户进程,如下图:我们可以看到,确实是在用户进程调用recvfrom()函数后直接响应,但是在没有拿到数据之前一直在轮询调用,虽然没有因为阻塞造成CPU上下文的切换,但是CPU一直处于空转状态,不能充分发挥CPU的作用。与BIO一样,在单线程的情况下,只能依次处理IO事件,单个线程依然处理不了多个IO事件。IO多路复用过程既然NIO与BIO一样并不能解决因阻塞可能会造成的C10K问题,那如何让一个线程可以处理多个IO事件呢?可不可以这样:用一个线程专门监听这些IO,一旦哪个IO有数据了再去接收数据。IO多路复用就是这个原理,如下图:我们可以看到,多了一个select()函数调用,select()会去监听指定的FD(这里注意一下,在Linux中,一切皆文件,包括socket),内核去监听FD对应的sockets。任意一个或多个socket有数据了就返回给select(),这个时候再去调用recvfrom()接收sockets的数据,从而实现了单个线程处理多个I/O操作,提高系统的效率和性能。在Linux下,常用的I/O多路复用方式有三种:select、poll和epoll。select和poll的原理是基于轮询,即不断地查询所有注册的I/O事件,如果有事件发生就立即通知应用程序。这种方式的效率较低,因为每次查询都需要遍历所有的I/O事件。epoll的原理是基于事件通知,即只有当I/O事件发生时,才会通知应用程序。这种方式的效率更高,因为它避免了无效的查询。Java NIO编程相比Java BIO编程,Java NIO编程理解起来没有那么直观,不过在理解多个IO模型(尤其是IO多路复用)后就相对容易理解了,Java NIO实际上就是IO多路复用。Java NIO 核心概念在Java NIO编程中,有几个核心的概念(组件)需要了解:通道(Channel):通道是对原始I/O操作的抽象,可以用于读取和写入数据。它可以与文件、套接字等进行交互。缓冲区(Buffer):缓冲区是一个容器,用于存储数据。在进行读写操作时,数据会先被读取到缓冲区中,然后从缓冲区中写入或读取。选择器(Selector):选择器是Java NIO提供的一种多路复用机制,可以通过一个线程管理多个通道的I/O操作。相对于BIO,开发者不直接与Socket交互,而是通过Selector与多个Channel交互,同时Buffer提供了方法来管理缓冲区的容量、位置和限制,通过设置这些属性,可以控制数据的读写位置和范围。总之NIO在提升IO处理效率和性能的同时支持更丰富的功能,Java NIO 示例以下是一个简单的Java NIO网络编程示例,用于创建一个基于NIO的服务器和客户端:服务端代码:import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NIOServer {
private Selector selector;
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.startServer();
}
public void startServer() throws IOException {
// 创建Selector
selector = Selector.open();
// 创建ServerSocketChannel,并绑定端口
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(8888));
// 将ServerSocketChannel注册到Selector上,并监听连接事件。当接收到一个客户端连接请求时就绪。该操作只给服务器使用。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 8888");
// 循环等待事件发生
while (true) {
// 等待事件触发,阻塞 | selectNow():非阻塞,立刻返回。
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
// 移除当前处理的SelectionKey
keys.remove();
if (key.isAcceptable()) {
// 处理连接请求
handleAccept(key);
}
if (key.isReadable()) {
// 处理读数据请求
handleRead(key);
}
}
}
}
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 监听到ServerSocketChannel连接事件,获取到连接的客户端
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 将clientChannel注册到Selector上,并监听读事件,当操作系统读缓冲区有数据可读时就绪(该客户端的)。
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Client connected: " + clientChannel.getRemoteAddress());
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端断开连接
key.cancel();
clientChannel.close();
System.out.println("Client disconnected ");
return;
}
byte[] data = new byte[bytesRead];
buffer.flip();
buffer.get(data);
String message = new String(data).trim();
System.out.println("Received message from client: " + message);
// 回复客户端
String response = "Server response: " + message;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(responseBuffer);
}
}
客户端代码:import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
public class NIOClient {
private Selector selector;
private SocketChannel socketChannel;
public static void main(String[] args) {
NIOClient client = new NIOClient();
new Thread(() -> client.doConnect("localhost", 8888)).start();
Scanner scanner = new Scanner(System.in);
while (true) {
String message = scanner.nextLine();
if ("bye".equals(message)) {
// 如果发送的消息是"bye",则关闭连接并退出循环
client.doDisConnect();
break;
}
client.sendMsg(message);
}
}
private void doDisConnect() {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void sendMsg(String message) {
// 发送消息到服务器
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
try {
socketChannel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
private void doConnect(String host, int port) {
try {
selector = Selector.open();
// 创建SocketChannel并连接服务器
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(host, port));
// 等待连接完成
while (!socketChannel.finishConnect()) {
// 连接未完成,可以做一些其他的事情
}
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("连接成功!");
while (true) {
// 等待事件触发,阻塞 | selectNow():非阻塞,立刻返回。
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
// 移除当前处理的SelectionKey
keys.remove();
if (key.isReadable()) {
// 处理读数据请求
handleRead(key);
}
}
}
} catch (IOException e) {
System.out.println("连接失败!!!");
e.printStackTrace();
}
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 释放资源
key.cancel();
clientChannel.close();
return;
}
byte[] data = new byte[bytesRead];
buffer.flip();
buffer.get(data);
String message = new String(data).trim();
System.out.println("Received message from server: " + message);
}
}
总结通过本文介绍,我们可以了解各个IO模型原理,并且对很多概念有了更清晰的认识,比如: 阻塞体现在:用户进程发起系统调用接口后,不论有无数据,是否直接响应结果?如果直接响应就是非阻塞,等待就是阻塞; IO多路复用原理就是单个线程处理多个I/O操作,从而提高系统的效率和性能; 并且通过对IO多路复用的理解快速的入门了Java NIO 编程。
跟着小潘学后端
【C++】c++入门之数组基础二
一、数组元素的移动问题一:1009 - 数组逆序给你 m 个整数,将其逆序输出。解法一: 不对数组进行改动,直接将数组倒序输出也能满足题目的要求。1.分析问题已知:m个整数未知:逆序输出关系:下标做减法2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int m,a[100];3.输入数据遍历输入。 //三、数据输入
cin>>m;
for(int i=0;i<m;i++){
cin>>a[i];
}4.数据计算数组下标是从0开始,到m-1结束。因此倒序输出要反过来,将i做减法。 //四、数据计算
for(int i=m-1;i>=0;i--){
//五、输出结果
cout<<a[i]<<" ";
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:m个整数
//未知:逆序输出
//关系:下标做减法
//二、数据定义
int m,a[100];
//三、数据输入
cin>>m;
for(int i=0;i<m;i++){
cin>>a[i];
}
//四、数据计算
for(int i=m-1;i>=0;i--){
//五、输出结果
cout<<a[i]<<" ";
}
return 0;
}解法二:将整个数组进行逆序,做法就是将0位置上和m-1位置上的元素交换,然后将1位置上和m-2位置上的元素交换…重复m/2次即可。不能懂为什么交换m/2次,可以在一张纸上画出数组,在进行对折观察。1.分析问题已知:m个整数未知:逆序输出关系:数组对称交换2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n,a[100];3.输入数据从键盘读入。 //三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}4.数据计算将首尾交换,然后下标依次增减1。//四、数据计算
int temp;
for(int i=0;i<n/2;i++){
temp=a[i];
a[i]=a[n-i-1];
a[n-i-1]=temp;
}5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:m个整数
//未知:逆序输出
//关系:数组对称交换
//二、数据定义
int n,a[100];
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
//四、数据计算
int temp;
for(int i=0;i<n/2;i++){
temp=a[i];
a[i]=a[n-i-1];
a[n-i-1]=temp;
}
//五、输出结果
for(int i=0;i<n;i++){
cout<<a[i]<<" ";
}
return 0;
}二、数组元素的删除问题二:1162 - 数组元素的删除把一个数组的第 x 个位置的元素删除掉。解法一: 不删除,输出数组所有元素,如果遇到下标为x(i == x),则不输出!1.分析问题已知:一个数组未知:x个位置的元素删除掉后的数组。关系:不删除,跳过输出2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n,a[100],x;3.输入数据从键盘读入。 //三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
cin>>x;4.数据计算不删除,跳过输出 。//四、数据计算
for(int i=0;i<n;i++){
if(i!=x-1){
cout<<a[i]<<" ";
}
}
5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个数组
//未知:x个位置的元素删除掉后的数组。
//关系:不删除,跳过输出
//二、数据定义
int n,a[100],x;
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
cin>>x;
//四、数据计算
for(int i=0;i<n;i++){
if(i!=x-1){
cout<<a[i]<<" ";
}
}
//五、输出结果
return 0;
}解法二: 真删除,将数组中下标为 x的元素删除!从删除下标 x 开始,将元素顺序向前移动!1.分析问题已知:一个数组。未知:x个位置的元素删除掉后的数组。关系:不删除,跳过输出 。2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n,a[100],x;3.输入数据从键盘读入。//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
cin>>x;4.数据计算用后一个元素覆盖掉前一个元素。//四、数据计算
--x;
for(int i=x;i<n-1;i++){
a[i]=a[i+1];
}
5.输出结果输出时注意数组长度-1,因为删除了1个元素。#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个数组
//未知:x个位置的元素删除掉后的数组。
//关系:不删除,跳过输出
//二、数据定义
int n,a[100],x;
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
cin>>x;
//四、数据计算
--x;
for(int i=x;i<n-1;i++){
a[i]=a[i+1];
}
//五、输出结果
for(int i=0;i<n-1;i++){
cout<<a[i]<<" ";
}
return 0;
}三、数组元素的插入问题三:1211 - 数组元素的插入在一个数组的第 x 个位置插入一个新的数y。思路:逆序循环下标为 n-1~x 结束,将元素顺序后移 (a[i+l] = a[i]),空出下标为x的位置。1.分析问题已知:一个数组。未知:更新后的数组。关系:x 个位置插入一个新的数y。2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n,a[100],x,y;3.输入数据从键盘读入。 //三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
cin>>x;
cin>>y;4.数据计算将数组向后扩展一位。//四、数据计算
--x;
for(int i=n;i>=x;i--){
a[i]=a[i-1];
}
a[x]=y;5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个数组
//未知:更新后的数组。
//关系:x 个位置插入一个新的数y
//二、数据定义
int n,a[100],x,y;
//三、数据输入
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
cin>>x;
cin>>y;
//四、数据计算
--x;
for(int i=n;i>=x;i--){
a[i]=a[i-1];
}
a[x]=y;
//五、输出结果
for(int i=0;i<n+1;i++){
cout<<a[i]<<" ";
}
return 0;
}问题四:1161 - 元素插入有序数组给你一个整数 n 和一个数列(数列个数不超过 1000 ),这个数列保证从小到大排列,现要求将这个整数 n 插入到数列中,使新的数列仍然从小到大排列。思路:第一步:找出要插入的元素的下标 x;从第一个数开始逐个比较,找到第一个 a[i] >= y,下标 就是x 的值。第二步:将元素顺序后移(逆序循环 n-1~x)。第三步:在下标为x的位置插入元素 y。1.分析问题已知:一个整数 n 和一个有序数列。未知:新的数列。关系:整数 n插入到有序数列,仍然从小到大排列。2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int n,m,a[10000],sub;3.输入数据从键盘读入。 //三、数据输入
cin>>n;
cin>>m;
for(int i=0;i<m;i++){
cin>>a[i];
if(a[i]<n){
sub=i+1;
}
} 4.数据计算//四、数据计算
for(int i=m;i>=sub;i--){
a[i]=a[i-1];
}
a[sub]=n;5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:一个整数 n 和一个有序数列
//未知:新的数列
//关系:整数 n插入到数列,仍然从小到大排列
//二、数据定义
int n,m,a[10000],sub;
//三、数据输入
cin>>n;
cin>>m;
for(int i=0;i<m;i++){
cin>>a[i];
if(a[i]<n){
sub=i+1;
}
}
//四、数据计算
for(int i=m;i>=sub;i--){
a[i]=a[i-1];
}
a[sub]=n;
//五、输出结果
for(int i=0;i<m+1;i++){
cout<<a[i]<<" ";
}
return 0;
}四、练习问题一:1213 - 删除数组的最小数题解:1213 - 删除数组的最小数1213 - 删除数组的最小数2问题二:1157 - 最小数题解:【C++】1157 - 最小数问题三:1212 - 移动数组元素题解:【C++】1212 - 移动数组元素问题四:1214 - 在最大数后面插入一个数题解:1214 - 在最大数后面插入一个数问题五:1217 - 小明排队做操迟到题解:1217 - 小明排队做操迟到问题六:1232 - 换位置题解:1232 - 换位置五、总结以上就是关于数组元素移动的内容,总结的来说如果题目只是对输出内容进行检查,那我们可以进行“偷跑”,但是如果后续还有其他计算操作,那么就需要完成相应的处理。
跟着小潘学后端
【C++】C++程序设计入门一
前言本文简单介绍了C++的基础内容,力求无基础也能看懂。学习本文之前,请安装Dev C++。一、人和计算机是如何交流的?计算机程序是一组计算机能识别和执行的指令,当我们编写好程序后,将程序传给计算机;计算机通过某种方式处理我们编写好的程序;然后将处理以后的结果又传回给我们。所以当我们想要指挥计算机干某件事的时候,就需要编写相关的程序指令,我们通过一道问题来看看这个过程。1.问题阅读下方图片上的文字内容,并思考如何完成。2.程序编写通过上面图片给的问题,我们编写出程序。3.运行结果我们将编写好的程序运行一下,看看能不能得到我们想要的结果。4.思考将我们刚才的编写程序过程进行梳理,可以发现完全符合三个步骤:人编写程序给计算机,计算机处理程序,计算机返回结果。4.1 程序语言规则当然在编写程序的时候也不是在随意的打字,而是有一定的程序语言规则。可以看到,我们大致把C++程序分成三个部分。1.头文件引用,2.命名空间引用,3.主函数。接下来分别对三个部分进行讲解。4.1.1 头文件引用include 包括,包含;相当于在程序之前嵌入了一条已经编写好的程序 。#include < iostream>,这句代码就是使用了其他人写好的输入输出代码,我们只需要使用cin,cout就能完成输入和输出的操作。如果想知道更多更专业的关于“头文件”的知识,可以前往学习C++头文件与名字空间4.1.2 命名空间引用那什么是命名空间?当在代码中引入了同名的变量或者方法,就会出现不知道是谁或者错误的情况,为了避免这种事情的发生,我们应当明确的告诉计算机或者程序我们使用的对象。using namespace ***;就是其中一个办法。using namespace std;它的作用是将命名空间std中的所有标识符引入当前的作用域中,这样就可以直接使用std命名空间中的函数和变量,而不需要在每个标识符前加上std::前缀。当然我们也可以不引入命名空间。例如,使用cout输出内容时,如果没有使用using namespace std,则需要写成std::cout<<“周长:”<<c<<" 面积:"<<s; 。当我们有多个输入输出语句时,每行代码前面都要写std::是一件很麻烦的事,所以引入头文件后,引入命名空间也是很必要的事。如果想知道更多更专业的关于“头文件”的知识,可以点击前往学习C++头文件与名字空间4.1.3 主函数主函数里面就是我们处理问题的程序。包含输入数据,数据处理,输出结果等。提示:我们编写的程序都需要使用英文下的符号,包括不限于, ; <> "" '' : () 等。4.1.3.1 变量的定义C++ 变量是程序中用于存储数据值的一种占位符,可以理解为用来存储东西的箱子。每个变量都有一个特定的类型,该类型确定了变量存储的大小和布局。C++ 中的变量可以是基本类型,如整数和浮点数,也可以是用户定义的类型,如结构和类。在 C++ 中,变量必须在使用之前声明。变量声明告诉编译器变量的名称和类型,这样编译器就可以为该变量分配所需的内存空间。变量定义为变量分配存储空间,也可以为变量赋初值。关于数据类型的内容,我们将不在这里展开。4.1.3.2 输入语句C++中的cin,用于从键盘读取数据。cin可以读取各种类型的数据,包括整数、浮点数、字符和字符串等。4.1.3.3 赋值语句赋值语句就是我们在对数据进行处理的过程中,我们需要对数据进行交换,计算等操作。4.1.3.4 表达式C++表达式是由运算符和操作数组成的组合。C++中的表达式可以包括算术、逻辑、关系、位运算等多种运算符。为了简化学习难度,本节只介绍上文使用的算术表达式。算术表达式是由算术运算符连接起来的表达式,算术运算符有+、-、*、/、%等。其逻辑与数学同理。int a = 10, b = 20;
int c = a + b; // 加法,表示a和b相加后的值赋给c,c的值为30。
int c = b - a; // 减法,表示b减去a的值后赋给c,c的值为10。
int c = a * b; // 乘法,表示a和b相乘后的值赋给c,c的值为200。
int c = b / a; // 除法,表示b除以a的值后赋给c,c的值为2。
int c = b % a; // 取余,表示b除以a的商的余数赋给c,c的值为0。整数之间的运算,结果为整数。实数之间的运算,结果为实数。例如:4+5/2=4+2=64+5%2=4+1=54+5.0/2=4+2.5=6.54.1.3.5 输出语句C++中的cout,用于将输出内容显示在屏幕上。它可以与<<运算符一起使用来输出各种类型的数据,例如整数、浮点数、字符串等。4.2 程序的编译程序编写好以后是不能直接交给计算机运行的。比如下方两人,如果两个人都是会相同的语言,那么就可以无障碍直接交流。但是,他们不会对方的语言怎么办?我们就需要为两个人添加一个翻译。同理,计算机是不能看懂人们编写的程序,它只认识01010101010000111这样的机器码。我们需要将编写好的代码转换成计算机能识别的机器语言,这个过程就叫编译。如果想知道更多更专业的关于“编译”的知识,可以点击前往学习C++编辑、编译、解释、调试4.3 程序的运行1.选择“运行”>“编译”或按F9编译您的代码。 2.如果编译成功,将在Dev-C++的输出窗口中看到编译结果。3.选择“运行”>“运行”或按F10运行您的代码。4.如果一切正常,您将在Dev-C++的控制台窗口中看到您的程序的输出。4.4 本节代码#include<iostream>
using namespace std;
int main(){
//问题:已知一个长方形的长为L和宽为W,求长方形的周长和面积。
//分析问题:已知l,w;求周长、面积
//定义变量
int l,w,c=0,s=0;
//输入数据
cin>>l>>w;
//处理数据
c=(l+w)*2;
s=l*w;
//输出结果
cout<<"周长C:"<<c<<" 面积S:"<<s<<endl;
//程序结束
return 0;
}二、课后作业请将我们的课后作业反复抄写或者在Dev C++上练习,直到能完全默写。三、总结以上就是今天要讲的所有内容,本文为了简化难度尽量将学习难度高的内容延后,也通过大量的配图增加理解,希望大家能够通过本节问题案例学习到c++的初步使用方法。
跟着小潘学后端
【C++】C++程序结构入门之顺序结构一
一、课前复习二、如何通过程序解决问题?2.1 导入当我们一般遇到问题时,是如何解决的呢?是这样?还是这样?开个玩笑。当我们拿到一个问题时,要有一个比较清晰的认识。首先问题是什么?问题中什么是知道的,什么是不知道。我们需要怎么去编写程序得到不知道的。大体流程如下:2.2 样例示范:1317 - 正多边形每个内角的度数?当我们看到这个问题时,应该如何去解决呢?首先写好框架,C++程序设计入门一让大家熟记的代码片段。#include<iostream>
using namespace std;
int main(){
return 0;
}2.2.1 分析问题已知:正多边形的边数、正多边形内角和等于:( n-2 ) ×180( n 大于等于 3且 n 为整数)。未知:该正 n 边形每个内角的度数。#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:正多边形的边数 未知:该正n边形每个内角的度数
return 0;
}2.2.2 数据计算在分析完问题以后,就可以进行数据计算的部分。在真正的数据计算之前还需要得到数据,分别是数据定义、数据输入。1.首先根据问题中输入和输出定义变量。代码如下:#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:正多边形的边数 未知:该正n边形每个内角的度数
//二、数据定义
//定义一个整数n,用来存放输入的正多边形边数。
int n;
//定义一个浮点数result(结果),用来存放该正 n 边形每个内角的度数。
double result;
return 0;
}2.将已知输入到程序中。代码如下:#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:正多边形的边数 未知:该正n边形每个内角的度数
//二、数据定义
//定义一个整数n,用来存放输入的正多边形边数。
int n;
//定义一个浮点数result(结果),用来存放该正 n 边形每个内角的度数。
double result;
//三、数据输入
//从键盘获取正多边形的边数n 。
cin>>n;
return 0;
}3.对数据进行计算。代码如下:#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:正多边形的边数 未知:该正n边形每个内角的度数
//二、数据定义
//定义一个整数n,用来存放输入的正多边形边数。
int n;
//定义一个浮点数result(结果),用来存放该正 n 边形每个内角的度数。
double result;
//三、数据输入
//从键盘获取正多边形的边数n 。
cin>>n;
//四、数据计算
//多边形内角和定理,正多边形内角和等于:
//( n-2 )×180 ( n大于等于 3且 n 为整数)
result=(n-2)*180.0/n;
return 0;
}2.2.3 输出结果将计算结果输出即可,需要注意的是程序要求保留小数点1位。保留小数点方法一:引入头文件 #include < iomanip>输出格式:cout<<fixed<<setprecision(保留几位填数字几)<<输出<<endl;代码如下:#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题,已知:正多边形的边数 未知:该正n边形每个内角的度数
//二、数据定义
//定义一个整数n,用来存放输入的正多边形边数。
int n;
//定义一个浮点数result(结果),用来存放该正 n 边形每个内角的度数。
double result;
//三、数据输入
//从键盘获取正多边形的边数n 。
cin>>n;
//四、数据计算
//多边形内角和定理,正多边形内角和等于:
//( n-2 )×180 ( n大于等于 3且 n 为整数)
result=(n-2)*180.0/n;
//五、输出结果
cout<<fixed<<setprecision(1)<<result<<endl;
return 0;
}保留小数点方法二:printf(“%.1f”,)是C语言中的格式化输出函数,用于将浮点数按照指定格式输出。其中%.1f表示输出浮点数,保留小数点后1位。在括号中填入需要输出的浮点数即可。输出格式:printf(“%.1f”,输出);代码如下:#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:正多边形的边数 未知:该正n边形每个内角的度数
//二、数据定义
//定义一个整数n,用来存放输入的正多边形边数。
int n;
//定义一个浮点数result(结果),用来存放该正 n 边形每个内角的度数。
double result;
//三、数据输入
//从键盘获取正多边形的边数n 。
cin>>n;
//四、数据计算
//多边形内角和定理,正多边形内角和等于:
//( n-2 )×180 ( n大于等于 3且 n 为整数)
result=(n-2)*180.0/n;
//五、输出结果
printf("%.1f",result);
return 0;
}2.2.4 小结以上就是关于1317 - 正多边形每个内角的度数?问题分析和解决过程,以及代码实现。通过这个例题我们可以明确体会到顺序结构的含义,程序从上向下依次执行。我们可以把初始代码片段进行补充:#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:未知:
//二、数据定义
//三、数据输入
cin>>;
//四、数据计算
//五、输出结果
cout<<;
return 0;
}三、例题讲解:已知一个圆的半径,求解该圆的面积和周长通过刚才的样题是不是感觉学废了,来试试下面这个问题。1005 - 已知一个圆的半径,求解该圆的面积和周长看到问题,我们直接上代码。#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:未知:
//二、数据定义
//三、数据输入
cin>>;
//四、数据计算
//五、输出结果
cout<<;
return 0;
}好了,进行第一步3.1 分析问题已知:一个圆的半径。未知:该圆的面积和周长。#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:一个圆的半径。未知:该圆的面积和周长。
//二、数据定义
//三、数据输入
cin>>;
//四、数据计算
//五、输出结果
cout<<;
return 0;
}3.2 定义变量圆的半径圆的周长(有小数)圆的面积(有小数)#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:一个圆的半径。未知:该圆的面积和周长。
//二、数据定义
int r; //r 圆的半径
double s,c; //s圆的面积、c圆的周长
//三、数据输入
cin>>;
//四、数据计算
//五、输出结果
cout<<;
return 0;
}3.3 数据输入根据已知,输入数据。#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:一个圆的半径。未知:该圆的面积和周长。
//二、数据定义
int r; //r 圆的半径
double s,c; //s圆的面积、c圆的周长
//三、数据输入
cin>>r;
//四、数据计算
//五、输出结果
cout<<;
return 0;
}3.4 数据计算圆面积公式:PI * r^2圆周长公式:2 * PI * r#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题,已知:一个圆的半径。未知:该圆的面积和周长。
//二、数据定义
int r; //r 圆的半径
double s,c; //s圆的面积、c圆的周长
//三、数据输入
cin>>r;
//四、数据计算
//圆面积公式:PI*r^2
s=r*r*3.1415926;
//圆周长公式:2*PI*r
c=r*2*3.1415926;
//五、输出结果
return 0;
}3.5 输出结果保留小数两位。#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题,已知:一个圆的半径。未知:该圆的面积和周长。
//二、数据定义
int r; //r 圆的半径
double s,c; //s圆的面积、c圆的周长
//三、数据输入
cin>>r;
//四、数据计算
//圆面积公式:PI*r^2
s=r*r*3.1415926;
//圆周长公式:2*PI*r
c=r*2*3.1415926;
//五、输出结果
cout<<fixed<<setprecision(2)<<s<<endl;
cout<<fixed<<setprecision(2)<<c<<endl;
return 0;
}或者#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:一个圆的半径。未知:该圆的面积和周长。
//二、数据定义
int r; //r 圆的半径
double s,c; //s圆的面积、c圆的周长
//三、数据输入
cin>>r;
//四、数据计算
//圆面积公式:PI*r^2
s=r*r*3.1415926;
//圆周长公式:2*PI*r
c=r*2*3.1415926;
//五、输出结果
printf("%.2f",s);
printf("\n");
printf("%.2f",c);
return 0;
}怎么样,做出来了吗?四、课后作业4.1 求梯形的面积1329 - 求梯形的面积4.2 求圆环的面积1338 - 求圆环的面积4.3 求花坛的面积1337 - 求花坛的面积五、总结以上就是今天要讲的内容,明天再学吧。
跟着小潘学后端
Python高阶技巧(十二)
一.闭包可以保存函数内变量,不会随着函数调用完而销毁(1) 基本定义在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数,我们把这个使用外部函数变量的内部函数称为闭包。使用示例:# 1.在函数嵌套(函数中定义函数)的前提下
def func_out(num1):
def func_inner(num2):
# 2.内部函数使用了外部函数的变量
num = num1 + num2
print(f"num的值为:{num}")
# 3.外部函数返回了内部函数
return func_inner
# 创建闭包实例
f = func_out(10)
# 执行闭包
f(6) # 打印 num的值为:16(2) 修改外部函数变量的值在闭包函数(内部函数中)想要修改外部函数的变量值,必须用nonlocal声明这个外部变量# 1.在函数嵌套(函数中定义函数)的前提下
def func_out(num1):
def func_inner(num2):
# 声明外部变量
nonlocal num1
# 2.内部函数使用了外部函数的变量
num1 += num2
print(f"num1的值为:{num1}")
# 3.外部函数返回了内部函数
return func_inner
# 创建闭包实例
f = func_out(10)
# 执行闭包
f(8) # 打印 num的值为:18(3) 小结优点:无需定义全局变量即可实现通过函数,持续的访问、修改某个值闭包使用的变量于所在的函数内,难以被错误的调用修改,可使变量更安全不易被恶意行为修改缺点:由于内部函数持续引用外部函数的值,所以会导致这一部分内存空间不被释放,一直占用内存(额外的内存占用)二.装饰器也是一种闭包,可在不破坏目标函数原有的代码和功能的前提下,为目标函数增加新功能(1) 基本使用装饰器就是把一个函数当做参数传递给闭包中的外部函数,同时在内部函数中使用这个函数,并给他添加新的功能。外部函数只能有一个参数,往往是被装饰的函数内部函数可以根据被装饰的函数提供多个参数以及返回值# 定义一个装饰器
def remind(func):
# 为目标函数增加新功能
def inner():
print("我睡觉了")
func()
print("我起床了")
return inner
# 需要被装饰的函数
def sleep():
import random
import time
print("睡眠中...")
time.sleep(random.randint(1, 5))
# 未装饰
sleep()
# 打印
# 睡眠中...
# 使用装饰器装饰函数(增加睡前起床提醒)
# 返回增强后的inner函数
fn = remind(sleep)
fn()
# 打印
# 我睡觉了
# 睡眠中...
# 我起床了(2) 语法糖使用可直接在需要被装饰的函数上加@装饰器名字,解释器碰到时会自动执行装饰过程,简化使用流程# 定义一个装饰器
def remind(func):
def inner():
print("我睡觉了")
func()
print("我起床了")
return inner
# 需要被装饰的函数
# 解释器遇到@remind 会立即执行 sleep = remind(sleep)
@remind
def sleep():
import random
import time
print("睡眠中...")
time.sleep(random.randint(1, 5))
# 通过语法糖注解,直接调用即可达到效果
sleep()
# 打印
# 我睡觉了
# 睡眠中...
# 我起床了(3) 多个装饰器使用将装饰器都写在需要被装饰的函数上面即可谁离被装饰的函数最近,谁就先去装饰函数# 定义装饰器1
def remind(func):
def inner():
print("我睡觉了")
func()
print("我起床了")
return inner
# 定义装饰器2
def study(func):
def inner():
func()
print("我要敲代码啦")
return inner
# 谁近谁先装饰
@study # 2.执行 sleep = study(remind(sleep))
@remind # 1.执行 sleep = remind(sleep)
def sleep():
import random
import time
print("睡眠中...")
time.sleep(random.randint(1, 5))
sleep()
# 打印
# 我睡觉了
# 睡眠中...
# 我起床了
# 我要敲代码啦(4) 带参数的装饰器需要再增加一层函数嵌套来接收传递的参数# 第一层:用于接收装饰器传递的参数
def logging(flag):
# 第二层:外部函数用于接收待装饰函数
def decorator(fn):
# 第三层:内部函数用于装饰接收的函数
def inner(num1, num2):
# 使用参数
if flag == "+":
print(">正在进行加法运算<")
elif flag == "-":
print(">正在进行减法运算<")
result = fn(num1, num2)
return result
return inner
# 返回装饰器
return decorator
# 被带有参数的装饰器装饰的函数
@logging('+')
def add(a, b):
result = a + b
return result
result = add(1, 3)
print(result)(5) 类装饰器(了解即可)一个类里面一旦实现了__call__方法,那么这个类创建的对象就是一个可调用对象,可以像调用函数一样进行调用# 定义类
class Login:
def __call__(self, *args, **kwargs):
print("登录中。。。")
# 创建实例
login = Login()
# 如函数般调用
login() # 打印 登录中。。。类装饰器装饰函数的功能通过call方法实现# 定义类装饰器
class Check:
# 接收待装饰的函数
def __init__(self, fn): # fn = comment
self.__fn = fn
def __call__(self, *args: object, **kwargs: object) -> object:
print("登录")
self.__fn() # comment()
# 被装饰的函数
@Check # comment = Check(comment)
def comment():
print("发表评论")
comment()三.property属性把类中的一个方法当作属性进行使用,简化开发例如我们如果想获取和修改私有属性必须通过类方法修改,示例代码:class Person:
def __init__(self):
self.__age = 18
def age(self):
return self.__age
def set_age(self, new_age):
self.__age = new_age
p = Person()
age = p.age()
print(f"修改前年龄是:{age}") # 打印 修改前年龄是:18
p.set_age(66)
age = p.age()
print(f"修改后年龄是:{age}") # 打印 修改后年龄是:66
通过使用如下两种方式可简化上述代码的使用(1) 装饰器方式使用@property表示把方法当作属性使用,表示当获取属性时执行下面修饰的方法@方法名.setter表示把方法当作属性使用,表示当设置属性值时会执行下面修饰的方法class Person:
def __init__(self):
self.__age = 18
@property
def age(self):
return self.__age
@age.setter
def age(self, new_age):
self.__age = new_age
p = Person()
# 可直接通过对象.属性使用
print(f"修改前年龄是:{p.age}") # 打印 修改前年龄是:18
p.age = 66
print(f"修改后年龄是:{p.age}") # 打印 修改后年龄是:66(2) 类属性方式使用property的参数说明:属性名 = property(获取值方法,设置值方法)第一个参数:获取属性时要执行的方法第二个参数:设置属性时要执行的方法class Person:
def __init__(self):
self.__age = 18
def get_age(self):
return self.__age
def set_age(self, new_age):
self.__age = new_age
# 类属性方式的property属性
age = property(get_age, set_age)
p = Person()
print(f"修改前年龄是:{p.age}") # 打印 修改前年龄是:18
p.age = 66
print(f"修改后年龄是:{p.age}") # 打印 修改后年龄是:66四.上下文管理器由实现了__enter__()和__exit__()方法的类创建的对象在文件操作篇提到过使用with语句可以自动调用关闭文件操作,即使出现异常也会自动调用关闭文件操作。with open("guanzhi.txt", "w") as f:
f.write("hello world")使用with语句简化操作是建立在上下文管理器上的,open函数创建的f文件对象就是一个上下文管理器对象__enter表示上文方法,需要返回一个操作文件对象__exit__表示下文方法,with语句执行完成会自动执行,即使出现异常也会执行该方法# 定义一个File类
class File:
def __init__(self, file_name, file_model):
self.file_name = file_name
self.file_model = file_model
# 实现__enter__()和__exit__()方法
def __enter__(self):
print("这是上文")
self.file = open(self.file_name, self.file_model)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
print("这是下文")
self.file.close()
# 使用with语句来完成文件操作
with File("1.txt", "w") as f:
f.write("hello world")五.深拷贝浅拷贝开辟新的内存空间接收变量调用id()可获得变量的内存地址(1) 浅拷贝(1.1) 可变类型浅拷贝使用copy函数进行浅拷贝,只对可变类型的第一层对象进行拷贝对拷贝的对象开辟新的内存空间进行存储不会拷贝对象内部的子对象import copy
a = [1, 2, 3]
b = [11, 22, 33]
c = [a, b]
# 普通赋值,指向同一空间
d = c
print(f"c内存地址:{id(c)}") # 打印 c内存地址:2265505547072
print(f"d内存地址:{id(d)}") # 打印 d内存地址:2265505547072
a = [1, 2, 3]
b = [11, 22, 33]
c = [a, b]
# 浅拷贝,指向不同空间
d = copy.copy(c)
print(f"c内存地址:{id(c)}") # 打印 c内存地址:2265505547648
print(f"d内存地址:{id(d)}") # 打印 d内存地址:2265505548608
# 不会拷贝对象内部的子对象
print(id(a)) # 打印 2135734964288
print(id(c[0])) # 打印 2135734964288
print(id(d[0])) # 打印 2135734964288(1.2) 不可变类型浅拷贝不可变类型进行浅拷贝不会给拷贝的对象开辟新的内存空间,只是拷贝了这个对象的引用a = (1, 2, 3)
b = (11, 22, 33)
c = (a, b)
# 浅拷贝效果与普通赋值一样
d = c
e = copy.copy(c)
print(f"c内存地址:{id(c)}") # c内存地址:1536064302016
print(f"d内存地址:{id(d)}") # d内存地址:1536064302016
print(f"e内存地址:{id(e)}") # e内存地址:1536064302016(2) 深拷贝保障数据的独立性(2.1) 可变类型深拷贝使用deepcopy函数进行深拷贝,会对可变类型内每一层可变类型对象进行拷贝,开辟新的内存空间进行存储import copy
a = [1, 2, 3]
b = [11, 22, 33]
c = [a, b]
d = copy.deepcopy(c)
print(f"c内存地址:{id(c)}") # 打印 c内存地址:2603978212160
print(f"d内存地址:{id(d)}") # 打印 d内存地址:2603978215488
# 内部的可变类型也会拷贝
print(id(a)) # 打印 2603978215104
print(id(c[0])) # 打印 2603978215104
print(id(d[0])) # 打印 2603978212992
(2.2) 不可变类型深拷贝不可变类型进行深拷贝不会给拷贝的对象开辟新的内存空间,只是拷贝了这个对象的引用a = (1, 2, 3)
b = (11, 22, 33)
c = (a, b)
d = copy.deepcopy(c)
print(f"c内存地址:{id(c)}") # 打印 c内存地址:1312354282432
print(f"e内存地址:{id(d)}") # 打印 e内存地址:1312354282432六.eval函数eval()函数可将字符串当成有效的表达式来求值并返回计算结果# 基本的数学运算
res = eval("(1+9)*5")
print(res)
# 打印 50
# 字符串重复
res = eval("'*'*10")
print(res)
# 打印 **********
# 字符串转换成列表
print(type(eval("[1,2,3,4]")))
# 打印 <class 'list'>
# 字符串转成字典
print(type(eval("{'name':'guanzhi','age':20}")))
# 打印 <class 'dict'>注意事项:开发时千万不要使用eval直接转换input的结果用户可能恶意输入有危害的终端指令input_str = input() # 输入 __import__('os').system('rm -rf /*')
eval(input_str) # 直接运行可能导致主机崩溃
# 等价于
import os
os.system("终端命令")
七.全文概览
跟着小潘学后端
使用 Netty 快速实现一个群聊功能
前言通过之前的文章介绍,我们可以深刻认识到Netty在网络编程领域的卓越表现和强大实力。这篇文章将介绍如何利用 Netty 框架开发一个 WebSocket 服务端,从而实现一个简单的在线聊天功能。声明文章中所提供的代码仅供参考,旨在帮助无 Netty 经验的开发人员快速上手。请注意,这些代码并不适用于实际应用中。功能说明聊天页面:用户进入页面后,会看到一个简单的文本框,可以用来发送消息。页面下方会显示聊天的消息内容。服务端主要有以下三个功能:响应聊天页面:用来接收和响应聊天页面的请求。处理消息:对接收到的消息进行处理。实现群聊功能:提供群聊的功能,使多个用户能够在同一个聊天室中进行交流。功能很简单,但是可以通过这个示例实现更多复杂的场景。实现步骤创建一个简单的 Maven 项目,直接引入 netty-all 包即可编码。<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.28.Final</version>
</dependency>
实现该功能共有五个类,如下:├── MakeIndexPage.java
├── ProcessWsIndexPageHandler.java
├── ProcesssWsFrameHandler.java
├── WebSocketServer.java
└── WebSocketServerInitializer.java
下面对实现该功能所涉及的五个类的代码进行详细说明WebSocket 服务启动这个类是一个基于 Netty 启动的常规服务端。它包含了一些配置项,包括 Reactor 模式、IO 类型以及消息处理配置,大部分都是这样。代码如下:/**
* 类说明:
*/
public final class WebSocketServer {
/*创建 DefaultChannelGroup,用来保存所
有已经连接的 WebSocket Channel,群发和一对一功能可以用上*/
private final static ChannelGroup channelGroup =
new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
static final boolean SSL = false;//是否启用ssl
/*通过ssl访问端口为8443,否则为8080*/
static final int PORT
= Integer.parseInt(
System.getProperty("port", SSL? "8443" : "80"));
public static void main(String[] args) throws Exception {
/*SSL配置*/
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(),
ssc.privateKey()).build();
} else {
sslCtx = null;
}
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WebSocketServerInitializer(sslCtx,channelGroup));
Channel ch = b.bind(PORT).sync().channel();
System.out.println("打开浏览器访问: " +
(SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/');
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
Channel 初始化这个类的主要功能是创建了一个 ChannelInitializer,用于初始化 ChannelPipeline,并添加了一些通道处理器。这些处理器包括由Netty提供的处理SSL协议、处理HTTP协议和支持WebSocket协议的功能,还有一些由业务自定义的处理器,用于处理页面展示和处理WebSocket数据。代码如下:/**
* 类说明:增加handler
*/
public class WebSocketServerInitializer
extends ChannelInitializer<SocketChannel> {
private final ChannelGroup group;
/*websocket访问路径*/
private static final String WEBSOCKET_PATH = "/chat";
private final SslContext sslCtx;
public WebSocketServerInitializer(SslContext sslCtx,ChannelGroup group) {
this.sslCtx = sslCtx;
this.group = group;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
/*增加对http的支持*/
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
/*Netty提供,支持WebSocket应答数据压缩传输*/
pipeline.addLast(new WebSocketServerCompressionHandler());
/*Netty提供,对整个websocket的通信进行了初始化(发现http报文中有升级为websocket的请求)
,包括握手,以及以后的一些通信控制*/
pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH,
null, true));
/*浏览器访问时展示index页面*/
pipeline.addLast(new ProcessWsIndexPageHandler(WEBSOCKET_PATH));
/*对websocket的数据进行处理*/
pipeline.addLast(new ProcesssWsFrameHandler(group));
}
}
HTTP 请求处理这个类的主要功能是在收到 HTTP 请求时,当 URI 为“/”或“/index.html”时,会返回一个聊天界面作为响应。代码如下:/**
* 类说明:对http请求,将index的页面返回给前端
*/
public class ProcessWsIndexPageHandler
extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String websocketPath;
public ProcessWsIndexPageHandler(String websocketPath) {
this.websocketPath = websocketPath;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx,
FullHttpRequest req) throws Exception {
// 处理错误或者无法解析的http请求
if (!req.decoderResult().isSuccess()) {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
return;
}
//只允许Get请求
if (req.method() != GET) {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
return;
}
// 发送index页面的内容
if ("/".equals(req.uri()) || "/index.html".equals(req.uri())) {
//生成WebSocket的访问地址,写入index页面中
String webSocketLocation
= getWebSocketLocation(ctx.pipeline(), req,
websocketPath);
System.out.println("WebSocketLocation:["+webSocketLocation+"]");
//生成index页面的具体内容,并送往浏览器
ByteBuf content
= MakeIndexPage.getContent(
webSocketLocation);
FullHttpResponse res = new DefaultFullHttpResponse(
HTTP_1_1, OK, content);
res.headers().set(HttpHeaderNames.CONTENT_TYPE,
"text/html; charset=UTF-8");
HttpUtil.setContentLength(res, content.readableBytes());
sendHttpResponse(ctx, req, res);
} else {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND));
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
/*发送应答*/
private static void sendHttpResponse(ChannelHandlerContext ctx,
FullHttpRequest req,
FullHttpResponse res) {
// 错误的请求进行处理 (code<>200).
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
HttpUtil.setContentLength(res, res.content().readableBytes());
}
// 发送应答.
ChannelFuture f = ctx.channel().writeAndFlush(res);
//对于不是长连接或者错误的请求直接关闭连接
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
/*根据用户的访问,告诉用户的浏览器,WebSocket的访问地址*/
private static String getWebSocketLocation(ChannelPipeline cp,
HttpRequest req,
String path) {
String protocol = "ws";
if (cp.get(SslHandler.class) != null) {
protocol = "wss";
}
return protocol + "://" + req.headers().get(HttpHeaderNames.HOST)
+ path;
}
}
HTTP 页面内容这个类的主要目的是生成一个包含消息发送框和内容展示功能的HTML页面,并实现WebSocket的相关功能,包括建立连接、向服务端发送消息以及接收服务端的响应。当然,也可以单独写一个HTML文件。代码如下:/**
* 类说明:生成index页面的内容
*/
public final class MakeIndexPage {
private static final String NEWLINE = "\r\n";
public static ByteBuf getContent(String webSocketLocation) {
return Unpooled.copiedBuffer(
"<html><head><title>Web Socket Test</title><meta charset=\"utf-8\" /></head>"
+ NEWLINE +
"<body>" + NEWLINE +
"<script type=\"text/javascript\">" + NEWLINE +
"var socket;" + NEWLINE +
"if (!window.WebSocket) {" + NEWLINE +
" window.WebSocket = window.MozWebSocket;" + NEWLINE +
'}' + NEWLINE +
"if (window.WebSocket) {" + NEWLINE +
" socket = new WebSocket(\"" + webSocketLocation + "\");"
+ NEWLINE +
" socket.onmessage = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = ta.value + '\\n' + event.data" + NEWLINE +
" };" + NEWLINE +
" socket.onopen = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = \"Web Socket opened!\";" + NEWLINE +
" };" + NEWLINE +
" socket.onclose = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = ta.value + \"Web Socket closed\"; "
+ NEWLINE +
" };" + NEWLINE +
"} else {" + NEWLINE +
" alert(\"Your browser does not support Web Socket.\");"
+ NEWLINE +
'}' + NEWLINE +
NEWLINE +
"function send(message) {" + NEWLINE +
" if (!window.WebSocket) { return; }" + NEWLINE +
" if (socket.readyState == WebSocket.OPEN) {" + NEWLINE +
" socket.send(message);" + NEWLINE +
" } else {" + NEWLINE +
" alert(\"The socket is not open.\");" + NEWLINE +
" }" + NEWLINE +
'}' + NEWLINE +
"</script>" + NEWLINE +
"<form onsubmit=\"return false;\">" + NEWLINE +
"<input type=\"text\" name=\"message\" " +
"value=\"Hi, 你好啊\"/>" +
"<input type=\"button\" value=\"发送\""
+ NEWLINE +
" onclick=\"send(this.form.message.value)\" />"
+ NEWLINE +
"<h3>消息内容</h3>" + NEWLINE +
"<textarea id=\"responseText\" " +
"style=\"width:500px;height:300px;\"></textarea>"
+ NEWLINE +
"</form>" + NEWLINE +
"</body>" + NEWLINE +
"</html>" + NEWLINE, CharsetUtil.UTF_8);
}
}
WebSocket 请求处理这个类的主要功能是处理与 Channel 相关的事件。例如,当一个 Channel 连接成功时,会将该 Channel 添加到一个 ChannelGroup 中。当接收到该 Channel 的数据时,可以通过向 ChannelGroup 写入数据来实现群聊效果。代码如下/**
* 类说明:对websocket的数据进行处理
*/
public class ProcesssWsFrameHandler
extends SimpleChannelInboundHandler<WebSocketFrame> {
private final ChannelGroup group;
public ProcesssWsFrameHandler(ChannelGroup group) {
this.group = group;
}
private static final Logger logger
= LoggerFactory.getLogger(ProcesssWsFrameHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx,
WebSocketFrame frame) throws Exception {
//判断是否为文本帧,目前只处理文本帧
if (frame instanceof TextWebSocketFrame) {
// Send the uppercase string back.
String request = ((TextWebSocketFrame) frame).text();
logger.info("{} received {}", ctx.channel(), request);
// ctx.channel().writeAndFlush(
// new TextWebSocketFrame(request.toUpperCase(Locale.CHINA)));
/*群发实现:一对一道理一样*/
group.writeAndFlush(new TextWebSocketFrame(
ctx.channel().remoteAddress() + " :" + request.toUpperCase(Locale.CHINA)));
} else {
String message = "unsupported frame type: "
+ frame.getClass().getName();
throw new UnsupportedOperationException(message);
}
}
/*重写 userEventTriggered()方法以处理自定义事件*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx,
Object evt) throws Exception {
/*检测事件,如果是握手成功事件,做点业务处理*/
if (evt == WebSocketServerProtocolHandler
.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
//通知所有已经连接的 WebSocket 客户端新的客户端已经连接上了
group.writeAndFlush(new TextWebSocketFrame(
"Client " + ctx.channel().remoteAddress() + " joined"));
//将新的 WebSocket Channel 添加到 ChannelGroup 中,
// 以便它可以接收到所有的消息
group.add(ctx.channel());
} else {
super.userEventTriggered(ctx, evt);
}
}
}
效果展示服务端启动聊天页面1聊天页面2总结总的来说,基于 Netty 实现一个 WebSocket 功能是非常方便且高效的,但是我们需要知其所以然,要理解 Websocket 协议,也要懂的在 Netty 中,通过添加 ChannelHandler 来处理各种异常情况,例如握手失败、连接关闭等,当然,还要考虑安全性问题,例如处理跨站脚本攻击(XSS)、防止恶意数据传输等。
跟着小潘学后端
【C++】C++算法入门之穷举
一、概念每个人都有自己擅长的事,比如有的人擅长体育项目,而有的人更擅长坐下来思考。那计算机擅长什么呢?没错,计算机更擅长数学计算和数据处理,它能按照设置好的程序对数据进行计算。举个例子,奥特·海因里希·凯勒(Ott-Heinrich Keller)于90年前提出了凯勒猜想。这个猜想是关于用相同瓷砖覆盖空间的问题,猜测如果用二维正方形瓷砖覆盖二维空间,则至少两个瓷砖必须共享一条边,以此类推,在使用12维“正方形”瓷砖覆盖12维空间时,也至少有两个彼此邻接的瓷砖。多年来,数学家不断进行证明,发现凯勒猜想在某些维度空间是正确的,但在另一些维度空间是错误的,截至去年10月,仅有七维空间是否正确仍是个谜。但现在,数学家终于利用计算机解决了这个问题,并发布了证明过程。这是人类独创性结合计算机计算能力解决数学难题的最新示例。解决这个问题的数学家使用了40台电脑进行计算,30分钟后,机器就给出了一个单词的答案:是的。能完成这一壮举是因为计算机的运行速度快,善于重复做一件事。而我们今天要学习的穷举就是利用这一特点,如果我们在数学层面我们短时间内找不到解的公式,那我们可以尝试将所有解的可能都 一 一列出,然后再逐个验证是否符合问题要求,从而找到问题的解。路人:那怎么使用穷举法呢?穷举法的步骤如下:确定问题的范围:根据问题的要求,确定需要尝试的数值范围。使用循环进行穷举:使用循环结构(如for循环)遍历范围内的每个数值。将数值带入问题中进行尝试:将当前数值带入问题中,判断是否满足问题的条件。如果满足条件,输出结果或进行其他操作:如果当前数值满足问题的条件,可以输出该数值作为问题的解,或进行其他操作。继续循环直到穷举完所有可能的解。光说不练假把式,让我们进入到下一环节。二、单循环样题讲解问题一:1015 - 鸡兔同笼问题鸡兔同笼问题:一个笼子里面有鸡若干只,兔若干只。共有头 50 个,共有腿 160 条。求鸡兔各多少只?1.分析问题已知:头50 个、腿有160个。未知:鸡,兔各多少。关系:鸡 头1腿2,兔 头1腿4。2.定义变量根据分析的已知,未知按需要定义变量。j:鸡t:兔 //定义鸡、兔的数量。
int j=0,t=0;3.输入数据无。4.数据计算4.1 确定问题的范围:根据问题的要求,确定需要尝试的数值范围。t + j = 50,t * 2 + j * 4 = 1404.2 使用循环进行穷举:使用循环结构(如for循环)遍历范围内的每个数值。4.3 将数值带入问题中进行尝试:将当前数值带入问题中,判断是否满足问题的条件。//四、数据计算
while(j*2+(50-j)*4!=160){
j++;
}
4.4 如果满足条件,输出结果或进行其他操作。t=50-j;
cout<<j<<" "<<t<<endl;5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题,已知:头50 个、腿有160个 未知:鸡,兔各多少
//二、数据定义
//定义鸡、兔的数量。
int j=0,t=0;
//三、数据输入
//四、数据计算
while(j*2+(50-j)*4!=160){
j++;
}
t=50-j;
//五、输出结果
cout<<j<<" "<<t<<endl;
return 0;
}问题二:1351 - 买公园门票某公园门票价格为:成人票 8元 /张,儿童票 3元 /张;某旅游团来公园游玩,该团内有成人和儿童(成人和儿童都有),共花了 40元买门票。请你分别计算出成人和儿童可能的人数,按照成人从少到多,儿童从多到少的规律数出结果。1.分析问题已知:成人票 8 元 / 张,儿童票 3 元 / 张;共花了 40 元买门票。未知:成人和儿童的人数。关系:成人从少到多,儿童从多到少。2.定义变量根据分析的已知,未知按需要定义变量。3.输入数据无。4.数据计算4.1 确定问题的范围:根据问题的要求,确定需要尝试的数值范围。成人票:最少1张,最多(40-3)/8张,要保证有一张儿童票。儿童票:最少1张,最多(40-8)/3张,要保证有一张成人票。4.2 使用循环进行穷举:使用循环结构(如for循环)遍历范围内的每个数值。i:成人票数量。for(int i=1;i<=(40-3)/8;i++){
}4.3 将数值带入问题中进行尝试:将当前数值带入问题中,判断是否满足问题的条件。j:购买成人票后剩余钱数。如果j%3==0,说明剩余的钱刚好能买整数个儿童票,符合题意。//四、数据计算
for(int i=1;i<=(40-3)/8;i++){
int j=40-8*i;
if(j%3==0){
}
}
4.4 如果满足条件,输出结果或进行其他操作。如果当前数值满足问题的条件,可以输出该数值作为问题的解,或进行其他操作。//四、数据计算
for(int i=1;i<=(40-3)/8;i++){
int j=40-8*i;
if(j%3==0){
//五、输出结果
cout<<i<<" "<<j/3;
}
}4.5 继续循环直到穷举完所有可能的解。5.输出结果#include<iostream>
#include<iomanip>
using namespace std;
int main(){
//一、分析问题
//1. 已知:成人票 8 元 / 张,儿童票 3 元 / 张;共花了 40 元买门票。
//2. 未知:成人和儿童的人数。
//二、数据定义
//三、数据输入
//四、数据计算
for(int i=1;i<=(40-3)/8;i++){
int j=40-8*i;
if(j%3==0){
//五、输出结果
cout<<i<<" "<<j/3;
}
}
return 0;
}
问题三:1016 - 买小猫小狗某动物饲养中心用 X 元专款购买小狗(每只 A 元)和小猫(每只B 元)两种小动物。要求专款专用,(至少猫狗各一),正好用完。请求出方案的总数。如没有请输出 0 。1.分析问题已知:小狗 A 元 / 只,小猫 B 元 / 只;共有 X 元。未知:猫狗各一,购买方案总数。关系:A*狗的数量+B * 猫的数量 = X。2.定义变量根据分析的已知,未知按需要定义变量。A:小狗每只价格。B:小猫每只价格。X:有多少钱。count:购买方案的总数。//二、数据定义
int A,B,X,count=0; 3.输入数据从键盘读入数据。 //三、数据输入
cin>>X>>A>>B;4.数据计算4.1 确定问题的范围:根据问题的要求,确定需要尝试的数值范围。狗:最少1只,最多(X-B)/A只,要保证有一只猫。猫:最少1只,最多(X-A)/B张,要保证有一只狗。4.2 使用循环进行穷举:使用循环结构(如for循环)遍历范围内的每个数值。a:狗的数量。//四、数据计算
for(int a=1;a<=(X-B)/A;a++){
}4.3 将数值带入问题中进行尝试:将当前数值带入问题中,判断是否满足问题的条件。b:购买狗后剩余钱数。如果b%B==0,说明剩余的钱刚好能买整数只猫,符合题意。 //四、数据计算
for(int a=1;a<=(X-B)/A;a++){
int b=X-A*a;
if(b%B==0){
}
}
4.4 如果满足条件,输出结果或进行其他操作。如果当前数值满足问题的条件,可以输出该数值作为问题的解,或进行其他操作。 //四、数据计算
for(int a=1;a<=(X-B)/A;a++){
int b=X-A*a;
if(b%B==0){
++count;
}
}4.5 继续循环直到穷举完所有可能的解。5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//1. 已知:小狗 A 元 / 只,小猫 B 元 / 只;共有 X 元。
//2. 未知:猫狗各一,购买方案总数。
//二、数据定义
int A,B,X,count=0;
//三、数据输入
cin>>X>>A>>B;
//四、数据计算
for(int a=1;a<=(X-B)/A;a++){
int b=X-A*a;
if(b%B==0){
++count;
}
}
//五、输出结果
cout<<count;
return 0;
}
三、嵌套循环样题讲解问题一:1022 - 百钱百鸡问题用 100 元钱买 100 只鸡,公鸡,母鸡,小鸡都要有。公鸡 5 元 1 只,母鸡 3 元 1 只,小鸡 1 元 3 只。请问公鸡,母鸡,小鸡各应该买多少只?1.分析问题已知:共有100元,买100只鸡,公鸡5元1只,母鸡3元1只,小鸡1元3只未知:公鸡g只,母鸡m只,小鸡x只关系:5g+3m+x/3=100,g+m+x=100.2.定义变量根据分析的已知,未知按需要定义变量。 //二、数据定义
int money=100,g=1,m=1,x=3;3.输入数据无。4.数据计算4.1 确定问题的范围:根据问题的要求,确定需要尝试的数值范围。钱:100。鸡总数:100。公鸡:最少1只,最多:(100-3-1)/5只。母鸡:最少1只,最多:总钱数-已买公鸡数量 * 5 - 最少小鸡,即(money-5*g-1)/3只。4.2 使用循环进行穷举:使用循环结构(如for循环)遍历范围内的每个数值。g:公鸡的数量。while(g<=(100-3-1)/5){
g++;
}4.3 将数值带入问题中进行尝试:将当前数值带入问题中,判断是否满足问题的条件。m:母鸡的数量。母鸡数从多到少。 //四、数据计算
//输出时,按公鸡数从少到多,母鸡数从多到少的顺序输出
while(g<=(100-3-1)/5){
//母鸡最多数量
m=(money-5*g-1)/3;
while(m>1){
x=(money-g*5-m*3)*3;
if(g+m+x==100){
}
m--;
}
g++;
}
4.4 如果满足条件,输出结果或进行其他操作。如果当前数值满足问题的条件,可以输出该数值作为问题的解,或进行其他操作。 //四、数据计算
//输出时,按公鸡数从少到多,母鸡数从多到少的顺序输出
while(g<=(100-3-1)/5){
//母鸡最多数量
m=(money-5*g-1)/3;
while(m>1){
x=(money-g*5-m*3)*3;
if(g+m+x==100){
//五、输出结果
cout<<g<<" "<<m<<" "<<x<<endl;
}
m--;
}
g++;
}
4.5 继续循环直到穷举完所有可能的解。5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:共有100元,买100只鸡,公鸡5元1只,母鸡3元1只,小鸡1元3只
//未知:公鸡g只,母鸡m只,小鸡x只
//二、数据定义
int money=100,g=1,m=1,x=3;
//三、数据输入
//四、数据计算
//输出时,按公鸡数从少到多,母鸡数从多到少的顺序输出
while(g<=(100-3-1)/5){
//母鸡最多数量
m=(money-5*g-1)/3;
while(m>1){
x=(money-g*5-m*3)*3;
if(g+m+x==100){
//五、输出结果
cout<<g<<" "<<m<<" "<<x<<endl;
}
m--;
}
g++;
}
return 0;
}问题二:1025 - 兑换硬币用一张一元票换 1 分、2 分和 5 分的硬币,每种至少一枚, 问有几种换法?1.分析问题整体放大100倍,不影响结果。已知:总钱数:100,1元、2元、5元硬币;。未知:1元数量:a;2元数量:b;五元数量:c 。关系:a+2b+5c=100。2.定义变量根据分析的已知,未知按需要定义变量。
//二、数据定义
int money=100,a,b,c,count=0;3.输入数据无。4.数据计算4.1 确定问题的范围:根据问题的要求,确定需要尝试的数值范围。钱:100。5元硬币:最少1个,最多:(100-1-2)/5个。2元硬币:最少1只,最多:总钱数-已换5元硬币数量 * 5 - 1个1元硬币,即(100-1-5*c)/2。4.2 使用循环进行穷举:使用循环结构(如for循环)遍历范围内的每个数值。这里我们要优先选择换钱数更大的硬币。原因1:钱数更大,意味外层循环次更少。原因2:将1元硬币留在最后,那么不管剩多少钱都是可以换整数个。4.3 将数值带入问题中进行尝试:将当前数值带入问题中,判断是否满足问题的条件。//四、数据计算
for(c=1;c<=(100-1-2)/5;c++){
for(b=1;b<=(100-1-5*c)/2;b++){
}
}
4.4 如果满足条件,输出结果或进行其他操作。如果当前数值满足问题的条件,可以输出该数值作为问题的解,或进行其他操作。//四、数据计算
for(c=1;c<=(100-1-2)/5;c++){
for(b=1;b<=(100-1-5*c)/2;b++){
count++;
}
}
4.5 继续循环直到穷举完所有可能的解。5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:总钱数:100,1元、2元、5元硬币;
//未知:1元数量:a;2元数量:b;五元数量:c
//二、数据定义
int money=100,a,b,c,count=0;
//三、数据输入
//四、数据计算
for(c=1;c<=(100-1-2)/5;c++){
for(b=1;b<=(100-1-5*c)/2;b++){
count++;
}
}
//五、输出结果
cout<<count;
return 0;
}问题三:1024 - 购买文具新学年就要开始了,爸爸把N元钱给了小青,让他购买一批文具,并作了以下要求:只能买圆珠笔、铅笔和铅笔芯,并且每样至少买一支,总数要超过30支,而且钱要全部花完。当小青去到文具店时,发现圆珠笔8角钱一支、铅笔2角钱一支、铅笔芯1角钱一支。小青怎么买才能符合爸爸的要求呢?请你编个程序帮他算出符合购买要求的所有方案总数。1.分析问题已知:n元钱 圆珠笔8角钱一支、铅笔2角钱一支、铅笔芯1角钱一支。未知:圆珠笔数量a; 铅笔数量b; 铅笔芯数量c。关系:每样一只,总数大于30,钱要花完。2.定义变量根据分析的已知,未知按需要定义变量。count:所有方案总数。 //二、数据定义
int n,a,b,c,count=0; 3.输入数据从键盘读入钱数。整体放大10倍,不影响最后的结果。 cin>>n;
n*=10;4.数据计算4.1 确定问题的范围:根据问题的要求,确定需要尝试的数值范围。钱:100。圆珠笔数量:最少1个,最多:(n-2-1)/8个。铅笔数量:最少1只,最多:(总钱数-已买圆珠笔数量 * 5 - 1个铅笔芯)/铅笔价格,即(n-a*8-1)/2。铅笔芯数量:n - a * 8 - b * 2。4.2 使用循环进行穷举:使用循环结构(如for循环)遍历范围内的每个数值。4.3 将数值带入问题中进行尝试:将当前数值带入问题中,判断是否满足问题的条件。如果圆珠笔数量+铅笔数量+铅笔芯数量>30,那么符合题意。//四、数据计算
for(a=1;a<=(n-2-1)/8;a++){
for(b=1;b<=(n-a*8-1)/2;b++){
c=n-a*8-b*2;
if((a+b+c)>30){
}
}
}
4.4 如果满足条件,输出结果或进行其他操作。如果当前数值满足问题的条件,可以输出该数值作为问题的解,或进行其他操作。//四、数据计算
for(a=1;a<=(n-2-1)/8;a++){
for(b=1;b<=(n-a*8-1)/2;b++){
c=n-a*8-b*2;
if((a+b+c)>30){
count++;
}
}
}
4.5 继续循环直到穷举完所有可能的解。5.输出结果#include<iostream>
using namespace std;
int main(){
//一、分析问题
//已知:n元钱 圆珠笔8角钱一支、铅笔2角钱一支、铅笔芯1角钱一支
//每样一只,总数大于30,钱要花完;
//未知:圆珠笔数量a; 铅笔数量b; 铅笔芯数量c;
//二、数据定义
int n,a,b,c,count=0;
//三、数据输入
cin>>n;
n*=10;
//四、数据计算
for(a=1;a<=(n-2-1)/8;a++){
for(b=1;b<=(n-a*8-1)/2;b++){
c=n-a*8-b*2;
if((a+b+c)>30){
count++;
}
}
}
//五、输出结果
cout<<count;
return 0;
}四、练习单循环问题一:1227 - 阿凡提的难题阿凡提去集市上买餐具,财主正好在卖餐具,所以准备为难一下阿凡提。财主的餐具有 2 种:大碗和小碗,财主和阿凡提说,你买我的碗,要花光你带的钱,而且,两种碗都要买,买的两种碗的数量都得是偶数。请你编程帮助阿凡提计算,可以有哪些购买的方案呢?问题二:1349 - 植树的人数某班学生分 2 组参加植树活动,甲组有 17 人,乙组有 25 人,后来由于需要,从甲组抽调了部分学生去乙组,结果乙组的人数是甲组的 2 倍,请问从甲组抽调了多少人去乙组?问题三:1396 - 开学大采购?新学期开始了,学校计划采购一批新的篮球和排球用来上体育课。学校共有 n 元经费,咨询体育用品店得知篮球 x 元 / 个,排球 y 元 / 个,现要求篮球和排球都至少采购 1 个, n 元经费全部用完,且篮球和排球的总数要超过 50 个。请问有哪些采购方案?(按照篮球从少到多,排球从多到少输出所有可行的方案)问题四:1394 - 恐龙园买玩具?小明暑假来到恐龙园游玩,在恐龙园的礼物店里,有一些形形色色的小恐龙玩偶,小明想购买其中霸王龙和三角龙玩偶送给自己的 5位好朋友。店员告诉小明,霸王龙玩偶一只需要 x 元,三角龙玩偶一只需要 y 元。 小明有 n 元,希望两种恐龙都能购买,购买的霸王龙的数量 ≥ 三角龙的数量,购买的总数要在 5 个或者 5 个以上(这样才够分),而且不能有钱剩下。请你编程帮助小明输出所有可能的购买方案,每组方案占 1行,先输出霸王龙的数量,再输出三角龙的数量(霸王龙的数量从少到多,三角龙的数量从多到少)问题五:1220 - 买糕点妈妈给了小明 n 元,给小明同学去面包店买糕点吃,小明非常喜欢吃切片面包和蛋挞,今天切片面包 x 元一件(一包),蛋挞 y 元一件(一盒);小明想花光 n 元买这两样糕点,而且每样都至少买一件,请问,小明可以采购的方案中,能够买最多面包的方案是什么?2.嵌套循环问题一:1249 - 搬砖问题36 块砖, 36人搬。男搬 4 ,女搬 3 ,两个小儿抬一砖。要求一次全搬完。问需男、女、小儿各若干?注意:假设男、女、小孩都有,请按照男、女、小孩的顺序输出所有可能的人数分配,每种人数分配方案占 1 行,每个数字空格隔开。问题二:1250 - 马克思手稿的问题马克思手稿中有一道趣味数学题:有 30 个人,其中可能有男人、女人和小孩,在一家饭馆里吃饭共花了 50 先令。假设每个男人各花 3先令,每个女人各花 2 先令,每个小孩各花 1先令。问男人、女人和小孩各有几人?(注意:不一定男人、女人、小孩都有)问题三:1076 - 桐桐的计算这个周末数学老师布置了一道有趣的题目,意思是:九头鸟(传说中的一种怪鸟,它有九个头,两只脚)、鸡和兔子关在一个笼子里。数数它们的头正好是 100 个,数数它们的脚也正好是 100 只。老师让桐桐编程计算其中九头鸟、鸡和兔子各有多少只,你能帮助桐桐吗?问题四:1077 - 桐桐去购物桐桐周末陪妈妈到市场购物。她和妈妈来到一个买鸡的摊位,发现鸡的价格有三种:公鸡每只 5 元钱,母鸡每只 3 元钱,小鸡 3 只 1元钱。妈妈就给桐桐出了一道计算题:如果用 n 元钱买 m 只鸡,问公鸡、母鸡和小鸡可以各买多少只?注意:必须把 n 元钱正好用完,且买的各种鸡的只数为大于等于 0 的整数。桐桐回到家里便拿起笔来认真计算,算了好久还没得出答案。聪明的你通过编写程序帮助桐桐找出结果好吗?问题五:1342 - 怎样种树?公园准备在小山上种桃树、梨树、苹果树,为了美观,总共准备种n棵树( n≥6 且 n 一定是 6 的倍数),要求三种树都得有,且每种树的数量都得是偶数,桃树的数量不能比梨树的数量多,梨树的数量不能比苹果树的数量多。请问有这三种树的数量分别有哪些可能的组合方法,从少到多分别数出桃树、梨树、苹果数可能的数量组合,每行 1 个方案。五、总结穷举作为一种古老的算法,它的学习难度并不高。只要按照穷举法5个步骤去思考问题,那么就能找出问题的解。但是在实际应用中,由于穷举法的时间复杂度较高,可能会导致运行时间过长。因此,在使用穷举法时,需要根据问题的规模和复杂度进行合理的范围选择和优化。
跟着小潘学后端
从底层出发,更好的理解Netty
前言上篇文章主要涵盖了 Netty 的入门知识,包括 Netty 的发展历程、核心功能与组件,并且通过实例演示了如何使用 Netty 构建一个 HTTP 服务器。由于 Netty 的抽象程度较高,因此理解起来可能会更加复杂和具有挑战性,所以本文将通过 Java NIO 的处理流程与 Netty 的总体流程比较,并结合 Netty 的源码更加清晰地理解Netty。Java NIO 工作原理首先我们知道Netty是基于Java NIO的一个网络应用框架,是在其基础上进行封装和扩展(所以在深入了解Netty之前,建议先对Java NIO有一定的了解),所以二者对网络的连接、读取和写入的操作方式是相似的。如上图的Java NIO的处理流程,与Java NIO代码示例结合,可以看到,将 ServerSocketChannel 注册到 Selector 并监听各个事件后,Selector 在接受到事件请求后我们业务代码对其进行判断并对应处理,在使用Netty时我们似乎不需要写这些代码,甚至都没有看见Selector、ServerSocketChannel这些字眼,那这些代码在Netty中怎么体现的,我们扒开裤子看个究竟:以上文Netty构建的HTTP服务器示例为例,直接关注 ServerSocketChannel 、Selector 是什么时候创建的,事件是什么时候注册以及处理的。Selector 的创建其实看了上篇的Netty入门,可以知道 EventLoop 负责处理各种事件,所以可以盲猜一下,Selector 应该是在 NioEventLoopGroup 中创建的,look 在 NioEventLoopGroup 的构造方法中调用 JDK 的 SelectorProvider 创建了Selector,也就是 Java NIO 的代码。ServerSocketChannel 的创建关于 ServerSocketChannel 的创建,直接找绑定端口的方法,如下图同样,在 Netty 的代码 NioServerSocketChannel 的newChannel() 中也看到 Java NIO 的代码。ServerSocketChannel 注册 Selector下图中 ServerSocketChannel 在创建后为其分配了一个 EventLoop 并开启新的线程(这也是Netty 多线程异步的体现),最终在 doRegister() 调用了JDK 的接口注册了Selector 并监听了事件,看见 selectionKey 应该什么都清楚了吧。对事件的处理既然 ServerSocketChannel 注册了Selector 并监听了事件,那接下来就是当有事件来时 EventLoop 对其进行处理,直接看 NioEventLoop 中的代码,因为他是通过新的线程启动的,所以直接看 run() processSelectedKeysPlain() 中的代码熟悉吧,是监听到了某个事件可以进行处理了,下面是对读事件的处理 图中 ChannelPipeline 采用了责任链模式是对事件的处理通道,方便扩展。所以 Netty 中的读取事件与 Java NIO 的关系如下图。总结所以在接触 Netty 的之前一定要先掌握 Java NIO,本文只是介绍了 Java NIO 在 Netty 中的体现、Netty 对 Java NIO 的封装,让大家更方便的理解 Netty,并不涉及 Netty 的高效、强大的设计之处,下文将会对此进行介绍。
跟着小潘学后端
Python数据容器(五)
一.数据容器一种可以容纳多份数据的数据类型,容纳的每一份数据称之为1个元素每一个元素,可以是任意类型的据数,如字符串、数字、布尔等根据特点的不同可分为5类:列表(list)、元组(tuple)、字符串(str)、集合(set)、字典(dict)二.列表(list)列表内的每一个数据,称之为元素(1) 基本格式# 定义列表
变量名称 = [元素1, 元素2, 元素3, 元素4, 元素5]
# 定义空列表
变量名称 = []
变量名称 = list()以中括号 [] 作为标识列表内每一个元素之间用, 逗号隔开列表可以一次存储多个数据,且可以为不同的数据类型,支持嵌套使用示例:my_list_1 = ["观止", 20, True, ["guanzhi", 20]]
my_list_2 = ["观止", 20, True]
print(my_list_2)
print(type(my_list_1))
# 输出
# ['观止', 20, True]
# <class 'list'>(2) 列表的下标(索引)我们可以使用下标索引从列表中取出特定位置的数据列表中的每一个元素,都有其对应位置下标索引要注意下标索引的取值范围(有值的位置),超出范围(没值的位置)无法取出元素,并且会报错语法:变量 = 列表[下标索引](2.1) 正向索引从前向后的方向,从0开始,依次递增使用示例:# 语法: 列表[下标索引]
my_list = ["李白", "章北海", "杜甫"]
print(my_list[0]) # 打印 李白
print(my_list[1]) # 打印 章北海
print(my_list[2]) # 打印 杜甫(2.2) 反向索引从后向前:从**-1**开始,依次递减(-1、-2、-3…)使用示例:my_list = ["李白", "章北海", "杜甫"]
print(my_list[-1]) # 打印 杜甫
print(my_list[-2]) # 打印 章北海
print(my_list[-3]) # 打印 李白(2.3) 嵌套列表的索引如果列表是嵌套的列表,同样支持下标索引,且用法与上述类似使用示例:# 语法: 列表[外层列表下标索引][内层列表下标索引]
my_list = [["李白", "章北海"], ["罗辑", "杜甫"]]
print(my_list[0][0]) # 打印 李白
print(my_list[0][1]) # 打印 章北海
print(my_list[1][0]) # 打印 罗辑
print(my_list[1][1]) # 打印 杜甫(3) 列表的常用操作列表提供了一系列方法:如果将函数定义为class(类)的成员,那么函数称之为:方法使用方式作用列表.append(元素)向列表中追加一个元素列表.extend(容器)将数据容器的内容依次取出,追加到列表尾部列表.insert(下标, 元素)在指定下标处,插入指定的元素del 列表[下标]删除列表指定下标元素列表.pop(下标)删除列表指定下标元素列表.remove(元素)从前向后,删除此元素第一个匹配项列表.clear()清空列表列表.count(元素)统计此元素在列表中出现的次数列表.index(元素)查找指定元素在列表的下标 找不到报错ValueErrorlen(列表)统计容器内有多少元素不需要硬记下来,有一个模糊印象,知晓有这样的用法,需要的时候,随时查阅资料即可(3.1) 查询元素查找某元素的下标查找指定元素在列表的下标,如果找不到,报错ValueError语法:列表.index(元素)my_list = ["李白", "章北海", "罗辑", "杜甫"]
print(my_list.index("罗辑")) # 打印 2
print(my_list.index("观止")) # 打印 ValueError: '观止' is not in list(3.2) 修改元素修改特定位置(索引)的元素值直接对指定下标(正向、反向下标均可)的值进行:重新赋值(修改)语法一:列表[下标] = 值my_list = ["李白", "章北海", "罗辑", "杜甫"]
my_list[0] = "观止"
my_list[-1] = "study"
print(my_list) # 打印 ['观止', '章北海', '罗辑', 'study'](3.3) 插入元素在指定的下标位置,插入指定的元素语法:列表.insert(下标, 元素)my_list = ["李白", "章北海", "罗辑"]
my_list.insert(1, "观止")
print(my_list) # 打印 ['李白', '观止', '章北海', '罗辑'](3.4) 追加元素将指定元素,追加到列表的尾部语法一:列表.append(元素)my_list = ["李白", "章北海", "罗辑"]
my_list.append("观止")
print(my_list) # 打印 ['李白', '章北海', '罗辑', '观止']语法一:列表.extend(其它数据容器)将其它数据容器的内容取出,依次追加到列表尾部my_list_1 = ["李白", "章北海"]
my_list_2 = ["罗辑", "观止"]
my_list_1.extend(my_list_2)
print(my_list_1) # 打印 ['李白', '章北海', '罗辑', '观止'](3.5) 删除元素语法一:del 列表[下标]my_list = ["李白", "章北海", "罗辑"]
del my_list[0]
print(my_list) # 打印 ['章北海', '罗辑']语法二:列表.pop(下标)my_list = ["李白", "章北海", "罗辑"]
my_list.pop(0)
print(my_list) # 打印 ['章北海', '罗辑']
语法三:列表.remove(元素)删除某元素在列表中的第一个匹配项my_list = ["李白", "章北海", "罗辑", "李白"]
my_list.remove("李白")
print(my_list) # 打印 ['章北海', '罗辑', '李白'](3.6) 清空列表内容语法:列表.clear()my_list = ["李白", "章北海", "罗辑"]
my_list.clear()
print(my_list) # 打印 []
(3.7) 统计某元素在列表内的数量语法:列表.count(元素)my_list = ["李白", "章北海", "罗辑", "李白"]
num = my_list.count("李白")
print(num) # 打印 2
(3.8) 统计列表内有多少元素语法:len(列表)my_list = ["李白", "章北海", "罗辑", "李白"]
print(len(my_list)) # 打印 4
(4) 列表的遍历将容器内的元素依次取出进行处理的行为,称之为:遍历、迭代。可以使用循环遍历列表的元素通过列表[下标]的方式在循环中取出列表的元素循环条件为 下标值 < 列表的元素数量(4.1) while循环遍历基本格式index = 0
while index < len(列表):
元素 = 列表[index]
# 对元素进行处理
index += 1
使用示例:my_list = [1,2,3,4]
index = 0
while index < len(my_list):
num = my_list[index]
print(num)
index += 1
# 输出
# 1
# 2
# 3
# 4
(4.2) for循环遍历依次取出元素并赋值到临时变量上,在每一次的循环中,可以对临时变量(元素)进行处理。基本格式:for 临时变量 in 数据容器:
对临时变量(元素)进行处理使用示例:my_list = [1, 2, 3, 4]
for x in my_list:
print(x)
# 输出
# 1
# 2
# 3
# 4(4.3) for与while对比在循环控制上:while循环可以自定循环条件,并自行控制for循环不可以自定循环条件,只可以一个个从容器内取出数据在无限循环上:while循环可以通过条件控制做到无限循环for循环理论上不可以,因为被遍历的容器容量不是无限的在使用场景上:while循环适用于任何想要循环的场景for循环适用于,遍历数据容器的场景或简单的固定次数循环场景(5) 列表小结列表特点:可以容纳多个元素(上限为2**63-1、9223372036854775807个)可以容纳不同类型的元素(混装)数据是有序存储的(有下标序号)允许重复数据存在可以修改(增加或删除元素等)三.元组(tuple)元组同列表一样,但一旦定义完成,就不可修改(1) 基本格式# 定义元组
变量名称 = (元素1, 元素2, 元素3, 元素4, 元素5)
# 定义只有一个元素的元组
变量名称 = (元素1,)
# 定义空元组
变量名称 = ()
变量名称 = tuple()以小括号 () 作为标识列表内每一个元素之间用, 逗号隔开列表可以一次存储多个数据,且可以为不同的数据类型,支持嵌套使用示例:my_tuple_1 = ("观止", 20, True, ("guanzhi", 20))
my_tuple_2 = ("观止", 20, True)
print(my_tuple_2)
print(type(my_tuple_1))
# 输出
# ('观止', 20, True)
# <class 'tuple'>元组只有一个数据,这个数据后面要添加逗号,否则不是元组my_tuple = ("观止")
print(type(my_tuple)) # 打印 <class 'str'>
my_tuple = ("观止",)
print(type(my_tuple)) # 打印 <class 'tuple'>
嵌套元组使用:# 获取值方式与列表一致
my_tuple = ((1, 2, 3), (4, 5, 6))
print(my_tuple[0][0]) # 打印 1(2) 元组的常用操作元组由于不可修改的特性,所以其操作方法非常少。方法作用元组.index(元素)查找某个数据,如果数据存在返回对应的下标,否则报错元组.count(元素)统计某个数据在当前元组出现的次数len(元组)统计元组内的元素个数(2.1) 查询元素根据下标(索引)取出数据语法:元组[下标索引]my_tuple = ("观止", True, 20)
print(my_tuple[0]) # 打印 观止查找特定元素的第一个匹配项语法:元组.index(元素)my_tuple = ("观止", True, "观止")
print(my_tuple.index("观止")) # 打印 0(2.2) 统计操作统计某个数据在元组内出现次数语法一:元组.count(元素)my_tuple = ("观止", True, "观止")
print(my_tuple.count("观止")) # 打印 2统计元组内元素个数语法二:len(元组)my_tuple = ("观止", True, "观止")
print(len(my_tuple)) # 打印 3(2.3) 注意事项不可以修改元组的内容,否则会直接报错my_tuple = (1, True, "观止")
my_tuple[0] = 5 # 报错 'tuple' object does not support item assignment可以修改元组内的list的内容(修改元素、增加、删除、反转等)my_tuple = (1, True, [2, 3, 4])
my_tuple[2][0] = 5
print(my_tuple) # 打印 (1, True, [5, 3, 4])不可以替换list为其它list或其它类型my_tuple = (1, True, [2, 3, 4])
my_tuple[2] = [1, 2, 3] # 报错(3) 元组小结可以与列表一样使用for与while循环遍历多数特性和list一致,不同点在于不可修改的特性可以容纳多个数据可以容纳不同类型的数据(混装)数据是有序存储的(下标索引)允许重复数据存在不可以修改(增加或删除元素等)四.字符串(str)字符串是字符的容器,一个字符串可以存放任意数量的字符。同元组一样,字符串是一个无法修改的数据容器(1) 索引取值同列表、元组一样,字符串也可以通过下标进行访问name = "guanzhi"
print(name[0]) # 打印 g
print(name[-1]) # 打印 i(2) 字符串的常用操作操作说明字符串[下标]根据下标索引取出特定位置字符字符串.index(字符串)查找给定字符的第一个匹配项的下标字符串.replace(字符串1, 字符串2)将字符串内的全部字符串1,替换为字符串2 不会修改原字符串,而是得到一个新的字符串.split(字符串)按照给定字符串,对字符串进行分隔 不会修改原字符串,而是得到一个新的列表字符串.strip() 字符串.strip(字符串)移除首尾的空格和换行符或指定字符串字符串.count(字符串)统计字符串内某字符串的出现次数len(字符串)统计字符串的字符个数(2.1) 查找元素查找特定字符串的下标索引值语法:字符串.index(字符串)name = "guanzhi"
print(name.index("a")) # 打印 2
(2.2) 替换元素将字符串内的全部:字符串1,替换为字符串2语法:字符串.replace(字符串1,字符串2)name = "guanzhi"
new_name = name.replace("guan", "study")
print(name) # 打印 guanzhi
print(new_name) # 打印 studyzhi
不是修改字符串本身,而是得到了一个新字符串(2.3) 分割元素按照指定的分隔符字符串,将字符串划分为多个字符串,并存入列表对象中语法:字符串.split(分隔符字符串)name = "guanzhi,study,20"
new_list = name.split(",")
print(name) # 打印 guanzhi,study,20
print(new_list) # 打印 ['guanzhi', 'study', '20']
print(type(new_list)) # 打印 <class 'list'>
# 字符串按照给定的,进行了分割,变成多个子字符串,并存入一个列表对象中字符串本身不变,而是得到了一个列表对象(2.4) 规整操作去前后空格以及换行符语法一:字符串.strip()name = " guanzhi "
new_name = name.strip()
print(new_name) # 打印 guanzhi去前后指定字符串语法二:字符串.strip(字符串)name = "20guanzhi20"
new_name = name.strip("20")
print(name) # 打印 20guanzhi20
print(new_name) # 打印 guanzhi字符串本身不变,而是得到了一个新字符串(2.5) 统计操作统计字符串中某字符串的出现次数语法一:字符串.count(字符串)name = "20guanzhi20"
print(name.count("20")) # 打印 2统计字符串的长度语法二:len(字符串)name = "20guanzhi 20"
print(len(name)) # 打印 12数字(1、2、3…),字母(abcd、ABCD等),符号(空格、!、@、#、$等),中文均算作1个字符(3) 字符串小结同列表、元组一样,字符串也支持while循环和for循环进行遍历特点:只可以存储字符串长度任意(取决于内存大小)支持下标索引允许重复字符串存在不可以修改(增加或删除元素等)基本和列表、元组相同不同与列表和元组的在于:字符串容器可以容纳的类型是单一的,只能是字符串类型。不同于列表,相同于元组的在于:字符串不可修改五.数据容器(序列)的切片序列:内容连续、有序,可使用下标索引的一类数据容器切片:从一个序列中,取出一个子序列序列的典型特征就是:有序并可用下标索引,字符串、元组、列表均满足这个要求序列支持切片,即:列表、元组、字符串,均支持进行切片操作(1) 基本格式语法:序列[起始下标:结束下标:步长]表示从序列中,从指定位置开始,依次取出元素,到指定位置结束,得到一个新序列:起始下标表示从何处开始,可以留空,留空视作从头开始结束下标(不含)表示何处结束,可以留空,留空视作截取到结尾步长表示,依次取元素的间隔步长1表示,一个个取元素步长2表示,每次跳过1个元素取步长N表示,每次跳过N-1个元素取步长为负数表示,反向取(注意,起始下标和结束下标也要反向标记)(2) 基本用法用法起始下标可以省略,省略从头开始结束下标可以省略,省略到尾结束步长可以省略,省略步长为1(可以为负数,表示倒序执行)用法一:my_list = [1, 2, 3, 4, 5]
new_list = my_list[1:4] # 下标1开始,下标4(不含)结束,步长1
print(new_list) # 结果:[2, 3, 4]用法二:my_tuple = (1, 2, 3, 4, 5)
new_tuple = my_tuple[:] # 从头开始,到最后结束,步长1
print(new_tuple) # 结果:(1, 2, 3, 4, 5)用法三:my_list = [1, 2, 3, 4, 5]
new_list = my_list[::2] # 从头开始,到最后结束,步长2
print(new_list) # 结果:[1, 3, 5]用法四:my_str = "12345"
new_str = my_str[:4:2] # 从头开始,到下标4(不含)结束,步长2
print(new_str) # 结果:"13"用法五:my_list = [1, 2, 3, 4, 5]
new_list = my_list[3:1:-1] # 从下标3开始,到下标1(不含)结束,步长-1(倒序)
print(new_list) # 打印 [4, 3]注:这个操作对列表、元组、字符串是通用的此操作不会影响序列本身,而是会得到一个新的序列(列表、元组、字符串)起始位置,结束位置,步长(正反序)都是可以自行控制的六.集合(set)不支持元素的重复(自带去重功能)、并且内容无序(1) 基本格式# 定义集合
变量名称 = {元素1, 元素2, 元素3, 元素4, 元素5}
# 定义空集合
变量名称 = set()以大括号 {} 作为标识集合内每一个元素之间用, 逗号隔开集合可以一次存储多个数据,且可以为不同的数据类型,支持嵌套使用示例:my_set = {"观止", True, "观止"}
print(my_set) # 打印 {True, '观止'}去重且无序(2) 集合的遍历集合不支持下标索引,所以也就不支持使用while。因为集合是无序的,所以集合不支持下标索引访问my_set = {"观止", True, "观止"}
for i in my_set:
print(i)
# 打印
# True
# 观止(3) 集合的常用操作操作说明集合.add(元素)集合内添加一个元素集合.remove(元素)移除集合内指定的元素集合.pop()从集合中随机取出一个元素集合.clear()将集合清空集合1.difference(集合2)得到一个新集合,内含2个集合的差集 原有的2个集合内容不变集合1.difference_update(集合2)在集合1中,删除集合2中存在的元素 集合1被修改,集合2不变集合1.union(集合2)得到1个新集合,内含2个集合的全部元素 原有的2个集合内容不变len(集合)得到一个整数,记录了集合的元素数量(3.1) 增加元素集合本身被修改,将指定元素,添加到集合内语法:集合.add(元素)my_set = {"观止", True, "观止"}
my_set.add("study")
print(my_set) # 打印 {'观止', True, 'study'}(3.2) 移除元素集合本身被修改,将指定元素,从集合内移除语法一:集合.remove(元素)my_set = {"观止", True, "观止"}
my_set.remove("观止")
print(my_set) # 打印 {True}从集合中随机取出一个元素,同时集合本身被修改,元素被移除语法二:集合.pop()my_set = {"观止", True}
num = my_set.pop()
print(my_set) # 打印 {'观止'}
print(num) # 打印 True
清空集合,集合本身被清空语法三:集合.clear()my_set = {"观止", True}
my_set.clear()
print(my_set) # 打印 set()(3.3) 两集合操作取出集合1和集合2的差集(集合1有而集合2没有的),得到一个新集合,集合1和集合2不变语法一:集合1.difference(集合2)my_set_1 = {1, 2}
my_set_2 = {1, 3}
my_set_3 = my_set_1.difference(my_set_2)
print(my_set_1) # 打印 {1, 2}
print(my_set_2) # 打印 {1, 3}
print(my_set_3) # 打印 {2}对比集合1和集合2,在集合1内,删除和集合2相同的元素,集合1被修改,集合2不变语法二:集合1.difference_update(集合2)my_set_1 = {1, 2}
my_set_2 = {1, 3}
my_set_1.difference_update(my_set_2)
print(my_set_1) # 打印 {2}
print(my_set_2) # 打印 {1, 3}将集合1和集合2组合成新集合(去重),集合1和集合2不变语法三:集合1.union(集合2)my_set_1 = {1, 2}
my_set_2 = {1, 3}
my_set_3 = my_set_1.union(my_set_2)
print(my_set_1) # 打印 {1, 2}
print(my_set_2) # 打印 {1, 3}
print(my_set_3) # 打印 {1, 2, 3}(3.4) 集合长度查看集合的元素数量,统计集合内有多少元素语法四:len(集合)my_set = {1, 3}
print(len(my_set)) # 打印 2(4) 集合小结特点:可以容纳多个数据可以容纳不同类型的数据(混装)数据是无序存储的(不支持下标索引)不允许重复数据存在可以修改(增加或删除元素等)七.字典、映射(dict)Python中字典和生活中字典十分相像(1) 基本格式# 定义字典
变量名称 = {key:value, key:value, key:value}
# 定义空字典
变量名称 = {}
变量名称 = dict()使用{}存储原始,每一个元素是一个键值对每一个键值对包含Key和Value(用冒号分隔)键值对之间使用逗号分隔Key和Value可以是任意类型的数据(key不可为字典)Key不可重复,重复会对原有数据覆盖使用示例:my_dict = {"观止": 99, "李白": 120, "观止": 110}
print(my_dict) # 打印 {'观止': 110, '李白': 120}
print(type(my_dict)) # 打印 <class 'dict'>(2) 数据的获取字典同集合一样,不可以使用下标索引取值字典可以通过Key值来取得对应的Valuemy_dict = {"李白": 120, "观止": 110}
print(my_dict["李白"]) # 打印 120字典的Key和Value可以是任意数据类型(Key不可为字典),即字典是可以嵌套的my_dict = {
"李白": {"语文": 110, "数学": 100},
"观止": {"语文": 90, "数学": 120}
}
print(my_dict["李白"]) # 打印 {'语文': 110, '数学': 100}
print(my_dict["李白"]["语文"]) # 打印 110字典不支持下标索引,同样不可以用while循环遍历my_dict = {"李白": 120, "观止": 110}
for name in my_dict:
print(f"key为:{name},value为:{my_dict[name]}")
# 打印
# key为:李白,value为:120
# key为:观止,value为:110(3) 字典的常用操作操作说明字典[Key]获取指定Key对应的Value值字典[Key] = Value添加或更新键值对字典.pop(Key)取出Key对应的Value并在字典内删除此Key的键值对字典.clear()清空字典字典.keys()获取字典的全部Key,可用于for循环遍历字典len(字典)计算字典内的元素数量(3.1) 新增元素字典被修改,新增了元素如果key不存在字典中执行上述操作,就是新增元素语法:字典[Key] = Valuemy_dict = {"李白": 120, "观止": 110}
my_dict["罗辑"] = 115
print(my_dict) # 打印 {'李白': 120, '观止': 110, '罗辑': 115}(3.2) 更新元素字典被修改,元素被更新字典Key不可以重复,所以对已存在的Key执行上述操作,就是更新Value值语法:字典[Key] = Valuemy_dict = {"李白": 120, "观止": 110}
my_dict["李白"] = 115
print(my_dict) # 打印 {'李白': 115, '观止': 110}(3.3) 删除元素获得指定Key的Value,同时字典被修改,指定Key的数据被删除语法:字典.pop(Key)my_dict = {"李白": 120, "观止": 110}
name = my_dict.pop("李白")
print(name) # 打印 120
print(my_dict) # 打印 {'观止': 110}(3.4) 清空字典字典被修改,元素被清空语法:字典.clear()my_dict = {"李白": 120, "观止": 110}
my_dict.clear()
print(my_dict) # 打印 {}(3.5) 获取全部的key得到字典中的全部Key语法:字典.keys()my_dict = {"李白": 120, "观止": 110}
my_keys = my_dict.keys()
print(my_keys) # 打印 dict_keys(['李白', '观止'])(3.6) 计算字典内键值对数量得到一个整数,表示字典内元素(键值对)的数量语法:len(字典)my_dict = {"李白": 120, "观止": 110}
print(len(my_dict)) # 打印 2(4) 字典小结特点:可以容纳多个数据可以容纳不同类型的数据每一份数据是Key-Value键值对可以通过Key获取到Value,Key不可重复(重复会覆盖)不支持下标索引可以修改(增加或删除更新元素等)八.数据容器对比总结(1) 简单分类是否支持下标索引支持:列表、元组、字符串 - 序列类型不支持:集合、字典 - 非序列类型是否支持重复元素:支持:列表、元组、字符串 - 序列类型不支持:集合、字典 - 非序列类型是否可以修改支持:列表、集合、字典不支持:元组、字符串(2) 特点对比列表元组字符串集合字典元素数量支持多个支持多个支持多个支持多个支持多个元素类型任意任意仅字符任意Key:Value Key:除字典外任意类型 Value:任意类型下标索引支持支持支持不支持不支持重复元素支持支持支持不支持不支持可修改性支持不支持不支持支持支持数据有序是是是否否使用场景可修改、可重复的一批数据记录场景不可修改、可重复的一批数据记录场景一串字符的记录场景不可重复的数据记录场景以Key检索Value的数据记录场景(3) 应用场景列表:一批数据,可修改、可重复的存储场景元组:一批数据,不可修改、可重复的存储场景字符串:一串字符串的存储场景集合:一批数据,去重存储场景字典:一批数据,可用Key检索Value的存储场景功能描述通用for循环遍历容器(字典是遍历key)max容器内最大元素min()容器内最小元素len()容器元素个数list()转换为列表tuple()转换为元组str()转换为字符串set()转换为集合sorted(序列, [reverse=True])排序,reverse=True表示降序 得到一个排好序的列表(4) 通用操作-遍历5类数据容器都支持for循环遍历列表、元组、字符串支持while循环,集合、字典不支持(无法下标索引)(5) 通用操作-统计(5.1)统计容器的元素个数语法:len(容器)my_list = [1, 2, 3]
my_tuple = (1, 2, 3, 4, 5)
my_str = "guanzhi"
print(len(my_list)) # 结果 3
print(len(my_tuple)) # 结果 5
print(len(my_str)) # 结果 7(5.2) 统计容器的最大元素语法:max(容器)my_list = [1, 2, 3]
my_tuple = (1, 2, 3, 4, 5)
my_str = "guanzhi"
print(max(my_list)) # 结果 3
print(max(my_tuple)) # 结果 5
print(max(my_str)) # 结果 z(5.3) 统计容器的最小元素语法:min(容器)my_list = [1, 2, 3]
my_tuple = (1, 2, 3, 4, 5)
my_str = "guanzhi"
print(min(my_list)) # 结果 1
print(min(my_tuple)) # 结果 1
print(min(my_str)) # 结果 a(5.4) 字符串大小比较ASCII码表:在程序中,字符串所用的所有字符如:大小写,英文单词,数字特殊符号(!、\、|、@、#、空格等),都有其对应的ASCII码表值每一个字符都能对应上一个:数字的码值,字符串进行比较就是基于数字的码值大小进行比较的。字符串是按位比较,也就是一位位进行对比,只要有一位大,那么整体就大:(6) 通用操作-排序将给定容器进行排序,排序后都会得到列表(list)对象。语法:sorted(容器, [reverse=True])my_list = [1, 4, 2]
my_list_asc = sorted(my_list)
print(my_list_asc) # 结果 [1, 2, 4]
my_list_des = sorted(my_list, reverse=True)
print(my_list_des) # 结果 [4, 2, 1]
(7) 通用操作-转换将给定容器转换为列表语法一:list(容器)my_tuple = (1, 4, 2)
print(type(my_tuple)) # 打印 <class 'tuple'>
my_list = list(my_tuple)
print(type(my_list)) # 打印 <class 'list'>将给定容器转换为字符串语法二:str(容器)my_tuple = (1, 4, 2)
print(type(my_tuple)) # 打印 <class 'tuple'>
my_str = str(my_tuple)
print(type(my_str)) # 打印 <class 'str'>将给定容器转换为集合语法三:set(容器)my_tuple = (1, 4, 2)
print(type(my_tuple)) # 打印 <class 'tuple'>
my_set = set(my_tuple)
print(type(my_set)) # 打印 <class 'set'>将给定容器转换为元组语法四:tuple(容器)my_list = [1, 4, 2]
print(type(my_list)) # 打印 <class 'list'>
my_tuple = tuple(my_list)
print(type(my_tuple)) # 打印 <class 'tuple'>全文概览
跟着小潘学后端
C++ 程序设计入门与算法基础
本专栏通过系统性地学习 C++ 程序设计的入门与结构,包括顺序结构、分支结构、循环结构和算法入门,涵盖 C++ 程序设计的基本语法和常用算法。进阶内容包括数组基础和排序算法,为初学者提供了一个全面且实用的 C++ 程序设计入门指南。
跟着小潘学后端
Python 进阶学习指南
专栏将全面介绍 Python 编程的基础和高阶技巧,从判断语句和循环语句、函数和数据容器到文件操作和异常处理、模块与包、面向对象、类型注解和 MySQL 操作等方面进行系统梳理。通过学习本专栏,读者将能够更好地应用 Python 语言来解决实际问题,提升自己的工作和生活水平。本专栏还提供思维导图,帮助读者更好地理解 Python 的学习结构和逻辑关系。
跟着小潘学后端
深入理解网络编程与Netty高效实现
这个专栏将带您深入探索网络编程的核心概念和技术,包括IO模型、Netty框架等。通过从底层出发,我们将全面理解Netty的工作原理和为何它如此高效和受欢迎。此外,本专栏还提供了使用Netty快速实现群聊功能的实践示例,使您能够将所学知识应用于实际项目中,加速您的网络应用开发。无论您是初学者还是有经验的开发人员,这个专栏都将为您提供深入的网络编程洞见和实用技巧。
跟着小潘学后端
SpringCloud微服务 【实用篇】| 认识微服务
一:认识微服务本课程学习于黑马,会通过分层次学习,分为三部分去讲解微服务:实用篇、高级篇、面试篇。分层次学习微服务技术栈1. 微服务框架介绍思考:什么是微服务?答:第一反应肯定是SpringCloud技术,但不仅仅是;微服务实际上是分布式架构的一种,就是把服务做拆分,拆分时会出现问题需要解决;而SpringCloud仅仅是解决服务治理问题。那么一个完整的微服务需要哪些知识呢?(1)微服务架构的第一件事就是项目的拆分,独立开发每个服务;(2)当业务越来越多,越来越复杂时,它们之间的调用关系就会越来越复杂,想要记录和维护,需要一个组件---注册中心(记录每个服务的IP、端口等信息);(3)每个服务都有自己的配置文件,将来要更改配置逐一去更改很麻烦,所以还有一个组件---配置中心(拉取配置信息,实现配置的热更新);(4)当微服务运行以后,用户就可以进行访问,这里就需要一个组件---服务网关(访问哪一个?谁能访问?对用户的身份进行验证,可以把用户的请求路由到具体的服务);(5)服务接到请求处理业务,访问数据库,再把数据返回给用户。数据库集群在庞大,也扛不住大量用户的高并发,此时就需要组件---分布式缓存(把数据库的数据放到内存当中,先到缓存,缓存未命中,再去数据库查,内存的查询效率肯定是比数据库高的);(6)还有一些复杂的搜索功能,简单数据可以走缓存,一些复杂的搜索缓存无法解决,此时就需要另外一个组件---分布式搜索;(7)在为微服务中还需要异步通信的消息队列组件,一个请求来了调用a,a调用b,b调用...整个业务的链路就会很长,调用时长就等于每个服务调用的时长之和,性能下降。而异步通信的消息队列不是去调用a而是通知a,通知完以后就结束了,业务的链路就变短了,响应时间就变短了,吞吐能力变强;(8)当然那么庞大的微服务,在出现问题排查是个大问题,所以需要引入两个新的组件---分布式日志服务(统治成千上百服务的日志,统一的存储和分析)和 系统的监控链路追踪(实时监控这个服务每一个节点的运行状态,CPU的占用等情况);(9)庞大复杂的微服务集群怎么部署呢?需要一个自动化的部署---Jenkins(自动化的编译),在使用Docker(打包形成镜像),在基于K8s实现自动化的部署;技术栈很多很杂,进行梳理学习2. 服务架构演变单体架构单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。例如:一个商城的项目,把所有的功能模块放到一个项目中进行打包部署到Tomcat服务器优点:①架构简单;②部署成本低;缺点:①耦合度高;适合部署一些小型的项目;分布式架构分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务。例如:把一个商城项目的每个模块都进行拆分成一个项目去开发。优点:①降低服务耦合;②有利于服务升级拓展服务治理拆分也会带来一些问题:都是一个单体项目,部署在不同的服务器,调用时会出现远程调用(跨越服务器)的问题。分布式架构的要考虑的问题:①服务拆分粒度如何?怎么拆,把服务作为独立的模块。②服务集群地址如何维护?上百个机器的地址怎么维护。③服务之间如何实现远程调用?跨服务的调用。④服务健康状态如何感知?服务器的状态,有可能宕机。微服务微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:①单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发;②面向服务:微服务对外暴露业务接口,用来远程调用;③自治:团队独立、技术独立、数据独立(每个服务独立的数据库)、部署独立;④隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题;例如:一个会员的功能,进行进一步的细化拆分总结:(1)单体架构特点简单方便,高度耦合,耦合度高,扩展性差,适合小型项目。例如:学生管理系统。(2)分布式架构特点松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝。(3)微服务:一种良好的分布式架构方案优点:拆分粒度更小、服务更独立、耦合度更低。缺点:架构非常复杂,运维、监控、部署难度提高。3. 微服务技术对比微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo。微服务技术对比企业需求 ①使用SpringCloud技术栈、服务接口采用Restful风格、服务调用采用Feign方式;②使用SpringCloudAlibaba技术栈、服务接口采用Restful风格、服务调用采用Feign方式;③使用SpringCloudAlibaba技术栈、服务接口采用Dubbo协议标准、服务调用采用Dubbo方式;④基于Dubbo老旧技术体系、服务接口采用Dubbo协议标准、服务调用采用Dubbo方式;4. SpringCloud(1)SpringCloud是目前国内使用最广泛的微服务框架。官网地址:Spring Cloud。(2)SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:SpringCloud与SpringBoot的版本兼容关系如下:
跟着小潘学后端
SpringCloudAlibaba微服务 【实用篇】| Nacos配置管理
一:Nacos配置管理Nacos除了可以做注册中心(前面已经讲了),同样可以做配置管理来使用!1. 统一配置管理当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新!如何在nacos中管理配置呢?登录nacos的控制界面然后在弹出的表单中,填写配置信息:注意:这里项目的核心配置application.yml不是全都一下子都放到nacos配置管理;只有需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地aaplication.yml当中比较好。从微服务拉取配置注:在没有Nacos时项目启动后直接读取本地的application.yml配置。注:有了Nacos微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动!思考: Nacos去哪读取?读取谁?注:在读取Nacos的配置文件时,要知道Nacos的服务地址,而Nacos的服务地址又是在application.yml中进行配置的(这就形成了一个死循环);所以要先提前知道Nacos的地址才行,这样才能读取到Nacos的配置文件。因此spring引入了一种新的配置文件:bootstrap.yaml文件,会在application.yml之前被读取,流程如下:所以关于Nacos的配置信息应该都放到bootstrap.yaml配置当中;具体流程如下:第一步:引入Nacos的配置管理客户端依赖注:Nacos作为注册中心引入的是nacos-discovery依赖,作为配置管理引入的是nacos-config依赖。 <!-- nacos 配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
第二步:在user-service中的resource目录添加一个bootstrap.yml文件,这个文件是引导文件,优先级高于application.yml注:下面的三要素的结合实际上就是与前面的Nacos管理配置的Data ID相对应。spring:
application:
name: user-service #服务名称,三要素之一
profiles:
active: dev #环境,三要素之二
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
config:
file-extension: yaml # 文件后缀名,三要素之三验证:到底没有没有拉取成功呢?我们就获取一下里面的配置属性注:前面几节我们使用了dev命名空间,这里需要把order-servicenamespace属性配置删除掉;不然跨空间访问是不行的。package cn.itcast.user.web;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
// 使用@Value注解访问配置的属性
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/now")
public String now(){
// 返回当前的时间
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}成功获取到格式化后的时间,表明成功获取到配置将配置交给Nacos管理的步骤:①在Nacos控制台中添加配置文件;②在微服务中引入nacos的config依赖;③在微服务中添加bootstrap.yml,配置nacos地址、当前环境、服务名称、文件后缀名。这些决定了程序启动时去nacos读取哪个文件。2. 配置热更新我们最终的目的,是修改Nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新。 要实现配置热更新,可以使用两种方式:第一种方式:在@Value注入的变量所在类上添加注解@RefreshScope(刷新范围)第二种方式:使用@ConfigurationProperties注解(建议使用的方式)新建一个类专门完成属性的加载,此时先注入一个Java对象当中去!Java类:与配置文件的属性进行绑定package cn.itcast.user.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "pattern") // 指定前缀
public class PatternProperties {
private String dateformat;
}
此时就不需要@RefreshScope注解,也不需要@Value注解,直接注入类package cn.itcast.user.web;
import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Slf4j
@RestController
@RequestMapping("/user")
// @RefreshScope
public class UserController {
/* @Value("${pattern.dateformat}")
private String dateformat;*/
// 注入Java类
@Autowired
private PatternProperties properties;
@GetMapping("/now")
public String now(){
// 返回当前的时间
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat())); // 调用类的get方法拿到值
}
}
总结:Nacos配置更改后,微服务可以实现热更新,方式:①通过@Value注解注入,结合@RefreshScope来刷新;这种方式是取出配置的属性直接使用。②通过@ConfigurationProperties注入,自动刷新;这种方式是先把配置的属性与一个Java类绑定,然后使用Java类调属性的方式。注意事项:①不是所有的配置都适合放到配置中心,维护起来比较麻烦;②建议将一些关键参数,需要运行时调整的参数放到nacos配置中心,一般都是自定义配置;3. 配置共享多环境配置共享例:对于一个配置在开发、测试和上线都需要的配置,只需要写一份即可,然后进行共享!微服务启动时会从nacos读取多个配置文件:①[spring.application.name]-[spring.profiles.active].yaml,例如:user-service-dev.yaml;②[spring.application.name].yaml,例如:user-service.yaml。无论profile如何变化,[spring.application.name].yaml这个文件一定会加载,因此多环境共享配置可以写入这个文件!第一步:添加一个环境共享配置在nacos中添加一个user-service.yaml文件(这个文件默认就会被加载进去的)。第二步:在user-service中读取共享配置在user-service服务中,修改PatternProperties类,读取新添加的属性 注:这个类实际上封装的是user-service-dev.yaml和user-serviceyaml配置的总和!package cn.itcast.user.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "pattern") // 指定前缀
public class PatternProperties {
private String dateformat;
// 添加新的属性
private String envShareValue;
}在user-service服务中,修改UserController,添加一个方法package cn.itcast.user.web;
import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
// 注入Java类
@Autowired
private PatternProperties properties;
// 多环境共享测试
@GetMapping("/prop")
public PatternProperties prop(){
return properties;
}
}第三步:运行两个UserApplication,使用不同的profile(1)前面我们配置的是dev环境,所以此时能读取到user-service-dev.yaml配置(拿到的是dateformat属性)和 user-service.yaml配置(拿到的是envSharedValue属性);此时两个值都能读取到。(2)此时把环境改为test,重新启动一个实例,对于Nacos控制中心并没有进行test环境的任何配置,此时dateformat属性的结果应该null。但是user-service.yaml配置默认还是能拿到的,此时envShareValue属性的值还是能正常拿到的。思考:当Nacos、服务本地application.yaml同时出现相同属性时,优先级有高低之分(1)首先远程的Nacos的配置 大于 服务本地的而配置;(2)在全是Nacos的配置下,user-service-dev.yaml 大于 user-service.yaml;服务名-profile.yaml >服务名称.yaml > 本地配置服务会从nacos读取的配置文件:①[服务名]-[spring.profile.active].yaml,具体环境配置;②[服务名].yaml,默认配置,多环境共享使用;③优先级:[服务名]-[环境].yaml >[服务名].yaml > 本地配置;4. 搭建Nacos集群Nacos生产环境下一定要部署为集群状态,所以接下来就学习一下集群部署!其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx,计划的集群结构:三个nacos节点的地址:第一步: 搭建数据库nacos,初始化数据库表结构CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) DEFAULT NULL,
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL,
`c_use` varchar(64) DEFAULT NULL,
`effect` varchar(64) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
`c_schema` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
`content` longtext NOT NULL COMMENT '内容',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(64) unsigned NOT NULL,
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) NOT NULL,
`group_id` varchar(128) NOT NULL,
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL,
`md5` varchar(32) DEFAULT NULL,
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` text,
`src_ip` varchar(50) DEFAULT NULL,
`op_type` char(10) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);
CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL,
`resource` varchar(255) NOT NULL,
`action` varchar(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');第二步:下载Nacosnacos在GitHub上有下载地址:Tags · alibaba/nacos · GitHub,可以选择任意版本下载。第三步:配置Nacos集群①进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf然后添加内容:#IP地址和端口号
172.16.43.49:8845
172.16.43.49:8846
172.16.43.49:8847
②修改application.properties文件,添加数据库配置# 第33行去掉注释,表示使用mysql的集群
spring.datasource.platform=mysql
#第36行去掉注释,表示集群的数量
db.num=1
# 第39行开始配数据库的信息
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123456第四步:启动将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3然后分别修改三个文件夹中的application.properties,配置不同的端口号 nacos1:server.port=8845nacos2:server.port=8846nacos3:server.port=8847然后分别启动三个nacos节点startup.cmd #不需要-m参数,默认就是集群的方式启动第五步:nginx反向代理下载并解压到任意非中文目录下:①修改conf/nginx.conf文件,配置如下#以下配置只要放到http下面都可以
upstream nacos-cluster {
# 集群中的nacos
server 127.0.0.1:8845;
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}
server {
# 反向代理的内容
listen 80; # 以后不需要8848直接80就可以
server_name localhost;
location /nacos { # 只要访问这个;路径,就能代理到上面的集群当中
proxy_pass http://nacos-cluster;
}
}
启动nginx:在浏览器访问:http://localhost/nacos即可 代码中bootstrap.xml文件配置: 把8848改成80spring:
cloud:
nacos:
server-addr: localhost:80 # Nacos地址添加配置:这个配置就会自动存入数据库当中:
跟着小潘学后端
SpringCloud微服务 【实用篇】| http客户端Feign
一:http客户端Feign1. Feign替代RestTemplateRestTemplate方式调用存在的问题先来看我们以前利用RestTemplate发起远程调用的代码String url = "http://user-service/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);存在下面的问题:①代码可读性差,编程体验不统一;②参数复杂URL难以维护;Feign的介绍Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。定义和使用Feign客户端第一步:引入依赖在order-service服务的pom文件中引入openfeign的依赖<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>第二步:添加注解,开启自动装配在order-service的启动类添加@EnableFeginClients注解开启Fegin的功能第三步:编写Fegin客户端在order-service中新建一个UserClient接口,并添加@FeginClient注解,里面用来封装所有对user-service发起的远程调用。package cn.itcast.order.client;
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("user-service") // 服务名称
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:服务名称:user-service请求方式:GET请求路径:/user/{id}请求参数:Long id返回值类型:User这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。第四步:定义和使用Feign客户端注:此时就完全废弃使用RestTempalte。修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate。package cn.itcast.order.service;
import cn.itcast.order.client.UserClient;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/* // 注入RestTemplate
@Autowired
private RestTemplate restTemplate;*/
// ------------------注入UserClient接口
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.发出请求查询用户信息
// String url = "http://localhost:8081/user/"+order.getUserId();
/*String url = "http://user-service/user/"+order.getUserId();
User user = restTemplate.getForObject(url, User.class);*/
// 2. ---------------------利用Fegin发起http请求,查询用户
User user = userClient.findById(order.getUserId());
// 3. 把用户信息封装到order
order.setUser(user);
// 4.返回
return order;
}
}也能执行成功,同时也能完成负载均衡!2. 自定义配置Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:一般我们需要配置的就是日志级别:NONE:不记录任何日志信息,这是默认值。BASIC:仅记录请求的方法,URL以及响应状态码和执行时间。HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息。FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。配置Feign日志有两种方式第一种方式:配置文件方式①全局生效:feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别 ②局部生效:feign:
client:
config:
user-service: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别第二种方式:java代码方式创建一个类,声明一个Bean注:这个类没有声明注解,所以目前肯定还没有起作用!package cn.itcast.order.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}①全局生效:放到@EnableFeignClients这个注解中(放到整个OrderApplication启动类上的注解)// 放到启动类上,使用defaultConfiguratio属性指定上面的类
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class) ②局部生效:放到@FeignClient这个注解中(放到具体UserFegin接口上的注解)// 放到具体的接口上,使用configuratio属性指定上面的类
@FeignClient(value = "user-service", configuration = DefaultFeignConfiguration .class) Feign的日志配置总结:方式一是配置文件,feign.client.config.xxx.loggerLevel①如果xxx是default则代表全局;②如果xxx是服务名称,例如userservice则代表某服务;方式二是java代码配置Logger.Level这个Bean①如果在@EnableFeignClients注解声明则代表全局;②如果在@FeignClient注解中声明则代表某服务;3. Feign性能优化Fegin底层的客户端实现方式有三种:①URLConnection:默认实现,不支持连接池;②Apache HttpClient :支持连接池;③OKHttp:支持连接池;因此优化Feign的性能主要包括两个点:①使用连接池代替默认的URLConnection;②日志级别,不要用full,最好用basic或none;Feign的性能优化-连接池配置第一步:order-service下引入fegin-httpClient依赖<!--引入HttpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>第二步:application.yml配置连接池feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient: # 配置连接池的信息
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数Feign的优化总结:1. 日志级别尽量用basic;2. 使用HttpClient或OKHttp代替URLConnection;①引入feign-httpClient依赖;②配置文件开启httpClient功能,设置连接池参数;4. 最佳实践所谓最近实践,就是使用过程中总结的经验,最好的一种使用方式。 通过仔细观察发现:Feign的客户端order-service与服务提供者user-service的controller代码非常相似!feign客户端: UserClient接口UserController:有没有一种办法简化这种重复的代码编写呢?第一种方式:继承方式消费者的FeignClient和提供者的controller定义统一的父接口为标准!优点:①简单,实现了代码共享;缺点:①服务提供方、服务消费方紧耦合;②参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解;第二种方式:抽取方式将FeginClient抽取为独立的模块,并且把接口有关的POJO,默认的Fegin配置都放到这个模块中,提供给所有的消费者使用!问题:假如现在有两个微服务都需要查询user-service,此时order-service和pay-service各写各的Client,造成了代码重复!解决:都不用写,服务提供者提供一个API:把Client、实体类、配置等全都写好;将来消费者order-service和pay-service想要使用的话直接引依赖的jar包,然后直接调!抽取Client步骤第一步:先创建一个Modul,命名为fegin-api,然后引入openfegin的start依赖<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>第二步:将order-service中编写的UserClient(原来定义的接口)、User(实体类)、DefaultFeginConfiguration(配置日志的类)都复制到fegin-api项目中第三步:在order-service中引入fegin-api依赖首先,删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。 然后在order-service的pom文件中中引入feign-api的依赖: <!--引入feign统一api-->
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>fegin-api</artifactId>
<version>1.0</version>
</dependency>第四步:修改order-service中的所有上述三个组件有关的import部分,改成导入feign-api中的包 第五步:重启测试思考:此时报错找不到UserClient,明明我们注入的时候没有问题;此时是ComponentScan注解的范围问题。这是因为UserClient现在在cn.itcast.clients包下,而order-service的@EnableFeignClients注解是在cn.itcast.order包下,不在同一个包,无法扫描到UserClient。解决扫描包的问题:在@EnableFeignClients主类上添加包扫描方式一:在启动类上使用basePackages属性指定Feign应该扫描的包@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class, basePackages = "cn.itcast.client")
public class OrderApplication {
}方式二:在启动类上使用client属性指定某个需要加载的Client接口@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class, clients = {UserClient.class})
public class OrderApplication {
}
跟着小潘学后端
SpringCloudAlibaba微服务 【实用篇】| Nacos注册中心
一:Nacos注册中心前面已经讲解了Eureka注册中心,接下来就学习一下Nacos注册中心!实际上Nacos还能作为配置中心,后面会详细叙述!1. 认识和安装NacosNacos阿里巴巴的产品(需要下载、安装、启动),现在是SpringCloud中的一个组件。相比Eureka(Spring已经集成了)功能更加丰富,在国内受欢迎程度较高!Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台,在SpringCloud Alibaba中,我们使用nacos进行服务的注册发现、服务的配置管理。第一步:下载安装包在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码:GitHub主页:https://github.com/alibaba/nacosGitHub的Release下载页:https://github.com/alibaba/nacos/releases第二步:解压、配置本次采用1.4.1.版本的Nacos,解压后目录如下:Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。 当然也可以修改Nacos的默认端口,在conf目录的application.properties中第三步:启动Nacos的启动非常简单,进入bin目录,结构如下打开cmd,执行以下命令进行启动startup.cmd -m standalone # 单机启动第四步:访问登录在浏览器输入地址:http://127.0.0.1:8848/nacos即可:账号密码都是nacos:登录后的页面2. Nacos快速入门Nacos的使用步骤和Eureka的使用步骤基本不变,把原来的Eureka配置Nacos即可!第一步:在cloud-demo父工程中添加spring-cloud-alilbaba-dependices的管理依赖回顾:Eureka是在cloud-demo父项目下创建了一个子项目eureka-server:进行eureka-server依赖的引入,启动类上加@EnableEurekaServer注解,application.yaml中配置端口号、服务器名称、服务器地址!背景:我们知道SpringCloud是微服务的一站式解决方案,是众多组件的集合,而因为SpringCloud中几乎所有的组件使用的都是Netflix公司的产品,其中大部分已经进入了停止更新或者维护阶段。我们需要一些别的组件来代替它们,基于此,SpringCloud Alibaba诞生了,其中Nacos就是其中一员,所以我们需要先引入SpringCloudAlibaba依赖。<!--引入SpringCloudAlibaba依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>第二步: 注释掉原来order-service和user-service的关于eureka依赖,添加nacosy依赖注:服务提供者和服务消费者引入的依赖都是nacos-discovery依赖<!--nacos客户端依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
第三步:修改user-service 和 order-service中的application.yml文件,注释eureka地址,添加nacos地址(服务器名称还用原来的)# nacos服务端地址
spring:
cloud:
nacos:
server-addr: localhost:8848第四步:启动并测试3. Nacos服务分级存储模型①Nacos服务分级存储模型场景:假设现在某个功能user-service有多个实例,之前都是两层的概念:一个服务可以有多个实例!但是把所有实例都部署在一个机房,不安全,所以把多个实例放入多个机房中部署。Nacos服务分级存储模型就是引入了这样的概念,把同在同一个机房的实例成为一个集群;所以对于Nacos模型:一级是服务、往下是集群、最后是实例。服务跨集群调用问题注:服务调用尽可能选择本地集群的服务,跨集群调用延迟较高;本地集群不可访问时,再去访问其它集群!服务集群属性回到Nacos控制台查看此时的集群属性:default(表示没有)如何设置服务集群的属性?在user-service原有的配置中增加discovery属性spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务端地址
discovery:
cluster-name: HZ # 配置集群名称,也就是机房位置,例如:HZ,杭州此时uservice-service准备3个实列:UserApplication---8081、UserApplication1---8082、UserApplication2---8083;先按照上述的配置启动UserApplication---8081和UserApplication1---8082;然后把上述的集群名称改为SH后在启动UserApplication2---8083给order-service也加入集群属性spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务端地址
discovery:
cluster-name: HZ # 配置集群名称
②NacosRule负载均衡 前面已经分析了要尽可能的使用本地的集群,不去跨域访问;所以此时就测试连续访问三次会不会全部访问的是HZ集群,不访问SH集群?运行结果: 还是轮循访问,没有优先访问本地的;此时要修改负载均衡(原来默认的就是轮循调度)!在order-service中设置负载均衡的IRule为NacosRule,这个规则优先会寻找与自己同集群的服务:user-service:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
此时再次连续访问三次:优先访问本地的集群,在本地集群的基础上【随机】方式负载均衡!那如果此时本地服务HZ都停掉了呢?本地服务找不到,会跨集群访问,也能访问成功,但是会收到警告!总结①优先选择同集群服务实例列表;②本地集群找不到提供者,才去其它集群寻找,并且会报警告;③确定了可用实例列表后,再采用随机负载均衡挑选实例;③根据权重负载均衡实际部署中会出现这样的场景:服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求;Nacos提供了权重配置来控制访问频率,权重值一般是0-1之间的,权重越大则访问频率越高!第一步:在Nacos控制台可以设置实例的权重值,首先选中实例后面的编辑按钮第二步:将权重设置为0.1,测试可以发现8081被访问到的频率大大降低思考:当权重为0时有什么用?实际上当权重为0时,服务器就完全不会被访问;当我们做版本升级时可以先设置为权重0,去访问其它服务器,版本升级结束,在设置权重为很小的一个值,放进来极少的客户进行测试;测试没问题在进行广泛的升级服务!总结①Nacos控制台可以设置实例的权重值,0~1之间;②同集群内的多个实例,权重越高被访问的频率越高;③权重设置为0则完全不会被访问;4. Nacos环境隔离Nacos首先是一个注册中心,还是一个数据中心;所以Nacos在做数据和服务的管理,会有一个环境隔离的概念。环境隔离-namespace①Nacos中服务存储和数据存储的最外层都是一个名为namespace(命名空间)的东西,用来做最外层隔离;②namespace内部会有一个group属性(组),同一个命名空间的多个实例还可以分组;业务相关性比较高的就可以放到同一个组;③组内就是具体的服务,服务下面就是集群,集群下面就是实例;实际上在Nacos控制台上有一个默认的命名空间public,我们原先的实例都是放到这个组里的需求:把order-service放到一个新的命名空间里第一步:Nacos控制台上创建namespace,用来隔离不同环境第二步:填写一个新的命名空间信息第三步:保存后会在控制台看到这个命名空间的id注:id可以不填写,根据UUID自动生成即可第四步:修改application.yml文件,添加namesapce命名空间spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务端地址
discovery:
cluster-name: SH # 集群
namespace: e70e8b60-ea7e-40e0-a995-0b714190f7bd # 填写ID ,命名空间第五步: 重启order-service后,再来查看控制台public命名空间:dev命名空间:第六步:此时访问order-service,因为命名空间namespace不同,会导致找不到user-service浏览器找不到服务:注:实际上是有三个服务user-service在public的,但是order-service在dev,不同的命名空间之间无法访问(前面我们学习的不同集群是可以访问的,只是会报警告)!控制台会报错:总结:①每个命名空间namespace都有唯一id;②服务设置命名空间namespace时要写id而不是名称;③不同命名空间namespace下的服务互相不可见;二:Nacos和Eureka的对比截止到现在我们已经学习了两个注册中心:Eureka和Nacos,下面就进行对比学习一下!细节:前面我们创建的是类默认都是临时实例从执行过程分析Eureka和Nacos的异同同①无论是Eureka还是Nacos当服务提供者启动时,都会把信息提交给注册中心,注册中心把这些信息保存下来;②当消费者需要时会找注册中心去定时拉取;实际上这个拉取的动作不是每一次都要做,服务消费者会把拉取到的信息缓存到一个列表当中(每隔30秒重新拉取一次);③消费者拿到服务列表之后,负载均衡挑选一个后远程调用提供者;异第一个差别:服务提供者的健康监测,Nacos会把服务提供者划分为:临时实例和非临时实列。对于临时实列采用心跳进行检测(这点是和Eureka保持一致,但是频率不一样,nacos会慢一点),发现挂了就进行剔除;对于非临时实列Nacos不会要求进行心跳,Nacos进行主动询问,发现挂了会标记不健康了,等待恢复、不会剔除。第二个差别:消费者的拉取服务,Eureka采用的定时拉取(每隔30秒),不能做到及时更新;Nacos会主动推送变更消息,Eureka采用的是pull,而Nacos是pull+push的结合;如果Nacos发现有服务挂了会立刻推送push给消费者,及时去更新。服务注册到Nacos时,可以选择注册为临时或非临时实例,通过下面的配置来设置:spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos服务端地址
discovery:
cluster-name: SH # 集群
namespace: e70e8b60-ea7e-40e0-a995-0b714190f7bd #填写ID,指定命名空间
ephemeral: false #设置为非临时实例①默认的是临时实列,此时关闭order-service,这个服务会被干掉②修改epemeral为false,修改为非实例,此时关闭服务不会被剔除,等待恢复!总结1. Nacos与eureka的共同点①都支持服务注册和服务拉取;②都支持服务提供者心跳方式做健康检测;2. Nacos与Eureka的区别①Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式;临时实例心跳不正常会被剔除,非临时实例则不会被剔除。Eureka采用的就是心跳检测,不正常直接被剔除。②Nacos支持服务列表变更的消息推送模式,服务列表更新更及时,是拉取和推送的结合。Eureka只是单纯的拉取,不能及时更新。③Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式。注:AP/CP模式是指在分布式系统中,数据的 可用性 和 一致性 是不可兼得的,因此需要在可用性和一致性之间做出权衡。其中A(Availability)可用性、C(Consistency)一致性、P(Partition tolerance)分区容错性。
跟着小潘学后端
SpringCloud微服务 【实用篇】| Docker镜像、容器、数据卷操作
一:Docker基本操作1. 镜像操作镜像相关命令镜像的命名规范镜像名称一般分两部分组成:[repository]:[tag]; 在没有指定tag时,默认是latest,代表最新版本的镜像。镜像操作命令:CRUD①获取镜像的方式有两种:第一种是从本地获取,需要一个Dockerfile文件,利用docker build命令把它构件成一个镜像。第二种是从镜像服务器拉取,使用docker pull进行拉取镜像,可以从私服拉,也可以从DockerHub拉取。②想知道本地有哪些镜像,使用docker images命令查看镜像。这里image理解为镜像的意思。③使用docker rmi命令删除镜像,rmi是remove image的缩写。④分享镜像的两种方式:第一种是把镜像推送到镜像服务器,使用docker push命令推送到Docker Register。第二种是使用docker save命令保存镜像为一个压缩包进行拷贝;然后在使用docker load命令解压加载镜像压缩包为镜像。⑤也可以使用docker --help命令查看帮助文档。案例1:从DockerHub中【拉取】一个nginx镜像并【查看】第一步:去镜像仓库搜索nginx镜像,比如DockerHub第二步:根据查看到的镜像名称,拉取自己需要的镜像通过命令docker pull nginx拉取,没指定版本号就是最新版本第三步:通过命令docker images 查看拉取到的镜像案例2:利用docker save将nginx镜像导出磁盘,然后再通过load加载回来步骤一:利用docker 命令 --help命令查看docker save和docker load的语法docker save --help查看命令,o压缩包的名称docker load --help查看命令,i参数会打印日志,q参数不会打印日志步骤二:使用docker save导出镜像到磁盘docker save -o nginx.tar nginx:lastest步骤三:使用docker load加载镜像 首先先删除原来的镜像,可以根据 镜像名称+版本号 或者 镜像id 进行删除docker rmi -f 镜像名称:版本号 # -f参数表示删除正在运行的镜像加载压缩包为镜像docker load -i nginx.tar2. 容器操作学习完了镜像,接下来就学习一下怎么基于镜像(image)创建容器(container),完成容器的各种操作!容器相关命令①创建容器比较常用的命令就是docker run,不仅仅可以创建容器,还可以让容器处于运行状态---三大状态之一。②三大状态之二就是暂停状态,使用docker pause命令,让容器处于暂停状态。要想恢复到运行状态,使用docker unpause命令。③三大状态之二就是停止状态,使用docker stop命令,从停止恢复运行使用docker start命令。Tip:暂停和停止的区别,为什么暂停可以使用pause和unpause;而stop确不使用unstop?暂停和停止的区别在于操作系统的处理方式不同。暂停:操作系统会将容器的进程挂起,内存暂存起来;一恢复就可以直接运行。停止:操作系统直接把进程杀死,容器所占为内存回收;进程被杀死只能重新创建一个新的容器。④使用docker ps命令可以查看当前所有运行的容器及状态。docker logs命令查看容器运行日志。docker exec命令可以进入容器的内部。⑤使用docker rm 可以删除指定容器。案例1:创建运行一个Nginx容器去Docker Hub查看Nginx的容器运行命令,以下命令为例:docker run --name some-nginx -d -p 8080:80 some-content-nginx*docker run :创建并运行一个容器;*--name:给容器起一个名字;*-p:将宿主机端口与容器端口映射,冒号左侧是宿主机端口(可以随意改变),右侧是容器端口;作用是让本来完全隔离的容器暴露一个窗口,让外界进行访问;*-d:后台运行容器*some-content-nginx:镜像名称,例如nginx解释:端口映射对于一个容器是对外隔离的,如果用户想要访问容器,直接访问肯定是不行的;这样就需要端口映射,让宿主机的端口与容器的端口产生映射关系;以后直接访问宿主机的80端口就会转发到容器的80端口去处理请求。创建并运行容器查看容器的状态:docker ps访问nginx# 前面是虚拟机的IP,后面是我们指定的端口
192.168.2.129:81查看容器的日志信息docker logs -f 容器名称 # -f 表示持续更新查看日志案例2:进入Nginx容器,修改HTML文件内容,添加“北京欢迎您”进入我们刚刚创建的nginx容器docker exec -it mn bash # bash表示使用linux命令操作*docker exec :进入容器内部,执行一个命令;*-it : 给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互 ;*mn :要进入的容器的名称 ;*bash:进入容器后执行的命令,bash是一个linux终端交互命令;进入容器内部查看nginx在那个目录,就需要查看DockerHub,静态页面的文件夹/usr/share/nginx/htmlcd /usr/share/nginx/html找到index.html镜像只是封装必要的函数库,对于vim命令没有封装不能使用,使用sed命令进行修改sed -i 's#Welcome to nginx#北京欢迎您#g' index.html # 替换原始的标签
sed -i 's#<head>#<head><meta charset="utf-8">#g' index.html #支持中文再次去访问:成功替换使用exit退出容器,然后停掉容器docker stop 容器名称
此时查看容器的状态docker ps -a # 默认是只能查看正在运行的容器,-a表示查看所有(包括停掉的)
要想在启动,使用docker start 容器名称此时在删除容器对于-f参数表示强制删除正在运行的容器;如果直接删除正在运行的容器会报错无法删除,需要我们stop停止容器才可以删除比较麻烦;所以可以使用-f参数强制删除!删除以后使用docker ps -a也无法查到,是真正意义的上被删除了。docker rm -f 容器名称案例3:创建并运行一个redis容器,并且支持数据持久化,进入redis容器,并执行redis-cli客户端命令,存入num=666创建并运行容器docker run --name mr -p 6379:6379 -d redis --appendonly yes #aof模式的持久化进入容器内部并连接redis存入数据查看容器状态:docker ps 添加 -a 参数查看所有状态的容器; 删除容器:docker rm 不能删除运行中的容器,除非添加 -f 参数; 进入容器:命令是docker exec -it [容器名] [要执行的命令] ;exec命令可以进入容器修改文件,但是在容器内修改文件是不推荐的:第一点不方便,连vim命令都没有,第二点:在容器中进行修改是没有任何记录的。3. 数据卷(容器数据管理)容器与数据耦合的问题不便于修改:当我们要修改Nginx的html内容时,需要进入容器内部修改,很不方便。数据不可复用:在容器内的修改对外是不可见的,所有修改对新创建的容器是不可复用的。升级维护困难:数据在容器内,如果要升级容器必然删除旧容器,所有数据都跟着删除了。数据卷数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录。例如:对于一个Docker主机管理很多的数据卷,而对应的数据卷一定会指向宿主机的/var/lib/docker/volumes目录创建。然后让容器的内部目录与数据卷进行关联,关联以后本质上就是与宿主机的目录进行关联!注:相当于通过数据卷进行容器和宿主机的文件建立了联系!就不要进入容器内部修改了!数据卷的作用: 将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全!操作数据卷数据卷操作的基本语法如下:docker volume [COMMAND] # 二级命令,后面才是真正的操作数据卷的命令
docker volume命令是数据卷操作,根据命令后跟随的command来确定下一步的操作:*create:创建一个volume*inspect:显示一个或多个volume的详细信息*ls:列出所有的volume *prune:删除未使用的所有的volume *rm:删除一个或多个指定的volume案例1:创建一个数据卷,并查看数据卷在宿主机的目录位置创建一个数据卷docker volume create 数据卷名称 # 创建一个数据卷查看数据卷docker volume ls # 列出所有的数据卷
docker volume inspect 数据卷名称 # 查看某个数据卷的详细信息
删除数据卷docker volume prune # 删除所有未使用的数据卷
docker volume rm 数据卷名称 # 删除某个指定的数据卷挂载数据卷前面的操作相当于让数据卷与宿主机的某个文件建立了联系;下面就需要容器挂载数据卷,让数据卷与容器建立联系!首先创建好数据卷,在运行容器时,可以通过 -v 参数来挂载一个数据卷到某个容器目录docker run --name mn -p 88:80 -v html:/root/html -d nginx*docker run :就是创建并运行容器*-- name mn :给容器起个名字叫mn*-v html:/root/htm :把html数据卷挂载到容器内的/root/html这个目录中*-p 8080:80 :把宿主机的88端口映射到容器内的80端口*nginx :镜像名称案例1:创建一个nginx容器,修改容器内的html目录内的index.html内容这个案例前面已经做过了,先进入nginx容器内部,然后进入到/usr/share/nginx/html目录,然后使用sed命令修改index.html比较麻烦!现在就使用数据卷volume来实现这个功能。步骤一:创建容器并挂载数据卷到容器内的html目录注:直接使用-v参数,如果此时html数据卷没有创建,docker会帮我们创建好!docker run --name mn -p 88:80 -v html:/usr/share/nginx/html -d nginx步骤二:进入html数据卷所在的位置,并修改html内容docker volume inspect html # 查看数据卷所在的位置
cd //var/lib/docker/volumes/html/_data # 进入数据卷所在的目录
ls # 查看目录下的文件,nginx文件会被同步过来
vim index.html # 可以使用vim命令进行修改
数据卷挂载方式: -v 数据卷名称: /容器的目标目录, 如果容器运行时volume不存在,会自动被创建出来!所以以后就没有必要刻意的去创建数据卷!案例二:创建并运行一个MySQL容器,将宿主机目录直接挂载到容器注:实际上不通过数据卷这个媒介,宿主机目录可以直接与容器的目录进行挂载!提示:目录挂载与数据卷挂载的语法是类似的:第一种方式:-v [宿主机目录]:[容器内目录] ;与数据卷挂载的结构是相同的。第二种方式:-v [宿主机文件]:[容器内文件];数据卷挂载方式所没有的,会用宿主机文件覆盖容器内的文件。第一步:从DockerHub拉取myql的镜像docker pull mysql也可以加载已经下载好的压缩包,把压缩包上传到一个目录,例如tmp目录,然后执行load命令docker load -i mysql.tar # 把压缩包加载为镜像第二步:创建两个目录mkdir -p /tmp/mysql/data # 创建mysql存储数据的目录,-p表示递归创建
mkdir -p /tmp/mysql/conf # 创建mysql配置目录,并把mysql的配置文件传进去第三步:运行mysql容器docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 \ # 设置密码
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \ # 挂载配置文件
-v /tmp/mysql/data:/var/lib/mysql \ #挂载数据目录
-d mysql:5.7.25 挂载的方式区别数据卷挂载的方式:Docker全自动创建数据卷对应的目录;耦合度低,由Docker来管理目录,但是目录较深,不好找。目录挂载的方式:目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看。
跟着小潘学后端
SpringCloud微服务 【实用篇】| 统一网关Gateway
一:统一网关Gateway前面我们已经学习了注册中心Eureka、Nacos和配置管理中心Nacos;但是此时存在很多安全的问题,服务器摆在那里谁都可以进行访问!1. 为什么需要网关网关功能:①身份认证和权限校验:微服务直接摆在那里允许任何人都可以访问,不太安全;需要进行身份验证,一切请求先到网关Gateway再到微服务,验证过后在进行放行!②服务路由、负载均衡:放行过后,问题又来了,当用户放松请求处理业务时,网关肯定处理不了业务,需要把请求给对应的微服务;但是需要判断是发给order-service还是user-service进行处理?每一个微服务后面肯定有很多实例,所以还需要进行服务路由和负载均衡!③请求限流:允许用户的请求量,限量;是对微服务的一种保护机制!网关的技术实现:在SpringCloud中网关的实现包括两种:①Gateway:SpringCloudGateway是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。②Zuul:Zuul是基于Servlet的实现,属于阻塞式编程。总结网关的作用:①对用户请求做身份认证、权限校验; ②将用户请求路由到微服务,并实现负载均衡 ;③对用户请求做限流;2. gateway快速入门搭建网关服务的步骤:第一步:创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖注:这里需要Nacos依赖是因为也要把网关Gateway也注入注册中心Nacos里! <!--网关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖,把自己注入Nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>第二步:服务的启动需要启动类package cn.itcast.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class,args);
}
}第三步:application.yml中编写路由规则配置及nacos地址server:
port: 10010 # 服务端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
gateway: # 服务路由配置
routes: # 表示规则
- id: userservice # 路由标识
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://user-service # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates:
- Path=/user/** # 路径断言,判断是否以/user开头
- id: orderservice
uri: lb://order-service
predicates:
- Path=/order/**启动服务,此时访问就不需要:http://localhost:8080/order/101 而是http://localhost:10010/order/101 这种形式成功把把请求从网关路由到微服务!原理剖析①首先发起请求,端口是10010,而网关端口号也是10010,一定会进入网关!网关无法处理业务,只能基于路由规则进行判断(前面定义了两个路由规则)。②根据路由规则匹配到的是user-service,然后就可以找到nacos注册中心进行服务拉取,再去负载均衡挑一个。网关搭建步骤:1. 创建项目,引入nacos服务发现和gateway依赖;2. 配置application.yml,包括服务基本信息、nacos地址、路由;路由配置包括:1. 路由id:路由的唯一标示;2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡;3. 路由断言(predicates):判断路由的规则;4. 路由过滤器(filters):对请求或响应做处理;(后面会讲)3. 断言工厂网关路由可以配置的内容包括:①路由id:路由唯一标示;②uri:路由目的地,支持lb和http两种;③predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地;④filters:路由过滤器,处理请求或响应;路由断言工厂Route Predicate Factory注:我们在配置文件中写的断言规则只是字符串,这些字符串会被路由断言工厂Predicate Factory读取并处理解析,转变为路由判断的条件。例如:Path=/user/**是按照路径匹配,这个规则是org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的像这样的断言工厂在SpringCloudGateway还有十几个!Spring提供了11种基本的Predicate工厂:详细的使用规则参考官网:Spring Cloud Gateway注:如果此时路由规则不符合,浏览器页面包404错误!增加时间路由规则:给order-service增加在2023后访问才符合规则predicates:
- Path=/order/**
- After=2031-01-20T17:42:47.789-07:00[America/Denver] # 表明在2023年后访问符合执行结果:PredicateFactory的作用是什么?读取用户定义的断言条件,对请求进行解析并做出判断。2. Path=/user/**是什么含义?对请求对路进行解析,路径是以/user开头的就认为是符合的。4. 过滤器工厂 过滤器工厂 GatewayFilterGatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理!Spring提供了31种不同的路由过滤器工厂。例如:更详细的可以参考官方网站:Spring Cloud Gateway案例:给所有进入user-service的请求添加一个请求头Truth=Itcast is freaking awesome!注:key和value之间是以 逗号 的方式连接!验证执行结果:在UserController中使用@RequestHeader注解拿到请求头信息 @GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth",required = false) String truth
) {
// 进行打印
System.out.println("Truth: "+truth);
return userService.queryById(id);
}
思考:此时只是给某个微服务增加请求头信息,那么如果是所有的微服务都添加呢?注:使用默认过滤器default-filter。配置的某一个微服务的过滤器,其filter在route的下面一级;而全局过滤器default-filter是与route同级!5. 全局过滤器全局过滤器 GlobalFilter全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样!区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现。定义方式是实现GlobalFilter接口exchange参数: 请求上下文,里面可以获取Request、Response等信息;
chain参数:过滤器链,用来把请求委托给下一个过滤器,放行;
Mono<Void>: 返回标示当前过滤器业务结束;
package org.springframework.cloud.gateway.filter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public interface GlobalFilter {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件参数中是否有authorization,authorization参数值是否为admin,如果同时满足则放行,否则拦截!在gateway启动类的同包下定义一个过滤器①首先通过exchange参数获取到request对象,调用request对象的getQueryParams方法获取到所有的请求参数。然后从请求参数中通过authorization这个key获取value值admin。如果这个值存在:就调用chain执行链的filter方法,把exchange传下去;如果这个值不存在:就通过exchange参数获取到response对象,通过这个对象的setComplete方法进行拦截。在拦截之前还可以通过通过response方法设置状态码,增加用户的体验感!②增加@Component注解组件扫描注解,纳入Spring的管理。③增加@Order注解,顺序组件;将来可能会定义很对组件,这里是为了先执行。还可以实现Ordered接口,重写getOrder方法进行设置。package cn.itcast.gateway;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
// @Order(-1)
@Component
public class AuthrizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 第一步:获取所有参数
// 获取request对象
ServerHttpRequest request = exchange.getRequest();
// 获取所有请求参数
MultiValueMap<String, String> params = request.getQueryParams();
// 第二步:根据authorization参数获取value值
String auth = params.getFirst("authorization");
// 第三步:判断请求参数的值是不是等于admin
if ("admin".equals(auth)){
// 是,放行
return chain.filter(exchange);
}
// 不是,拦截
// 结束之前设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 实现Ordered接口的方法也可以
@Override
public int getOrder() {
return -1;
}
}进行访问:如果不增加参数就会报401错误(未登录错误)1. 全局过滤器的作用是什么?对所有路由都生效的过滤器(这点和默认过滤器default-filter效果相同),并且可以自定义处理逻辑,比较灵活;2. 实现全局过滤器的步骤?①实现GlobalFilter接口;②添加@Order注解或实现Ordered接口 和 添加组件扫描@Component注解;③编写处理逻辑;过滤器执行顺序前面已经讲解了三个过滤器:路由过滤球、默认的过滤器、全局过滤器;接下来就分析一下这三个过滤器的执行顺序!注:请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器思考1:从目前来看,这三个过滤器不是同一种类型,怎么能放到同一个集合当中呢?1. 对于路由过滤器和默认过滤器default-filter从配置文件来看配置方式相同:其本质上实际都是AddRequestHeaderGatewayFilterFactory对象!这个过滤器的工厂就会读取配置文件生成一个真正的过滤器GatewayFilter;所以路由过滤器默认过滤器都是同一类:GatewayFilter!2. 在FilteringWebHandler类里面有一个FilteringWebHandler(过滤器适配器)这个类适配器实现了GatewayFilter接口,在适配器内部又接收了一个全局过滤器参数GlobalFiter;通过适配器模式进行传参当做GatewayFilter来使用,这样就建立了联系!所以可以认为这三种过滤器都是GatewayFilter类型,同一种类型就可以放到List集合当中进行排序!思考2:这样新的问题就引出来了,怎么进行排序呢?①我们已经知道,每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。②对于全局过滤器GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定。③对于路由过滤器和默认过滤器我们并没有去指定顺序!路由过滤器和默认过滤器defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。例如:各排各的: ④如果此时过滤器的order值都是1怎么办呢?当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。详情可以参考源码:很清晰,可以自己看一下①org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。②org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链。6. 跨域问题跨域问题处理在微服务当中,所有的请求都要先经过网关,在到微服务;这样就不要在每个微服务进行处理,只需要在网关中进行处理!跨域:域名不一致就是跨域,主要包括:域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com域名相同,端口不同:localhost:8080和localhost8081跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题!解决方案:CORS(浏览器去询问服务器的方式)通过axios发送get请求,请求地址就是网关的地址<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<pre>
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
</pre>
</body>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios.get("http://localhost:10010/user/1?authorization=admin")
.then(resp => console.log(resp.data))
.catch(err => console.log(err))
</script>
</html>以8090端口进行运行,此时在控制台就可以看到报错请求:进行配置server:
port: 10010 # 服务端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
gateway: # 服务路由配置
routes:
- id: userservice # 路由标识
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://user-service # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates:
- Path=/user/** # 路径断言,判断是否以/user开头
# filters:
# - AddRequestHeader=Truth,Itcast is freaking awesome! # 注意key和value之间是以逗号隔开
- id: orderservice
uri: lb://order-service
predicates:
- Path=/order/**
- Before=2031-01-20T17:42:47.789-07:00[America/Denver]
default-filters:
- AddRequestHeader=Truth,Itcast is freaking awesome! # 注意key和value之间是以逗号隔开
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题(防止CORS浏览器询问服务器拦截) corsConfigurations:
corsConfigurations:
'[/**]': # 拦截所有的请求
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期,有效期内直接放行 重启网关,此时再次以8090端口发送请求就可以跨域访问啦!
跟着小潘学后端
SpringCloud微服务 【实用篇】| Dockerfile自定义镜像、DockerCompos
一:Dockerfile自定义镜像前面我们怎么拉取镜像,怎么去创建运行容器;但是都是基于DockerHub官方制作的镜像。接下来就学习一下怎么自己制作镜像!1. 镜像结构镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成!镜像结构:镜像是分层结构,每一层称为一个Layer基础镜像(BaseImage):一定是某个镜像依赖的系统函数库,这一层称为基础镜像。入口(Entrypoint):等所有的安装步骤完成,就需要暴露一个端口出去,用来启动使用。层(Layer):在基础层的基础上给应用配置环境变量,下载安装包,依赖等,每一个操作都是新的一层。2. Dockerfile语法什么是DockerfileDockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。更详细语法说明,请参考官网文档:Dockerfile reference | Docker Docs3. 构建Java项目案例1:基于Ubuntu镜像构建一个新镜像,运行一个java项目步骤1:新建一个空文件夹docker-demomkdir docker-demo步骤2:拷贝资料中的docker-demo.jar(项目)、jdk8.tar.gz(Java运行环境)、Dockerfile(构建说明书)文件到docker-demo这个目录DockerFile文件是构建说明书: # 指定基础镜像FROM ubuntu:16.04# 配置环境变量,JDK的安装目录ENV JAVA_DIR=/usr/local
# 拷贝jdk和java项目的包到指定目录
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
#安装JDK
# 进入目录,解压JDK,修改名字
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
#-------------------以上配置基本上都是安装JDK
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
步骤3:运行命令,构建镜像-t表示tag版本号,点“.”表示当前目录,当前Dockerfile文件所在的目录!docker build -t javaweb:1.0 . #当前就在Dockerfile所在的目录,所以使用点
构建完成后查看镜像docker images运行容器docker run --name web -p 8090:8090 -d javaweb:1.0浏览器进行访问 http://192.168.2.129:8090/hello/count 成功构建并部署成功案例2:基于java:8-alpine镜像,将一个Java项目构建为镜像上面的过程看起来很繁琐,但是大部分的操作都是安装JDK而已;所以可以基于java:8-alpine镜像来构建Java项目项目,这个镜像默认已经安装了JDK。Dockerfile文件# 指定基础镜像
FROM java:8-alpine
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
再次构建docker build -t javaweb:2.0 . 二: DockerCompose1. 初识DockerCompose什么是DockerComposeDocker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。version: "3.8" # 版本
services: #---------------------第一种方式
mysql: # 服务名称,相当于--name
image: mysql:5.7.25 # 镜像名称
environment: # 配置密码,相当于 -e ;对于端口不需要暴露,在集群内部使用,所以不需要配
MYSQL_ROOT_PASSWORD: 123
volumes: # 数据卷配置,-d默认就是
- "/tmp/mysql/data:/var/lib/mysql"
- "/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf"
#---------------------第二种方式
web: # 名称web
build: . # 点表示在当前目录构建
ports: # 构建完以后直接启动容器
- "8090:8090"
# -------------原来的方式
#---------------------第一种方式
# 容器的启动
docker run \
--name mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \ # 设置密码
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \ # 挂载配置文件
-v /tmp/mysql/data:/var/lib/mysql \ #挂载数据目录
-d mysql:5.7.25
#---------------------第二种方式
# 构建镜像
docker build -t web:1.0
# 容器的启动‘
docker run --name web -p 8080:80 -d web:1.0DockerCompose的详细语法参考官网:Overview | Docker Docs安装DockerCompose第一步:Linux下需要通过命令下载# 安装
curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose当然如果有现成的docker-compose文件直接上传到/usr/local/bin/目录也可以第二步:修改文件的权限# 修改权限,增加可执行权限
chmod +x /usr/local/bin/docker-compose补充:Base自动补全命令,以后编写docker-compose命令会有提示# 补全命令
curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
如果上述出现错误,需要修改自己的hosts文件:# 上述出现错误可能是域名识别不了raw.githubusercontent.com
echo "185.199.108.133 raw.githubusercontent.com" >> /etc/hosts2. 部署微服务集群将之前学习的cloud-demo微服务集群利用DockerCompose部署到服务器上第一步:对于cloud-demo需要部署网关gateway、order-service、user-service、nacos、mysql第二步:在gateway、order-service、user-service编写DockerfileFROM java:8-alpine
COPY ./app.jar /tmp/app.jar # 这个app.jar就是我们需要打的jar包
ENTRYPOINT java -jar /tmp/app.jar第三步:编写docker-compose(每个服务启动)version: "3.2" # 版本
services: # 服务
nacos: # ---------------第一个服务nacos
image: nacos/nacos-server # nacos的镜像
environment: # 环境,相当于-m单击运行的配置
MODE: standalone
ports: # 端口
- "8848:8848"
mysql: # ---------第二个服务mysql
image: mysql:5.7.25 # 镜像名称
environment:
MYSQL_ROOT_PASSWORD: 123 # 密码
volumes: # 数据卷挂载
- "$PWD/mysql/data:/var/lib/mysql" # $PWD得到当前的目录
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
user-service: # ---------第三个服务user-service
build: ./user-service
order-service: # ---------第四个服务order-service
build: ./order-service
gateway: # ---------第五个服务gateway,网关暴露了接口
build: ./gateway
ports:
- "10010:10010"
第四步:修改自己的cloud-demo项目(gateway,order-service,user-service),将数据库、nacos地址都命名为docker-compose中的服务名------------》localhost改为对应的服务名,使用Docker-compose部署,所有服务之间都可以用服务名进行访问。例如:以下这种情况,localhost改为服务名称nacos第五步:使用maven打包工具,将项目中的每个微服务(gateway,order-service,user-service)都打包为app.jar <build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>对cloud-demo项目,利用生命周期,先clean,然后在打包package;将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录中整体目录每一个服务的目录结构第六步:将cloud-demo上传至虚拟机,进入cloud-demo目录docker-compose up -d # up参数表示创建并执行容器,-d后台运行docker ps查看创建的容器注:如果你的网速够快一下子就把镜像构建出来,在启动时查看docker-compose logs -f查看日志会发现有的服务启动有问题,这是因为nacos还没有完全启动的原因# 建议重启一下除nacos的其它服务
docker-compose restart order-service gateway user-service打开浏览器去访问http://192.168.2.129:10010/user/2?authorization=admin能正常访问,表示部署成功!
跟着小潘学后端
SpringCloud微服务 【实用篇】| 服务拆分及远程调用
一:服务拆分及远程调用1. 服务拆分服务拆分注意事项1. 单一职责:不同微服务,不要重复开发相同业务;2. 数据独立:不要访问其它微服务的数据库;3. 面向服务:将自己的业务暴露为接口,供其它微服务调用;案例:父工程cloud-demo,子工程order-service根据id查询订单,user-service根据id查询用户表第一步:准备数据库表---数据库分离cloud_user数据库的tb_user表cloud_order数据库的tb_order表第二步:父项目cloud-demo---项目分离主要使用pom.xml指定一些版本号<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itcast.demo</groupId>
<artifactId>cloud-demo</artifactId>
<version>1.0</version>
<modules>
<module>user-service</module>
<module>order-service</module>
</modules>
<packaging>pom</packaging>
<!--SpringBoot项目-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.9.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!--版本号-->
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR10</spring-cloud.version>
<mysql.version>5.1.47</mysql.version>
<mybatis.version>2.1.1</mybatis.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- springCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>第三步:子工程order-service项目结构目录:pom.xml<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>PoJo类Orderpackage cn.itcast.order.pojo;
import lombok.Data;
@Data
public class Order {
private Long id;
private Long price;
private String name;
private Integer num;
private Long userId;
private User user;
}Pojo类Userpackage cn.itcast.order.pojo;
import lombok.Data;
@Data
public class User {
private Long id;
private String username;
private String address;
}mapper接口package cn.itcast.order.mapper;
import cn.itcast.order.pojo.Order;
import org.apache.ibatis.annotations.Select;
public interface OrderMapper {
@Select("select * from tb_order where id = #{id}")
Order findById(Long id);
}Service调用mapper接口package cn.itcast.order.service;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 4.返回
return order;
}
}Controller调用Servicepackage cn.itcast.order.web;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/{orderId}")
// @RequestMapping("/order") 和@GetMapping("/{orderId}")等价于@GetMapping("/user/{orderId})
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
}application.yml配置server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS第四步:子工程user-service项目结构目录:pom.xml<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Pojo类Userpackage cn.itcast.order.pojo;
import lombok.Data;
@Data
public class User {
private Long id;
private String username;
private String address;
} mapper接口package cn.itcast.user.mapper;
import cn.itcast.user.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
public interface UserMapper {
@Select("select * from tb_user where id = #{id}")
User findById(@Param("id") Long id);
}Service调用mapper接口package cn.itcast.user.service;
import cn.itcast.user.mapper.UserMapper;
import cn.itcast.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) {
return userMapper.findById(id);
}
}Controller调用Servicepackage cn.itcast.user.web;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 路径: /user/110
*
* @param id 用户id
* @return 用户
*/
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}application.yml配置server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS启动这两个服务器(两个项目的启动类都需要加上@MapperScan注解)Order单独进行访问,User信息为nullUser单独进行访问2. 服务间调用前面项目已经部署好,单独的根据id查订单信息Order,查用户信息User都可以查到!但是现在客户要求查询订单需求的同时把用户信息也查出来!需求:根据订单id查询订单的同时,把订单所属的用户信息一起返回 分析:订单模块只能查到订单信息,用户模块只能查出用户信息;但是现在用户要求的是订单信息查询的同时把用户信息查出来所以不得不去修改订单模块的功能,要求在查询出,订单信息的同时,还可以通过订单信息里面的userId查询相互用户信息。思考:直接使用订单模块去查用户模块的数据行不行?显然不可以,前面已经分析了不要重复的开发业务;只能从订单向用户发起远程调用!解决:①实际上用户信息对外面暴露了一个链接地址@GetMapping("/user/{id}),通过这个链接地址就能访问数据库拿到用户信息给浏览器。②那么此时的订单模块如果能发出http协议的链接请求,也能返回用户信息数据然后与原来的订单信息数据结合就能完成需求。所以现在核心问题:如何在订单信息模块的Java代码中发Http请求!Spring提供了一个RestTemplate类工具可以完成!具体实现步骤如下:第一步:注册RestTemplate在启动类OrderApplication中注入RestTemplate类!package cn.itcast.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
// 注册RestTemplate
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}第二步:服务远程调用RestTemplate通过这个RestTemplate这个类去调用getForObject()方法:第一个参数是URL地址,第二个参数是要返回的类型。package cn.itcast.order.service;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 注入RestTemplate
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.发出请求查询用户信息
String url = "http://localhost:8081/user/"+order.getUserId();
User user = restTemplate.getForObject(url, User.class);
// 3. 把用户信息封装到order
order.setUser(user);
// 4.返回
return order;
}
}此时Order单独进行访问,User信息就能显示出来总结微服务调用方式①基于RestTemplate发起的http请求实现远程调用;②http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可;3. 提供者和消费者 服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务);服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口);那么对于上面的order-service和user-service哪一个是提供者?哪一个是消费者呢?思考:服务A调用服务B,服务B调用服务C,那么服务B是什么角色?服务B相对于服务A是提供者,服务B相对服务C是消费者;这是一个相对的,所以对于一个服务是什么角色不能剖开业务。总结服务调用关系①服务提供者:暴露接口给其它微服务调用。②服务消费者:调用其它微服务提供的接口。③提供者与消费者角色其实是相对的;一个服务可以同时是服务提供者和服务消费者。
跟着小潘学后端
SpringCloud微服务 【实用篇】| Eureka注册中心、Ribbon负载均衡
一:Eureka注册中心前面已经分析了,无论是SpringCloud还是SpringCloudAlibaba,两者的注册中心都有Eureka,所以现在就来学习一下Eureka。1. Eureka原理服务调用出现的问题①服务消费者该如何获取服务提供者的地址信息?--------》注册中心②如果有多个服务提供者,消费者该如何选择?--------》负载均衡③消费者如何得知服务提供者的健康状态?--------》心跳反应Eureka的原理在Eureka的结构当中,分为两个角色:第一个角色:eureka-server(注册中心)服务端,记录和管理微服务。第二个角色:user-service(服务提供者)和 order-service(服务消费者);不管是服务者还是消费者都是微服务,所以提供者和消费者统称为Eureka的客户端client。①user-service在启动的那一刻,会把自己的信息注册给eureka;注册中心会记录服务器名称、IP端口等信息。此时如果有人想消费,就不需要自己去记录信息,直接找eureka,拉取服务器信息;这实际上就解决了上述的第一个问题。②例如:此时拿到了3个记录信息,如何选呢?通过负载均衡的知识选出来一个,然后就可以远程调用发请求;这就解决了上述的第二个问题。③那选出来的这个会不会是挂的呢?不会,因为提供者服务每隔30秒都会向eureka发一次心跳,来确认自己的状态;如果检测到不跳了、挂了,eureka就会把它从注册中心的列表中剔除掉,消费者就可以拉取到最新的信息;这就解决了上述的第三个问题。总结:在Eureka架构中,微服务角色有两类(1)EurekaServer:服务端,注册中心作用:记录服务信息,进行心跳监控。(2)EurekaClient:客户端Provider:服务提供者,例如上述的 user-service①注册自己的信息到EurekaServer。②每隔30秒向EurekaServer发送心跳。Consumer:服务消费者,例如上述的 order-service①根据服务名称从EurekaServer拉取服务列表。②基于服务列表做负载均衡,选中一个微服务后发起远程调用。2. 动手实践接下来就动手实践:总共分为三个部分:搭建注册中心、服务注册、服务发现。①搭建EurekaServer以下搭建信息是基于这篇博客的内容SpringCloud 【实用篇】| 服务拆分及远程调用http://t.csdnimg.cn/pj77E第一步:创建Maven的独立项目eureka-server,引入spring-cloud-starter-netflix-eureka-server的依赖注:从这里也可以看出实际上Spring已经集成了Erueka,直接引入依赖使用即可!与我们后面学习的Nacos注册中心不同!<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<!--引入eureka起步依赖-->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
第二步:编写启动类,添加@EnableEurekaServer注解,表示自动装配的开关package cn.itcast.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
// 假如@EnableEurekaServer注解
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class,args);
}
}第三步:添加application.yml文件,配置信息注:为什么还要配置eureka的地址信息,自己配置自己?答:eureka自己也是一个微服务,在启动时,会将自己也注入到eureka上,为了以后集群之间通信去用,相互做注册进行通信。#配置端口号
server:
port: 10086
#eureka的服务器名称
spring:
application:
name: eureka-server
#eureka的地址
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka启动eureka,进行访问此时的注册列表就自己,把自己注册进去了!②服务注册将user-service服务和order-service服务注册到EurekaServer(已user-service为例),步骤如下:第一步:在项目user-service引入spring-cloud-starter-netflix-eureka-client的依赖<!--加如eureka的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
第二步:在application.yml文spring:
application:
name: user-service #新加入的eureka服务器的名称
eureka: #新加入的eureka地址
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka启动order-service和user-service,此时的注册列表此时的注册列表就把order-service和user-service项目注册进去了!补充:我们可以将user-service多次启动, 模拟多实例部署,但为了避免端口冲突,需要修改端口设置:此时的eureka页面此时对于注册进去的user-service启动了两个实列!③服务发现 在order-service完成服务拉取:服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡。第一步:修改OrderService的代码,修改访问的url路径,用服务名代替ip、端口:// 原来是硬编码的方式,写死了
String url = "http://localhost:8081/user/"+order.getUserId();
// 现在使用加入eureka注册中心的服务器名称代替IP和端口号
String url = "http://user-service/user/"+order.getUserId();第二步:在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解@LoadBalanced:package cn.itcast.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
// 注册RestTemplate
@Bean
@LoadBalanced // 加入负载均衡的注解
public RestTemplate restTemplate(){
return new RestTemplate();
}
}此时重启order-service,连续进行两次访问,此时应该访问UserApplication--->8081还是UserApplication2--->8082呢?UserApplication日志信息显示:UserApplication2日志信息显示:发现两个是轮循方式访问,成功实现了负载均衡,至于负载均衡的原理下面会讲!总结:①搭建EurekaServer引入eureka-server依赖;启动类上添加@EnableEurekaServer注解;在application.yml中配置eureka端口号、地址、服务器名称;②服务注册引入eureka-client依赖;在application.yml中配置eureka地址、服务器名称;③服务发现用服务提供者的服务名称(代替端口号和IP)远程调用;给注入的RestTemplate添加@LoadBalanced注解;二:Ribbon负载均衡前言:对于高版本的SpringCloud的Ribbon组件已经弃用了被Spring Cloud Loadbalancer替代!前面我们用EurekaClient(注册中心)实现了服务的拉取和负载均衡,我们只是指定了一个服务器的名称、加了@LoadBalanced注解,一切就自动完成了!那么是什么时候进行的服务拉取?什么时候进行的负载均衡?负载均衡的原理是什么?策略是什么?接下来就学习SpringCloud的第二个组件Ribbon。1. 负载均衡原理order-service发起请求http://userservice/user/1,实际上这个请求并不是真实可用的地址,在浏览器是无法进行访问,是无法到达后面某个服务的;中间会被Ribbon把请求拦下来进行处理,去找到真实的地址;通过服务器名称userservice找eureka确定真实的地址(IP和Port),拉取服务,然后做负载均衡。源码分析 @LoadBalanced注解就相当于一个标记,标记RestTemplate发起的请要被Ribbon拦截。 // 注册RestTemplate
@Bean
@LoadBalanced // 假如负载均衡的注解
public RestTemplate restTemplate(){
return new RestTemplate();
}这个拦截的动作是通过LoadBalancerInterceptor类去完成的这个类实现了ClientHttpRequestInterceptor接口(客户端Http请求的拦截器);会去拦截由客户端发起的Http请求;而RestTemplate就是发Http请求的客户端。package org.springframework.cloud.client.loadbalancer;
import java.io.IOException;
import java.net.URI;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
// 重写的Intercept方法
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
}在重写的intercept方法上打断点浏览器发送localhost:8080/order/101请求回到断点拿到地址后,调用getHost方法拿到主机名拿到服务器名称去找eureka完成服务的拉取;把服务器名称交给loadBalancer去执行,实际上loadBalancer对象就是一个RibbonLoadBalanceClient(负载均衡客户端)execute方法就是执行的意思,所以要跟进这个方法,会进入RibbonLoadBalanceClient对象的excute方法(一些版本的Jar包会进入接口LoadBalanceClient的excute方法,在其实现类多打一个断点即可)这个ILoadBalancer对象(实际上是DynamicServerListLoadBalance-动态服务列表负载均衡器)就可以拿到对应的服务(成功被拉取到服务列表)拉取到服务列表,接下来就是进行负载均衡! 此时getServer方法又调用了chooseServer方法,又去转到ZoneAwareLoadBalancer类调用父类的chooseServer方法最终调用到BaseLoadBalancer类的rule类的choose方法rule类是一个IRule接口,默认的负载均衡规则ZoneAvoidanceRule这个接口还有其它负载均衡规则的实现类负载均衡流程2. 负载均衡策略上面我们学写了负载均衡的原理,知道IRule接口决定了负载均衡的策略;接下来就分析IRule接口有哪些实现,以及将来如何修改每个实现! 常见的负载均衡策略通过定义IRule实现可以修改负载均衡规则,有两种方式:第一种方式:在OrderApplication启动类中注册新的Rule-------这是全局的配置package cn.itcast.order;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
// 注册RestTemplate
@Bean
@LoadBalanced // 加入负载均衡的注解
public RestTemplate restTemplate(){
return new RestTemplate();
}
// 修改负载均衡的规则---随机
@Bean
public IRule randomRule(){
return new RandomRule();
}
}第二种方式:在application.yml文件中修改配置-------针对某个服务进行配置user-service: # 给某个微服务配置负载均衡规则,微服务的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡规则
3. 懒加载Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:ribbon:
eager-load:
enabled: true # 开启饥饿加载,clients是一个集合,后面可以配多个
clients:
- user-service # 指定对user-service这个服务饥饿加载
Ribbon负载均衡总结①Ribbon负载均衡规则:规则接口是IRule,默认实现是ZoneAvoidanceRule,根据区域选择服务列表,然后轮询。②负载均衡自定义方式代码方式:配置灵活,但修改时需要重新打包发布;配置方式:直观,方便,无需重新打包发布,但是无法做全局配置;③饥饿加载:开启饥饿加载,指定饥饿加载的微服务名称。
跟着小潘学后端
SpringCloud微服务 【实用篇】| Docker启示录
一:Docker启示录学习完前面的微服务,我们发现一个很麻烦的问题:那就是服务的部署,微服务很多,部署起来很麻烦!今天就学习一下Docker来解决一下这个微服务部署问题!1. Docker启示录项目部署的问题大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题: 依赖关系复杂,容易出现兼容性问题; 开发、测试、生产环境有差异!各种微服务的前端部分依赖于Node、JS,服务端需要数据库MySQL,缓存系统Redis,异步通信MQ等。这些都需要部署到服务器上(Linux操作系统上);每个应用都需要依赖和函数库,但是每个应用的依赖和函数库又有所差异(依赖关系复杂就容易产生兼容)。搞定了开发环境,后面还有测试、生产环境(环境对应的操作系统环境还可能不同:Centos、Ubuntu)Docker怎么解决依赖的兼容问题?既然每个应用都有自己的依赖和函数库,那么Docker就可以把应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包!并且将每个应用放到一个隔离容器去运行,避免互相干扰Docker怎么解决不同环境的操作系统?不同环境的操作系统不同(Linux和Ubuntu的本身函数库不同),Docker如何解决?我们先来了解下操作系统结构:所有的Linux操作系统都可以分为两层:Linux内核(都是相同的)、系统应用(区别在于上层的应用不同)。内核负责与计算机的硬件进行沟通,提供操作硬件的指令 。系统应用封装内核指令为函数,便于程序员调用;用户程序基于系统函数库实现功能。程序调用函数库---》函数库调用内核指令---》指定调用计算机硬件,从而实现应用的执行。Ubuntu和CentOS都是基于Linux内核,只是系统应用不同,提供的函数库有所差异;所以根据Ubuntu环境的打包程序,放到Centos上有可能运行不了!Docker将用户程序与所需要调用的系统(比如Ubuntu和Centos)函数库一起打包。Docker运行到不同操作系统时,直接基于打包的库函数,借助于操作系统的Linux内核来运行。1. Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?①Docker允许开发中将应用、依赖、函数库、配置一起打包,形成可移植镜像;②Docker应用运行在容器中,使用沙箱机制,相互隔离 ;2. Docker如何解决开发、测试、生产环境有差异的问题①Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行;总结:Docker是一个快速交付应用、运行应用的技术①可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意Linux操作系统 ;②运行时利用沙箱机制形成隔离容器,各个应用互不干扰 ;③启动、移除都可以通过一行命令完成,方便快捷;2. Docker和虚拟机的区别Docker与虚拟机Docker实现原理:Docker让一个应用在不同的Linux环境上运行是通过,会把应用及其依赖函数库,甚至于操作系统的函数库也一起打包。这样当应用运行时可以直接调用本地函数库,然后与操作系统的内核进行交互,这样就不需要关心什么样的系统,就可以实现跨系统的运行!(直接调用操作系统的内核,性能比较好)虚拟机实现原理:使用Hypervisor技术在一个操作系统上装另一个操作系统,模拟出计算机的各种的硬件,在模拟的计算机上就可以安装任意的操作系统,然后就可以安装应用、依赖等。在一个系统装另外一个系统,所以当应用执行时应用会以为自己在一个真是的电脑上运行:会去先调用内置的操作系统---》与Hypevisor交互---》把信息传给外部真实的操作系统----》外部操作系统再去调用计算机硬件。(层层传递,性能比较差)两者的对比 Docker和虚拟机的差异:①docker是一个系统进程;虚拟机是在操作系统中的操作系统 ;②docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般;3. Docker架构镜像和容器镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。对于MySQL就是一个镜像,里面含有很多文件组成了MySQL本身,所以可以说镜像就是硬盘中的文件;MySQL跑起来的进程就是容器,只不过在Docker里面容器还要做隔离!对于容器不能把自己的东西写到镜像当中(造成镜像污染),可以基于容器创建全新的镜像,镜像都是只读的!对于一个容器怎么写数据呢?容器写数据可以拷贝一份文件到自己独立的容器当中!DockerHub(镜像共享) 怎么把镜像共享给别人使用呢?这就需要DockerHub(镜像托管)和GitHub很像!DockerHub:DockerHub是一个Docker镜像的托管平台;这样的平台称为Docker Registry(镜像服务器)。 国内也有类似于DockerHub 的公开服务,比如 网易云镜像服务、阿里云镜像库等。程序员可以利用Docker提供的命令进行镜像的构建,例如:MySQL、Nginx镜像;然后把这些镜像上传到DockerHub服务器上(会公开比较危险);也可以搭建一个私有云。Docker架构怎么利用Docker完成镜像的构建和拉取、运行容器呢?这就需要了解Docker的架构了!Docker是一个CS架构的程序,由两部分组成:服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、构建容器等 ;客户端(client):通过命令(本地)或RestAPI请求(远程)向Docker服务端发送指令;可以在本地或远程向服务端发送指令;例如: 通过本地构建一个镜像,使用docker build命令,这个命令到达DockerServer以后会被守护进程docker daemon,利用提供的数据构建一个镜像!还可以去DockerRegister使用docker pull命令经过DockerServer端去拉取镜像!然后就可以运行镜像,创建容器,此时就需要docker run命令,它会告诉DockerServer的守护进程docker daemon去完成容器的创建;最终完成部署!镜像: 将应用程序及其依赖、环境、配置打包在一起(可以看成系统文件);容器:镜像运行起来就是容器,一个镜像可以运行多个容器(可以看成运行的进程);总结Docker结构:服务端:接收命令或远程请求,操作镜像或容器; 客户端:发送命令或者请求到Docker服务端 ;DockerHub:一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry;4. Centos7安装Docker企业部署一般都是采用Linux操作系统,而其中又数CentOS发行版占比最多,因此我们在CentOS下安装Docker。Docker 分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。注:Docker CE 支持 64 位版本 CentOS 7,并且要求内核版本不低于 3.10,CentOS 7 满足最低内核的要求,所以我们在CentOS 7安装Docker。4.1. 卸载如果之前安装过旧版本的Docker,可以使用下面命令卸载:注:反斜杠\ 表示命令的拼接!yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine \
docker-ce4.2. 安装docker首先需要大家虚拟机联网,安装yum工具 (下载工具)yum install -y yum-utils \
device-mapper-persistent-data \
lvm2 --skip-broken然后更新本地镜像源(默认是连接国外的,容易断还慢)# 设置docker镜像源为阿里云
yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo
yum makecache fast
安装docker社区版yum install -y docker-ce4.3. 启动dockerDocker应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦,因此建议大家直接关闭防火墙!启动docker前,一定要关闭防火墙后!!# 关闭防火墙
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld
# 查看防火墙的状态
systemctl status firewalld通过命令启动dockersystemctl status docker # 查看当前docker的状态
systemctl start docker # 启动docker服务
systemctl stop docker # 停止docker服务
systemctl restart docker # 重启docker服务然后输入命令,可以查看docker版本(启动成功才可以查看到版本号)docker -v4.4. 配置镜像加速docker官方镜像仓库网速较差,我们需要设置国内镜像服务;参考阿里云的镜像加速文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器;在daemon.json中配置阿里云的镜像地址!# 创建一个路径
sudo mkdir -p /etc/docker
# 把阿里云的地址追加到daemon.json文件
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://6aqbcoj9.mirror.aliyuncs.com"]
}
EOF # 表示终止
# 重新加载上面的文件
sudo systemctl daemon-reload
# 重启docker
sudo systemctl restart docker
跟着小潘学后端
Spring Cloud 微服务实用指南
深入认识微服务,学习实用的Spring Cloud微服务内容,包括服务拆分与远程调用,Eureka注册中心以及Ribbon负载均衡。掌握构建可伸缩、高效的微服务架构的关键知识。
跟着小潘学后端
NextJS开发:使用EventBus实现跨组件消息通知
NextJS、React中跨组件消息事件通知1.创建event-bus.tsimport { EventEmitter } from 'events'
export default new EventEmitter()2. 设置监听useEffect(() => {
EventBus.on("test_event", handleGenerateTopic);//监听事件总线
return () => {
EventBus.removeListener("test_event", handleGenerateTopic);//移出事件总线
}
}, []);
const handleGenerateTopic = (content: string) => {
}
————————————————
版权声明:本文为CSDN博主「芝士思维」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhbzhb324/article/details/134669485
useEffect(() => {
EventBus.on("test_event", handleGenerateTopic);//监听事件总线
return () => {
EventBus.removeListener("test_event", handleGenerateTopic);//移出事件总线
}
}, []);
const handleGenerateTopic = (content: string) => {
}3. 发送事件EventBus.emit("test_event", "hello")
跟着小潘学后端
NextJS开发:封装shadcn/ui中的AlertDialog确认对话框
shadcn/ui很灵活可以方便的自己修改class样式,但是仅仅一个确认删除弹窗,需要拷贝太多代码和导入太多包,重复的代码量太多,不利于代码维护。所以进一步封装以符合项目中使用。封装cx-alert-dialog.tsximport {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { CustomButton } from "./custom-button"
export const CxAlertDialog = (props: {
visible: boolean,
title?: string,
content?: string,
cancelText?: string,
okText?: string,
okColor?: string,
loading?: boolean,
disabled: boolean,
onClose: ()=>void,
onOk: ()=>void,
}) => {
const buildOkButton = () => {
if(props.okColor == "red") {
return (
<CustomButton variant="destructive" loading={props.loading} disabled={props.disabled} onClick={props.onOk}>{props.okText}</CustomButton>
)
}
else {
return (
<CustomButton loading={props.loading} disabled={props.disabled} onClick={props.onOk}>{props.okText}</CustomButton>
)
}
}
return (
<>
<AlertDialog open={props.visible}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{props.title}</AlertDialogTitle>
<AlertDialogDescription>
{props.content}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={props.onClose} disabled={props.disabled}>{props.cancelText}</AlertDialogCancel>
{ buildOkButton() }
{/* {
props.okColor == "red"
?
<AlertDialogAction className="bg-red-500 hover:bg-red-600" onClick={props.onOk}>{props.okText}</AlertDialogAction>
:
<AlertDialogAction onClick={props.onOk}>{props.okText}</AlertDialogAction>
} */}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}custom-button.tsx"use client"
import React, { MouseEventHandler } from "react";
import { Button } from "../ui/button";
import LcIcon from "./lc-icon";
import { cn } from "@/lib/utils";
/**
* Button扩展,增加图标功能
* <CustomButton icon="Loader" onClick={handleSubmit}>Button</CustomButton>
* */
export const CustomButton = (props: {
variant?: string,
size?: string,
className?: string,
iconClassName?: string,
icon?: string,
loading?: boolean
disabled?: boolean,
type?: string,
onClick?: MouseEventHandler<HTMLButtonElement>,
children?: any
}) => {
const buildIcon = () => {
if(props.loading != null && props.loading) {
return <LcIcon name="Loader" size={16} className={cn("animate-spin", props.iconClassName ?? 'mr-1' )}/>
}
else if(props.icon != null) {
return <LcIcon name={props.icon} size={16} className={props.iconClassName ?? 'mr-1'}/>
}
return ""
}
return (
<Button size={props.size as any ?? "default"} variant={props.variant as any ?? "default"} type={props.type ?? 'button' as any} className={props.className} disabled={props.disabled} onClick={props.onClick}>
{ buildIcon() }
{ props.children }
</Button>
)
}
使用CxAlertDialog组件const [delAlertVisible, setDelAlertVisible]:[boolean, Dispatch<SetStateAction<boolean>>] = React.useState(false);
const [delAlertLoading, setDelAlertLoading]:[boolean, Dispatch<SetStateAction<boolean>>] = React.useState(false);
const currOperId = useRef(BigInt(0))
const handleDelAlertOk = async () => {
setDelAlertLoading(true)
await ChapterApi.del(Number(props.docId), currOperId.current).catch((e) => ErrUtils.apiHandle(e)).then((resp)=>{
//console.log(resp)
if(!resp) return
if(resp?.code == RespCode.Success) {
setDelAlertVisible(false)
ToastUtils.success({ msg: resp?.msg })
currChapterId.current = ""
refresh()
} else {
ToastUtils.error({ msg: resp?.msg ?? "22" })
}
})
.finally(()=>{
setDelAlertLoading(false)
})
}
const buildDel = () => {
return (
<CxAlertDialog visible={delAlertVisible} okColor="red" title="提示" content="确认删除?" cancelText="取消" okText="删除"
onClose={() => setDelAlertVisible(false)} onOk={() => handleDelAlertOk()} loading={delAlertLoading} disabled={delAlertLoading}/>
)
}