btcWallet系列之一-grpc模块

btcwallet对外服务

btcwallet除了像btcd对外提供rpc服务以外,还提供了grpc服务,同时grpc采用的是protobuf来实现.
这方便与不同语言进行交互,降低客户端代码编写量.

阅读这个模块,顺便了解一下proto的使用,更详细的细节问题.

Service分类

总共有三种Service,分别是VersionService,WalletService和WalletLoaderService,
从中可以看出

VersionService

只是提供版本查询服务,为什么会做成一个独立的服务,设计者是出于什么考虑的呢?
这里重点考察grpc服务的启动过程

  1. walletMain函数中传递wallet.Loader调用startRPCServers
  2. 配置grpc所需参数,包括证书
  3. 创建grpcServer
  4. 通过rpcserver.StartVersionService注册VersionService
  5. 通过pb.RegisterVersionServiceServer 注册versionServer
    这里的RegisterVersionServiceServer是自动生成,
  6. versionServer实现了Version接口,对外提供服务

下面是proto自动生成的Service Description ,其中HandlerType为空,需要我们自己实现.

var _VersionService_serviceDesc = grpc.ServiceDesc{
	ServiceName: "walletrpc.VersionService",
	HandlerType: (*VersionServiceServer)(nil),
	Methods: []grpc.MethodDesc{
		{
			MethodName: "Version",
			Handler:    _VersionService_Version_Handler,
		},
	},
	Streams:  []grpc.StreamDesc{},
	Metadata: "api.proto",
}

VersionService client的实现

同时proto自动生成了客户端访问代码,

  1. 通过NewVersionServiceClient创建VersionServiceClient
  2. 通过VersionServiceClient的Version来访问


相关参数

grpc调用的所有参数都是通过Message来定义,
可以看出,虽然VersionRequest什么都没偶,还是要定义

message VersionRequest {}
message VersionResponse {
	string version_string = 1;
	uint32 major = 2;
	uint32 minor = 3;
	uint32 patch = 4;
	string prerelease = 5;
	string build_metadata = 6;
}

客户端和服务端的实现

客户端,由proto自动生成, 完全不用管理

type VersionServiceClient interface {
	Version(ctx context.Context, in *VersionRequest, opts ...grpc.CallOption) (*VersionResponse, error)
}

