Golang beego从零开始基于thrift-0.12.0创建一个RPC服务,提供源码参考

Thrift最初由Facebook研发,主要用于各个服务之间的RPC通信,支持跨语言,常用的语言比如C++, Java, golang,Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml都支持。Thrift是一个典型的CS(客户端/服务端)结构,客户端和服务端可以使用不同的语言开发。

Thrift 是一种被广泛使用的 rpc 框架,可以比较灵活的定义数据结构和函数输入输出参数,并且可以跨语言调用。为了保证服务接口的统一性和可维护性,我们需要在最开始就制定一系列规范并严格遵守,降低后续维护成本。

我们公司主要用于C++,Java,golang跨语言调用。

Thrift IDL

本节介绍Thrift的接口定义语言,Thrift IDL支持的数据类型包含:

基本类型

thrift不支持无符号类型,因为很多编程语言不存在无符号类型,比如java

  • byte: 有符号字节
  • i16: 16位有符号整数
  • i32: 32位有符号整数
  • i64: 64位有符号整数
  • double: 64位浮点数
  • string: 字符串

容器类型

集合中的元素可以是除了service之外的任何类型,包括exception。

  • list<T>: 一系列由T类型的数据组成的有序列表,元素可以重复
  • set<T>: 一系列由T类型的数据组成的无序集合,元素不可重复
  • map<K, V>: 一个字典结构,key为K类型,value为V类型,相当于Java中的HMap<K,V>

结构体(struct)

就像C语言一样,thrift也支持struct类型,目的就是将一些数据聚合在一起,方便传输管理。struct的定 义形式如下:

struct People {

           1: string name;

           2: i32 age;

           3: string sex;

}

枚举(enum)

枚举的定义形式和Java的Enum定义差不多,例如:

enum Sex {

        MALE, FEMALE

}

异常(exception)

thrift支持自定义exception,规则和struct一样,如下:

exception RequestException {

      1: i32 code;

      2: string reason;

}

服务(service)

thrift定义服务相当于Java中创建Interface一样,创建的service经过代码生成命令之后就会生成客户端和服务端的框架代码。

定义形式如下:

service HelloWordService {

        // service中定义的函数,相当于Java interface中定义的函数

        string doAction(1: string name, 2: i32 age);

}

类型定义

thrift支持类似C++一样的typedef定义,比如:

typedef i32 Integer

typedef i64 Long

注意,末尾没有逗号或者分号

常量(const)

thrift也支持常量定义,使用const关键字,例如:

const i32 MAX_RETRIES_TIME = 10

const string MY_WEBSITE = "http://qifuguang.me";

末尾的分号是可选的,可有可无,并且支持16进制赋值

命名空间

thrift的命名空间相当于Java中的package的意思,主要目的是组织代码。thrift使用关键字namespace定义命名空间,例如:

namespace java com.winwill.thrift

格式是:namespace 语言名 路径, 注意末尾不能有分号。

文件包含

thrift也支持文件包含,相当于C/C++中的include,Java中的import。使用关键字include定义,例 如:

include "global.thrift"

注释

thrift注释方式支持shell风格的注释,支持C/C++风格的注释,即 # 和 // 开头的语句都单当做注释,/**/包裹的语句也是注释。

可选与必选

thrift提供两个关键字required,optional,分别用于表示对应的字段时必填的还是可选的。例如:

struct People {

         1: required string name;

         2: optional i32 age;

}

表示name是必填的,age是可选的。

 

Thrift开发流程是:先定义IDL,使用thrift工具生成目标语言接口(interface)代码,然后进行开发。

 

官网: http://thrift.apache.org/ 
github:https://github.com/apache/thr...

安装Thrift

将Thrift IDL文件编译成目标代码需要安装Thrift二进制工具。

Mac

建议直接使用brew安装,节省时间:

brew install thrift

安装后查看版本:

$ thrift -version

Thrift version 0.12.0

也可以下载源码安装,参考:http://thrift.apache.org/docs...

源码地址:http://www.apache.org/dyn/clo...

CentOS

需下载源码安装,参考:http://thrift.apache.org/docs...

Debian/Ubuntu

需下载源码安装,先安装依赖:http://thrift.apache.org/docs...,然后安装thrift:http://thrift.apache.org/docs...

