Go项目实战:02-微服务micro services

1、微服务(micro services)

请添加图片描述

单体式架构服务

过往大家熟悉的服务器。

特性:
  • 1、复杂性随着开发越来越高,遇到问题解决困难。
  • 2、技术债务逐渐上升。
  • 3、耦合度高,维护成本大。
    - 1、出现bug,不容易排查
    - 2、解决旧bug,会出新bug
  • 4、持续交付时间较长。
  • 5、技术选型成本高,风险大。
  • 6、扩展性较差
    - 1、垂直扩展:通过增加单个系统成员的负荷来实现扩展
    - 2、水平扩展:通过增加更多的系统成员来实现扩展

微服务架构

优点:

1、职责单一
2、轻量级通信 --可跨语言
3、独立性
4、迭代开发

缺点:
  • 1、运维成本高!
  • 2、分布式复杂度变高
  • 3、接口成本高
  • 4、重复性劳动
  • 5、业务分离困难–中台开发

单体式服务和微服务对比

请添加图片描述


2、RPC框架

2.1socket网络编程回顾

请添加图片描述

2.2RPC使用步骤

  • 服务端

1、注册rpc服务对象—要给对象绑定方法( 1、定义类,2、绑定类方法)

rpc.RegisterName("服务名", 回调对象)

2、创建监听器

listener, err := net.Listen()

3、建立连接

conn, err := listener.Accept()

4、将连接绑定rpc服务

rpc.ServeConn(conn)
  • 客户端

1、用rpc先去连接服务器 rpc.Dial()

conn, err := rpc.Dial()

2、调用远程函数

conn.Call("服务名.方法名", 传入参数, 传出参数)

2.3RPC相关函数

1、注册rpc服务

func (server *Server) RegisterName(name string, rcvr interface{}) error
参数1:服务名。字符串类型
参数2:对应的rpc对象。空接口类型。该对象绑定的方法要满足如下条件:
- 1、方法必须是导出的 -- 包外可见。 首字母大写。
- 2、方法必须有两个参数,并且都是导出类型(自定义的切片、map)、内建类型(int、double...)。
- 3、方法的第二个参数必须是**指针**(传出参数)
- 4、方法只有一个 **error** 接口类型的 **返回值**
- 举例:
type world struct {
} 
func (this * world ) Helloworld (name string, rest *string) error {
}
rpc.RegisterName("服务名", new(world))

2、绑定rpc服务

func (server *Server) ServeConn(conn io.ReadWriteCloser)

参数conn:成功建立好连接的socket–conn

3、调用远程函数

func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error

参数1:“服务名.方法名”
参数2: 传入参数。方法所需要的数据。
参数3:传出参数。定义var变量,&变量名。完成传参


2.4编码实现

服务端
package main

import (
	"fmt"
	"net"
	"net/rpc"
)
//定义类对象
type  World struct{

}
//绑定类方法
func (this *World) HelloWrold (name string, resp * string) error {
	*resp = name + "你好!"
	return nil
}

func main() {
	//1、注册rpc服务,绑定对象方法
	err := rpc.RegisterName("hello", new(World))
	if err != nil {
		fmt.Println("注册rpc服务失败!", err)
		return
	}
	//2、设置监听
	listener, err := net.Listen("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("net.Listen err: ", err)
		return
	}
	defer listener.Close()
	fmt.Println("开始监听...")
	//3、建立连接
	conn, err := listener.Accept()
	if err != nil {
		fmt.Println("listener.Accept(): ", err)
		return
	}
	defer conn.Close()
	fmt.Println("连接建立成功...")

	//4、绑定服务
	rpc.ServeConn(conn)



}
客户端
package main

import (
	"fmt"
	"net/rpc"
)

func main () {
	//1、 用rpc连接服务器--Dail()
	conn,err := rpc.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("Dial err:", err)
		return
	}
	//2、调用远程函数
	var reply string //接收函数返回值--传出参数
	err = conn.Call("hello.HelloWrold", "李白", &reply)
	if err != nil {
		fmt.Println("Call err:", err)
		return
	}

	fmt.Println(reply)
}


2.5json 版rpc

  • 使用nc - 类127.0.0.1 8080 充当服务器
  • clinet.go 充当客户端,发起通信 —乱码
    - 因为:RPC使用了go语言特有的数据序列化gob。 其他编程语言不能解析。
  • 使用 通用的序列化和反序列化 ---- json、protobuf
