互联网架构微服务已经成为主流,Go 语言如何去打造呢?

过去 20 年,是互联网高速发展的 20 年,互联网的架构也进行了一系列的演进。从最初的单体架构,发展成为水平切分架构,再到目前比较主流的微服务架构。当然我们并不是说现在不管做什么项目都要用微服务架构,这是显然不对的!不同的架构在不同的公司以及不同的时期,有不同的作用。

单体架构就是后端整体就是一个进程,所有的事情都一刀切搞定,这样的好处就是快速实现和部署,适合初期创业的公司。而一旦公司业务规模壮大了,原有的结构已经不满足需求了,这样也简单,把后端服务进行拆分,比如分成用户管理层,订单管理层,数据访问层等等。当规模再进一步壮大的时候,我们就该考虑系统架构变为微服务架构了,所有的功能都拆分成一个一个服务,然后封装成服务层,服务之间不直接联系,这就是简单描述了一下微服务架构。

微服务架构的核心问题其实就是实现 RPC 远程调用,服务器端开发了一个服务,客户端访问这个服务的时候可以像访问本地的接口一样。这个原理听上去很高大上,实际上用通俗的话来说就是,客户端发起一个请求的时候,服务端必须知道是什么请求,一说服务器端和客户端,很容易就想到了,这不就是网络编程嘛。既然是网络编程,而又想满足我们前面说的客户端要像调用本地接口一样,那这就需要定义一套协议了,双方都遵守相同的协议,说白了就是经过网络传输后的信息,大家都明白是什么意思,这样就可以继续往下操作了!对应的原理图就是下面这个样子:

和我们正常编写的网络通信没什么两样,只不过要借助特殊的协议。在 go 语言官方的 net/rpc 包已经支持了这样的协议,官方采用 gob 的编码形式,将请求序列化,服务器端再进行反序列化,然后将结果以序列化的形式再发给客户端,客户端再反序列化拿到数据结果。


package main


import (
“log”
“net”
“net/rpc”
)


//壳结构体,注册服务使用
type HelloService struct{}


//通过结构体封装的服务
func (p *HelloService) Hello(user string, reply *string) error {
*reply = “hello:” + user
return nil
}


func main() {


//注册以结构体为名称的整体服务
rpc.RegisterName(“HelloService”, new(HelloService))

//启动网络侦听
listener, err := net.Listen(“tcp”, “:8888”)
if err != nil {
log.Fatal(“ListenTCP error:”, err)
}

//等待连接请求
conn, err := listener.Accept()
if err != nil {
log.Fatal(“Accept error:”, err)
}


//把请求方伺候好
rpc.ServeConn(conn)
}

上述就是一个非常简单的通过官方 rpc 包编写的一个服务示例,这里有一个关键点是服务函数的样子:

func (p *HelloService) Hello(user string, reply *string) error {

必须长成这样模样,可以抽象一下:

func (p *TypeName) ServiceName(param1 type, reply *type) error {

这个函数有这样的要求:

·函数可导出(首字母大写)

·结构体可导出(首字母大写)

·只能是 2 个参数,参数类型必须是 go 语言原生类型或自定义类型

·返回值必须是 error

rpc.RegisterName 函数起到了关键的注册服务作用, rpc.ServeConn 则是对请求的网络连接提供服务,具体是什么服务,则由客户端的请求来决定,在程序中已经完全封装好了,客户端代码如下:


package main


import (
“fmt”
“log”
“net/rpc”
)


func main() {
//连接到服务器
client, err := rpc.Dial(“tcp”, “localhost:8888”)
defer client.Close()


var reply string
//同步调用远程服务
if client.Call(“HelloService.Hello”, “yekai”, &reply) != nil {
log.Panic(“failed to rpc-call “, err)
}


fmt.Println(“rpccall retutn:”, reply)
}

客户端使用 rpc 连接到服务器后,可以使用 Call 同步调用具体的服务,在代码里我们可以看到,第一个参数已经表明了服务是哪个结构体的哪个服务,第二个参数是传入参数,需要提前知道服务的请求参数,第三个参数是调用返回结果,也需要提前知道。

除了 rpc 这样的, go 语言还提供了 jsonrpc 的远程调用方式,他们的区别就是 rpc gob 序列化的方式, jsonrpc 则是 json 数据定义的方式,我们可以来看看具体代码。


/*
file : server.go
author : yekai
company : pdj(pdjedu.com)
*/
package main


import (
“log”
“net”
“net/rpc”
“net/rpc/jsonrpc”
)


//注意字段必须是导出
type Params struct {
Width, Height int
}


type Rect struct{}


func (r *Rect) Area(p Params, ret *int) error {
*ret = p.Width * p.Height
return nil
}


func (r *Rect) Perimeter(p Params, ret *int) error {
*ret = (p.Width + p.Height) * 2
return nil
}


func main() {
rect := new(Rect)
//注册rpc服务
rpc.Register(rect)
//获取tcpaddr
tcpaddr, err := net.ResolveTCPAddr(“tcp4”, “127.0.0.1:8080”)
if err != nil {
log.Panic(“failed to ResolveTCPAddr “, err)
}
//监听端口
tcplisten, err := net.ListenTCP(“tcp”, tcpaddr)
if err != nil {
log.Panic(“failed to ListenTCP “, err)
}
for {
conn, err := tcplisten.Accept()
if err != nil {
continue
}
//使用goroutine单独处理rpc连接请求
//这里使用jsonrpc进行处理
go jsonrpc.ServeConn(conn)
}
}

客户端代码如下:


/*
file : client.go
author : yekai
company : pdj(pdjedu.com)
*/
package main


import (
“fmt”
“log”
“net/rpc/jsonrpc”
)


//注意字段必须是导出
type Params struct {
Width, Height int
}


func main() {
//连接远程rpc服务
//这里使用jsonrpc.Dial
rpc, err := jsonrpc.Dial(“tcp”, “127.0.0.1:8080”)
if err != nil {
log.Fatal(err)
}
ret := 0
//调用远程方法
//注意第三个参数是指针类型
err = rpc.Call(“Rect.Area”, Params{50, 100}, &ret)
if err != nil {
log.Fatal(err)
}
fmt.Println(ret)
err = rpc.Call(“Rect.Perimeter”, Params{50, 100}, &ret)
if err != nil {
log.Fatal(err)
}
fmt.Println(ret)
}

这两种方式大家也都看到了,客户端和服务器端对于接口的定义还是必须一致的,否则无法调用到有效的服务。鉴于这个原因,很多公司又做出了研究,推出了 rpc 接口调用规范语言,比如 google 推出的 protobuf ,他可以很好的支持 google 推出的 rpc 框架 grpc

protobuf 很显然就是用来定义接口的,说白了也就是定义那个请求参数和返回结果类型的,不管是什么框架,都需要传递消息的,所谓消息就是结构。至于 proto 的语法我们就不去详细介绍了,虽然我也不太懂,不过可以看看具体的例子。


syntax = “proto3”;
package goprotoc;
message MsgValue {
string value = 1;
}


message MethodVal {
string method = 1;
int32 x = 2;
int32 y = 3;
}


message MethodReply {
string msg = 1;
int32 code = 2;
int32 reply = 3;
}

· syntax = “proto3”; 代表该文件编译的时候采用 proto3 协议,如果不写本行,则采用 proto2 协议进行编译。

· package  则代表了定义了编译好的包名叫什么(因为要用它生成 go 语言代码)。

· message  代表定义的参数结构体类型,每个参数可以用, 1 2 3 代表顺序编号

可以将上述代码保存为 test.proto ,最好将该文件存放在 goprotoc 目录下,然后可以用特殊的编译器去编译它,当然需要安装 protoc ,不过要支持 go 语言,需要安装 protoc-gen-go 插件,编译语法:

protoc ./test.proto --go_out=./

这样会成得到一个 test.pb.go 的文件,这个文件不需要我们去改!


// Code generated by protoc-gen-go. DO NOT EDIT.
// source: test.proto


package goprotoc


import (
fmt “fmt”
math “math”


proto “github.com/golang/protobuf/proto”
)


// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf


// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package


type MsgValue struct {
Value string `protobuf:”bytes,1,opt,name=value,proto3″ json:”value,omitempty”`
XXX_NoUnkeyedLiteral struct{} `json:”-“`
XXX_unrecognized []byte `json:”-“`
XXX_sizecache int32 `json:”-“`
}


func (m *MsgValue) Reset() { *m = MsgValue{} }
func (m *MsgValue) String() string { return proto.CompactTextString(m) }
func (*MsgValue) ProtoMessage() {}
func (*MsgValue) Descriptor() ([]byte, []int) {
return fileDescriptor_c161fcfdc0c3ff1e, []int{0}
}


func (m *MsgValue) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_MsgValue.Unmarshal(m, b)
}
func (m *MsgValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_MsgValue.Marshal(b, m, deterministic)
}
func (m *MsgValue) XXX_Merge(src proto.Message) {
xxx_messageInfo_MsgValue.Merge(m, src)
}
func (m *MsgValue) XXX_Size() int {
return xxx_messageInfo_MsgValue.Size(m)
}
func (m *MsgValue) XXX_DiscardUnknown() {
xxx_messageInfo_MsgValue.DiscardUnknown(m)
}


var xxx_messageInfo_MsgValue proto.InternalMessageInfo


func (m *MsgValue) GetValue() string {
if m != nil {
return m.Value
}
return “”
}


type MethodVal struct {
Method string `protobuf:”bytes,1,opt,name=method,proto3″ json:”method,omitempty”`
X int32 `protobuf:”varint,2,opt,name=x,proto3″ json:”x,omitempty”`
Y int32 `protobuf:”varint,3,opt,name=y,proto3″ json:”y,omitempty”`
XXX_NoUnkeyedLiteral struct{} `json:”-“`
XXX_unrecognized []byte `json:”-“`
XXX_sizecache int32 `json:”-“`
}


func (m *MethodVal) Reset() { *m = MethodVal{} }
func (m *MethodVal) String() string { return proto.CompactTextString(m) }
func (*MethodVal) ProtoMessage() {}
func (*MethodVal) Descriptor() ([]byte, []int) {
return fileDescriptor_c161fcfdc0c3ff1e, []int{1}
}


func (m *MethodVal) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_MethodVal.Unmarshal(m, b)
}
func (m *MethodVal) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_MethodVal.Marshal(b, m, deterministic)
}
func (m *MethodVal) XXX_Merge(src proto.Message) {
xxx_messageInfo_MethodVal.Merge(m, src)
}
func (m *MethodVal) XXX_Size() int {
return xxx_messageInfo_MethodVal.Size(m)
}
func (m *MethodVal) XXX_DiscardUnknown() {
xxx_messageInfo_MethodVal.DiscardUnknown(m)
}