Windows

可以直接下载二进制包thrift-0.12.0.exe;更多下载地址:https://www.apache.org/dyn/closer.cgi

实战-源码

该小节我们通过一个例子,讲述如何使用Thrift快速开发出一个RPC微服务,涉及到Golang服务端、Golang客户端。项目名就叫做T_go_RPC,代码托管在码云:https://gitee.com/chenthe1/T_go_RPC

推荐使用Golang服务端实现微服务,客户端实现调用。后续有时间会增加与Java的交互,敬请期待。

编写thrift IDL

thrift
├── Service.thrift
└── User.thrift

User.thrift

namespace go T_go_RPC.models.rpc

struct User {
    1:required i32 id;
    2:required string name;
    3:required string avatar;
    4:required string address;
    5:required string mobile;
}

struct UserList {
    1:required list<User> userList;
    2:required i32 page;
    3:required i32 limit;
}

Service.thrift

include "User.thrift"

namespace go T_go_RPC.models.rpc  //格式:namespace 语言名 路径,注意末尾不能有分号

typedef map<string, string> Data  // 类型定义

struct Response {
    1:required i32 errCode; //错误码
    2:required string errMsg; //错误信息
    3:required Data data;
}

//定义服务
service Greeter {
    Response SayHello(
        1:required User.User user
    )

    Response GetUser(
        1:required i32 uid
    )
}

说明: 
1、namespace用于标记各语言的命名空间或包名。每个语言都需要单独声明。
2、struct在Java、PHP里相当于class,golang里还是struct。 
3、service在Java、PHP里相当于interface,golang里是interfaceservice里定义的方法必须由服务端实现。 
4、typedef和c语言里的用法一致,用于重新定义类型的名称。 
5、struct里每个都是由1:required i32 errCode;结构组成,分表代表标识符、是否可选、类型、名称。单个struct里标识符不能重复,required表示该属性不能为空,i32表示int32。

接下来我们生产目标语言的代码:

#编译
$ ./thrift-0.12.0.exe --gen go ./Service.thrift

其它语言请参考上述示例编写。

编译成功后,生成的代码文件有:

gen-go
└── T_go_RPC\models\rpc
                    ├── GoUnusedProtection__.go
                    ├── Service-consts.go
                    ├── Service.go
                    ├── User-consts.go
                    ├── User.go
                    └── greeter-remote
                    └── greeter-remote.go
注:根据自己项目的路径,修改(T_go_RPC\models\rpc)对于自己的项目。

golang服务端

本节我们实行golang的服务端,需要实现的接口我们简单实现。本节参考了官方的例子,做了删减,官方的例子代码量有点多,而且是好几个文件,对新手不太友好。建议看完本节再去看官方示例。官方例子:https://github.com/apache/thr...

然后编写服务端代码: 
main.go

package main

import (
	"github.com/astaxie/beego"
	"T_go_RPC/models/rpc"
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"github.com/apache/thrift/lib/go/thrift"
	"os"
)

func Usage() {
	fmt.Fprint(os.Stderr, "Usage of ", os.Args[0], ":\n")
	flag.PrintDefaults()
	fmt.Fprint(os.Stderr, "\n")
}

//定义服务
type Greeter struct {
}

//实现IDL里定义的接口
//SayHello
func (this *Greeter) SayHello(ctx context.Context, u *rpc.User) (r *rpc.Response, err error) {
	strJson, _ := json.Marshal(u)
	return &rpc.Response{ErrCode: 0, ErrMsg: "success", Data: map[string]string{"User:": string(strJson)}}, nil
}

//GetUser
func (this *Greeter) GetUser(ctx context.Context, uid int32) (r *rpc.Response, err error) {
	return &rpc.Response{ErrCode: 1, ErrMsg: "this user not exist."}, nil
}