修改客户端
conn,err := jsonrpc.Dial("tcp", "127.0.0.1:8080")

请添加图片描述

修改服务器端
jsonrpc.ServeConn(conn)
//终端1
go run server.go
//终端2
tianyi@SPACE-STATION src % echo -e '{"method":"hello.HelloWorld","params":["李白"],"id":0}' | nc 127.0.0.1 8080//使用nc - 类127.0.0.1 8080 充当服务器
//返回值
{"id":0,"result":"李白你好!","error":null}

如果,绑定方法返回值的error不为空?无论传出参数是否有值,服务端都不会返回数据

func (this *World) HelloWorld (name string, resp * string) error {
	*resp = name + "你好!"
	//return nil
	return errors.New("unknown error")//
}
tianyi@SPACE-STATION src % echo -e '{"method":"hello.HelloWorld","params":["李白"],"id":0}' | nc 127.0.0.1 8080  
{"id":0,"result":null,"error":"unknown error"}


2.6rpc封装

服务器端封装

1、

//定义接口
type xxx interface {
	方法名(传入参数, 传出参数) error 
}
//例: 
type MyIntercace Interface{
	HelloWorld(string, *string) error
}

2、

//封装注册服务方法
func RegisterService (I MyInterface) {
	rpc.RegisterName("hello", I)
}
客户端封装

1、

//定义类
type MyClient struct {
	c *rpc.Client	
}

2、

//绑定类方法
func (this *MyClient) HelloWorld (a string, b *string) error {
	return this.c.Call("hello.HelloWorld", a, b)	
}

3、

//初始化客户端
func Init (addr string) {
	conn,_ := jsonrpc.Dail("tcp", addr)	
}

3、protobuf 认识与使用

3.1 protobuf简介

Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,是一种轻便高效的结构
化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储RPG数据交
换格式
。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前 提供了C+、Java、Python三种语言的APl。

Protobuf刚开源时的定位类似于XML、JSON等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。这里我们更关注的是Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言
RPC接口的基础
工具。

需要了解两点
1。protobuf是类似与json一样的数据描述语言(数据格式)
2。protobuf非常适合于RPC数据交换格式

接着我们来看一下protobuf的优势和劣势:

优势:
1:序列化后体积相比json和XML很小,适合网络传输
2:支持跨平台多语言
3:消息格式升级和兼容性还不错
4:序列化反序列化速度很快,快于json的处理速度
劣势:
1:应用不够广(相比xml和json)
2:二进制格式导致可读性差
3:缺乏自描述

请添加图片描述

我们先安装一下protobuf,然后通过例子来看一下protobuf的具体作用。

3.2 protobuf的安装

1、下载protobuf

方法一: ===>git clone https://github.com/protocolbuffers/protobuf.git
方法二:=>或者将准备好的压缩包进行拖入
解压到$GOPATH/src/github.com/protocolbuffers/下面
Unzip protobuf.zip

2、安装(ubuntu)

(1)安装依赖工具(联网)
sudo apt-get install autoconf automake libtool curl make g++ unzip
libffi -dev -y
(2)进入protobuf文件
cd protobuf/
(3)进行安装检测 并生成自动安装脚本
/autogen.sh
./configure
(4)进行编译c代码
make
(5)进行安装
sudo make install
(6)刷新linux共享库关系
sudo ldconfig

3、测试protobuf工具

protoc -h
如果正常输出 相关指令 没有报任何error, 为安装成功

4、安装protobuf的go语言插件

由于protobuf并没有支持go语言需要我们手动安装相关插件
(1)下载
方法一:===>go get -v -u github.com/golang/protobuf/proto
方法二:=>或者将github.com-golang-protobuf.zip拖入 进行解压到
$GOPATH/src/github.com/golang
(2)进入到文件夹内进行编译
cd $GOPATH/src/github.com/golang/protobuf/protoc-gen-go
go build
(3)将生成的protoc-gen-go可执行文件,放在/bin目录下
sudo cp protoc-gen-go /bin/
(4)尝试补齐protoc-gen-go 如果可以补齐代表成功,如果执行不报错 代表工具成功

3.3 protobuf 简单语法