var xxx_messageInfo_MethodVal proto.InternalMessageInfo


func (m *MethodVal) GetMethod() string {
if m != nil {
return m.Method
}
return “”
}


func (m *MethodVal) GetX() int32 {
if m != nil {
return m.X
}
return 0
}


func (m *MethodVal) GetY() int32 {
if m != nil {
return m.Y
}
return 0
}


type MethodReply struct {
Msg string `protobuf:”bytes,1,opt,name=msg,proto3″ json:”msg,omitempty”`
Code int32 `protobuf:”varint,2,opt,name=code,proto3″`
Reply int32 `protobuf:”varint,3,opt,name=reply,proto3″ json:”reply,omitempty”`
XXX_NoUnkeyedLiteral struct{} `json:”-“`
XXX_unrecognized []byte `json:”-“`
XXX_sizecache int32 `json:”-“`
}


func (m *MethodReply) Reset() { *m = MethodReply{} }
func (m *MethodReply) String() string { return proto.CompactTextString(m) }
func (*MethodReply) ProtoMessage() {}
func (*MethodReply) Descriptor() ([]byte, []int) {
return fileDescriptor_c161fcfdc0c3ff1e, []int{2}
}


func (m *MethodReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_MethodReply.Unmarshal(m, b)
}
func (m *MethodReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_MethodReply.Marshal(b, m, deterministic)
}
func (m *MethodReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_MethodReply.Merge(m, src)
}
func (m *MethodReply) XXX_Size() int {
return xxx_messageInfo_MethodReply.Size(m)
}
func (m *MethodReply) XXX_DiscardUnknown() {
xxx_messageInfo_MethodReply.DiscardUnknown(m)
}


var xxx_messageInfo_MethodReply proto.InternalMessageInfo


func (m *MethodReply) GetMsg() string {
if m != nil {
return m.Msg
}
return “”
}


func (m *MethodReply) GetCode() int32 {
if m != nil {
return m.Code
}
return 0
}


func (m *MethodReply) GetReply() int32 {
if m != nil {
return m.Reply
}
return 0
}


func init() {
proto.RegisterType((*MsgValue)(nil), “goprotoc.MsgValue”)
proto.RegisterType((*MethodVal)(nil), “goprotoc.MethodVal”)
proto.RegisterType((*MethodReply)(nil), “goprotoc.MethodReply”)
}


func init() { proto.RegisterFile(“test.proto”, fileDescriptor_c161fcfdc0c3ff1e) }


