go语言基础操作---七

news2024/10/7 20:31:33

socket简单介绍—套接字编程

什么是Socket
Socket,英文含义是【插座、插孔】,一般称之为套接字,用于描述IP地址和端口。可以实现不同程序间的数据通信。
Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
在这里插入图片描述
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair(套接字对)就唯一标识一个连接因此可以用Socket来描述网络连接的一对一关系。
常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)流式是一种面向连接的Socket,针对于面向连接的TCP服务应用数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用

网络应用程序设计模式

C/S模式
传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
B/S模式
浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。
优缺点
对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯所采用的通信协议,即为ftp协议的修改剪裁版。
因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。
C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。
B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。
B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。
因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。

TCP的C/S架构

在这里插入图片描述
TCP服务器代码编写

package main

import (
	"fmt"
	"net"
)

func main() {
	//监听nerwork为tcp和udp,address为ip:端口,本地ip地址可以不写
	listener, err1 := net.Listen("tcp", "127.0.0.1:8080")
	if err1 != nil {
		fmt.Println("err =", err1)
		return
	}
	defer listener.Close() //监听关闭
	//阻塞等待用户连接
	conn, err := listener.Accept()
	if err != nil {
		fmt.Println("阻塞:err = ", err)
		return
	}

	//接收用户请求
	buf := make([]byte, 1024) //1024大小的缓冲区
	n, err2 := conn.Read(buf)
	if err2 != nil {
		fmt.Println("接收: err2 = ", err2)
		return
	}

	fmt.Println("buf = ", string(buf[:n])) //指定处理的读多少

	defer conn.Close() //关闭当前用户连接
}

在这里插入图片描述

TCP客户端代码编写

package main

import (
	"fmt"
	"net"
)

func main() {

	//主动连接服务器
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("err = ", err)
		return
	}

	defer conn.Close() //关闭

	//发送数据
	conn.Write([]byte("are you ok?"))
}

并发的C/S模型通信

并发Server

现在已经完成了客户端与服务端的通信,但是服务端只能接收一个用户发送过来的数据,怎样接收多个客户端发送过来的数据,实现一个高效的并发服务器呢?
Accept()函数的作用是等待客户端的链接,如果客户端没有链接,该方法会阻塞。如果有客户端链接,那么该方法返回一个Socket负责与客户端进行通信。所以,每来一个客户端,该方法就应该返回一个Socket与其通信,因此,可以使用一个死循环,将Accept()调用过程包裹起来。
需要注意的是,实现并发处理多个客户端数据的服务器,就需要针对每一个客户端连接,单独产生一个Socket,并创建一个单独的goroutine与之完成通信。

简单版并发服务器

package main

import (
	"fmt"
	"net"
	"strings"
)

// 处理用户请求
func HandleConn(conn net.Conn) {
	//函数调用完毕,自动关闭conn
	defer conn.Close()

	//获取客户端的网络地址信息
	addr := conn.RemoteAddr().String()
	fmt.Println(addr, "addr conncet sucessful") //连接成功

	//读取用户数据
	buf := make([]byte, 2048)
	for true {
		//读取用户数据
		read, err := conn.Read(buf)
		if err != nil {
			//read tcp 127.0.0.1:8080->127.0.0.1:56933:
			//wsarecv: An existing connection was forcibly closed by the remote host.
			fmt.Println("err = ", err)
			return
		}
		fmt.Printf("[%s]: = %s\n", addr, string(buf[:read])) //read为读取数据的个数
		fmt.Println("len = ", len(string(buf[:read])))
		if "exit" == string(buf[:read-1]) {
			fmt.Println(addr, "exit")
			return
		}
		//把数据转化为大写,再给用户发送
		conn.Write([]byte(strings.ToUpper(string(buf[:read]))))
	}
}

func main() {
	//监听nerwork为tcp和udp,address为ip:端口,本地ip地址可以不写
	listener, err1 := net.Listen("tcp", "127.0.0.1:8080")
	if err1 != nil {
		fmt.Println("err =", err1)
		return
	}
	defer listener.Close() //监听关闭

	//接收多个用户
	for true {
		conn, err := listener.Accept() //等待客户端的链接,如果客户端没有链接,该方法会阻塞
		if err != nil {
			fmt.Println("err = ", err)
			return
		}

		//处理用户请求,新建一个协程,每来一个就单独为它获取
		go HandleConn(conn)
	}
}

在这里插入图片描述

并发Client

客户端不仅需要持续的向服务端发送数据,同时也要接收从服务端返回的数据。因此可将发送和接收放到不同的协程中。
主协程循环接收服务器回发的数据(该数据应已转换为大写),并打印至屏幕;子协程循环从键盘读取用户输入数据,写给服务器。读取键盘输入可使用 os.Stdin.Read(str)。定义切片str,将读到的数据保存至str中。
这样,客户端也实现了多任务。