参考文档(需翻墙):https://developers.google.com/protocol-buffers/docs/proto3

首先看一个简单的例子


syntax = "proto3";  //指定版本信息,不指定会报错
package pb; //后期生成go文件的包名
//message为关键字,作用为定义一种消息类型


// 定义枚举类型
enum Week {
  Monday = 0 ; // 枚举值,必须从 0 开始
  Turesday = 1 ;
}

// 定义消息体
message Person{
  // 可以不从1开始,但不能重复。
  string name = 1 ;
  int32 age = 2 ;
  bool sex = 3;
  // 数组
  repeated string test = 4;
  // 枚举
  Week w = 5 ;
  //联合体
  oneof data {
    string teacher = 6 ;
  }
  map <string,Person> test_map = 7;
}

//消息体可以嵌套
message Home{
  repeated Person persons = 1;
  message V {
    string name = 1;
    string name1 = 2;
    string name2 = 3;
  }
}


  • protobuf消息的定义(或者称为描述)通常都写在一个以.proto结尾的文件中。
  • 该文件的第一行指定正在使用proto3语法:如果不这样做,协议缓冲区编译器将假定正在使用proto2。这也必须是文件的第一个非空的非注释行。
  • 第二行package关键字定义一个Person消息体,类似于go语言中的结构体,是包含一系列类型数据的集合。许多标准的简单数据类型都可以作为字段类型,包括bool,int32,float,double,和string。也可以使用其他message类型作为字段类型。
  • 在message中有一个字符串类型的value成员,该成员编码时用1代替名字。我们知道,在json中是通过成员的名字来绑定对应的数据,但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,能够快速传输,缺点是不利于阅读。
message的格式说明

消息由至少一个字段组合而成,类似于Go语言中的结构体,每个字段都有一定的格式:

//注释格式 注释尽量也写在内容上方
(字段修饰符)数据类型 字段名称 = 唯一的编号标签值;

  • 唯一的编号标签:代表每个字段的一个唯一的编号标签,在同一个消息里不可以重复。这些编号标签用与在消息二进制格式中标识你的字段,并且消息一旦定义就不能更改。 需要说明的是标签在1-15范围的采用一个字节进行编码,所以通常是将标签1-15用于频繁发生的消息字段。 编号标签大小的范围是1-2的29次。19000-19999是官方预留的值,不能使用。
  • 注释格式:向.proto文件添加注释,可以使用C/C++/Java/Go风格的双斜杠(//)语法格式或者//

3.4编写的注意事项!

  • 1、message成员编号,可以不从1开始,但是不能重复。–不能使用19000-19999
  • 2、可以使用message嵌套。
  • 3、定义数组、切片使用repeated关键字
  • 4、 可以使用枚举enum
  • 5、 可以使用联合体。oneof关键字。成员编号,不能重复。

3.5编译protobuf


回顾:c++编译 命令:
protocol --cpp_out=./ *.proto —>xxx.pb.cc 和 xxx.pb.h 文件

  • go 语言中的编译命令
protoc --go_out=./ *proto // --->xxx.pb.go 文件

3.6添加rpc服务

  • 语法
service 服务名 {
	rpc 函数名 (参数:消息体)returns (返回值:消息)
}
message People {
	string name = 1 ;
}
message Student {
	int32 age = 2 ;
}
// 例:
service hello {
	rpc HelloWorld(People) returns (Student);
}
  • 知识点:
    • 默认protobuf,编译期间,不编译服务。要想使之编译。需要使用grpc
    • 使用的编译指令为:
      - protoc --go_out=plugins=grpc:./ *.protoprotoc --go-grpc_out=./ *.proto
  • 生成的xxx.pd.go文件与我们自己封装的rpc对比:

客户端

//--------------grpc
type bj38Client struct {
	cc grpc.ClientConnInterface
}
func NewBj38Client(cc grpc.ClientConnInterface) Bj38Client {
	return &bj38Client{cc}
}
func (c *bj38Client) Say(ctx context.Context, in *Person, opts ...grpc.CallOption) (*Home, error) {
	out := new(Home)
	err := c.cc.Invoke(ctx, "/pb.bj38/Say", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}
//-----------------rpc封装
//1、定义类
type MyClient struct {
	c *rpc.Client	
}
//2、绑定类方法
func (this *MyClient) HelloWorld (a string, b *string) error {
	return this.c.Call("hello.HelloWorld", a, b)	
}
//3、初始化客户端
func Init (addr string) {
	conn,_ := jsonrpc.Dail("tcp", addr)	
}

服务端

//--------------grpc
type Bj38Client interface {
	Say(ctx context.Context, in *Person, opts ...grpc.CallOption) (*Home, error)
}
func RegisterBj38Server(s grpc.ServiceRegistrar, srv Bj38Server) {
	s.RegisterService(&Bj38_ServiceDesc, srv)
}

//-----------------rpc封装
//1、定义接口
type xxx interface {
	方法名(传入参数, 传出参数) error 
}
//例: 
type MyIntercace Interface{
	HelloWorld(string, *string) error
}
//2、封装注册服务方法
func RegisterService (I MyInterface) {
	rpc.RegisterName("hello", I)
}


4、GRPC框架

GRPC是Google公司基于Protobuf开发的跨语言的开源RPC框架。GRPC基于HTTP/2协议设计,可以基于一个HTTP/2链接提供多个服务,对于移动设备更加友好。目前提供C、Java 和Go 语言版本,分别是:grpc, grpc-java,grpc-go.其中C版本支持 C, C++, Node.is, Python, Ruby, Objective-C, PHP 和C#支持.
在gRPC里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC系統类似,gRPC也是基于以下理念:

  • 定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。
  • 在服务端实现这个接口,并运行一个 gRPC服务器来处理客户端调用。

在客户端拥有一个存根能够像服务端一样的方法。gRPC客户端和服务端可以在多种环境中运行和交互-从google内部的服务器到你自己的笔记本,并旦可以用任何 gRPC支持的语言来编写。

所以,你可以很容易地用Java创建一个gRPC服务端,用G0、Python、Ruby来创建客户端。此外,Google最新 AP1将有gRPC版本的接口,使你很容易地将 Google的功能集成到你的应用里。

参考资料
gPC 官方文档中文版:http://doc.oschina.net/grpc2t=60133
gRPC官网:https:/lgrpcio

再详细了解使用GRPC之前先来了解一下上面定义中的一些关键词。

首先我们来看一下HTTP/2是什么内容?
其实本质上就是http2.0版本,http目前为止主要有四个版本,分别为http 1.0、 http.1.1、 http2.0、
https.

4.1grpc环境安装

方法1、官方推荐安装方法:

go get -u -v google.golang.org/grpc    -u updata更新。 -v view查看细节

但是由于一些原因,大部分同学不能直接访问google官网

方法2.通过github下载各种依赖库,然后配置。

git clone https://github.com/grpc/grpc-go.git
	$GOPATH/src/google.golang.org/grpc
git clone https://github.com/golang/net.git
	$GOPATH/src/golang.org/x/net
git clone https://github.com/golang/text.git
	$GOPATH/src/golang.org/x/text
git clone https://github.com/google/go-genproto.git
	$GOPATH/src/google.golang.org/genproto
cd $GOPATH/src/ 
go install google.golang.org/grpc

网络畅通可以用上达方法,但如果网速较慢,我们也可以选择离线安装方法。

方法3.用x.zip和 google.golang.org.zip 西介离袋包来安装。

#将x.zip 解压到 $GOPATH/SrC/golang.org/x 目录下
unzip x.zip -d $GOPATH/src/golang.org/x
3 #将google.golang.org.zip解压到 $GOPATH/src/google.golang.org 目录下
unzip google. golang.org. zip -d $GOPATH/src/google. golang.org
5 #然后进入到$GOPATH/srC/ google.golang.org/grpc下面执行go instal1
 go install

4.2GRPC使用

如果从Protobuf的角度看,GRPC只不过是一个针对service接口生成代码的生成器。接着我们来学习一下GRPC的用法。这里我们创建一个简单的proto文件,定义一个HelloService接口:

syntax = "proto3";
package pb;

message Person{
  //名字
  string name = 1 ;
  //年龄
  int32 age = 2 ;
}

//定义RPC服务
service HelloService {
  rpc Hello (Person)returns (Person);
}

对proto文件进行编译:

protoc --go_out=plugins=grpc:. * .proto

作业:grpc远程调用

person.proto

syntax = "proto3";
package person;
option go_package="pb/person;person";
//消息体
message PersonReq{
  string name = 1 ;
  int32 age = 2 ;
}

message PersonRes{
  string name = 1 ;
  int32 age = 2 ;
}
//定义RPC服务
service SearchService {
  rpc Search (PersonReq) returns (PersonRes);
  rpc SearchIn (stream PersonReq) returns (PersonRes);
  rpc SearchOut (PersonReq) returns (stream PersonRes);
  rpc SearchIO (stream PersonReq) returns (stream PersonRes);
}

build.bat

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./person/person.proto
  • 服务端grpc
    • 1、初始化一个grpc对象
    • 2、注册服务
    • 3、设置监听,指定ip、port
    • 4、启动服务。----serve()
package main

import (
	"awesomeProject2/pb/person"
	"context"
	"fmt"
	"google.golang.org/grpc"
	"net"
	"time"
)

type personServer struct {
	person.UnimplementedSearchServiceServer
}
func (*personServer) Search(ctx context.Context, req *person.PersonReq) (*person.PersonRes, error) {
	name := req.GetName()
	res := &person.PersonRes{Name:"我收到了"+name+"的信息"}
	return res, nil
}
func (*personServer) SearchIn(server person.SearchService_SearchInServer) error {
	for{
		req, err := server.Recv()
		fmt.Println("req : ", req)
		if err!= nil{
			server.SendAndClose(&person.PersonRes{Name: "完成了"})
			break
		}
	}
	return nil
}
func (*personServer) SearchOut(req *person.PersonReq, server person.SearchService_SearchOutServer) error {
	name := req.Name
	i := 0
	for {
		if i>10{
			break
		}
		time.Sleep(1*time.Second)
		server.Send(&person.PersonRes{Name: "我拿到了"+name})
		i++
	}
	return nil
}
func (*personServer) SearchIO(server person.SearchService_SearchIOServer) error {
	i := 0
	str := make(chan string)
	go func() {
	for {
		i++
		req, _ := server.Recv()
		if i >10 {
			str <- "结束"
			break
		}
		str <- req.Name
	}
	}()
	for{
		s := <- str
		if s == "结束"{
			server.Send(&person.PersonRes{Name: s})
			break
		}
		server.Send(&person.PersonRes{Name: s})
		fmt.Println(s)
	}
	return nil
}



func main() {
	 s := grpc.NewServer()
	 l,_ := net.Listen("tcp",":8888")
	 person.RegisterSearchServiceServer(s, &personServer{})
	 s.Serve(l)
}

  • 客户端grpc
    • 1、连接grpc服务
    • 2、初始化grpc客户端
    • 3、调用远程服务
package main

import (
	"awesomeProject2/pb/person"
	"context"
	"fmt"
	"google.golang.org/grpc"
	"sync"
	"time"
)

func main() {
	l,_ := grpc.Dial("127.0.0.1:8888", grpc.WithInsecure())
	client := person.NewSearchServiceClient(l)

	/* 1、普通传入参数、传出参数
	res,err := client.Search(context.Background(), &person.PersonReq{Name:"田毅"})
	if err != nil {
		fmt.Println("Search err", err)
		return
	}
	fmt.Println(res)
	 */

	/* 2、流式传入
	c, _ := client.SearchIn(context.Background())
	i := 0
	for {
		if i>10{
			res, _ := c.CloseAndRecv()
			fmt.Println("res:", res)
			break
		}
		time.Sleep(1*time.Second)
		c.Send(&person.PersonReq{Name: "我是进来的信息"})
		i++
	}
	 */

	 /*3、流式传出
	c, _ := client.SearchOut(context.Background(), &person.PersonReq{Name: "tianyi"})
	for {
		req, err := c.Recv()
		if err != nil {
			fmt.Println(err)
			break
		}
		fmt.Println(req)
	}
	 */
	  /* 4、 流式传入传出 */
	c, _ := client.SearchIO(context.Background())
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		for {
			time.Sleep(time.Second)
			err := c.Send(&person.PersonReq{Name: "田毅"})
			if err != nil{
				wg.Done()
				break
			}
		}
	}()
	go func() {
		for  {
			req, err := c.Recv()
			if err != nil{
				fmt.Println("Recv err:", err)
				wg.Done()
				break
			}
			fmt.Println(req)
		}
	}()
	wg.Wait()
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值