var fileDescriptor_c161fcfdc0c3ff1e = []byte{
// 163 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x2c, 0x8e, 0x31, 0x0f, 0x82, 0x30,
0x14, 0x84, 0x53, 0x11, 0x02, 0x4f, 0x07, 0xf3, 0x62, 0x0c, 0x63, 0xd3, 0x89, 0xc9, 0xc5, 0x1f,
0xe0, 0xec, 0xc0, 0xd2, 0x81, 0x1d, 0xa1, 0xa9, 0x43, 0x49, 0x09, 0xad, 0x86, 0xfe, 0x7b, 0xd3,
0xf6, 0x6d, 0xdf, 0x97, 0xdc, 0x5d, 0x0e, 0xc0, 0x2b, 0xe7, 0xef, 0xeb, 0x66, 0xbd, 0xc5, 0x5a,
0xdb, 0x04, 0x93, 0xe0, 0x50, 0xf7, 0x4e, 0x0f, 0xa3, 0xf9, 0x2a, 0xbc, 0x42, 0xf9, 0x8b, 0xd0,
0x32, 0xce, 0xba, 0x46, 0x66, 0x11, 0x4f, 0x68, 0x7a, 0xe5, 0x3f, 0x76, 0x1e, 0x46, 0x83, 0x37,
0xa8, 0x96, 0x24, 0x94, 0x21, 0xc3, 0x33, 0xb0, 0xbd, 0x3d, 0x70, 0xd6, 0x95, 0x92, 0xed, 0xd1,
0x42, 0x5b, 0x64, 0x0b, 0xe2, 0x05, 0xa7, 0x3c, 0x20, 0xd5, 0x6a, 0x02, 0x5e, 0xa0, 0x58, 0x9c,
0xa6, 0x7e, 0x44, 0x44, 0x38, 0x4e, 0x76, 0x56, 0xd4, 0x4f, 0x1c, 0xbf, 0x6c, 0x31, 0x4e, 0x33,
0x59, 0xde, 0x55, 0x7a, 0xfd, 0xf8, 0x07, 0x00, 0x00, 0xff, 0xff, 0xb7, 0x39, 0x6e, 0xc6, 0xcc,
0x00, 0x00, 0x00,
}

我们再新建一个目录,编写一个 server.go 文件


/*
file : server.go
author : yekai
company : pdj(pdjedu.com)
*/
package main


import (
“errors”
“log”
“net”
“net/rpc”
“tutorial-go/rpc/proto”
)


type HelloService struct{}


func (p *HelloService) Hello(request *goprotoc.MsgValue, reply *goprotoc.MsgValue) error {
reply.Value = “hello:” + request.GetValue()
return nil
}


func (p *HelloService) Method(request *goprotoc.MethodVal, reply *goprotoc.MethodReply) error {
//reply.Value = “hello:” + request.GetValue()
reply.Code = 0
reply.Msg = “OK”
switch request.GetMethod() {
case “add”:
reply.Reply = request.GetX() + request.GetY()
case “mul”:
reply.Reply = request.GetX() * request.GetY()
case “div”:
if request.GetY() == 0 {
reply.Code = 100
reply.Msg = “Divisor is zero”
return errors.New(“params err”)
}
reply.Reply = request.GetX() / request.GetY()
case “sub”:
reply.Reply = request.GetX() – request.GetY()
default:
reply.Code = 404
reply.Msg = “resource func err”
return errors.New(“func err”)
}
return nil
}


func main() {
rpc.RegisterName(“HelloService”, new(HelloService))


listener, err := net.Listen(“tcp”, “:1234”)
if err != nil {
log.Fatal(“ListenTCP error:”, err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal(“Accept error:”, err)
}
go rpc.ServeConn(conn)
}


}

运行起来:


localhost:prototest yekai$ go run server.go
再编写一个client.go文件


package main


import (
“fmt”
“log”
“net/rpc”
“tutorial-go/rpc/proto”
)


func main() {
client, err := rpc.Dial(“tcp”, “localhost:1234”)
if err != nil {
log.Fatal(“dialing:”, err)
}


var reply = &goprotoc.MsgValue{}
var param = &goprotoc.MsgValue{
Value: “yekai”,
}


err = client.Call(“HelloService.Hello”, &param, &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)


var reply1 = &goprotoc.MethodReply{}
var param1 = &goprotoc.MethodVal{
Method: “mul”,
X: 10,
Y: 23,
}


err = client.Call(“HelloService.Method”, &param1, &reply1)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply1)


}


再运行客户端


localhost:prototest yekai$ go run client.go
value:“hello:yekai”

msg:“OK” reply:230

好,本文主要介绍了 go 语言如何实现 rpc 调用,以及如何使用 jsonrpc ,如何使用 proto 协议去实现远程调用,这是为了实现微服务框架的基础,当然我们为了实现微服务架构,最后多数都会选择一种 rpc 框架进行使用,本文就介绍到这里。

课后福利:

关注更多GO语言公开课视频教学,请锁定柏链项目学院( http://pdjedu.com

在柏链项目学院官网注册,私信客服,发送用户名,即可领取免费的Go语言和区块链VIP课程视频!

扫码进入Go语言学习社群,和高野讨论学习疑问!

扫码关注此号,了解更多IT资讯和技能干货!