Golang学习笔记-大魂师篇
十三、socket编程
13.1、OSI分层
OSI七层模型
- 物理层:负责传递双绞线、光纤、无线电波等方式。负责传播0和1的电信号
- 数据链路层:多少个电信号(0和1)算一组,早期每个公司都有自己的电信号分组方式。后来"Ethernet"的协议,占据了主导地位。一组电信号构成一个数据包。每一帧分成两个部分:标头(Head)和数据(Data)。其中”标头”包含数据包的一些说明项,比如发送者、接受者、数据类型等等;”数据”则是数据包的具体内容。”标头”的长度,固定为18字节。”数据”的长度,最短为46字节,最长为1500字节。因此,整个”帧”最短为64字节,最长为1518字节。如果数据很长,就必须分割成多个帧进行发送。
那么,发送者和接受者是如何标识呢?以太网规定,连入网络的所有设备都必须具有”网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做MAC地址。每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用12个十六进制数表示。前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就可以定位网卡和数据包的路径了。
我们会通过ARP协议来获取接受方的MAC地址,有了MAC地址之后,如何把数据准确的发送给接收方呢?其实这里以太网采用了一种很”原始”的方式,它不是把数据包准确送到接收方,而是向本网络内所有计算机都发送,让每台计算机读取这个包的”标头”,找到接收方的MAC地址,然后与自身的MAC地址相比较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做”广播”(broadcasting)。
- 网络层:
按照以太网协议的规则我们可以依靠MAC地址来向外发送数据。理论上依靠MAC地址,你电脑的网卡就可以找到身在世界另一个角落的某台电脑的网卡了,但是这种做法有一个重大缺陷就是以太网采用广播方式发送数据包,所有成员人手一”包”,不仅效率低,而且发送的数据只能局限在发送者所在的子网络。也就是说如果两台计算机不在同一个子网络,广播是传不过去的。
“网络层”出现以后,每台计算机有了两种地址,一种是MAC地址,另一种是网络地址。两种地址之间没有任何联系,MAC地址是绑定在网卡上的,网络地址则是网络管理员分配的。网络地址帮助我们确定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理MAC地址。
规定网络地址的协议,叫做IP协议。它所定义的地址,就被称为IP地址。目前,广泛采用的是IP协议第四版,简称IPv4。IPv4这个版本规定,网络地址由32个二进制位组成,我们通常习惯用分成四段的十进制数表示IP地址,从0.0.0.0一直到255.255.255.255。
根据IP协议发送的数据,就叫做IP数据包。IP数据包也分为”标头”和”数据”两个部分:”标头”部分主要包括版本、长度、IP地址等信息,”数据”部分则是IP数据包的具体内容。IP数据包的”标头”部分的长度为20到60字节,整个数据包的总长度最大为65535字节。
- 传输层:
有了MAC地址和IP地址,我们已经可以在互联网上任意两台主机上建立通信。但问题是同一台主机上会有许多程序都需要用网络收发数据,比如QQ和浏览器这两个程序都需要连接互联网并收发数据,我们如何区分某个数据包到底是归哪个程序的呢?也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做”端口”(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。0-1023,1024-65535
UDP数据包非常简单,”标头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。
UDP协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否收到。为了解决这个问题,提高网络可靠性,TCP协议就诞生了。TCP协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
- 应用层
应用程序收到”传输层”的数据,接下来就要对数据进行解包。由于互联网是开放架构,数据来源五花八门,必须事先规定好通信的数据格式,否则接收方根本无法获得真正发送的数据内容。”应用层”的作用就是规定应用程序使用的数据格式,例如我们TCP协议之上常见的Email、HTTP、FTP等协议,这些协议就组成了互联网协议的应用层。
13.2、Socket
Socket是BSD UNIX的进程通信机制,通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄。Socket可以理解为TCP/IP网络的API,它定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。电脑上运行的应用程序通常通过”套接字”向网络发出请求或者应答网络请求。
Socket
是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket
其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket
后面,对用户来说只需要调用Socket规定的相关函数,让Socket
去组织符合指定的协议数据然后进行通信。
13.3、C/S 通信模型
网络应用程序涉及模式:
- C/S: 需要在通讯两端各自部署客户机和服务器来完成数据通信。
- B/S: 只需在一端部署服务器,而另外一端使用每台 PC 都默认配置的浏览器即可完成数据的传输。
13.3.1、TCP服务端
一个goroutine启动一个goroutine,一个goroutine内部一直循环和client交互
TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题
一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。
TCP服务端程序的处理流程:
- 监听端口
- 接收客户端请求建立链接
- 创建goroutine处理链接。
package main
import (
"net"
"fmt"
)
func processConn(conn net.Conn){
// 3、与客户端进行通信
var tmp [128]byte
for {
n, err := conn.Read(tmp[:]) //读取了多少个 byte。流式数据,一次从里面读取(n)多少byte
if err != nil {
fmt.Println("read失败 ", err)
return
}
fmt.Println(string(tmp[:n])) //注意这里
writeBack = []byte("Server端已收到")
conn.Write(writeBack) //回复已经收到
}
}
func main(){
//1、本地监听端口服务
listener,err := net.Listen("tcp","127.0.0.1:2000")
if err !=nil {
fmt.Println("监听地址失败 ",err)
return
}
// 2、等待别人来通信,一个连接建立一个goroutine 进行处理
for {
conn, err := listener.Accept() //等待下一个连接
if err != nil {
fmt.Println("接收通信失败:", err)
return
}
go processConn(conn)
}
}
13.3.2、TCP客户端
- 建立与服务端的链接
- 进行数据收发
- 关闭链接
package main
import (
"net"
"fmt"
"bufio"
"os"
"strings"
)
func main(){
//1、与服务端建立连接
conn,err := net.Dial("tcp","127.0.0.1:2000")
if err !=nil {
fmt.Println("和服务端建立连接失败",err)
}
// 2、发送数据
var msg string
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("请输入:")
input,_ = reader.ReadString('\n')
msg = strings.ToUpper(input)
if msg == "EXIT\r\n" { //注意Read.ReadString的读取内容后面带了 \r\n 这样才能匹配
break
}
fmt.Println("msg is:",msg)
_,err := conn.Write([]byte(msg))
if err !=nil {
fmt.Println("发送数据失败",err)
break
}
}
//3、关闭连接
conn.Close()
}
13.4、文件传输小工具
D:\Program_language\PRD\PG1\src>main.exe -h 用法
Usage of main.exe:
-d string
目录名 (default "/opt/")
-f string
文件名 (default "/opt/a.file")
-m string
工作模式server还是client (default "server|client")
-s string
服务端地址 (default "127.0.0.1:2000")
server模式会一直在前台运行,client可以向同一个server发送多次文件,路径和文件名不能包含中文
-----------------------> 代码内容
package main
import (
"flag"
"fmt"
"io"
"net"
"os"
"regexp"
"strings"
)
func main() {
//1、参数解析
var fileName, workMode, serverAddr, dirName string
flag.StringVar(&fileName, "f", "/opt/a.file", "文件名")
flag.StringVar(&dirName, "d", "/opt/", "目录名")
flag.StringVar(&workMode, "m", "server|client", "工作模式server还是client")
flag.StringVar(&serverAddr, "s", "127.0.0.1:2000", "服务端地址")
flag.Parse() //解析参数
r, err := regexp.MatchString(`([0-9]{1,3}\.)([0-9]{1,3}\.)([0-9]{1,3}\.)([0-9]{1,3}):(\d){2,5}`, serverAddr)
if r != true || err != nil {
fmt.Println("地址错误:", r)
os.Exit(126)
}
if workMode == "server" {
//目录判断,暂时不做
fmt.Printf("参数解析完毕: 保存的目录为:%s 监听地址:%s\n", dirName, serverAddr)
err = server(serverAddr, dirName)
if err != nil {
fmt.Println("something error: ", err)
os.Exit(126)
}
fmt.Println("服务端正常退出")
} else if workMode == "client" {
f, err := os.Open(fileName)
if err != nil {
fmt.Println("文件打开异常,错误信息为: ", err)
os.Exit(127)
}
f.Close()
fmt.Printf("参数解析完毕: 要传递的文件:%s 接收端地址:%s\n", fileName, serverAddr)
err = client(serverAddr, fileName)
if err != nil {
fmt.Println("客户端错误:", err)
os.Exit(125)
}
fmt.Println("客户端正常退出")
} else {
fmt.Println("必须选择一种工作模式: server或者client")
os.Exit(128)
}
}
func server(serverAddr string, dirName string) (err error) {
//1、监听端口
listerer, err := net.Listen("tcp", serverAddr)
if err != nil {
fmt.Println("端口监听失败")
return err
}
//2、创建channel同步完成情况
//3、异步处理数据
for {
conn, err := listerer.Accept()
defer listerer.Close()
if err != nil {
fmt.Println("accept Error", err)
break
}
//先读取fileName
tmp := make([]byte, 1024)
n, err := conn.Read(tmp[:])
if err != nil {
fmt.Println("读取文件名 error", err) //如果读取错误,注意返回
break
}
fName := dirName + "/" + string(tmp[:n])
//先读取fileName
go processConn(conn, fName)
}
return
}
func processConn(conn net.Conn, fName string) {
fileObj, err := os.OpenFile(fName, os.O_CREATE|os.O_RDWR, os.ModePerm)
if err != nil {
fmt.Println("文件打开错误: ", err)
return
}
defer fileObj.Close()
tmp := make([]byte, 4096)
for {
n, err := conn.Read(tmp[:])
if err != nil {
fmt.Println("Read error", err) //如果读取错误,注意返回
return
}
fmt.Printf("已经读读取了: %d\n", n)
_, err = fileObj.Write(tmp[:n])
if err != nil {
fmt.Println("写入失败")
break
}
defer conn.Close()
}
}
func client(serverAddr string, fileName string) (err error) {
//1、创建和关闭连接
conn, err := net.Dial("tcp", serverAddr)
if err != nil {
fmt.Println("连接打开失败", err)
return
}
defer conn.Close()
//2、打开文件
fileObj, err := os.Open(fileName)
if err != nil {
fmt.Println("Open error")
return err
}
defer fileObj.Close()
//3、传输文件
data := make([]byte, 4096)
//先传递文件名
n := strings.LastIndex(fileName, "/") //提取文件名
fName := fileName[n+1:]
_, err = conn.Write([]byte(fName))
if err != nil {
fmt.Println("写入失败fName", err)
return err
}
fileObj.Seek(0, io.SeekStart) //从头开始读
for {
n, err := fileObj.Read(data)
if err == io.EOF || n == 0 {
fmt.Println("文件读取完毕")
break
}
if err != nil {
fmt.Println("Read失败: ", err)
return err
}
_, err = conn.Write(data[:n])
if err != nil {
fmt.Println("写入失败", err)
return err
}
}
return nil //到最后了说明正常读取完毕了,传递内容为空
}
13.4、黏包
13.4.1、粘包示例
- server端
package main
import (
"net"
"fmt"
"io"
)
func process(conn net.Conn){
defer conn.Close() //3、处理完毕请求,注意关闭
var msg [1024]byte
for {
n,err := conn.Read(msg[:]) //4、读取内容,一次读取124字节
if err == io.EOF { //5、这里在client 主动关闭连接的时候才会匹配到,如果客户端没有主动关闭连接,这里也没有匹配 io.EOF 会出现打印的内容缺失几条
break
}
if err != nil {
fmt.Println("读取错误",err)
return
}
fmt.Println("收到数据为:",string(msg[:n]))
}
}
//三类资源: listener(打开监听端口后) conn(listener,Accept())后,
func main(){
listener,err := net.Listen("tcp","127.0.0.1:20000") //1、监听端口,注意关闭
if err != nil {
fmt.Println("监听失败,",err)
return
}
defer listener.Close()
for {
conn, err := listener.Accept() //2、接收新的链接
if err != nil {
fmt.Println("接收连接失败",err)
}
go process(conn)
}
}
- client端
package main
import (
"net"
"fmt"
)
func main(){
conn,err := net.Dial("tcp","127.0.0.1:20000") //1、打开tcp连接
if err != nil {
fmt.Println("打开连接失败,",err)
return
}
defer conn.Close() //2、开启连接后注意关闭 //如果客户端没有主动关闭连接,这里也没有匹配 io.EOF 会出现打印的内容缺失几条
info := []byte("hello world !")
for i:=0;i<20;i++ {
//time.Sleep(500*time.Millisecond) 加上延迟这样也可以实现 server端逐行输出的效果
conn.Write(info)
}
}
- 输出结果
收到数据为: hello world !hello world !hello world !hello world ! 收到数据为: hello world !hello world !hello world !hello world !hello world !hello world !hello world ! 收到数据为: hello world !hello world !hello world !hello world ! 收到数据为: hello world !hello world !hello world !hello world !hello world !
客户端分10次发送的数据,在服务端并没有成功的输出10次,而是多条数据“粘”到了一起。我们期望的是,一条数据只返回一次,每个hello world对应一行,而不是多条黏到一起成为一行。如果接收区满的情况下,还有可能会出现 半截数据。
- 粘包原因
主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。
“粘包”可发生在发送端也可发生在接收端:
- 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
- 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
13.4.2、粘包处理方法1
粘包解决方法:
- 客户端发送一次就断开连接,需要发送数据的时候再次连接,典型如http
- 包头+数据的格式,根据包头信息读取到需要分析的数据
方法1:客户端发送一次断开一次
客户端代码修改为:
func main() {
info := []byte("hello world !")
for i := 0; i < 20; i++ { //在循环内打开和关闭连接
conn, err := net.Dial("tcp", "127.0.0.1:20000") //1、打开tcp连接
if err != nil {
fmt.Println("打开连接失败,", err)
return
}
//time.Sleep(500*time.Millisecond) 加上延迟这样也可以实现 server端逐行输出的效果
conn.Write(info)
conn.Close()
}
}
13.4.3、buffer(后续处理)
出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。其实就是协议,协议定义了包头长度,数据大小等信息。只不过这里不像tcp协议那么完整
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
- 大端小端(高位在前还是高位在后)
1)bufio包
三个对象:
Reader
Writer
ReadWriter
方法:
reader.read*
writer.write*
2)bytes包
三个对象:
bytes.Reader
bytes.buffer:
bytes:newbuffer: func NewBuffer(buf []byte) *Buffer
方法:
Reader.Read*
buffer.Read*
buffer.Write*
1、创建缓冲区
- bytes.buffer
bytes.buffer是一个缓冲byte类型的缓冲器存放着都是byte,初始化 b1 := new(bytes.Buffer) //直接使用 new 初始化,可以直接使用
Buffer 就像一个集装箱容器,可以存东西,取东西(存取数据)
创建 一个 Buffer (其实底层就是一个 []byte, 字节切片)
向其中写入数据 (Write mtheods)
从其中读取数据 (Readmethods)
- bytes:newbuffer
NewBuffer使用buf作为参数初始化Buffer bytesBuffer := bytes.NewBuffer([]byte{})
func main() {
buf1 := bytes.NewBufferString("hello")
buf2 := bytes.NewBuffer([]byte("hello"))
buf3 := bytes.NewBuffer([]byte{'h', 'e', 'l', 'l', 'o'})
// 以上三者等效,输出//hello
buf4 := bytes.NewBufferString("")
buf5 := bytes.NewBuffer([]byte{})
// 以上两者等效,输出//""
fmt.Println(buf1.String(), buf2.String(), buf3.String(), buf4, buf5, 1)
}
2、写入到缓冲区
- bufio.write: func Write(w io.Writer, order ByteOrder, data interface{}) error
参数列表:
1)w 可写入字节流的数据
2)order 特殊字节序,包中提供大端字节序和小端字节序
3)data 需要解码的数据
返回值:error 返回错误
功能说明:
Write将data序列化成字节流写入w中。data必须是固定长度的数据值或固定长数据的slice,或指向此类数据的指针。写入w的字节流可用特殊的字节序来编码。另外,结构体中的(_)名的字段讲忽略。
err := binary.Write(pkg, binary.LittleEndian, length) 将lenght (4字节)的数据写入到pkg中
- bufio.read: func Read(r io.Reader, order ByteOrder, data interface{}) error
1)r 可以读出字节流的数据源
2)order 特殊字节序,包中提供大端字节序和小端字节序
3)data 需要解码成的数据
返回值:error 返回错误
功能说明:Read从r中读出字节数据并反序列化成结构数据。data必须是固定长的数据值或固定长数据的slice。从r中读出的数据可以使用特殊的 字节序来解码,并顺序写入value的字段。当填充结构体时,使用(_)名的字段讲被跳过。
err := binary.Read(lengthBuff, binary.LittleEndian, &length)
从 lengthBuff 中读出字节数据并反序列化
13.4.5、粘包处理方法2
方法2:读取包头数据格式进行分析
13.5、udp
UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。
- 服务端
package main
import (
"net"
"fmt"
"strings"
)
func main(){
udpconn,err := net.ListenUDP("udp",&net.UDPAddr{ //1、server监听地址
IP:net.IPv4(127,0,0,1), //net.IPv4 返回一个IP类型的变量
Port:30000,
})
if err != nil {
fmt.Println("listen udp failed.",err)
return
}
//2、注意及时关闭
defer udpconn.Close()
//不需要建立连接,直接通信
var data [1024]byte
for {
n,addr,err := udpconn.ReadFromUDP(data[:]) //3、server循环读取数据
//函数签名:func (c *UDPConn) ReadFrom(b []byte) (int, Addr, error) //int代表读取了多少个字节,Addr是对应的地址i,error错误信息
if err != nil {
fmt.Println("read from udp failed..",err)
return
}
fmt.Printf("收到数据:%v addr:%v count:%v\n",string(data[:n]),addr,n) //3、server打印读取到的数据
reply := strings.ToUpper(string(data[:n]))
//4、回复数据
udpconn.WriteToUDP([]byte(reply),addr)
//函数签名:func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
}
}
- 客户端
package main
import (
"net"
"fmt"
"bufio"
"os"
)
func main(){
//1、建立连接
conn,err := net.DialUDP("udp",nil,&net.UDPAddr{
IP:net.IPv4(127,0,0,1),
Port:30000,
}) //func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error) ,本地地址,远程地址
if err != nil {
fmt.Println("建立连接失败",err)
return
}
//2、建立连接后注意关闭连接
defer conn.Close()
var reply [1024]byte
reader := bufio.NewReader(os.Stdin)
//3、循环发送数据
for {
fmt.Print("请输入内容:")
msg,_ := reader.ReadString('\n')
conn.Write([]byte(msg)) //注意这里是Write而不是 WriteToUDP
//服务端已经获取到了client的addr,因此要使用WriteToUdp,而client已经和DialUDP了server,一次你可以直接使用 Write
n,remoteaddr,err := conn.ReadFromUDP(reply[:]) //4、收到的内容放到reply中
if err != nil {
fmt.Println("reply msg failed,err:",err)
}
fmt.Printf("收到回复信息:%v 远程地址:%v\n",string(reply[:n]),remoteaddr)
}
}
十四、http协议
1、http服务端
简单的web不再需要框架即可
HTTP协议:hypertext transfer Protocol 是互联网上应用最为广泛的一种网络传输协议,所有WWW文件都必须遵守这个标准。设计 HTTP最初的目的是为了提供一种发布和接收HTML页面的方法(裸体的人)
浏览器发起请求(request),服务端response。浏览器在收到response之后,按照HTML/CSS/JS的规则去渲染整个页面
HTML:超文件标记语言
CSS:层叠样式表,规定了HTTP中标签的具体样式(颜色\背景\大小\位置) //(让人穿上衣服,动起来)
JavaScript:一种跑在浏览器的编程语言(让人动起来)
package main
import (
"net/http"
"io/ioutil"
"fmt"
)
func f1(w http.ResponseWriter,r *http.Request){ //w为响应,r为请求的内容
// 1、直接输出字符
// str := "<h1>hello World</h1>"
//2、 直接输出文件
b,err := ioutil.ReadFile("./xx.txt")
if err != nil {
w.Write([]byte(fmt.Sprintf("%v",err)))
}
w.Write([]byte(b))
}
func main(){
http.HandleFunc("/apis/go",f1) //2、如果要访问/apsi/go 这个uri,使用f1函数处理
//func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
//因此函数要要实现该接口: func(ResponseWriter, *Request)
http.ListenAndServe("127.0.0.1:9000",nil) //1、监听并提供服务,接收2个参数,参数1:是要监听的地址,参数2是它的根页面使用的handler处理对象
}
<h1 style="color:red">Hello World</h1>
<a href="https://www.baidu.com"> 点我到百度 </a>
<meta name="referrer" content="never">
<img src="https://pic2.zhimg.com/80/117bb3a6db2be950f5cabb8c6bc80bd_720w.jpg" width="200px" height="200px" />
<button id="b1">点我</button>
<script>
document.getElementById("b1").onclick=function(){
alert("你好..")
}
</script>
2、Client服务端
package main
import (
"net/http"
"fmt"
"io/ioutil"
"net/url"
)
func main(){
//请求1:不使用urlencode
resp,err := http.Get("http://127.0.0.1:9000/query?name=小刚&Age=18")
//?前是url,后面是参数,参数为key=value格式
//对于GET请求,参数都放在URL上(query param)请求体中是没有的
defer resp.Body.Close() //注意:程序在使用完response后必须关闭回复的主体
if err != nil {
fmt.Println(err)
return
}
b,err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("ioutil读取失败,",err)
return
}
fmt.Println("body:",string(b))
//请求2,传递参数
apiUrl := "http://127.0.0.1:9000/query"
q1 := url.Values{}
q1.Set("name","小明")
q1.Set("age","22")
u, err := url.ParseRequestURI(apiUrl) //返回一个*URL的 结构体
if err != nil {
fmt.Printf("parse url requestUrl failed, err:%v\n", err)
}
u.RawQuery = q1.Encode() //RawQuery:encoded query values, q1.Encode 加密
//u这个*URL对象的RawQuery属性为 q1的内容,server需要响应的解码
fmt.Println(u.String())
resp,err = http.Get(u.String())
defer resp.Body.Close() //注意:程序在使用完response后必须关闭回复的主体
//请求3
data := url.Values{}
urlObj,_ := url.Parse("http://127.0.0.1:9000/query")
data.Set("name","小红")
data.Set("age","20")
queryStr := data.Encode() //编码后得到string
fmt.Println("queryStr:",queryStr)
urlObj.RawQuery = queryStr
req,err := http.NewRequest("get",urlObj.String(),nil)
//resp,_ := http.DefaultClient.Do(req)
//tr := &http.Transport{
// DisableKeepAlives:true,
//}
//适用于,请求不是特别频繁,用完就关闭
//client := http.Client{
// Transport:tr,
//}
resp,_ = client.Do(req)
defer resp.Body.Close()
}
package main
import (
"net/http"
"io/ioutil"
"fmt"
)
func f1(w http.ResponseWriter,r *http.Request){ //w为响应,r为请求的内容
// 1、直接输出字符
// str := "<h1>hello World</h1>"
//2、 直接输出文件
b,err := ioutil.ReadFile("./xx.txt")
if err != nil {
w.Write([]byte(fmt.Sprintf("%v",err)))
}
w.Write([]byte(b))
}
func f2(w http.ResponseWriter,r *http.Request) {
fmt.Printf("Method:%v \nURL:%v \n",r.Method,r.URL) //对于Get请求需要用下面的方法获取Method 本行输出:URL:/query?name=ss&Age=18
queryparam := r.URL.Query()
name := queryparam.Get("name")
age := queryparam.Get("Age")
fmt.Printf("name:%v age:%v\n",name,age) //name:ss age:18
fmt.Println(ioutil.ReadAll(r.Body)) //打印client的请求内容
w.Write([]byte("ok")) //返回client的请求
}
func main(){
http.HandleFunc("/apis/go",f1) //2、如果要访问/apsi/go 这个uri,使用f1函数处理
http.HandleFunc("/query",f2)
//func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
//因此函数要要实现该接口: func(ResponseWriter, *Request)
http.ListenAndServe("127.0.0.1:9000",nil) //1、监听并提供服务,接收2个参数,参数1:是要监听的地址,参数2是它的根页面使用的handler处理对象
}
十五、RPC
微服务后,微服务之间的调用。在开发中,通信协议,涉及到通信双方的数据传输问题。需要对数据进行序列化和反序列化操作。有很多我们选择的序列化方式,xml,json,gob格式(仅go语言)。还有protobuf 协议格式。基于效率的考虑,这里选择protobuf通信协议; 序列化的目的是把对象转换成byte流,反序列化是用于把字节流恢复成原有的对象。
15.1、RPC概述
1、本地调用
package main
import "fmt"
func main(){
a,b := 10,20
result := sum(a,b)
fmt.Println("a+b=",result)
}
func sum(a,b int)int{
return a+b
}
调用者和本调用都在一个代码中实现
2、远程过程调用RPC
远程过程调用,调用的是远端服务器上的程序或方法的整个过程
- 客户端发送数据,以字节流的方式发送
- 服务端接收,并解析,根据约定知道要执行什么。然后把结果返回客户端
- rpc就是把上面的过程进行封装。使其操作更加优化,使用大家都认可的协议使其规范化。协议本身就是一个约定,大家都遵循这个约定就可以。
RPC技术架构:
- 客户端:服务调用发起方,也可称之为消费者
- 服务端:远端计算机上运行的程序,其中包含客户端调用和访问的方法
- 客户端存根:存放服务端的地址,端口信息。将客户端的请求参数打包成网络消息,发送到服务方。接收服务方返回的数据包。这段程序运行在客户端;客户端存根的作用是把请求的参数以约定的通讯协议打包好发送给服务端然后解析服务端返回消息
- 服务端存根:接收客户端发送的数据包,解析数据包,调用具体的服务方法,将调用结果打包发送给客户端。
RPC用到的技术:
- proxy: 动态代理,客户端和服务端stub程序,在具体的编码和实现过程中,都是使用动态代理技术自动生成的一段
- protocol:序列化和反序列化,对象转换为byte,或者byte转换为对象
- 序列化:把对象转换为字节序列的过程,也就是编码的过程
- 反序列化:把字节流恢复为对象的过程,也就是解码的过程
我们常见的JSON,XML
等相关框架都可以对数据进行序列化和反序列化操作。Protobuf也是一种数据编码的协议。
15.2、官方RPC库
官方链接:https://golang.google.cn/pkg/net/rpc/
rpc调用有两个参与者,分别是:服务端
和客户端
。首先提供方法暴漏的是服务端
15.2.1、服务定义和暴露
在编程实现中,服务端需要注册结构体对象,然后通过对象所属的方法暴露给调用者,从而提供服务,该方法称之为输出方法,此输出方法可以被远程调用。当然在定义输出方法时,能够被远程调用的方法需要遵顼一定的规则。
func (t *T) MethodName(request T1,response *T2) error
go语言官方给出的对外暴露的服务和定义标准,其中包含了几个主要的规则,分别是:
- 对外暴漏的方法有且只能有两个参数,这两个参数只能是输出类型或内建类型,两种类型中的一种
- 方法的第二个参数必须是指针类型
- 方法的返回类型必须是error
- 方法的类型是可输出的
- 方法本身也是可输出的
type Mathut struct {
}
func (m Mathut) CalcSpace(req float32,result *float32) error {
*result = math.Pi * req * req //求圆面积
return nil
}
15.2.2、注册服务及监听请求
步骤:服务端:1)rpc.Register 或 rpc.RegisterName
;2) 可选rpc.HandlerHTTP()
默认rpc不注册在http;3)rpc.ServerConn 或 http.Serve 或 http.ListenAndServe
; 客户端:1) rpc.DialHTTP & client.Call()/client.Go()
或 rpc.Dial() 和client.Call()
>>> server.go
package main
import (
"fmt"
"math"
"net"
"net/http"
"net/rpc"
)
type Mathut struct {
}
func (m Mathut) CalcSpace(req float32,result *float32) error {
*result = math.Pi * req * req //求圆面积
return nil
}
func main(){
//1、注册服务对象,要先初始化服务对象为指针数据类型
mahtUtil := new(Mathut)
err := rpc.Register(mahtUtil)
if err != nil {
fmt.Println("注册失败")
panic(err.Error())
}
//2、该函数把服务注册到http协议上,方便调用这可以用http的方式调用
rpc.HandleHTTP()
//这个可以不要,相应的客户端就要变为rpc.Dial()而不是rpc.DialHTTP()
//3、监听
lsn,err := net.Listen("tcp",":1234")
if err != nil {
fmt.Println("监听失败")
panic(err.Error())
}
http.Serve(lsn,nil)
//如果没有注册在http协议上,这里应该为 conn,err := lsn.Accent(); rpc.ServerConn(conn)
/* 这里是只有一个goroutine处理请求,如果要一个请求对应一个goroutine 要调整
*/
}
>>>> client.go
package main
import (
"fmt"
"net/rpc"
)
func main(){
client,err := rpc.DialHTTP("tcp","127.0.0.1:1234")
if err != nil {
panic(err.Error())
}
req := float32(3) //请求的入参
var rst *float32 //返回值
//1、调用方式1-同步调用
//err = client.Call("MathGui.Calcspace",req,&rst)
//if err != nil {
// panic(err.Error())
//}
//fmt.Println("结果是:",*rst)
//2、调用方式2-异步调用
syncCall := client.Go("MathGui.Calcspace",req,&rst,nil)
//多出来一个参数是,一个通道。借助于通道变量获取响应的值
relayDone := <- syncCall.Done
fmt.Println(relayDone)
fmt.Println(*rst)
}
15.2.3、多参数调用和返回
用结构体即可
>>> server.go
package main
import (
"math"
"net"
"net/http"
"net/rpc"
)
type Param struct {
Arg1 int
Arg2 int
}
type MathGui struct {
}
func( m MathGui) CicuSpace(req float32,result *float32) error { //求圆面积
*result = math.Pi * req * req
return nil
}
func( m MathGui) CaltwoSum(req Param,sum *int) error { //求两数之和
*sum = req.Arg1 + req.Arg2
return nil
}
func main(){
//1、注册接口对象,要先
mathNew := new(MathGui)
//err := rpc.Register(mathNew)
err := rpc.RegisterName("com.mt",mathNew) //新的注册方式,第一个是别名
if err != nil {
panic(err.Error())
}
//提供http协议的服务
rpc.HandleHTTP()
//3、监听
lsn ,err := net.Listen("tcp",":1234")
if err != nil {
panic(err.Error())
}
http.Serve(lsn,nil)
}
>>> client.go
package main
import (
"fmt"
"net/rpc"
)
type Param struct {
Arg1 int
Arg2 int
}
func main(){
client,err := rpc.DialHTTP("tcp","127.0.0.1:1234")
if err != nil {
panic(err.Error())
}
req := float32(3) //请求的入参
var rst *float32 //返回值
//1、调用方式1-同步调用
err = client.Call("com.mt.CicuSpace",req,&rst)
if err != nil {
panic(err.Error())
}
fmt.Println("圆面积是:",*rst)
//2、调用方式2-异步调用
var sum *int
req2 := Param{
Arg1: 10,
Arg2: 20,
}
syncCall := client.Go("com.mt.CaltwoSum",req2,&sum,nil)
//多出来一个参数是,一个通道。借助于通道变量获取响应的值
relayDone := <- syncCall.Done
fmt.Println(relayDone)
fmt.Println("两数之和是:",*sum)
}
15.3、RPC和Protobuf结合
Protobuf见 十六章节
需求,一个订单系统,有订单模块。其他模块想要实现RPC的远程工程调用。根据订单ID
和时间戳
可以获取订单信息。如果获取成功就返回相应的订单信息。如果查询不到就返回失败信息。
15.3.1、传输数据格式定义
我们需要先定义客户端和服务端传递数据的格式和方法
syntax = "proto3";
package message;
option go_package = "./message";
message OrderRequest {
string orderID = 1;
int64 timeStamp = 2;
}
message OrderInfo {
string OrderId = 1;
string OrderName = 2;
string OrderStatus = 3;
}
protoc ./message.proto --go_out=./ #编译
-
错误记录1:goland 报错: "Unresolved type"
- 尝试1:goland->文件->invalidate Cachses/Restart 不行
- 尝试2:$GOPATH/ 执行
go mod init src
;再次运行报错$GOPATH/go.mod exists but should not
原因分析:gomod 和 gopath 两个包管理方案,并且相互不兼容,在 gopath 查找包,按照 goroot 和多 gopath 目录下 src/xxx 依次查找。在 gomod 下查找包,解析 go.mod 文件查找包,mod 包名就是包的前缀,里面的目录就后续路径了。在 gomod 模式下,查找包就不会去 gopath 查找,只是 gomod 包缓存在 gopath/pkg/mod 里面。这里
go mod init message
生成go.mod为message包的go.mod就可以索引到了。
15.3.2、服务端
package main
import (
"errors"
"fmt"
msg "message/message"
net2 "net"
"net/http"
"net/rpc"
"time"
)
//1、定义注册对象和方法
type OrderStruct struct {
}
//rpc函数格式要求,第二个参数必须是指针
func (o OrderStruct) GetOrderinfo(req msg.OrderRequest,resp *msg.OrderInfo) (err error ){
//1、创建字典模拟数据库
orderMap := map[string]msg.OrderInfo{
"202109092000": msg.OrderInfo{OrderId:"202109092000",OrderName:"衣服",OrderStatus:"已付款"},
"202109092001": msg.OrderInfo{OrderId:"202109092001",OrderName:"零食",OrderStatus:"未付款"},
"202109092002": msg.OrderInfo{OrderId:"202109092002",OrderName:"玩具",OrderStatus:"已付款"},
}
fmt.Println("请求参数是:",req)
current := time.Now().Unix()
if req.TimeStamp > current {
//判断1:时间戳对不对
return errors.New("时间错误")
} else if orderMap[req.OrderID].OrderId != req.OrderID { //注意这里:传递的是值,不需要使用 .GetOrderID()方法;.GetOrderID()为指针方法
//判断2:是否存在
fmt.Println("判断2")
//resp = &msg.OrderInfo{OrderId:"0",OrderStatus:"未知",OrderName:"未知"}
//指针的赋值方式为: 1)a *int 和*a是等价的;2) $指针 = 值 ,而不是 $指针 = $指针
*resp = msg.OrderInfo{OrderId:"0",OrderStatus:"订单未知",OrderName:"状态异常"}
//return nil
} else {
//判断3:正常返回
//resp = &orderMap[req.GetOrderID()] //不能用?
fmt.Println("判断3")
//resp = &msg.OrderInfo{OrderId:orderMap[req.OrderID].OrderId,OrderName:orderMap[req.OrderID].OrderName,OrderStatus:orderMap[req.OrderID].OrderStatus}
*resp = msg.OrderInfo{OrderId:orderMap[req.OrderID].OrderId,OrderName:orderMap[req.OrderID].OrderName,OrderStatus:orderMap[req.OrderID].OrderStatus}
//*resp = orderMap[req.OrderID] //这样也可以
fmt.Println("结果为:",*resp)
//return nil
}
return nil
}
func main(){
//1、注册接口,先初始化对象
interface1 := new(OrderStruct)
err := rpc.RegisterName("com.mt",interface1)
if err != nil {
panic(err.Error())
}
//2、提供服务在http
rpc.HandleHTTP()
//3、监听和提供服务
net,err := net2.Listen("tcp",":1234")
if err != nil {
panic(err.Error())
}
err = http.Serve(net,nil)
if err != nil {
panic(err.Error())
}
}
15.3.3、客户端
package main
import (
"fmt"
"message/message"
"net/rpc"
"time"
)
func main(){
client,err := rpc.DialHTTP("tcp","127.0.0.1:1234")
if err != nil {
panic(err.Error())
}
current := time.Now().Unix()
req := message.OrderRequest{OrderID:"202109092004",TimeStamp:current}
var rst *message.OrderInfo
//1、调用方式1-同步调用
err = client.Call("com.mt.GetOrderinfo",req,&rst)
if err != nil {
panic(err.Error())
}
fmt.Println(rst)
fmt.Println("客户端")
}
15.4、GRPC介绍
gRPC是Google公司开源的一款高性能的远程过程调用(RPC)框架,可以在任何环境下运行,该框架提供了负载均衡,跟踪,智能监控,身份验证等功能,可以实现系统间的高效连接。另外在分布式系统中,gRPC框架也有广泛分布,实现移动社会,浏览器和服务器的连接。http协议是重量级的rpc协议,grpc和rpc重在提供在tcp协议之上的相对轻量级的数据传输。主要特性是跨语言。官网: https://grpc.io/
- 代码存放:
https://github.com/grpc
- 支持的的语言:
Go,c,c++,java,PHP,Python,...
- 中文文档:
http://doc.oschina.net/grpc?t=58008
gRPC中默认采用protocol buffers
去描述和定义服务信息。
安装gRPC库
go get google.golang.org/grpc
如果装不上 git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
; 或者手动github上下载zip包master-grpc-go.zio
解压并重命名为grpc
,并存放到 $GOPATH/src/google.golang.org/grpc
下。注意,如果使用clone方式安装,因为grpc-go
依赖其他的外部库,也可以从githu下载,然后存放到对应目录。
示例代码:
https://github.com/grpc/grpc-go/tree/master/examples
这里对hello world做出介绍
目录结构和rpc类似
- helloworld
- greeter_client
- main.go
- greeter_server
- main.go
- helloworld
- helloworld.pb.go
- helloworld.proto
- helloworld_grpc.pb.go
- greeter_client
grpc允许定义服务的四种方法
- 简单调用:client调用server,server返回一个结果响应
rpc SayHello(HelloRequest) returns (HelloResponse);
- 服务端流式RPC:其中客户端向服务器发送请求,并获取一个流以读回一系列消息。客户端读取返回的流,直到不再有消息为止。gRPC保证单个RPC调用中的消息顺序。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
- 客户端流式RPC: 其中客户端写入一系列消息,并再次使用提供的流将它们发送到服务器。一旦客户机完成消息的编写,它将等待服务器读取消息并返回响应。同样,gRPC保证单个RPC调用中的消息顺序。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
- 双向流式RPC:其中双方使用读写流发送一系列消息。这两个流独立运行,因此客户机和服务器可以按照他们喜欢的顺序进行读写操作:例如,服务器可以在写入响应之前等待接收所有客户机消息,也可以交替地读取消息然后写入消息,或者其他读写组合。每个流中消息的顺序都被保留。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
grpc工作原理
从.proto
文件中的服务定义开始,gRPC提供生成客户端和服务器端代码的protocol buffers
编译器插件。gRPC用户通常在客户端调用这些API,并在服务器端实现相应的API。
- 服务端:实现服务声明的方法,并运行grpc服务器来处理客户端调用。gRPC内部实现对请求内容进行解码,执行服务方法。并对服务端响应进行编码
- 客户端:客户端有
stub
的对象,实现了服务端相同的方法。客户端可以像在本地调用这些方法一样,将调用的参数包装在protocol buffers messages
中。gRPC负责将请求发送到server并接收服务端响应
RPC生命周期
- Unray RPC:
- client一旦调用
stub
方法,服务端将收到通知,内容包含(客户端元数据、方法名称、指定的截 止时间[可选]) - 服务端可以直接返回自己的初始元数据(必须在任何响应之前),或者等待客户端的请求消息。具体看应用定义
- 服务端接收到了客户端的请求,将开始构造响应信息(包含状态信息、可选状态信息,以及尾部元数据) 然后返回给客户端
- 如果状态码为ok,则client获得响应,从而完成client的调用
- client一旦调用
- Server streaming RPC
- 类似于
Unray RPC
只是服务端返回消息流以响应客户端的请求。发送所有消息后,服务端的状态详细信息(状态代码和可选状态消息)以及可选的尾部元数据将发送到客户端。这就完成了服务器端的处理。客户机在拥有所有服务器消息后完成。类似于pull
- 类似于
- Client streaming RPC
- 类似于
Unray RPC
除了客户端向服务端发送消息流而不是单个消息。服务端通常(但不一定)在接收到所有客户端消息后,以单个消息(以及其状态详细信息和可选的尾部元数据)进行响应,类似于push
- 类似于
- Bidirectional streaming RPC
- 调用由调用方法的客户端和接收客户端元数据、方法名称和截止日期的服务器发起。服务器可以选择发回其初始元数据或等待客户端启动流式消息。
- 客户端和服务器端流处理是特定于应用程序的。由于这两个流是独立的,因此客户端和服务器可以按任意顺序读取和写入消息。例如,服务器可以等到收到客户端的所有消息后再编写消息,或者服务器和客户端可以玩“乒乓球”——服务器收到请求,然后发回响应,然后客户端根据响应发送另一个请求,依此类推
gRPC其他概念
- Deadlines/Timeout: grcp允许客户端设置
DEADLINE_EXCEEDED
去等待超时时间。服务端月可以查询特定rpc是否已经超时,或者还有多少时间超时。指定截止日期或超时是特定于语言的:一些语言API根据超时(持续时间)工作,而一些语言API根据截止日期(固定时间点)工作,可能有也可能没有默认截止日期。 - RPC终止: 在gRPC中,客户端和服务器都对呼叫的成功进行独立和本地的确定,并且它们的结论可能不匹配。这意味着,例如,您可能有一个RPC在服务器端成功完成(“我已发送所有响应!”),但在客户端失败(“响应在我的截止日期之后到达!”)。服务器也可能在客户端发送其所有请求之前决定完成。
- RPC取消:客户端或服务器可以随时取消RPC。取消操作会立即终止RPC,以便不再进行进一步的工作。注意:取消前所做的更改不会回滚。
- 元数据(metadata:): 元数据是关于特定RPC调用的信息(例如身份验证详细信息),其形式为密钥-值对列表,其中密钥是字符串,值通常是字符串,但可以是二进制数据。元数据对gRPC本身是不透明的——它允许客户端向服务器提供与调用相关的信息,反之亦然。对于元数据的访问取决于语言
- Channels:gRPC通道提供到指定主机和端口上gRPC服务器的连接。它在创建客户端存根时使用。客户端可以指定通道参数来修改gRPC的默认行为,例如打开或关闭消息压缩。通道具有状态,包括已连接和空闲。gRPC如何关闭频道取决于语言。某些语言还允许查询通道状态。
15.5、GRPC实现
15.5.1、Proto定义
安装插件:go get -a github.com/golang/protobuf/protoc-gen-go
基本用法:protoc --go_out=. *.proto
gRPC编译:protoc --go_out=plugins=grpc:. *.proto
如果想要使用gRPC框架实现RPC调用,需要使用protoctol-gen-go库中提供的插件功能,生成兼容gRPC框架的go语言代码。生成message/msg.pb.go
syntax = "proto3";
package message;
option go_package = "./";
message OrderRequest {
string orderID = 1;
int64 timeStamp = 2;
}
message OrderInfo {
string OrderId = 1;
string OrderName = 2;
string OrderStatus = 3;
}
//订单服务定义
service OrderService {
rpc GetOrderInfo(OrderRequest) returns (OrderInfo);
}
在.proto定义后,并生成相应的go语言文件后,需要对服务接口做具体实现。定义服务接口具体由OrderServicelmpi
进行实现,并实现GetOrderInfo详细内容,服务实现逻辑与前文所述内容相同。不同点是服务接口参数的变化。
15.5.2、服务端实现
服务端:1)生成grpcserver对象server := grpc.NewServer() ;2)注册grpc服务msg.Register${Proto定义的服务名}Server(grpcServer,new(OrderServicelmpi)) ;3 ) 提供服务server.Serve(listen)
客户端:1)连接grpcservergrpc.Dial()
;2)orderClient := msg.NewOrderServiceClient(client)
创建客户端对象;3)执行请求方法orderClient.GetOrderInfo(context.Background(),&req)
在生成的msg.pb.go
中有定义,服务端的实现接口
type OrderServiceServer interface {
GetOrderInfo(context.Context, *OrderRequest) (*OrderInfo, error)
}
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
msg "message/message"
"net"
"time"
)
//1、定义注册对象和方法
type OrderServicelmpi struct {
}
//rpc函数格式要求,第二个参数必须是指针
func (o *OrderServicelmpi) GetOrderInfo(ctx context.Context, req *msg.OrderRequest) (response *msg.OrderInfo,err error){
//1、创建字典模拟数据库
orderMap := map[string]msg.OrderInfo{
"202109092000": msg.OrderInfo{OrderId:"202109092000",OrderName:"衣服",OrderStatus:"已付款"},
"202109092001": msg.OrderInfo{OrderId:"202109092001",OrderName:"零食",OrderStatus:"未付款"},
"202109092002": msg.OrderInfo{OrderId:"202109092002",OrderName:"玩具",OrderStatus:"已付款"},
}
//var response *msg.OrderInfo
current := time.Now().Unix()
if req.TimeStamp > current {
//判断1:时间戳对不对
response = &msg.OrderInfo{OrderId:"0",OrderName:"",OrderStatus:"时间异常"}
} else if orderMap[req.OrderID].OrderId != req.OrderID { //注意这里:传递的是值,不需要使用 .GetOrderID()方法;.GetOrderID()为指针方法
//判断2:是否存在
fmt.Println("判断2")
//*response = msg.OrderInfo{OrderId:"0",OrderName:"",OrderStatus:"订单不存在"} //这样会导致空指针错误,response本身就是指针,再加上*就成了 ** msg.OrderInfo 二级指针
response = &msg.OrderInfo{OrderId:"0",OrderName:"",OrderStatus:"订单不存在"}
//*response = &msg.OrderInfo{OrderId:"0",OrderName:"",OrderStatus:"订单不存在"}
} else {
//判断3:正常返回
//resp = &orderMap[req.GetOrderID()] //不能用?
fmt.Println("判断3")
//注意:指针地址的使用方法;1)map中获取包含结构体,要提取结构体地址,必须使用中间变量。2)
//response = &(orderMap[req.OrderID]) //这样就不行,报错 canot take the address of orderMap[req.OrderID]
//result := orderMap[req.OrderID] //这样就可以
//response = &result
//如下也可以。
response = &msg.OrderInfo{OrderId:orderMap[req.OrderID].OrderId,OrderName:orderMap[req.OrderID].OrderName,OrderStatus:orderMap[req.OrderID].OrderStatus}
fmt.Println(response)
}
return response,nil
}
func main(){
//1、提供grpc服务
server := grpc.NewServer() //生成grpc server对象
msg.RegisterOrderServiceServer(server,new(OrderServicelmpi)) //注册服务对象
//2、监听和提供服务
listen,err := net.Listen("tcp",":1234")
if err != nil {
panic(err.Error())
}
err = server.Serve(listen) //grpc对象和监听端口 绑定;grpc对象在 listen的端口上提供服务
if err != nil {
panic(err.Error())
}
}
或者
func (o *OrderServicelmpi) GetOrderInfo(ctx context.Context, req *msg.OrderRequest) (response *msg.OrderInfo,err error){
//1、创建字典模拟数据库
orderMap := map[string]msg.OrderInfo{
"202109092000": msg.OrderInfo{OrderId:"202109092000",OrderName:"衣服",OrderStatus:"已付款"},
"202109092001": msg.OrderInfo{OrderId:"202109092001",OrderName:"零食",OrderStatus:"未付款"},
"202109092002": msg.OrderInfo{OrderId:"202109092002",OrderName:"玩具",OrderStatus:"已付款"},
}
//var response *msg.OrderInfo
current := time.Now().Unix()
if req.TimeStamp > current {
//判断1:时间戳对不对
response = &msg.OrderInfo{OrderId:"0",OrderName:"",OrderStatus:"时间异常"}
} else {
result := orderMap[req.OrderID]
if result.OrderId != "" {
fmt.Println(result)
return &result,nil
} else {
return nil,errors.New("server error")
}
}
return response,nil
}
15.5.3、客户端实现
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
msg "message/message"
)
func main(){
//1、创建连接
client,err := grpc.Dial("127.0.0.1:1234",grpc.WithInsecure()) //这里不是https,因此使用 gprc.WithInsecure
if err != nil {
panic(err.Error())
}
defer client.Close()
//2、创建客户端对象
orderClient := msg.NewOrderServiceClient(client)
//req := msg.OrderRequest{OrderID:"202109092001",TimeStamp:time.Now().Unix()}
req := msg.OrderRequest{OrderID:"202109092001",TimeStamp:1632238945}
order,err := orderClient.GetOrderInfo(context.Background(),&req)
if err != nil {
panic(err.Error())
}
fmt.Println("订单信息为:",order)
}
15.5.4、其他
注意:发现如下坑
- 在go自带的rpc框架中 对应返回值
resp *msg.OrderInfo
;- 函数签名:
func (o *OrderStruct) GetOrderinfo(req msg.OrderRequest,resp *msg.OrderInfo) (err error )
- 1)resp的赋值:
*resp = msg.OrderInfo{...}
或者*resp = orderMap[req.OrderID]
都可以获取到值 - 2)使用
resp = &msg.OrderInfo{OrderId:orderMap[req.OrderID].OrderId
和tmp := orderMap[req.OrderID] ;resp = &tmp
不报错语法错误,但是客户端调用返回结果为空 - 3)判断语句:
orderMap[req.OrderID].OrderId != req.OrderID
- 函数签名:
- 在gRPC框架中:
response *msg.OrderInfo
(重要),默认指针变量应该用&赋值才对- 函数签名:
func (o *OrderServicelmpi) GetOrderInfo(ctx context.Context, req *msg.OrderRequest) (response *msg.OrderInfo,err error)
-
- 使用
*response = msg.OrderInfo{OrderId:"0"...
会报空指针错误
- 使用
-
- 使用
response = &msg.OrderInfo{OrderId:orderMap[req.OrderID].OrderId,...
可以
- 使用
-
- 使用
result := orderMap[req.OrderID] ; response = &result
也 可以;但是使用response = &(orderMap[req.OrderID])
//这样就不行,报错 canot take the address of orderMap[req.OrderID]
- 使用
- 4)判断语句:
orderMap[req.OrderID].OrderId != req.OrderID
- 函数签名:
15.6、客户端与服务端流模式
简单模式:client调用server,server返回一个结果响应rpc SayHello(HelloRequest) returns (HelloResponse);
15.6.1、服务端流模式
其中客户端向服务器发送请求,并获取一个流以读回一系列消息。客户端读取返回的流,直到不再有消息为止。服务端在发送完所有的消息后,服务端的状态详情和可选的元数据发送给客户端。gRPC保证单个RPC调用中的消息顺序。 rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
- 1、服务定义
章节 15.5.1
的 Proto中的Service定义改为如下:
...
service OrderService {
rpc GetOrderInfos (OrderRequest) returns (stream OrderInfo) {};
}
接口返回使用了stream
关键字进行修饰,表示该接口调用时,服务端会以数据流的形式将数据返回给客户端
重新编译: protoc --go_out=plugins=grpc:. message.proto
流模式下的数据发送和接收,使用新的方法完成。在自动生成的go代码程序中。每一个流模式对应的服务接口,都会自动生成对应的client和server程序,以及对应的数据结构体实现。
服务端代码自动生成:
type OrderService_GetOrderInfosServer interface {
Send(*OrderInfo) error
grpc.ServerStream
}
type orderServiceGetOrderInfosServer struct {
grpc.ServerStream
}
func (x *orderServiceGetOrderInfosServer) Send(m *OrderInfo) error {
return x.ServerStream.SendMsg(m)
}
流模式下,服务接口的服务提供Send方法。将数据以流的形式进行发送
客户端自动生成代码;
type OrderService_GetOrderInfosClient interface {
Recv() (*OrderInfo, error)
grpc.ClientStream
}
type orderServiceGetOrderInfosClient struct {
grpc.ClientStream
}
func (x *orderServiceGetOrderInfosClient) Recv() (*OrderInfo, error) {
m := new(OrderInfo)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
流模式下,服务接口的客户端提供Recv()方法接收服务端发送的流数据
服务定义文件内容解析:
OrderRequest对象方法
GetOrderID()
GetTimeStamp()
OrderInfo对象方法:
GetOrderId()
GetOrderName()
GetOrderStatus()
GetOrderStatus()
客户端:定义了一个 OrderServiceClient 的struct和 interface{},并且结构体实现了GetOrderInfos的方法
接口方法签名为:
GetOrderInfos(ctx context.Context, in *OrderRequest, opts ...grpc.CallOption) (OrderService_GetOrderInfosClient, error)
OrderService_GetOrderInfosClient{} 也是一个接口:定义 orderServiceGetOrderInfosClient 实现了 该接口的方法 Recv()
服务端:
OrderServiceServer 有interface{} 和方法
方法签名:GetOrderInfos(*OrderRequest, OrderService_GetOrderInfosServer) error
OrderService_GetOrderInfosServer 也是一个struct 和方法,并实现了 Send 方法
服务定义:
var _OrderService_serviceDesc = grpc.ServiceDesc{
ServiceName: "message.OrderService",
HandlerType: (*OrderServiceServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "GetOrderInfos",
Handler: _OrderService_GetOrderInfos_Handler,
ServerStreams: true,
},
},
Metadata: "message.proto",
}
- 2、服务端编码实现
服务端要实现的接口:
type OrderServiceServer interface {
GetOrderInfos(*OrderRequest, OrderService_GetOrderInfosServer) error
}
package main
import (
"fmt"
"google.golang.org/grpc"
msg "message/message"
"net"
"time"
)
//1、定义注册对象和方法
type OrderServicelmpi struct {
}
//rpc函数格式要求,第二个参数必须是指针
func (o *OrderServicelmpi) GetOrderInfos(request *msg.OrderRequest,stream msg.OrderService_GetOrderInfosServer) error{
fmt.Println("服务端流RPC模式")
//1、创建字典模拟数据库
orderMap := map[string]msg.OrderInfo{
"202109092000": msg.OrderInfo{OrderId:"202109092000",OrderName:"衣服",OrderStatus:"已付款"},
"202109092001": msg.OrderInfo{OrderId:"202109092001",OrderName:"零食",OrderStatus:"未付款"},
"202109092002": msg.OrderInfo{OrderId:"202109092002",OrderName:"玩具",OrderStatus:"已付款"},
}
//var response *msg.OrderInfo
for id,info := range orderMap {
if time.Now().Unix() >= request.TimeStamp {
fmt.Println("订单序号id:",id)
fmt.Println("订单详情:",info)
err := stream.Send(&info)
if err != nil {
panic(err)
}
}
}
return nil
}
func main(){
//1、提供grpc服务
server := grpc.NewServer() //生成grpc server对象
msg.RegisterOrderServiceServer(server,new(OrderServicelmpi)) //注册服务对象
//2、监听和提供服务
lsn,err := net.Listen("tcp",":1234")
if err != nil {
panic(err.Error())
}
server.Serve(lsn) //grpc对象和监听端口 绑定;grpc对象在 listen的端口上提供服务
}
- 客户端实现
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"io"
msg "message/message"
"time"
)
func main(){
//1、创建连接
client,err := grpc.Dial("127.0.0.1:1234",grpc.WithInsecure()) //这里不是https,因此使用 gprc.WithInsecure
if err != nil {
panic(err.Error())
}
defer client.Close()
//2、创建客户端对象,执行客户端对对象的 具体方法
orderServiceClient := msg.NewOrderServiceClient(client)
request := msg.OrderRequest{TimeStamp: time.Now().Unix()} //不传递orderId,因为要接收所有的
orderInfoClient,err := orderServiceClient.GetOrderInfos(context.TODO(),&request)
if err != nil {
panic(err.Error())
}
for {
orderInfo,err := orderInfoClient.Recv()
if err == io.EOF {
fmt.Println("读取结束")
return
}
if err != nil {
panic(err.Error())
}
fmt.Println("订单信息为:",orderInfo)
}
}
15.6.2、客户端流模式
其中客户端写入一系列消息,并再次使用提供的流将它们发送到服务器。一旦客户机完成消息的编写,它将等待服务器读取消息并返回响应。同样,gRPC保证单个RPC调用中的消息顺序。rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
一般在客户端提交大量数据的时候使用
- 服务定义
...
service OrderService {
rpc AddOrderList (stream OrderRequest) returns (OrderInfo) {};
}
服务定义时,传递的参数改成了使用stream
修饰。相对应的 服务端流模式是返回的参数使用了stream
修饰
- 服务端代码
package main
import (
"fmt"
"google.golang.org/grpc"
"io"
msg "message/message"
"net"
)
type OrderIpml struct {
}
func (o *OrderIpml) AddOrderList(stream msg.OrderService_AddOrderListServer) (err error) {
fmt.Println("客户端流模式")
for {
request,err := stream.Recv() //1、调用 Recv方法接收数据
if err == io.EOF {
fmt.Println("文件读取完毕")
result := msg.OrderInfo{OrderStatus:"读取数据结束"}
return stream.SendAndClose(&result) //2、处理完毕后回复结束
}
if err != nil {
fmt.Println(err.Error())
return err
}
fmt.Printf("订单ID:%v\t订单状态:%v\t订单名称:%v\t\n",request.GetOrderId(),request.GetOrderStatus(),request.GetOrderName())
}
}
func main(){
//1、注册端口
server := grpc.NewServer()
msg.RegisterOrderServiceServer(server,new(OrderIpml))
//2、监听和提供服务
lsn,err := net.Listen("tcp",":1234")
if err != nil {
panic(err)
}
server.Serve(lsn)
}
- 客户端代码
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"io"
msg "message/message"
)
func main(){
//1、创建连接
conn,err := grpc.Dial("127.0.0.1:1234",grpc.WithInsecure()) //这里不是https,因此使用 gprc.WithInsecure
if err != nil {
panic(err.Error())
}
defer conn.Close()
orderMap := map[string]msg.OrderInfo{
"202109092000":{OrderId:"202109092000",OrderName:"衣服",OrderStatus:"已付款"},
"202109092001":{OrderId:"202109092001",OrderName:"零食",OrderStatus:"未付款"},
"202109092002":{OrderId:"202109092002",OrderName:"水果",OrderStatus:"付款中"},
}
//2、创建客户端对象,执行客户端对对象的 具体方法
addOrderClient := msg.NewOrderServiceClient(conn)
addOrderListClient,err := addOrderClient.AddOrderList(context.Background())
if err !=nil {
panic(err.Error())
}
for _,info := range orderMap {
err := addOrderListClient.Send(&info)
if err != nil {
panic(err.Error())
}
}
//3、接收服务端返回的信息
for {
orderInfo,err := addOrderListClient.CloseAndRecv()
if err == io.EOF {
fmt.Print("读取结束")
return
}
if err != nil {
fmt.Println(err.Error())
}
fmt.Println(orderInfo.GetOrderStatus())
}
}
15.6.3、双向流模式
其中双方使用读写流发送一系列消息。这两个流独立运行,因此客户机和服务器可以按照他们喜欢的顺序进行读写操作:例如,服务器可以在写入响应之前等待接收所有客户机消息,也可以交替地读取消息然后写入消息,或者其他读写组合。每个流中消息的顺序都被保留。rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
- 服务定义
service OrderService {
rpc GetOrderInfos (stream OrderRequest) returns (stream OrderInfo) {};
}
- 服务端定义
package main
import (
"fmt"
"google.golang.org/grpc"
"io"
msg "message/message"
"net"
)
type OrderIpml struct {
}
func (o *OrderIpml) GetOrderInfos(stream msg.OrderService_GetOrderInfosServer) (err error) {
orderMap := map[string]msg.OrderInfo{
"202109092000":{OrderId:"202109092000",OrderName:"衣服",OrderStatus:"已付款"},
"202109092001":{OrderId:"202109092001",OrderName:"零食",OrderStatus:"未付款"},
"202109092002":{OrderId:"202109092002",OrderName:"水果",OrderStatus:"付款中"},
}
fmt.Println("双向流模式")
for {
request,err := stream.Recv() //1、调用 Recv方法接收数据
if err == io.EOF {
fmt.Println("数据读取完毕")
return err
}
if err != nil {
panic(err.Error())
return err
}
fmt.Printf("请求的订单ID:%v\n",request.GetOrderID())
result := orderMap[request.GetOrderID()]
err = stream.Send(&result)
if err == io.EOF {
fmt.Println(err)
panic(err.Error())
}
if err != nil {
panic(err.Error())
return err
}
//fmt.Printf("订单ID:%v\t订单状态:%v\t订单名称:%v\t\n",request.GetOrderId(),request.GetOrderStatus(),request.GetOrderName())
}
}
func main(){
//1、注册端口
server := grpc.NewServer()
msg.RegisterOrderServiceServer(server,new(OrderIpml))
//2、监听和提供服务
lsn,err := net.Listen("tcp",":1234")
if err != nil {
panic(err)
}
server.Serve(lsn)
}
- 客户端
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"io"
msg "message/message"
)
func main(){
//1、创建连接
conn,err := grpc.Dial("127.0.0.1:1234",grpc.WithInsecure()) //这里不是https,因此使用 gprc.WithInsecure
if err != nil {
panic(err.Error())
}
defer conn.Close()
//2、创建客户端对象,执行客户端对对象的 方法,发送数据
OrderClient := msg.NewOrderServiceClient(conn)
fmt.Println("客户端双向流模式")
orderIDS := []string{"202109092000","202109092000","202109092000"}
addOrderListClient,err := OrderClient.GetOrderInfos(context.Background())
for _,orderId := range orderIDS {
reqOrder := msg.OrderRequest{OrderID:orderId}
err = addOrderListClient.Send(&reqOrder)
if err != nil {
panic(err.Error())
}
}
//关闭发送,不再发送
addOrderListClient.CloseSend()
//3、接收服务端返回的信息
for {
orderInfo,err := addOrderListClient.Recv()
if err == io.EOF {
fmt.Print("读取结束")
return
}
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println(orderInfo)
}
}
15.7、token和SSL认证
gRPC中默认支持两种认证方式,SSL/TLS认证方式和基于Token的认证方式。
SSL全称是Seucre Sockets Layer
安全套阶层,是一种标准安全协议。用于在通信过程中建立客户端与服务端之间的加密连接
TLS的全称是Transport Layer Security
,TLS是SSL的升级版。在使用的过程中。习惯将SSL和TLS组合在一起写作TLS/SSL;
使用SSL/TLS协议对通信连接进行安全加密,是通过非对称加密的方式来实现的。所谓非对称加密方式又称之为公钥加密,密钥对由公钥和私钥两种密钥组成。私钥和公钥成对存在,先生成私钥,通过私钥生成对应的公钥。公钥可以公开。私钥进行妥善保存。、
15.7.1、证书生成
1)创建server.conf文件
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_exntensions = mt.com
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CN
stateOrProvinceName = State or Province Name (full name)
localityName = Locality Name (eg, city)
localityName_default = BJ
organizationName = Organization Name (eg, company)
organizationName_default = BJ
commonName = Common Name (eg, your name or your server\'s hostname)
commonName_max = 64
commonName_default = mt.com
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = www.mt.com
IP = 127.0.0.1
DNS.2 = mt.com
IP = 127.0.0.1
2)生成ca相关证书
openssl genrsa -out ca.key 4096
openssl req -new -sha256 -out ca.csr -key ca.key -config server.conf
openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt
3)生成客户端证书
openssl genrsa -out server.key 2048
openssl req -new -sha256 -out server.csr -key server.key -config server.conf
openssl x509 -req -days 360 -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.pem -extensions req_ext -extfile server.conf
存放server.key
和server.pem
文件到 代码路径中。
15.7.2、服务定义
//订单服务定义
syntax = "proto3";
package message;
option go_package = "./";
message Line {
int32 Arg1 = 1;
int32 Arg2 = 2;
}
message LineResult {
int32 Cycle = 1;
int32 Space = 2;
}
service MathInfo {
rpc CalcResult (Line) returns (LineResult);
}
15.7.3、SSL认证
建立连接失败 rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs o
r temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0"
如果出现上述报错,是因为 go 1.15 版本开始废弃 CommonName,因此推荐使用 SAN 证书。 如果想兼容之前的方式,需要设置环境变量 GODEBUG 为 x509ignoreCN=0
。SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。请按照 章节 15.7.1
生成证书
- 服务端代码
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
msg "message/message"
"net"
"os"
)
type mathImml struct {
}
func (m mathImml) CalcResult(ctx context.Context,request *msg.Line) (result *msg.LineResult, err error) {
fmt.Println("传递的内容为:",request.Arg1,request.Arg2)
space := request.Arg1 * request.Arg2
cycle := ( request.Arg1 + request.Arg2 ) * 2
result = &msg.LineResult{Space:space,Cycle:cycle}
return result,nil
}
func main() {
//1、注册和监听
cred,err := credentials.NewServerTLSFromFile("../keys/server.pem","../keys/server.key")
if err != nil {
fmt.Println("证书错误",err)
os.Exit(1)
}
server := grpc.NewServer(grpc.Creds(cred))
msg.RegisterMathInfoServer(server,new(mathImml))
lsn,err := net.Listen("tcp",":1234")
if err != nil {
fmt.Println("监听错误",err)
os.Exit(1)
}
err = server.Serve(lsn)
if err != nil {
fmt.Println("监听错误",err)
os.Exit(1)
}
}
- 客户端代码
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
msg "message/message"
"os"
)
func main(){
//1、建立连接
cred,err := credentials.NewClientTLSFromFile("../keys/server.pem","mt.com")
if err != nil {
fmt.Println("证书错误",err)
os.Exit(1)
}
client,err := grpc.Dial("127.0.0.1:1234",grpc.WithTransportCredentials(cred))
if err != nil {
fmt.Println("建立连接失败",err)
return
}
//2、创建客户端对象,并调用方法
mClient := msg.NewMathInfoClient(client)
nums := msg.Line{Arg1:2,Arg2:3}
result,err := mClient.CalcResult(context.Background(),&nums)
if err != nil {
fmt.Println("建立连接失败",err)
return
}
fmt.Println("结果为:",result)
}
注意:如果传递的 CommoName
不正确,会报错: certificate is valid for www.mt.com, mt.com, not $错误的CommonName"
这里客户端和服务端都用的同一个证书,正常应该是客户端用自己的证书才对,
客户端和服务端相同,也要引入server的证书和ca的证书
certificate,err := tls.LoadX509KeyPair("client.crt","client.key") //1、加载客户端证书
if err != nil {
log.Fatal(err)
}
//2、加载ca证书
cretPool := x509.NewCertPool()
ca,err := ioutil.ReadFile("ca.crt")
if ok := cretPool.AppendCertsFromPEM(ca) ; !ok {
log.Fatal("failed to append ca certs")
}
creds := credentials.NewTLS(&tls.Config{ //3、构建所需证书内容
Certificates: []tls.Certificate{certificate},
ServerName: "com.mt",
RootCAs: cretPool,
})
//客户端调用
client,err := grpc.Dial("127.0.0.1:1234",grpc.WithTransportCredentials(creds))
//服务端调用
server := grpc.NewServer(grpc.Creds(creds))
15.7.4、Token认证
基于Token的身份验证是无状态的,不需要将信息服务存在服务器或者session中。基于Token认证的身份验证主要过程是:客户端在发送请求前,首先向服务端发起请求,服务端返回一个生成的token给客户端。客户端将token保存下来,用于后续每次请求时,携带着token参数。服务端在进行处理请求之前,会首先针对token进行验证,只有token验证成功了,才会处理并返回相关数据。
在gRPC中,允许开发人员自定义认证规则。通过 grpc.WithPerRPCCredentials()
设置自定义的认证规则,grpc.WithPerRPCCredentials()
方法接收一个PerRPCCredentials
类型的参数。并且PerRPCCredentials
是一个接口,实现了GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
和 RequireTransportSecurity() bool
方法 ;可以自定义实现这两个方法。来实现自定义认证规则
注意:RequireTransportSecurity()
方法表示要求底层是否是 ssl
//要实现接口的token认证,需要实现 grpc.PerRPCCredentials 方法
func PerRPCCredentials(creds credentials.PerRPCCredentials) CallOption {
return PerRPCCredsCallOption{Creds: creds}
}
type PerRPCCredentials interface {
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
RequireTransportSecurity() bool
}
- 服务端代码
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
msg "message/message"
"net"
"os"
)
type mathImml struct {
}
func (m mathImml) CalcResult(ctx context.Context,request *msg.Line) (result *msg.LineResult, err error) {
//1、通过metadata 获取token信息; md是 map[string][]string 类型
md,exist := metadata.FromIncomingContext(ctx)
if ! exist {
return nil,status.Errorf(codes.Unauthenticated,"无token认证信息")
}
var appKey,appSecret string
if key,ok := md["appid"] ;ok { //appid和appkey 是 客户端传递的
appKey = key[0]
}
if secret,ok := md["appkey"] ;ok {
appSecret = secret[0]
}
if appKey != "hello" || appSecret != "20210916" {
return nil,status.Errorf(codes.Unauthenticated,"Token 不合法")
}
//2、执行剩下逻辑
fmt.Println("传递的内容为:",request.Arg1,request.Arg2)
space := request.Arg1 * request.Arg2
cycle := ( request.Arg1 + request.Arg2 ) * 2
result = &msg.LineResult{Space:space,Cycle:cycle}
return result,nil
}
func main() {
//1、注册和监听
cred,err := credentials.NewServerTLSFromFile("../keys/server.pem","../keys/server.key")
if err != nil {
fmt.Println("证书错误",err)
os.Exit(1)
}
server := grpc.NewServer(grpc.Creds(cred))
msg.RegisterMathInfoServer(server,new(mathImml))
lsn,err := net.Listen("tcp",":1234")
if err != nil {
fmt.Println("监听错误",err)
os.Exit(1)
}
err = server.Serve(lsn)
if err != nil {
fmt.Println("监听错误",err)
os.Exit(1)
}
}
- 客户端代码
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
msg "message/message"
"os"
)
type TokenAuthentication struct {
AppKey string
AppSecret string
}
//自定义token信息,需要重写 PerRPCCredentials 接口,重写 PerRPCCredentials 接口需要实现这两个方法
func (ta *TokenAuthentication) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": ta.AppKey,
"appkey": ta.AppSecret,
},nil
}
func (ta *TokenAuthentication) RequireTransportSecurity() bool {
return true
}
func main(){
//1、建立连接
//1.1、构造 tls认证信息
cred,err := credentials.NewClientTLSFromFile("../keys/server.pem","mt.com")
if err != nil {
fmt.Println("证书错误",err)
os.Exit(1)
}
//1.2、构造 token认证信息
auth := TokenAuthentication{
AppKey: "hello",
AppSecret: "20210916",
}
//1.3、建立grpc连接
client,err := grpc.Dial("127.0.0.1:1234",grpc.WithTransportCredentials(cred),grpc.WithPerRPCCredentials(&auth))
if err != nil {
fmt.Println("建立连接失败",err)
return
}
//2、创建客户端对象,并调用方法
mClient := msg.NewMathInfoClient(client)
nums := msg.Line{Arg1:2,Arg2:3}
result,err := mClient.CalcResult(context.Background(),&nums)
if err != nil {
fmt.Println("建立连接失败",err)
return
}
fmt.Println("结果为:",result)
}
15.7.5、拦截器
在服务端的方法中,每个方法都要进行token验证,这样效率太低。可以优化一下处理逻辑。在调用服务端的具体方法之前。先进行拦截。并进行token验证和判断。这种方式称之为拦截器处理。这种方式称之为拦截器处理。
注意:gRPC中只能为每个服务设施之一个截取器,因此所有的截取工作只能在一个函数中完成。grpc-ecosystem
已经基于gRPC对截取器实现了链式截取。有需要再研究
- intercepter
使用拦截器,首先要注册 grpc.UnayIntercepter
,拦截器有多种,除了一元的之外,还有很多拦截器。
func UnaryInterceptor(i UnaryServerInterceptor) ServerOption { //grpc.UnayIntercepter函数需要传递 一个参数 UnaryServerInterceptor ,返回一个 ServerOption,这个ServerOption 即 服务端配置参数。在注册grpc.NewServer的时候传递的参数。
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error) //要传递的参数。是一个函数类型的参数 是一个方法定义。方便我们实现自定义拦截器
- 服务端
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
msg "message/message"
"net"
"os"
)
type mathImml struct {
}
func (m mathImml) CalcResult(ctx context.Context,request *msg.Line) (result *msg.LineResult, err error) {
//1、通过metadata 获取token信息; md是 map[string][]string 类型
//2、执行剩下逻辑
fmt.Println("传递的内容为:",request.Arg1,request.Arg2)
space := request.Arg1 * request.Arg2
cycle := ( request.Arg1 + request.Arg2 ) * 2
result = &msg.LineResult{Space:space,Cycle:cycle}
return result,nil
}
func main() {
//1、注册和监听
cred,err := credentials.NewServerTLSFromFile("../keys/server.pem","../keys/server.key")
if err != nil {
fmt.Println("证书错误",err)
os.Exit(1)
}
server := grpc.NewServer(grpc.Creds(cred),grpc.UnaryInterceptor(TokenInterceptor))
msg.RegisterMathInfoServer(server,new(mathImml))
lsn,err := net.Listen("tcp",":1234")
if err != nil {
fmt.Println("监听错误",err)
os.Exit(1)
}
err = server.Serve(lsn)
if err != nil {
fmt.Println("监听错误",err)
os.Exit(1)
}
}
func TokenInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
//1、获取token信息
md,exist := metadata.FromIncomingContext(ctx)
if ! exist {
return nil,status.Errorf(codes.Unauthenticated,"无token认证信息")
}
var appKey,appSecret string
if key,ok := md["appid"] ;ok { //appid和appkey 是 客户端传递的
appKey = key[0]
}
if secret,ok := md["appkey"] ;ok {
appSecret = secret[0]
}
if appKey != "hello" || appSecret != "20210916" {
return nil,status.Errorf(codes.Unauthenticated,"Token 不合法")
}
//通过token验证,继续处理请求
return handler(ctx, req) //这个handler会调用函数实现 mathImm.CalcResult 方法
}
- 客户端
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
msg "message/message"
"os"
)
type TokenAuthentication struct {
AppKey string
AppSecret string
}
//自定义token信息,需要重写 PerRPCCredentials 接口,重写 PerRPCCredentials 接口需要实现这两个方法
func (ta *TokenAuthentication) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": ta.AppKey,
"appkey": ta.AppSecret,
},nil
}
func (ta *TokenAuthentication) RequireTransportSecurity() bool {
return true
}
func main(){
//1、建立连接
//1.1、构造 tls认证信息
cred,err := credentials.NewClientTLSFromFile("../keys/server.pem","mt.com")
if err != nil {
fmt.Println("证书错误",err)
os.Exit(1)
}
//1.2、构造 token认证信息
auth := TokenAuthentication{
AppKey: "hello",
AppSecret: "20210916",
}
//1.3、建立grpc连接
client,err := grpc.Dial("127.0.0.1:1234",grpc.WithTransportCredentials(cred),grpc.WithPerRPCCredentials(&auth))
if err != nil {
fmt.Println("建立连接失败",err)
return
}
//2、创建客户端对象,并调用方法
mClient := msg.NewMathInfoClient(client)
nums := msg.Line{Arg1:2,Arg2:3}
result,err := mClient.CalcResult(context.Background(),&nums)
if err != nil {
fmt.Println("建立连接失败",err)
return
}
fmt.Println("结果为:",result)
}
15.7.6、grpcurl
Protobuf本身具有反射功能。gRPC同样也提供了一个名为reflection
的反射包,用于为gRPC服务提供查询。gRPC官方提供了一个C++实现的grpc_cli
工具,可以用于查询gRCP服务列表或调用gRPC方法。我们这里介绍go语言实现的grpcurl
,需要代码中启动反射的服务才能查询
同token认证 server代码
1)改动之一
import 多了 "google.golang.org/grpc/reflection" 包
2)改动值2 main函数
func main() {
cred,err := credentials.NewServerTLSFromFile("../keys/server.pem","../keys/server.key")
if err != nil {
fmt.Println("证书错误",err)
os.Exit(1)
}
server := grpc.NewServer(grpc.Creds(cred))
msg.RegisterMathInfoServer(server,new(mathImml))
//添加内容:
reflection.Register(server)
lsn,err := net.Listen("tcp",":1234")
if err != nil {
fmt.Println("监听错误",err)
os.Exit(1)
}
err = server.Serve(lsn)
if err != nil {
fmt.Println("监听错误",err)
os.Exit(1)
}
}
1)服务定义信息
syntax = "proto3";
package message;
option go_package = "./";
message Line {
int32 Arg1 = 1;
int32 Arg2 = 2;
}
message LineResult {
int32 Cycle = 1;
int32 Space = 2;
}
service MathInfo {
rpc CalcResult (Line) returns (LineResult);
}
2)grpcurl安装
> go get github.com/fullstorydev/grpcurl
> go install github.com/fullstorydev/grpcurl/cmd/grpcurl
2)服务查询
D:\Program_language\Project1\src\client>grpcurl localhost:1234 list #没有忽略证书
Failed to dial target host "localhost:1234": x509: certificate signed by unknown authority
D:\Program_language\Project1\src\keys>grpcurl -cacert ca.crt -key server.key -cert server.pem 127.0.0.1:1234 list
Failed to list services: server does not support the reflection API #表示没有启动反射服务
查看服务
D:\Program_language\Project1\src\keys>grpcurl -cacert ca.crt -key server.key -cert server.pem 127.0.0.1:1234 list
grpc.reflection.v1alpha.ServerReflection #这个是反射服务本身
message.MathInfo
查看服务方法列表,本例中只有一个方法
D:\Program_language\Project1\src\keys>grpcurl -cacert ca.crt -key server.key -cert server.pem 127.0.0.1:1234 list message.MathInfo
message.MathInfo.CalcResult
查看更详细的描述信息
D:\Project1\src\keys>grpcurl -cacert ca.crt -key server.key -cert server.pem 127.0.0.1:1234 describe message.MathInfo
message.MathInfo is a service:
service MathInfo {
rpc CalcResult ( .message.Line ) returns ( .message.LineResult );
}
查看服务类型信息
D:\Program_language\Project1\src\keys>grpcurl -cacert ca.crt -key server.key -cert server.pem 127.0.0.1:1234 describe message.Line
message.Line is a message:
message Line {
int32 Arg1 = 1;
int32 Arg2 = 2;
}
D:\Program_language\Project1\src\keys>grpcurl -cacert ca.crt -key server.key -cert server.pem 127.0.0.1:1234 describe message.LineResult
message.LineResult is a message:
message LineResult {
int32 Cycle = 1;
int32 Space = 2;
}
调用
D:\Program_language\Project1\src\keys>grpcurl -cacert ca.crt -key server.key -cert server.pem -d "{\"Arg1\":1,\"Arg2\":2}" 127.0.0.1:1234 message.MathInfo/CalcResult
{
"Cycle": 6,
"Space": 2
}
15.8、其他特性
1、上下文
grpc通过context.context
参数,为每个方法提供了上下文支持。客户端在调用方法的时候,可以通过可选的grpc.CallOption
类型的参数提供额外的上下文信息。
type CallOption interface {
// before is called before the call is sent to any server. If before
// returns a non-nil error, the RPC fails with that error.
before(*callInfo) error
// after is called after the call has completed. after cannot return an
// error, so any failures should be reported via output parameters.
after(*callInfo, *csAttempt)
}
type callInfo struct {
compressorType string
failFast bool
maxReceiveMessageSize *int
maxSendMessageSize *int
creds credentials.PerRPCCredentials
contentSubtype string
codec baseCodec
maxRetryRPCBufferSize int
}
func defaultCallInfo() *callInfo {
return &callInfo{
failFast: true,
maxRetryRPCBufferSize: 256 * 1024, // 256KB
}
}
2、默认参数
在 protocol v2
中支持默认参数,但是v3已经不支持,但是可以 基于proto3的扩展属性,实现 google/protobuf/descriptor.proto
3、超时
参考:https://grpc.io/blog/deadlines/ ,在需要的时候再研究
4、4pbgo
基于protobuf的扩展语法,通过插件自动生成 RPC和REST相关代码。
5、nginx
高版本的nginx,对gRPC提供了深度的支持。可以通过Nginx将后端多个gRPC服务聚合到一个Nginx服务。同时NGinx也提供了为同一种gRPC服务注册多个后端的功能,这样可以实现对gRPC负载均衡的支持。参考: https://www.nginx.com/blog/nginx-1-13-10-grpc/
十六、微服务
16.1、微服务概述
单体应用:所有的功能模块都放在一个程序中实现,
单体应用发展遇到的问题:
- 局限性:随着代码量的增长,维护单体应用的性能和成本越来越高。项目的功能 越来越多
- 扩展困难,牵一发而动全身
- 升级困难,技术迭代代码复用较难
- 持续部署困难,单个模块升级,整个程序都要停止进行发布
- ...
微服务:一个服务通常实现了一组不同的特性或功能。合在一起组成整个系统
- 微服务化后,每个模块对应自己的库和表。原有的单个数据库不再适用
- 负载均衡:同一种类型的多个微服务进程之间如何均衡。
- 大数据情况下,数据分片存储到多个表。分库分表
伸缩立方:
- X轴 伸缩:运行多个负载据衡器后的多个实例
- Y轴:对应用进一步分解为微服务,分库
- Z轴:大数据量时,对数据进行分区,分表
微服务优点:
- 解决复杂问题:微服务把庞大的单体分解为一套服务。每个微服务都有一个定义边界的方式,如远程过程调用RPC或者API。微服务更容易被开发与维护
- 团队分工协作更容易,微服务这种架构使得每个服务团队专注于开发。开发者可以选择任意的符合服务API的技术。
- ...
分布式:分散压力。微服务:分散能力。
微服务的定义:将一个单体应用拆分成一组微小的服务组件,每个微小的服务组件运行在自己的进程上,组件之间通过Restful API这样的轻量级机制进行交互,这些服务以业务能力为核心。用自动化部署机制独立部署,另外,这些服务可以用不同的语言进行开发,用不同的技术来存储数据
- 组件间通信,可以有:rpc,json,mq等
- 按照业务边界进行划分
- 微服务是一种编程架构思想,有不同的语言实现
微服务遇到的问题:{具体一个服务拆成了三个微服务,三个微服务多实例部署}
-
问题1:客户端如何访问拆分后的服务,原来单个服务,只需要访问单个ip即可
- 配置为多个ip?不太现实,因为后续会扩容
- 中间添加一个api gateway,主要功能如下:
- 提供统一的服务入口,让微服务对前台透明
- 聚合后台的服务,节省流量,提升性能
- 提供安全,过滤,流控等API管理功能
-
问题2:每个服务之间如何进行通信,三个微服务之间如何通信
- 微服务与微服务间的通信就是IPC(inter Process communication),常见的通信方案有:
- 同步调用:一致性较强,但是容器出调用问题,性能体验上也会有点差,特比是调用次数较多的时候,同步调用有两种实现方式:
- REST:rest是基于http,跨语言
- RPC:传输效率高,安全可控,一般对外是rest,对内是rpc,这里主要涉及
protocol buffer协议
- 异步消息调用:减低调用之间的耦合协调调用之间的缓冲。代价为接受最终一致性,常见的异步消息调用框架
- Kafaka,Notify,MesageQueue
- 同步调用:一致性较强,但是容器出调用问题,性能体验上也会有点差,特比是调用次数较多的时候,同步调用有两种实现方式:
- 微服务与微服务间的通信就是IPC(inter Process communication),常见的通信方案有:
-
问题3:如何实现众多微服务,服务之间如何识别、发现和管理
- 当服务上线时,服务提供者将自己的服务信息注册到某个专门的框架中,并通过心跳维持长连接,实时更新链接信息,服务调用者通过调用服务管理框架进行寻址,根据特定的算法,找到对应的服务,或者将服务的注册信息缓存到本地,这样提高性能。当服务下线时,服务管理框架会发送服务下线的通知给其他服务(基于心跳)。常见的框架有zk
- 如上的问题解决方案有两种主要的实现:
- 基于客户端的服务注册和发现
- 客户端向服务注册重新请求地址,自己选择提供服务的实例(选择算法)
- 基于服务端的服务注册与发现
- 客户端向负载均衡器发送请求,负载均衡器从服务注册框架获取地址列表,根据特定算法选择合适的地址提供服务
- 基于客户端的服务注册和发现
-
问题4:如果服务出现宕机,如何解决
- 重试机制
- 限流
- 熔断机制
- 负载均衡
- 降级(本地缓存)
16.2、Protobuf
16.2.1、Protobuf介绍
Xml、Json是目前常用的数据交换格式,它们直接使用字段名称维护序列化后类实例中字段与数据之间的映射关系,一般用字符串的形式保存在序列化后的字节流中。消息和消息的定义相对独立,可读性较好。但序列化后的数据字节很大,序列化和反序列化的时间较长,数据传输效率不高。
protobuf 即 Protocol Buffers,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。protobuf 性能和效率大幅度优于 JSON、XML 等其他的结构化数据格式。protobuf 是以二进制方式存储的,占用空间小,但也带来了可读性差的缺点。protobuf 在通信协议和数据存储等领域应用广泛。例如著名的分布式缓存工具 Memcached 的 Go 语言版本groupcache 就使用了 protobuf 作为其 RPC 数据格式。
1、格式定义
参考:
-
1)protobuf中的数据类型定义和 go语言中的 数据类型不是一一对应。参考 链接
-
2)Protobuf协议规定,使用该协议进行序列化和反序列化操作时,首先定义数据传输的格式,并命名以 .proto 为扩展名
-
3)Message定义一个消息
message定义一个消息:假设有一个订单系统,每个订单都包含一个订单号ID、订单金额Num、订单时间Timestamp字段,可以采用如下来定义消息类型的.proto文件
syntax = "proto2"; //新版本要求说明使用的proto版本,有2和3
package main;
message Order{
required string order_id = 1;
required int64 num =2;
optional int32 timestamp =3;
}
Order消息格式有三个字段,在消息中承载的数据分别对应每一个字段。其中每个字段都有一个名字和一种类型。
1)字段类型:在proto协议中,字段的类型包括字符串string,整形(int32,int64,,,) ,枚举(enum)等类型
2)分配标识符:在消息字段中,每个字段都有唯一的标识符,最小的标识可以从1开始到2^99-1
。不可以使用其中的[19000-199999]的标识号,Protobuf协议实现中对这些进行了预留。如果非要在.proto中使用这些预留标识号,编译时会报错;使用分配标识符用于确定入参的顺序。
3)指定字段限制:字段的修饰符包含三种类型,分别是:
- required 必须拥有
- optional 可选,消息格式中可以有0或者1个
- repeated:可以重复多次,重复值的顺序将会被保留,相当于go中的slice
个人理解:序号的作用,类似于mbr的分区,传递过去式01二进制,我告诉你第一个byte到第几个byte是对应哪个字段。根据这个顺序解析就行了
-
- 枚举类型
enum Age{ male=1; female=2; }
-
- 字段默认值
message Address { required sint32 id = 1 [default = 1]; required string name = 2 [default = '北京']; //string的默认值使用 单引号 赋值 optional string pinyin = 3 [default = 'beijing']; required string address = 4; required bool flag = 5 [default = true]; }
-
- 嵌套,可以进行嵌套定义
syntax = "proto2"; package example; message Person { required string Name = 1; required int32 Age = 2; required string From = 3; optional Address Addr = 4; message Address { required sint32 id = 1; required string name = 2; optional string pinyin = 3; required string address = 4; } }
-
- Proto V2和V3的区别 ; 1)v3
不再使用 required/optionnal/repeated进行修饰字段
;3) v3中不再使用default
- Proto V2和V3的区别 ; 1)v3
-
- message定义以后如果需要进行修改,为了保证之前的序列化和反序列化能够兼容新的message,message更新需要满足以下规则
- 不可以修改已存在域中的标识号。(字段中设定的1,2,3,4标识号)
- 所有新增添的域必须是 optional 或者 repeated。
- 非required域可以被删除。但是这些被删除域的标识号不可以再次被使用。
- 非required域可以被转化,转化时可能发生扩展或者截断,此时标识号和名称都是不变的。
- sint32和sint64是相互兼容的。
- fixed32兼容sfixed32。 fixed64兼容sfixed64。
- optional兼容repeated。发送端发送repeated域,用户使用optional域读取,将会读取repeated域的最后一个元素。
-
- Protocol序列化后生成的数据非常紧凑,序列化原理如下:
Protobuf的message中有很多字段,每个字段的格式为:**修饰符 字段类型 字段名 = 域号; ** 在序列化时,Protobuf按照TLV的格式序列化每一个字段,T即Tag,也叫Key;V是该字段对应的值value;L是Value的长度,如果一个字段是整形,这个L部分会省略。序列化后的Value是按原样保存到字符串或者文件中,Key按照一定的转换条件保存起来,序列化后的结果就是 KeyValueKeyValue…依次类推的样 如下图:
采用这种Key-Pair结构无需使用分隔符来分割不同的Field。对于可选的Field,如果消息中不存在该field,那么在最终的Message Buffer中就没有该field,这些特性都有助于节约消息本身的大小。
16.2.2、Protobuf安装
Go语言中对应的实现Protobuf协议库,GitHUB介绍:https://github.com/golang/protobuf
- 1、安装编译器
从 https://github.com/protocolbuffers/protobuf/releases
选择合适自己的系统,这里选择 https://github.com/protocolbuffers/protobuf/releases/download/v3.17.3/protoc-3.17.3-win64.zip
bin/protoc
放到系统的PATH
中,
go get github.com/golang/protobuf/protoc-gen-go
进入GOPATH/github.com/golang/protobuf/
go语言实现protobuf协议的源码
- 2、protobuf协议语法
Protobuf协议规定:适用该协议进行数据序列化和反序列化操作时,首先定义传输数据格式,并命名为.proto
为扩展名的消息定义文件
message定义一个消息:假设有一个订单系统,每个订单都包含一个订单号ID、订单金额Num、订单时间Timestamp字段,可以采用如下来定义消息类型的.proto文件
syntax = "proto2"; //新版本要求说明使用的proto版本,有2和3
package main;
message Order{
required string order_id = 1;
required int64 num =2;
optional int32 timestamp =3;
}
Order消息格式有三个字段,在消息中承载的数据分别对应每一个字段。其中每个字段都有一个名字和一种类型。
1)指定字段类型:在proto协议中,字段的类型包括字符串string,整形(int32,int64,,,) ,枚举(enum)等类型
2)分配标识符:在消息字段中,每个字段都有唯一的标识符,最小的标识可以从1开始到2^99-1
。不可以使用其中的[19000-199999]的标识号,Protobuf协议实现中对这些进行了预留。如果非要在.proto中使用这些预留标识号,编译时会报错
3)指定字段限制:字段的修饰符包含三种类型,分别是:
- required 必须拥有
- optional 可选,消息格式中可以有0或者1个
- repeated:可以重复多次(包括0次),重复值的顺序顺序会被保留,相当于go中的slice
注意,在实际开发中required弊大于利,在实际开发中更应该多使用optional和repeated而不是required;在同一个.proto
文件中,可以定义多个消息类型,多个消息类型分开定义即可
- 4、编译
protoc --go_out=./ example.proto
创建好.proto
文件后,需要进行编译
报错:protoc-gen-go: unable to determine Go import path for "test.proto"
改为:
syntax = "proto2";
package example;
option go_package = "./example"; //
message Order{
required string order_id = 1;
optional int64 num =2;
optional string timestamp =3;
}
生成 src/main/test.pb.go
文件,由protoc编译器生成。
- 5、示例代码
package main
import (
"fmt"
"github.com/golang/protobuf/proto"
"os"
"src/example"
)
func main(){
fmt.Println("helloworld" )
//如果没有自动补全,可以先 go get -u github.com/golang/protobuf/proto ,然后再import 就会有了
//1、创建序列化对象
msg := &example.Order{
OrderId: proto.String("100001") ,
Num: proto.Int64(1024),
Timestamp: proto.String("202109091050"),
}
//2、序列化
msgEncoding,err := proto.Marshal(msg)
if err != nil {
fmt.Println("序列化失败")
os.Exit(1)
}
//3、反序列化
msgEntity := example.Order{} //创建一个接收解码后数据的实体
err = proto.Unmarshal(msgEncoding,&msgEntity)
if err != nil {
fmt.Println("反向序列化失败",err)
os.Exit(2)
}
fmt.Printf("订单ID:%v\t订单号:%v\t订单时间:%v\t\n",msgEntity.GetOrderId(),msgEntity.GetNum(),msgEntity.GetTimestamp())
}
执行输出:
helloworld
订单ID:100001 订单号:1024 订单时间:202109091050
16.3、微服务发现机制
微服务遇到的四个主要问题:
- 1、客户端如何访问服务端
- 2、多个服务之间如何进行通信?
- 同步过程调用:rpc,rest API
- RPCj过程调用:1)Call ID映射,在客户端有对服务端方法的映射; 2)序列化和反序列化(Protocbuf效率高跨语言); 3)网络传输: Grpc
- 异步消息调用:Kafaka,Notify,MesageQueue
- 同步过程调用:rpc,rest API
- 3、多个微服务如何实现:使用服务管理框架,比如zk(java),go自身的比如consul
- 4、服务出现异常或者宕机,该如何解决:
1、为什么需要服务发现
我们写了一些通过REST API或 Thrift API
调用某个服务的代码。为了发起这个请求。代码需要知道服务实例的网络地址(IP地址和端口号),在传统的应用运行在物理机上的应用中。某个服务实例的网络地址一般是静态的,比如,代码可以只会偶尔更新的配置文件中读取网络地址。然而现在流行的基于云平台的资源的微服务中,有更多的问题i需要解决。因此引入了服务发现。服务发现主要有两种机制,客户端服务发现和服务端服务发现。
16.3.1、客户端发现模式
客户端负责决定可用服务实例的网络地址并且在集群中对请求负载均衡,客户端访问服务注册信息,然后客户端使用一种负载均衡的算法选择一个可用的服务实例然后发起请求。
左侧的服务A要访问右侧的服务B。但是并不知道右侧服务B的地址,1)先发送请求到Service REGISTRY
;2)从 SERVICE REGISTRY
中获取服务B的IP地址列表发送给左侧服务A; 3)根据获取到的IP:port 访问对应的实例B。因为是客户端调用查找被调服务的实例访问信息,因此成为客户端模式
服务实例的网络地址在服务启动的时候被登记到服务注册表中 ,当实例终止服务时从服务注册表中移除。服务实例的注册一般是通过心跳机制阶段性的进行刷新。
- 客户端发现机制的优势:
- 该模式中只增加了服务注册表,整体架构也相对简单;
- 客户端可以使用更加智能的,特定于应用的负载均衡机制,如一致性哈希。
- 客户端发现机制的缺点: 客户端发发现机制中,客户端与服务注册表紧密耦合在一起,开发者必须为每一种消费服务的客户端对应的编程语言和框架版本都实现服务发现逻辑(微服务一般实现为多种语言)
- 客户端发现模式的应用:往往大公司会采用客户端发现机制来实现服务的发现与注册的模式。
16.3.2、服务端发现模式
服务注册表为实时更新,比客户端模式多了一个负载均衡。左侧实例需要访问某个服务时,1)先访问LB; 2)LB首先去访问服务注册表查询地址列表,并接受返回信息;3)LB拿着请求信息,以及从服务注册表获取到的地址信息,发起请求; 中间多了一个负载均衡器,客户端不再需要自己去查询某个服务的地址信息。
在该种模式下,客户端通过一个负载均衡器向服务发送请求,负载均衡器查询服务注册表并把请求路由到一台可用的服务实例上。和客户端发现一样,服务实例通过服务注册表进行服务的注册和注销。
同样的,服务端发下模式也有优势和缺点:
- 服务端发现模式的优点:
- 服务发现的细节对客户端来说是抽象的,客户端仅需向负载均衡器发送请求即可。
- 这种方式减少了为消费服务的不同编程语言与框架实现服务发现逻辑的麻烦。很多部署环境已经提供了该功能。
- 服务端发现模式的缺点: 除非部署环境已经提供了负载均衡器,否则这又是一个需要额外设置和管理的可高可用的系统组件。
常见的服务注册和发现组件:Eureka、Zookeeper、consul + nginx
16.4、服务注册方式
服务实例必须使用服务注册表来进行服务的注册和注销,在实践过程中有不同的方式来实现服务的注册和注销:
- self-registration模式:这种模式下,服务实例自己负责通过服务注册表对自己进行注册和注销,另外如果有必要的话,服务实例可以通过发送心跳包请求来防止注册过期。self-registration优势之一是它相对简单,而且不强制使用其他的系统组件。然而,一个很大的劣势是 它使得服务实例和服务注册表强耦合 ,你必须在每一个使用服务的客户端编程语言和架构代码中实现注册逻辑。
- third-party registration模式: 服务实例本身并不负责通过服务注册表注册自己,相反的,通过另一个被称作service registrar系统组件来处理注册。service registrar通过轮询或者订阅事件来检测一些运行实例的变化,当它检测到一个新的可用服务实例时就把该实例注册到服务注册表中去,service registrar还负责注销已经被终止的服务实例; third-party registration模式主要优势是使得服务从服务注册表中被解耦,你不必为开发者使用的每种开发语言和框架实现服务注册的逻辑,相反,服务实例的注册被一个专有服务以集中式的方式处理。该模式的劣势是,除非它被内置在部署环境中,不然这又是一个需要被设置和管理的高可用系统组件。
- 在一个微服务的应用中,一组运行的服务实例时动态的,实例有动态分配的网络地址,因此为了使得客户能够向服务发起请求。必须要有服务发现机制。服务发现的关键时服务注册表,服务注册表时可用的服务实例的数据库,提供了管理和查询的API,服务实例使用这些管理API进行服务的注册和注销。
16.5、Consul
16.5.1、Consul概述
Consul时google开源的基于go开发的服务发现、配置管理中心,consul 属于微服务架构的基础设施中用于发现和配置服务的一个工具。Consul提供如下的几个核心功能:
- 服务发现:Consul的某些客户端可以提供一个服务,其他客户端可以使用Consul去发现这个服务的提供者。
- 健康检查:Consul客户端可以提供一些健康检查,这些健康检查关联到一个指定的服务,比如心跳包的检测
- 键值存储:应用实例可以使用Consul提供的分层键值存储,比如动态配置,特征标记,协作等。通过HTTP API的方式进行获取
- 多数据中心:Consul对多数据中心有非常好的支持。
安装:
- 解压官网下载的zip包,然后把
consul
的二进制包存放到PATH
中
分布式单点故障:
- 分布式系统,一般采用主从模式(master+多slave模式)进行部署。由于单一的节点故障导致整条链路故障的称为单点故障
- 在主节点宕机的情况下如何解决?
- 提供一个备用的主节点,slave同时和两个master 上报心跳,数据写入写双份;master(m)和master(s)之间通过心跳确认状态。如果master(m)和master(s)之间存在心跳异常则master(s)切换为主,此时如果仅仅时master(m)和master(s)之间的网络有问题,而不是真正的异常。那么就会出现master(s)出现误判,这样就会出现两个主节点提供服务,这样就会出现数据不一致的问题。
- Consul中使用Raft(一种基于Paxos的一致性算法,和Paxos相比状态更少,算法更简单易懂)。Raft中的节点总是处于以下三种状态之一,
follower|candidate|leader
;所有节点的初始状态都是follower,在这种状态下,节点可以接受来自leader的日志条目并进行投票(一个节点发起投票,发送自己的信息给其他节点)。如果在一段时间内没有收到条目,节点将自动提升为候选状态。在候选状态中,节点请求同级的选票。如果一个候选人获得了法定人数的票数,那么他就被提升为leader。leader必须接受新的日志条目。并将其复制给所有其他的follower。
- 单点故障->部署多台->解决数据一致性和同步的问题
consul内部的查询:
-
首先Consul支持多数据中心,在下图中有两个DataCenter,他们通过Internet互联,同时请注意为了提高通信效率,只有Server节点才加入跨数据中心的通信。
-
在单个数据中心中,Consul分为Client和Server两种节点(所有的节点也被称为Agent),Server节点保存数据,Client负责健康检查及转发数据请求到Server。
-
Server节点有一个Leader和多个Follower,Leader节点会将数据同步到Follower,Server的数量推荐是3个或者 5个,在Leader挂掉的时候会启动选举机制产生一个新的 Leader。
-
集群内的Consul节点通过gossip协议(流言协议)维护成员关系,也就是说某个节点了解集群内现在还有哪些节点,这些节点是Client还是Server。
单个数据中心的流言协议同时使用TCP和UDP通信,并且都使用8301端口。跨数据中心的流言协议也同时使用TCP和UDP 通信,端口使用8302。
集群内数据的读写请求既可以直接发到Server,也可以通过Client使用RPC转发到Server,请求最终会到达Leader节点。
在允许数据轻微陈旧的情况下,读请求也可以在普通的Server节点完成,集群内数据的读写和复制都是通过TCP的8300端口完成。
16.5.2、Consul服务发现原理
1、部署集群。首先需要有一个正常的Consul集群,有Server,有Leader。这里在服务器Server1、Server2、Server3上分别部署了Consul Server。
2、选举Leader节点。假设他们选举了Server2上的 Consul Server 节点为Leader。这些服务器上最好只部署Consul server 程序,以尽量维护Consul Server的稳定。
3、注册服务。然后在服务器 Server7 和 Server5 上通过Consul Client分别注册Service A、B、C,这里每个Service 分别部署在了两个服务器上,这样可以避免Service的单点问题。服务注册到Consul可以通过 HTTP API(8500 端口)的方式,也可以通过 Consul 配置文件的方式。
4、Consul client转发注册消息。Consul Client 可以认为是无状态的,它将注册信息通过RPC转发到Consul Server,服务信息保存在Server的各个节点中,并且通过Raft实现了强一致性。
5、服务发起通信请求。最后在服务器Server 6中Program D需要访问Service B,这时候Program D首先访问本机Consul Client提供的HTTP API,本机Client会将请求转发到 Consul Server。
6、Consul Server查询到Service B当前的信息返回,最终Program D拿到了Service B的所有部署的IP和端口,然后就可以选择Service B的其中一个部署并向其发起请求了。
16.5.3、启动
C:\Users\admin>consul agent -dev
==> Starting Consul agent...
Version: '1.10.2'
Node ID: '96c3b96d-d492-62d1-7dd3-afaefbcacf8d' //节点id
Node name: 'DESKTOP-AUTVJI4'
Datacenter: 'dc1' (Segment: '<all>') //数据中心,consul支持多数据中心
Server: true (Bootstrap: false) //是否是server节点
Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false, Auto-Encrypt-TLS: false //加密和通信协议
==> Log data will now stream in as it occurs:
2021-09-21T17:38:37.983+0800 [INFO] agent.server.raft: initial configuration: index=1 servers="[{Suffrage:Voter ID:96c3b96d-d492-62d1-7dd3-afaefbcacf8d Address:127.0.0.1:8300}]"
2021-09-21T17:38:38.036+0800 [INFO] agent.server.raft: entering follower state: follower="Node at 127.0.0.1:8300 [Follower]" leader=
2021-09-21T17:38:38.037+0800 [INFO] agent.server.serf.wan: serf: EventMemberJoin: DESKTOP-AUTVJI4.dc1 127.0.0.1
2021-09-21T17:38:38.037+0800 [INFO] agent.server.serf.lan: serf: EventMemberJoin: DESKTOP-AUTVJI4 127.0.0.1
2021-09-21T17:38:38.038+0800 [INFO] agent.router: Initializing LAN area manager
2021-09-21T17:38:38.038+0800 [INFO] agent.server: Adding LAN server: server="DESKTOP-AUTVJI4 (Addr: tcp/127.0.0.1:8300) (DC: dc1)"
2021-09-21T17:38:38.038+0800 [WARN] agent: grpc: addrConn.createTransport failed to connect to {dc1-127.0.0.1:8300 0 DESKTOP-AUTVJI4 <nil>}. Err :connection error: desc = "transport: Error while dialing dial tcp 127.0.0.1:0->127.0.0.1:8300: operation was canceled". Reconnecting...
2021-09-21T17:38:38.038+0800 [INFO] agent.server: Handled event for server in area: event=member-join server=DESKTOP-AUTVJI4.dc1 area=wan
2021-09-21T17:38:38.038+0800 [WARN] agent: grpc: addrConn.createTransport failed to connect to {dc1-127.0.0.1:8300 0 DESKTOP-AUTVJI4.dc1 <nil>}. Err :connection error: desc = "transport: Error while dialing dial tcp 127.0.0.1:0->127.0.0.1:8300: operation was canceled". Reconnecting...
2021-09-21T17:38:38.039+0800 [INFO] agent: Started DNS server: address=127.0.0.1:8600 network=udp
2021-09-21T17:38:38.039+0800 [INFO] agent: Started DNS server: address=127.0.0.1:8600 network=tcp
2021-09-21T17:38:38.040+0800 [INFO] agent: Starting server: address=127.0.0.1:8500 network=tcp protocol=http
2021-09-21T17:38:38.040+0800 [WARN] agent: DEPRECATED Backwards compatibility with pre-1.9 metrics enabled. These metrics will be removed in a future version of Consul. Set `telemetry { disable_compat_1.9 = true }` to disable them.
2021-09-21T17:38:38.040+0800 [INFO] agent: started state syncer
2021-09-21T17:38:38.040+0800 [INFO] agent: Consul agent running!
2021-09-21T17:38:38.040+0800 [INFO] agent: Started gRPC server: address=127.0.0.1:8502 network=tcp
2021-09-21T17:38:38.110+0800 [WARN] agent.server.raft: heartbeat timeout reached, starting election: last-leader= //心跳包
2021-09-21T17:38:38.110+0800 [INFO] agent.server.raft: entering candidate state: node="Node at 127.0.0.1:8300 [Candidate]" term=2 //进入candidate 状态
2021-09-21T17:38:38.110+0800 [DEBUG] agent.server.raft: votes: needed=1
2021-09-21T17:38:38.111+0800 [DEBUG] agent.server.raft: vote granted: from=96c3b96d-d492-62d1-7dd3-afaefbcacf8d term=2 tally=1
2021-09-21T17:38:38.111+0800 [INFO] agent.server.raft: election won: tally=1
2021-09-21T17:38:38.111+0800 [INFO] agent.server.raft: entering leader state: leader="Node at 127.0.0.1:8300 [Leader]" //进入leader状态
2021-09-21T17:38:38.111+0800 [INFO] agent.server: cluster leadership acquired
2021-09-21T17:38:38.111+0800 [INFO] agent.server: New leader elected: payload=DESKTOP-AUTVJI4
2021-09-21T17:38:38.111+0800 [DEBUG] agent.server: Cannot upgrade to new ACLs: leaderMode=0 mode=0 found=true leader=127.0.0.1:8300
2021-09-21T17:38:38.113+0800 [DEBUG] agent.server.autopilot: autopilot is now running
2021-09-21T17:38:38.113+0800 [DEBUG] agent.server.autopilot: state update routine is now running
2021-09-21T17:38:38.113+0800 [INFO] agent.leader: started routine: routine="federation state anti-entropy"
2021-09-21T17:38:38.113+0800 [INFO] agent.leader: started routine: routine="federation state pruning"
2021-09-21T17:38:38.114+0800 [DEBUG] connect.ca.consul: consul CA provider configured: id=07:80:c8:de:f6:41:86:29:8f:9c:b8:17:d6:48:c2:d5:c5:5c:7f:0c:03:f7:cf:97:5a:a7:c1:68:aa:23:ae:81 is_primary=true
2021-09-21T17:38:38.123+0800 [INFO] agent.server.connect: initialized primary datacenter CA with provider: provider=consul
2021-09-21T17:38:38.123+0800 [INFO] agent.leader: started routine: routine="intermediate cert renew watch"
2021-09-21T17:38:38.124+0800 [INFO] agent.leader: started routine: routine="CA root pruning"
2021-09-21T17:38:38.124+0800 [DEBUG] agent.server: successfully established leadership: duration=12.5921ms
2021-09-21T17:38:38.125+0800 [INFO] agent.server: member joined, marking health alive: member=DESKTOP-AUTVJI4
2021-09-21T17:38:38.130+0800 [INFO] agent.server: federation state anti-entropy synced
2021-09-21T17:38:38.329+0800 [DEBUG] agent: Skipping remote check since it is managed automatically: check=serfHealth
2021-09-21T17:38:38.330+0800 [INFO] agent: Synced node info
2021-09-21T17:38:38.609+0800 [DEBUG] agent: Skipping remote check since it is managed automatically: check=serfHealth
2021-09-21T17:38:38.610+0800 [DEBUG] agent: Node info in sync
2021-09-21T17:38:38.612+0800 [DEBUG] agent: Node info in sync
新开终端:
C:\Users\admin>consul members
Node Address Status Type Build Protocol DC Segment
DESKTOP-AUTVJI4 127.0.0.1:8301 alive server 1.10.2 2 dc1 <all>
web控制台: http://127.0.0.1:8500/
可以查看consul控制台
ctrl +c 关闭进程
..
2021-09-21T17:52:33.551+0800 [DEBUG] agent.server.autopilot: autopilot is now stopped
2021-09-21T17:52:33.553+0800 [INFO] agent.router.manager: shutting down
2021-09-21T17:52:33.553+0800 [INFO] agent.router.manager: shutting down
2021-09-21T17:52:33.553+0800 [INFO] agent: consul server down //关闭server
2021-09-21T17:52:33.558+0800 [INFO] agent: shutdown complete
2021-09-21T17:52:33.558+0800 [INFO] agent: Stopping server: protocol=DNS address=127.0.0.1:8600 network=tcp //关闭dns tcp
2021-09-21T17:52:33.559+0800 [INFO] agent: Stopping server: protocol=DNS address=127.0.0.1:8600 network=udp //关闭dns udp
2021-09-21T17:52:33.559+0800 [INFO] agent: Stopping server: address=127.0.0.1:8500 network=tcp protocol=http //关闭8500 http
2021-09-21T17:52:33.575+0800 [INFO] agent: Waiting for endpoints to shut down
2021-09-21T17:52:33.576+0800 [INFO] agent: Endpoints down
2021-09-21T17:52:33.576+0800 [INFO] agent: Exit code: code=1
16.5.4、集群docker启动
单节点consul启动: docker run -p 8500:8500/tcp consul agent -server -ui --bootstrap-expect=1 --client=0.0.0.0
参数:
- --bootstrap-expect=1 节点个数,当达到节点个数(这里为1),进行leader选举。
验证:
curl localhost:8500/v1/catalog/nodes
- UI界面查看
localhost:8500
集群方式启动-serverr:
- 第一个:
docker run -itd -p 8500:8500 -e CONSUL_BIND_INTERFACE='eth0' --name=consul_server1 consul agent -server -bootstrap -ui -node=1--client='0.0.0.0'
#-node是为启动的节点赋予一个id值。记录 启动后的容器的ip地址。docker inspect -f '{{ .NetworkSettings.IPAddress }}'
- 第二个:
docker run -itd -e CONSUL_BIND_INTERFACE='eth0' --name=consul_server2 consul agent -server -node=2 -join="$IP1"
#加入到第一个 启动的consul - 第三个:
docker run -itd -e CONSUL_BIND_INTERFACE='eth0' --name=consul_server2 consul agent -server -node=3 -join="$IP1"
#加入到第一个 启动的consul,这里除了 -node参数value不一样,其他和第二个启动的一样。
集群方式启动-client:
- 第一个:
docker run -itd -e CONSUL_BIND_INTERFACE='eth0' --name=consul_client4 consul agent -client -node=4 -join="$IP1" -client='0.0.0.0'
#client主要用于转发请求 - 第一个:
docker run -itd -e CONSUL_BIND_INTERFACE='eth0' --name=consul_client5 consul agent -client -node=5 -join="$IP1" -client='0.0.0.0'
- 第一个:
docker run -itd -e CONSUL_BIND_INTERFACE='eth0' --name=consul_client consul agent -client -node=6 -join="$IP1" -client='0.0.0.0'
在ui界面可以看到 node和server信息。
16.5.6、consul常用命令
consul命令的使用形式为:
consul command [option]
- agent:consul的节点分为client和server两类,这两类节点统称为agent节点。
- join:该命令的作用是将agent加入到consul的集群当中。当新启动一个agent节点后,往往需要指定节点需要加入到特定的consul集群中,此时使用join命令进行指定。
- members:列出consul集群中的所有的成员节点信息,包括ip,端口,状态,类型等信息。
除command命令外,还有option选项供开发者使用,常见的和常使用的option有:
- -data-dir:该选项用于指定agent储存状态的数据目录,这是所有agent都必须的,对于server尤其重要,因为他们必须持久化集群的状态。
- -config-dir:该选项用于指定service的配置文件和检查定义所在的位置。通常会指定为"某一个路径/consul.d"(通常情况下,.d表示一系列配置文件存放的目录)
- -config-file:指定一个要装载的配置文件。该选项可以配置多次,进而配置多个配置文件。
- -dev:该选项用于创建一个开发环境下的server节点,该参数配置下,不会有任何持久化操作,即不会有任何数据写入到磁盘。dev模式仅仅是在开发和测试环境中使用,不能用于生产环境。
- -bootstrap-expect:该选项用于通知consul server类型节点,指定集群的server节点个数,该参数是为了延迟选举启动,直到全部的节点启动完毕以后再进行启动。
- -node:该node选项用于指定节点在集群中的名称,该名称在集群中需要是唯一的,推荐直接使用机器的IP。
- -bind:该选项用于指定节点所在的IP地址。
- -server:该选项用于指明consul节点类型为server类型。每个数据中心(DC)的server数量推荐3到5个。所有的server节点加入到集群后要经过选举,采用raft一致性算法来确保数据操作的一致性。
- -client:该参数用于指定consul界定为client节点类型。
- -join:英文为加入的意思,join选项用于指定要将节点添加到具体哪个集群中。
- -dc:dc是datacenter的简称,该选项用于指定节点加入的dc实例。
16.6、服务注册与查询
16.6.1、简单定义和查询
服务注册方式:1)sdk的方式,调用别人写的第三方代码实现;2)使用consul定义consul支持的格式。
如下是一个简单的服务定义(非标准) D:\Program_language\install2\bin\consul.d\firstservice.json
{
"service":
{
"ID": "firstService",
"NAME": "firestService",
"tags": [
"dev"
],
"port": 80
}
}
启动服务: consul agent -dev -config-dir D:\Program_language\install2\bin\consul.d
查看方式1:web界面查看
查看方式2:命令行查看
dig -t A dev.firstService.service.consul @127.0.0.1:8600
服务格式: ${tag}.${serviceName}.service.consul
#大写和小写都可以解析
dig -t A dev.firstervice.service.consul @127.0.0.1:8600
服务格式: ${tag}.${serviceName}.service.consul
#小写
查看方式3:curl
curl http://localhost:8500/v1/catalog/service/firestService
,结果如下:
[
{
"ID": "d79900c7-18a0-f70f-ba7a-871e19a3558f",
"Node": "DESKTOP-AUTVJI4",
"Address": "127.0.0.1",
"Datacenter": "dc1",
"TaggedAddresses": {
"lan": "127.0.0.1",
"lan_ipv4": "127.0.0.1",
"wan": "127.0.0.1",
"wan_ipv4": "127.0.0.1"
},
"NodeMeta": {
"consul-network-segment": ""
},
"ServiceKind": "",
"ServiceID": "firstService", //自定义了id
"ServiceName": "firestService", //自定义的name
"ServiceTags": [ //标签
"dev"
],
"ServiceAddress": "",
"ServiceWeights": {
"Passing": 1,
"Warning": 1
},
"ServiceMeta": {},
"ServicePort": 80,
"ServiceSocketPath": "",
"ServiceEnableTagOverride": false,
"ServiceProxy": {
"Mode": "",
"MeshGateway": {},
"Expose": {}
},
"ServiceConnect": {},
"CreateIndex": 14,
"ModifyIndex": 14
}
]
多个微服务,把服务json存放到consul.d
中,然后重启consul 才能加载新的服务。
16.6.2、微服务定义标准
1、consul配置定义,包含service定义 以及 agent(server和client)
除了命令行选项,微服务的定义和配置也可以放入文件中。在某些情况下,这可能更容易,比如当使用配置管理系统配置时。配置文件是JSON格式的,使得它们易于被人和计算机读取和编辑。配置被格式化为单个JSON对象,其中包含配置。
配置文件不仅用于设置代理,还用于提供检查和服务定义。这些配置文件同样可以被其他软件和功能所识别。它们分别记录在检查配置和服务配置下。服务和检查定义支持在重新加载期间更新。
{
"datacenter": "east-aws",
"data_dir": "/opt/consul",
"log_level": "INFO",
"node_name": "foobar",
"server": true,
"watches": [
{
"type": "checks",
"handler": "/usr/bin/health-check-handler.sh"
}
],
"telemetry": {
"statsite_address": "127.0.0.1:2180"
}
}
上述的json文件格式是一个案例,consul的json文件配置可选项有很多,比如:
- addresses:该配置选项用于设置绑定地址。在Consul 1.0和更高版本中,可以将这些设置为要绑定到的地址列表。支持绑定设置多种类型的地址,包括:dns、http、https、grpc等共四种类型。
- bootstrap:该配置相当于在命令行中添加了-bootstrap命令行标志。
- bootstrap_expect:该配置相当于在命令行中添加了-bootstrap_expect命令行标志。
- bind_addr:该配置相当于在命令行中添加-bind指令操作。
- ca_file:该配置用于指定ca证书文件的目录。
- ca_path:该配置用于指定ca证书的整体目录。
- client_addr:该配置与命令行中-client指令功能相同。
- config_entries:在该配置项下,通过配置二级配置项来进行,二级配置项可以配置bootstrap选项。
- connect:关于连接的一些配置项通过该配置进行设置,同样是通过二级配置项来完成,可支持的二级配置项有:enabled、ca_provider、ca_config。
- datacenter:该配置项与命令行中-datacenter指令作用相同。
- data_dir:该配置项与命令行中-data-dir指令作用相同,用于指定微服务json定义文件所在的目录。
- dns_config:该选项用于配置dns相关参数。
- domain:该配置项与命令行中的-domain指令作用相同。
- node_id:该配置项与命令行中的-node-id指令作用相同,用于自定义设定node-id。
- node_name:该配置项与命令行中的-node指令作用相同,用于为node指定名称。
- ports:该配置项用于配置节点的端口号,通过二级配置选项可以配置:dns、http、https、grpc、serf_lan、serf_wan、server等不同类型的端口。
- protocol:该配置选项与命令行中的-protocol指令功能相同。
如上,只是列出了部分json配置文件的配置项,全部的配置选项在consul的官方网站有相关的说明,可以访问如下链接进行查看:https://www.consul.io/docs/agent/options.html
2、服务定义配置信息
服务定义参数信息:https://www.consul.io/docs/discovery/services
{
"service": {
"id": "redis",
"name": "redis",
"tags": ["primary"],
"address": "",
"meta": {
"meta": "for my service"
},
"tagged_addresses": {
"lan": {
"address": "192.168.0.55",
"port": 8000,
},
"wan": {
"address": "198.18.0.23",
"port": 80
}
},
"port": 8000,
"socket_path": "/tmp/redis.sock",
"enable_tag_override": false,
"checks": [
{
"args": ["/usr/local/bin/check_redis.py"],
"interval": "10s"
}
],
"kind": "connect-proxy",
"proxy_destination": "redis", // Deprecated
"proxy": {
"destination_service_name": "redis",
"destination_service_id": "redis1",
"local_service_address": "127.0.0.1",
"local_service_port": 9090,
"local_service_socket_path": "/tmp/redis.sock",
"mode": "transparent",
"transparent_proxy": {
"outbound_listener_port": 22500
},
"config": {},
"upstreams": [],
"mesh_gateway": {
"mode": "local"
},
"expose": {
"checks": true,
"paths": [
{
"path": "/healthz",
"local_path_port": 8080,
"listener_port": 21500,
"protocol": "http2"
}
]
}
},
"connect": {
"native": false,
"sidecar_service": {}
"proxy": { // Deprecated
"command": [],
"config": {}
}
},
"weights": {
"passing": 5,
"warning": 1
},
"token": "233b604b-b92e-48c8-a253-5f11514e4b50",
"namespace": "foo"
}
}