func (c *versionServiceClient) Version(ctx context.Context, in *VersionRequest, opts ...grpc.CallOption) (*VersionResponse, error) {
	out := new(VersionResponse)
	err := grpc.Invoke(ctx, "/walletrpc.VersionService/Version", in, out, c.cc, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

服务端

type VersionServiceServer interface {
	Version(context.Context, *VersionRequest) (*VersionResponse, error)
}

func (*versionServer) Version(ctx context.Context, req *pb.VersionRequest) (*pb.VersionResponse, error) {
	return &pb.VersionResponse{
		VersionString: semverString,
		Major:         semverMajor,
		Minor:         semverMinor,
		Patch:         semverPatch,
	}, nil
}

这里给的例子比较特殊,就是输入参数根本没用,不过看得出如何使用proto以及grpc了.

WalletLoaderService

此服务主要用于打开关闭钱包,
StartConsensusRpc是在btcwallet启动的时候没有指定btcd的情形下,可以连接指定的btcd.

service WalletLoaderService {
	rpc WalletExists (WalletExistsRequest) returns (WalletExistsResponse);
	rpc CreateWallet (CreateWalletRequest) returns (CreateWalletResponse);
	rpc OpenWallet (OpenWalletRequest) returns (OpenWalletResponse);
	rpc CloseWallet (CloseWalletRequest) returns (CloseWalletResponse);
	rpc StartConsensusRpc (StartConsensusRpcRequest) returns (StartConsensusRpcResponse);
}

WalletLoaderService启动方式和VersionService完全一致.

我的问题:
钱包不存在的时候只能通过--create创建完成以后再启动,是否这个服务目前根本没用?

核心服务WalletService

接口

service WalletService {
	// Queries
	rpc Ping (PingRequest) returns (PingResponse);
	rpc Network (NetworkRequest) returns (NetworkResponse);
	rpc AccountNumber (AccountNumberRequest) returns (AccountNumberResponse);
	rpc Accounts (AccountsRequest) returns (AccountsResponse);
	rpc Balance (BalanceRequest) returns (BalanceResponse);
	rpc GetTransactions (GetTransactionsRequest) returns (GetTransactionsResponse);

	// Notifications
	rpc TransactionNotifications (TransactionNotificationsRequest) returns (stream TransactionNotificationsResponse);
	rpc SpentnessNotifications (SpentnessNotificationsRequest) returns (stream SpentnessNotificationsResponse);
	rpc AccountNotifications (AccountNotificationsRequest) returns (stream AccountNotificationsResponse);

	// Control
	rpc ChangePassphrase (ChangePassphraseRequest) returns (ChangePassphraseResponse);
	rpc RenameAccount (RenameAccountRequest) returns (RenameAccountResponse);
	rpc NextAccount (NextAccountRequest) returns (NextAccountResponse);
	rpc NextAddress (NextAddressRequest) returns (NextAddressResponse);
	rpc ImportPrivateKey (ImportPrivateKeyRequest) returns (ImportPrivateKeyResponse);
	rpc FundTransaction (FundTransactionRequest) returns (FundTransactionResponse);
	rpc SignTransaction (SignTransactionRequest) returns (SignTransactionResponse);
	rpc PublishTransaction (PublishTransactionRequest) returns (PublishTransactionResponse);
}

###启动过程

  1. walletMain中等待钱包打开以后获取到钱包句柄,然后调用startWalletRPCServices
  2. 注意startWalletRPCServices传递进去三个参数,一个是钱包句柄,一个是grpc server,另一个是普通的http rpc server
  3. rpcserver.StartWalletService启动grpc WalletService
  4. legacyServer.RegisterWallet 注册http rpc服务
  5. pb.RegisterWalletServiceServer注册rpc.walletServer
  6. rpc.walletServer实现了接口
    go type WalletServiceServer interface { // Queries Ping(context.Context, *PingRequest) (*PingResponse, error) Network(context.Context, *NetworkRequest) (*NetworkResponse, error) AccountNumber(context.Context, *AccountNumberRequest) (*AccountNumberResponse, error) Accounts(context.Context, *AccountsRequest) (*AccountsResponse, error) Balance(context.Context, *BalanceRequest) (*BalanceResponse, error) GetTransactions(context.Context, *GetTransactionsRequest) (*GetTransactionsResponse, error) // Notifications TransactionNotifications(*TransactionNotificationsRequest, WalletService_TransactionNotificationsServer) error SpentnessNotifications(*SpentnessNotificationsRequest, WalletService_SpentnessNotificationsServer) error AccountNotifications(*AccountNotificationsRequest, WalletService_AccountNotificationsServer) error // Control ChangePassphrase(context.Context, *ChangePassphraseRequest) (*ChangePassphraseResponse, error) RenameAccount(context.Context, *RenameAccountRequest) (*RenameAccountResponse, error) NextAccount(context.Context, *NextAccountRequest) (*NextAccountResponse, error) NextAddress(context.Context, *NextAddressRequest) (*NextAddressResponse, error) ImportPrivateKey(context.Context, *ImportPrivateKeyRequest) (*ImportPrivateKeyResponse, error) FundTransaction(context.Context, *FundTransactionRequest) (*FundTransactionResponse, error) SignTransaction(context.Context, *SignTransactionRequest) (*SignTransactionResponse, error) PublishTransaction(context.Context, *PublishTransactionRequest) (*PublishTransactionResponse, error) }

stream返回的实现

stream就是持续不断的有返回的意思吧.
rpc TransactionNotifications (TransactionNotificationsRequest) returns (stream TransactionNotificationsResponse); proto中的接口被转换成了TransactionNotifications(*TransactionNotificationsRequest, WalletService_TransactionNotificationsServer) error
其中TransactionNotificationsResponse被转换成了

type WalletService_TransactionNotificationsServer interface {
	Send(*TransactionNotificationsResponse) error
	grpc.ServerStream
}

服务端TransactionNotifications实现

func (s *walletServer) TransactionNotifications(req *pb.TransactionNotificationsRequest,
	svr pb.WalletService_TransactionNotificationsServer) error {

	n := s.wallet.NtfnServer.TransactionNotifications()
	defer n.Done()

	ctxDone := svr.Context().Done()
	for {
		select {
		case v := <-n.C:
			resp := pb.TransactionNotificationsResponse{
				AttachedBlocks:           marshalBlocks(v.AttachedBlocks),
				DetachedBlocks:           marshalHashes(v.DetachedBlocks),
				UnminedTransactions:      marshalTransactionDetails(v.UnminedTransactions),
				UnminedTransactionHashes: marshalHashes(v.UnminedTransactionHashes),
			}
			err := svr.Send(&resp)
			if err != nil {
				return translateError(err)
			}

		case <-ctxDone:
			return nil
		}
	}
}

其他: 与http rpc服务的简单比较

通过代码实现对比就可以发现http rpc服务实现起来比较繁琐,各种客户端编解码需要自己处理,
不过从代码完善度来说,http接口明显更胜一筹,无论是注释还是测试case,包括api文档.

如果生产中使用,还是使用http rpc更好,如果熟悉代码的话,使用grpc更清晰.