func main() {

	//命令行参数
	flag.Usage = Usage
	protocol := flag.String("P", "binary", "Specify the protocol (binary, compact, json, simplejson)")
	framed := flag.Bool("framed", false, "Use framed transport")
	buffered := flag.Bool("buffered", false, "Use buffered transport")
	addr := flag.String("addr", "localhost:9090", "Address to listen to")

	flag.Parse()

	//protocol
	var protocolFactory thrift.TProtocolFactory
	switch *protocol {
	case "compact":
		protocolFactory = thrift.NewTCompactProtocolFactory()
	case "simplejson":
		protocolFactory = thrift.NewTSimpleJSONProtocolFactory()
	case "json":
		protocolFactory = thrift.NewTJSONProtocolFactory()
	case "binary", "":
		protocolFactory = thrift.NewTBinaryProtocolFactoryDefault()
	default:
		fmt.Fprint(os.Stderr, "Invalid protocol specified", protocol, "\n")
		Usage()
		os.Exit(1)
	}

	//buffered
	var transportFactory thrift.TTransportFactory
	if *buffered {
		transportFactory = thrift.NewTBufferedTransportFactory(8192)
	} else {
		transportFactory = thrift.NewTTransportFactory()
	}

	//framed
	if *framed {
		transportFactory = thrift.NewTFramedTransportFactory(transportFactory)
	}

	//handler
	handler := &Greeter{}

	//transport,no secure
	var err error
	var transport thrift.TServerTransport
	transport, err = thrift.NewTServerSocket(*addr)
	if err != nil {
		fmt.Println("error running server:", err)
	}

	//processor
	processor := rpc.NewGreeterProcessor(handler)

	fmt.Println("Starting the simple server... on ", *addr)

	//start tcp server
	server := thrift.NewTSimpleServer4(processor, transport, transportFactory, protocolFactory)
	err = server.Serve()

	if err != nil {
		fmt.Println("error running server:", err)
	}


	beego.Run()
}

编译并运行,或者直接使用JetBrains客户端工具运行:

$ go run main.go
Starting the simple server... on  localhost:9090

客户端

我们先使用go test写客户端代码:
client_test.go

package main

import (
	"context"
	"fmt"
	"github.com/apache/thrift/lib/go/thrift"
	"testing"
	"T_go_RPC/models/rpc"
)

var ctx = context.Background()

func GetClient() *rpc.GreeterClient {
	addr := ":9090"
	var transport thrift.TTransport
	var err error
	transport, err = thrift.NewTSocket(addr)
	if err != nil {
		fmt.Println("Error opening socket:", err)
	}

	//protocol
	var protocolFactory thrift.TProtocolFactory
	protocolFactory = thrift.NewTBinaryProtocolFactoryDefault()

	//no buffered
	var transportFactory thrift.TTransportFactory
	transportFactory = thrift.NewTTransportFactory()

	transport, err = transportFactory.GetTransport(transport)
	if err != nil {
		fmt.Println("error running client:", err)
	}

	if err := transport.Open(); err != nil {
		fmt.Println("error running client:", err)
	}

	iprot := protocolFactory.GetProtocol(transport)
	oprot := protocolFactory.GetProtocol(transport)

	client := rpc.NewGreeterClient(thrift.NewTStandardClient(iprot, oprot))
	return client
}

//GetUser
func TestGetUser(t *testing.T) {
	client := GetClient()
	rep, err := client.GetUser(ctx, 100)
	if err != nil {
		t.Errorf("thrift err: %v\n", err)
	} else {
		t.Logf("Recevied: %v\n", rep)
	}
}

//SayHello
func TestSayHello(t *testing.T) {
	client := GetClient()

	user := &rpc.User{}
	user.ID = 2
	user.Name = "thrift"
	user.Address = "address"

	rep, err := client.SayHello(ctx, user)
	if err != nil {
		t.Errorf("thrift err: %v\n", err)
	} else {
		t.Logf("Recevied: %v\n", rep)
	}
}

首先确保服务端已运行,然后运行测试用例,或者直接使用JetBrains客户端工具运行:

$ go test -v

client_test.go:53: Recevied: Response({ErrCode:1 ErrMsg:this user not exist. Data:map[]})
client_test.go:70: Recevied: Response({ErrCode:0 ErrMsg:success Data:map[User::{"id":2,"name":"thrift","avatar":"","address":"address","mobile":""}]})

可以看到,客户端成功将请求发到了服务端,服务端成功地将请求结果返回给客户端,整个通信过程完成。

注意事项

  • 本文为作者个人理解,如理解有误,请留言相告,感激不尽;

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值