客户端即可输入也可接收服务器回复

package main

import (
	"fmt"
	"net"
	"os"
)

func main() {

	//主动连接服务器
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("net Dial err = ", err)
		return
	}

	//main调用完毕,关闭连接
	defer conn.Close() //关闭

	//接收服务器回复的数据,新任务
	go func() {
		//从键盘输入内容,给服务器发送内容
		str := make([]byte, 1024)
		for true {
			n, err2 := os.Stdin.Read(str) //从键盘读取内容,放在str
			if err2 != nil {
				fmt.Println("os.Stdin.err = ", err)
				return
			}
			//把输入的内容给服务器发送
			conn.Write(str[:n])
		}
	}()
	//切片缓存
	buf := make([]byte, 1024)
	//不停地接收
	for true {
		n, err := conn.Read(buf) //接收服务器的请求
		if err != nil {
			fmt.Println("conn.Read err = ", err)
			return
		}
		fmt.Println(string(buf[:n])) //打印接收到的内容,转换为字符串再打印
	}
}

在这里插入图片描述

TCP通信

下图是一次TCP通讯的时序图。TCP连接建立断开。包含大家熟知的三次握手和四次握手。
在这里插入图片描述
三次握手:
所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。好比两个人在打电话:
Client:“喂,你听得到吗?”
Server:“我听得到,你听得到我吗?”
Client:“我能听到你,今天balabala…”
建立连接(三次握手)的过程:
1.客户端发送一个带SYN标志的TCP报文到服务器。这是上图中三次握手过程中的段1。客户端发出SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况。
另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。
mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
2.服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。
服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。
3.客户必须再次回应服务器端一个ACK报文,这是报文段3。
客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出。
因此一共有三个段用于建立连接,称为“三方握手”。在建立连接的同时,双方协商了一些信息,例如,双方发送序号的初始值、最大段尺寸等。
数据传输的过程:
1.客户端发出段4,包含从序号1001开始的20个字节数据。
2.服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据。
3.客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。
在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。
四次挥手:
所谓四次挥手(Four-Way-Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务器任一方执行close来触发。好比两个人打完电话要挂断:
Client:“我要说的事情都说完了,我没事了。挂啦?”
Server:“等下,我还有一个事儿。Balabala…”
Server:“好了,我没事儿了。挂了啊。”
Client:“ok!拜拜”
关闭连接(四次握手)的过程:
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
1.客户端发出段7,FIN位表示关闭连接的请求。
2.服务器发出段8,应答客户端的关闭连接请求。
3.服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
4.客户端发出段10,应答服务器的关闭连接请求。
建立连接的过程是三次握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。

UDP通信

在之前的案例中,我们一直使用的是TCP协议来编写Socket的客户端与服务端。其实也可以使用UDP协议来编写Socket的客户端与服务端。
UDP服务器
由于UDP是“无连接”的,所以,服务器端不需要额外创建监听套接字,只需要指定好IP和port,然后监听该地址,等待客户端与之建立连接,即可通信。
创建监听地址:
func ResolveUDPAddr(network, address string) (*UDPAddr, error)
创建监听连接:
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
接收udp数据:
func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
写出数据到udp:
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

文件传输原理

在这里插入图片描述

UDP与TCP的差异

在这里插入图片描述

os.Stat的使用

package main

import (
	"fmt"
	"os"
)

func main() {
	list := os.Args
	fmt.Println(len(list))
	if len(list) != 2 {
		fmt.Println("useage :xxx file")
		return
	}

	for i, s := range list {
		fmt.Println(i, s)
	}
	fileName := list[1]
	fmt.Printf("fileName = %s\n", fileName)

	//Stat返回一个描述name指定的文件对象的FileInfo。如果指定的文件对象是一个符号链接,
	//返回的FileInfo描述该符号链接指向的文件的信息,本函数会尝试跳转该链接。如果出错,返回的错误值为*PathError类型。
	//过滤路径
	info, err := os.Stat(fileName)

	if err != nil {
		fmt.Println("err = ", err)
	}
	fmt.Println("name = ", info.Name()) //name =  01_昨日回顾.mp4
	fmt.Println("size = ", info.Size()) //size =  67010611
}

在这里插入图片描述

传输文件:发送方

package main

import (
	"fmt"
	"io"
	"net"
	"os"
)

func sendFile(path string, conn net.Conn) {
	//以只读方式打开文件
	file, err := os.Open(path)
	if err != nil {
		fmt.Println("send Open err = ", err)
		return
	}

	defer file.Close()

	//读文件内容,读多少发多少,一点不差
	buf := make([]byte, 1024*4)
	for true {
		read, err := file.Read(buf) //从文件读取内容
		if err != nil {
			if err == io.EOF {
				fmt.Println("文件发送完毕")
			} else {
				fmt.Println("send Rend err = ", err)
			}
			return
		}
		//发送内容
		conn.Write(buf[:read]) //给服务器发送内容
	}
}

func main() {

	//提示输入文件
	fmt.Println("请输入需要传输的文件: ")
	var path string
	_, err := fmt.Scan(&path)
	if err != nil {
		fmt.Println("Scan open err = ", err)
		return
	}

	//获取文件名
	fileInfo, err := os.Stat(path)
	if err != nil {
		fmt.Println("os.Stat err = ", err)
		return
	}

	//主动连接服务器
	conn, err := net.Dial("tcp", "127.0.0.1:8000")
	if err != nil {
		fmt.Println("net.Dial err = ", err)
		return
	}

	defer conn.Close()

	//给接收方,先发送文件名
	_, err = conn.Write([]byte(fileInfo.Name()))
	if err != nil {
		fmt.Println("conn.write err = ", err)
		return
	}

	//接收对方的回复,如果回复ok,说明对方准备好,可以发文件
	buf := make([]byte, 1024)
	readSize, err := conn.Read(buf)
	if err != nil {
		fmt.Println("conn.Read err = ", err)
		return
	}

	if "ok" == string(buf[:readSize]) {
		//发送文件内容
		sendFile(path, conn)
	}
}

传输文件:接收方

package main

import (
	"fmt"
	"io"
	"net"
	"os"
)

// RecvFile 接收文件内容
func RecvFile(fileName string, conn net.Conn) {
	//新建文件
	file, err := os.Create(fileName)
	if err != nil {
		fmt.Println("os create err =", err)
		return
	}

	buf := make([]byte, 1024*4)

	//接收多少,写多少,一点不差
	for true {
		readSize, err := conn.Read(buf) //接收对方发过来的文件内容
		if err != nil {
			if err == io.EOF {
				fmt.Println("文件接收完毕")
			} else {
				fmt.Println("conn read err =", err)
			}
			return
		}
		if readSize == 0 {
			fmt.Println("n == 0 文件接收完毕")
			return
		}

		file.Write(buf[:readSize])
	}
}

func main() {
	//监听
	listener, err := net.Listen("tcp", "127.0.0.1:8000")
	if err != nil {
		fmt.Println("net listener err = ", err)
		return
	}

	defer listener.Close()

	//阻塞等待用户连接
	conn, err := listener.Accept()
	if err != nil {
		fmt.Println("listener accept err =", err)
		return
	}

	defer conn.Close()
	buf := make([]byte, 1024)

	//读取对方发送的文件名
	readSize, err := conn.Read(buf)
	if err != nil {
		fmt.Println("conn read err =", err)
		return
	}

	fileName := string(buf[:readSize])

	//回复"ok"
	_, err1 := conn.Write([]byte("ok"))
	if err1 != nil {
		return
	}

	//接收文件内容
	RecvFile(fileName, conn)
}

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

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

相关文章

【漏洞复现】天OA存在任意文件上传漏洞

漏洞描述 华天动力协同办公系统将先进的管理思想、管理模式和软件技术、网络技术相结合,为用户提供了低成本、高效能的协同办公和管理平台。睿智的管理者通过使用华天动力协同办公平台,在加强规范工作流程、强化团队执行、推动精细管理、促进营业增长等工作中取得了良好的成…

linux系统中驱动框架基本分析

大家好,今天分享一篇Linux驱动软件设计思想的文章。由于文章较长,可以先收藏后再慢慢看。 一、Linux驱动的软件架构 1.1 出发点 为适应多种体系架构的硬件,增强系统的可重用和跨平台能力。 1.2 分离思想 为达到一个驱动最好一行都不改就…

Spring全家桶相关注解总结

spring相关 Controller 【控制器】效验有效参数的合法性(相当于安检系统) Service 【服务】业务组装(客服中心) Repository 【数据持久层】实际业务处理(实际办理的业务) Component 【组件】工具类…

代码随想录 -- day42 -- 01背包问题、416. 分割等和子集

01背包问题 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大 416. 分割等和子集 思路: 前提条件: 我们要求的是让两…

力扣每日一题---207. 课程表

Problem: 207. 课程表 文章目录 解题方法复杂度Code 解题方法 y总的 Topsort 模板题 复杂度 时间复杂度: 添加时间复杂度, 示例: O ( n ) O(n) O(n) 空间复杂度: 添加空间复杂度, 示例: O ( n ) O(n) O(n) Code class Solution {int res 0; public…

【python自动化】playwright长截图切换标签页JS注入实战

前言 当前教程使用的playwright版本为1.37.0,selenium版本为3.141.0 官方文档:https://playwright.dev/python/docs/screenshots 本教程目录如下 文章目录 前言playwright各类截图源码阅读ElementHandle类下的截图Page类下的截图Locator类下的截图 Playwright快速…

JetBrains设置文件名格式

如题,在使用CLion创建C类时,希望创建的文件名符合Google编码规范。设置如下图所示: 创建的C类是PascalCase格式,对应的文件名是pascal-case格式。

例举onekey一键还原如何使用

onekey一键还原怎么使用呢​​​​​​​?随着数字化的发展,现在电脑已成为人们工作学习娱乐的必备工具,想要放心的使用电脑,不仅需要杀毒软件,还需要一款一键还原软件。接下来,我就教大家如何使用onekey一键还原。还…

word文档如何引用参考文献

参考 word文档如何引用参考文献 说明

华南理工大学811信号与系统考研分数线,招生人数,报考统计,考情分析,就业,真题,大纲,参考书,华工811

华南理工大学811信号与系统考研分数线,招生人数,报考统计,考情分析,就业,真题,大纲,参考书,华工811 华南理工大学811信号与系统考研分数线,招生人数,报考统…

数据结构:线性表(栈的实现)

文章目录 1. 栈(Stack)1.1 栈的概念1.2 栈的结构链表栈数组栈 2. 栈的定义3. 栈的实现3.1 初始化栈 (StackInit)3.2 入栈 (StackPush)3.3 出栈 (StackPop)3.4 检测栈是否为空 (StackEmpty)3.5 获取栈顶元素 (StackTop)3.6 获取栈中有效元素个数 (StackSize)3.7 销毁栈 (StackDe…

Unity Animation、Animator 的使用

文章目录 1. 添加动画2. Animation2.1 制作界面2.2 制作好的 Animation 动画2.3 添加和使用事件 3. Animator3.1 制作界面3.2 一些参数解释3.3 动画参数 4. Animator中相关类、属性、API4.1 类4.2 属性4.3 API4.4 几个关键方法 5. 动画播放和暂停控制 1. 添加动画 选中待提添加…

2023 INCLUSION·外滩大会丨拓数派科技战略深度披露,大模型数据计算系统蓄势待发

近日,被亿欧网誉为最值得关注的全球化大模型数据计算科技新锐拓数派亮相在黄浦区世博园举行的2023 INCLUSION外滩大会。作为国际顶尖的科技盛会,来自全球各地的著名经济学家、诺奖得主、企业家和技术大咖们济济一堂,围绕“科技创造可持续未来…

聚观早报|小米14渲染图曝光;蚂蚁金融大模型正式发布

【聚观365】9月9日消息 小米14渲染图曝光 蚂蚁金融大模型正式发布 路特斯推出全电动轿车Emeya 上汽集团8月整车销量42.3万辆 ChatGPT网站流量连续三个月下滑 小米14渲染图曝光 高通旗下全新一代旗舰芯片骁龙8 Gen3将于10月24-26日举办的骁龙技术峰会上亮相,相…

安装SAPGUI 8.0

SAP_GUI_for_Windows_8.00_Comp.64\PRES1\GUI\Windows\Win64

windows11安装docker时,修改默认安装到C盘

1、修改默认安装到C盘 2、如果之前安装过docker,请删除如下目录:C:\Program Files\Docker 3、在D盘新建目录:D:\Program Files\Docker 4、winr,以管理员权限运行cmd 5、在cmd中执行如下命令,建立软联接: m…

【Mysql】给查询记录增加序列号方法

在MySQL 8.0版本中,你可以使用ROW_NUMBER()函数来添加序号。以下是一个示例查询,演示如何添加序号: SELECT ROW_NUMBER() OVER (ORDER BY column_name) AS serial_number,column1, column2, ... FROMyour_table;请将column_name替换为你想要…

FPGA时序分析与约束(5)——时序路径

一、前言 在之前的文章中我们分别介绍了组合电路的时序,时序电路的时序和时钟的时序问题,我们也对于时序分析,时序约束和时序收敛几个基本概念进行了区分,在这篇文章中,我们将介绍时序约束相关的最后一部分基本概念&am…

【网络知识点】三次握手和四次挥手

文章目录 一、三次握手二、四次挥手 一、三次握手 三次握手的原理如下: 客户端向服务器发送一个SYN(同步)包,其中包含一个随机生成的初始序列号(ISN)。 服务器收到SYN包后,会发送一个SYNACK&…

SpringBoot配置文件加载顺序

Spring Boot 启动时,会自动加载 JAR 包内部及 JAR 包所在目录指定位置的配置文件(Properties 文件、YAML 文件),下图中展示了 Spring Boot 自动加载的配置文件的位置及其加载顺序,同一位置下,Properties 文…