在本教程中,我们将介绍gRPC的基础知识,它是一个高性能、开源、通用的RPC框架,回顾一下Dart编程语言,并演示如何在Dart中建立一个gRPC服务器。
我们将引导你了解以下内容。
什么是gRPC?
gRPC是一个进程间通信(RPC)框架,由Google建立,于2015年发布。它是开源的,语言中立的,并且有一个紧凑的二进制大小。gRPC还支持HTTP/2,并且是跨平台的。
gRPC与传统的RPC非常不同,它使用协议缓冲区作为其IDL来定义其服务接口。协议缓冲区是一个由谷歌建立的序列化工具,它允许你定义你的数据结构,然后使用协议缓冲区编译器从这些数据结构生成源代码到你选择的语言。生成的语言被用来写和读数据结构到我们想要的任何上下文。根据官方文档,”协议缓冲区是谷歌的语言中立、平台中立、可扩展的结构化数据序列化机制–想想XML,但更小、更快、更简单。”
协议缓冲区用于编写服务定义接口,该接口用于定义数据结构和方法。数据结构就像Java等静态类型语言中的数据类型;它们告诉编译器/解释器打算如何使用这些数据。服务定义接口中的数据结构是将被传递给方法的参数类型和方法的返回类型。这个服务定义接口被保存在一个文本文件中,扩展名为.proto
。服务接口中的方法是gRPC服务器将公开的、由gRPC客户端调用的方法。
gRPC有三个组成部分。
server
托管方法的实现并监听客户端的请求protocol buffer
保存数据结构和方法的信息格式,包括其参数和返回类型client
调用服务器所托管的方法。客户端从proto
文件中的服务定义接口了解这些方法以及它们的返回和参数类型。
使用这个服务接口,gRPC服务器设置其服务器代码,实现服务接口中的方法。它设置自己并监听来自客户端的请求(方法调用)。
客户端使用服务定义接口来生成客户端存根。这个客户端子是服务器中的方法被调用的地方。一个gRPC客户端应用程序可以直接向服务器应用程序提出请求。客户端和服务器都拥护一个共同的接口,就像一个合同,它决定了每个操作要有哪些方法、类型和返回。
协议缓冲区如何工作
gRPC最吸引人的地方是它对协议缓冲区的使用,它使协议能够被平台诊断和多语言化。这意味着服务器可以用一种特定的语言编写,而客户端则用另一种语言开发。协议缓冲区使这成为可能,因为它有编译器,可以从其定义中的数据结构生成语言源代码。
例如,我们假设服务器要用JavaScript编写。我们将使用proto编译器,从.proto
文件中的定义生成JavaScript源代码。然后,服务器可以使用JavaScript代码访问和操作数据结构和方法。
对于客户端,我们希望它是用Java开发的,所以我们将从定义中生成Java源代码。然后,客户端可以使用Java代码调用这些方法并访问数据结构。这就是我们说gRPC是多语言和平台无关的意思。
请注意,协议缓冲区不仅被gRPC使用。它们也可以用于序列化。它通常用于通过流来发送数据,因此你可以在没有任何开销损失的情况下读写你的数据结构。
在Dart中建立一个gRPC服务器
现在我们了解了gRPC和协议缓冲区的基础知识,是时候在Dart中建立我们的gRPC服务器了。
在我们开始之前,请确保你的机器上安装了Dart SDK。Dart的可执行文件必须在你的系统中全局可用。运行下面的命令来检查。
➜ grpc-dart dart --version
Dart SDK version: 2.10.5 (stable) (Tue Jan 19 13:05:37 2021 +0100) on "macos_x64"
复制代码
我们还需要一些protoc工具。由于我们要用Dart开发gRPC服务器,我们必须安装Dart语言的proto编译器。这个编译器将从.proto
文件中的服务定义生成Dart源代码。
协议缓冲区编译器是一个命令行工具,用于编译.proto
文件中的IDL代码并为其生成指定的语言源代码。关于安装说明,请参阅gRPC文档。请确保下载版本3。
最后,protoc编译器的Dart插件可以从.proto
文件中的IDL代码生成Dart源代码。
对于Mac用户,通过运行以下命令来安装Dart protoc插件。
dart pub global activate protoc_plugin
复制代码
这将在你的机器中全面安装protoc_plugin
。
接下来,更新$PATH
,这样protoc
就会看到我们的插件。
export PATH="$PATH:$HOME/.pub-cache/bin"
复制代码
现在是创建服务器的时候了。
对于我们的演示,我们将创建一个gRPC服务器,管理一个图书服务。这个服务将暴露出一些方法,这些方法将被用来。
- 获取所有书籍 (
GetAllBooks
) - 通过其ID从服务器上获取一本书 (
GetBook
) - 删除一本书 (
DeleteBook
) - 编辑一本书 (
EditBook
) - 创建一本书 (
CreateBook
)
我们的Dart项目将是一个console-simple
项目。运行下面的命令来建立Dart项目的支架。
dart create --template=console-simple dart_grpc
复制代码
create
子命令告诉Dart可执行文件,我们希望创建一个Dart项目。--template=console-simple
告诉Dart执行文件,我们希望Dart项目是一个简单的控制台应用程序。
输出结果将如下。
Creating /Users/.../dart_grpc using template console-simple...
.gitignore
CHANGELOG.md
README.md
analysis_options.yaml
bin/dart_grpc.dart
pubspec.yaml
Running pub get... 10.2s
Resolving dependencies...
Downloading pedantic 1.9.2...
Downloading meta 1.2.4...
Changed 2 dependencies!
Created project dart_grpc! In order to get started, type:
cd dart_grpc
➜
复制代码
我们的项目将驻留在dart_grpc
文件夹中。
打开pubspec.yaml
文件。这就是我们设置Dart应用程序的配置和依赖关系的地方。我们要安装grpc
和protobuf
的依赖项。在pubspec.yaml
文件中添加以下一行,并保存。
dependencies:
grpc:
protobuf:
复制代码
现在,在你的控制台中运行pub get
,这样依赖性就安装好了。
编写服务定义
我们在.proto
文件中定义我们的服务定义。因此,让我们创建一个book.proto
文件。
touch book.proto
复制代码
在book.proto
文件中添加下面的Protobuf
代码。
syntax = "proto3";
service BookMethods {
rpc CreateBook(Book) returns (Book);
rpc GetAllBooks(Empty) returns (Books);
rpc GetBook(BookId) returns (Book);
rpc DeleteBook(BookId) returns (Empty) {};
rpc EditBook(Book) returns (Book) {};
}
message Empty {}
message BookId {
int32 id = 1;
}
message Book {
int32 id = 1;
string title = 2;
}
message Books {
repeated Book books = 1;
}
复制代码
那是很多的代码。让我们一行一行地看下去。
syntax = "proto3";
复制代码
在这里,我们告诉协议缓冲区编译器,我们将使用版本3的协议缓冲区语言。
service BookMethods {
rpc CreateBook(Book) returns (Book);
rpc GetAllBooks(Empty) returns (Books);
rpc GetBook(BookId) returns (Book);
rpc DeleteBook(BookId) returns (Empty) {};
rpc EditBook(Book) returns (Book) {};
}
复制代码
在这里,我们正在声明方法和它们所处的服务。service
关键字表示gRPC中的一个服务,所以我们创建一个服务BookMethods
。要调用一个方法,该方法必须被其服务所引用。这与class
和methods
相似;methods
是通过它们的类实例调用的。我们可以在一个proto中定义几个服务。
每个服务中的方法都由rpc
关键字来表示。rpc
告诉编译器,该方法是一个rpc
端点,将被暴露出来,并从客户端远程调用。在我们的定义中,我们在BookMethods
服务中有五个方法:CreateBook
、GetAllBooks
、GetBook
、DeleteBook
和EditBook
。
CreateBook
接受一个Book
数据类型作为参数,并返回一个Book
类型。这个方法的实现将创建一个新书GetAllBooks
接受一个Empty
类型作为参数,并返回一个Books
类型。它的实现将返回所有的书GetBook
方法接受一个类型为BookId
的输入参数,并返回一个Book
。它的实现将返回一个特定的书DeleteBook
方法接受一个BookId
类型作为输入参数,并返回一个Empty
类型。它的实现将从集合中删除一个图书条目EditBook
以一个Book
类型作为参数,并返回一个Book
类型。它的实现将修改集合中的一本书
从这一点开始,所有其他的数据都代表数据或消息类型。我们有。
message Empty {}
复制代码
message
关键字表示消息类型。每个消息类型都有字段,每个字段都有一个数字来唯一标识它在消息类型中的地位。
Empty
表示一个空的数据结构。当我们想不向rpc
方法发送任何参数或方法不返回任何值时,就会用到它。它与C/C++中的void
相同。
message BookId {
int32 id = 1;
}
复制代码
这个数据结构代表一个图书ID消息对象。id
字段将持有一个整数,由前面的int32
关键字决定。id
字段将持有一本书的ID。
message Book {
int32 id = 1;
string title = 2;
}
复制代码
这个数据结构代表一本书。id
字段持有该书的唯一ID,title
持有该书的标题。title
字段将是一个字符串,由前面的string
关键字来决定。
message Books {
repeated Book books = 1;
}
复制代码
这代表了一个书的数组。books
字段是一个存放书籍的数组。repeated
表示一个字段,将是一个列表或数组。字段名前的Book
表示该数组将是Book
类型。
现在我们已经完成了服务定义的编写,让我们来编译book.proto
文件。
编译proto
protoc工具是用来编译我们的.proto
文件的。请确保protoc工具在你的系统中是全局可用的。
protoc --version
libprotoc 3.15.8
复制代码
这是我写这篇文章时的protoc工具的版本。你的版本可能不同,这并不重要。
现在,确保你的终端在dart_grpc
根文件夹下打开。运行下面的命令来编译book.proto
文件。
protoc -I=. --dart_out=grpc:. book.proto
复制代码
I=.
告诉编译器我们要编译的源文件夹是proto
字段。
dart_out=grpc:.
子命令告诉protoc编译器,我们要从book.proto
的定义中生成Dart源代码,并将其用于gRPC=grpc:
。.
告诉编译器将dart文件写在我们操作的根文件夹中。
这个命令将生成以下文件。
book.pb.dart
book.pbenum.dart
book.pbgrpc.dart
book.pbjson.dart
最重要的文件是book.pb.dart
,它包含了book.proto
文件中消息数据结构的Dart源代码。它还包含了Empty
、BookId
、Book
和Books
的Dart类。从这些类中,我们创建它们的实例,并在调用rpc
方法时使用它们。
book.grpc.dart
文件包含了BookMethodClient
类,我们将用它来创建实例以调用rpc
方法和一个接口BookMethodsServiceBase
。这个接口将由服务器实现,以增加方法的实现。
接下来,我们将编写我们的服务器代码。
创建gRPC服务器
我们将在dart_grpc.dart
文件中编写我们的gRPC服务器代码。打开该文件并粘贴下面的代码。
import 'package:grpc/grpc.dart';
import 'package:grpc/src/server/call.dart';
import './../book.pb.dart';
import './../book.pbgrpc.dart';
class BookMethodsService extends BookMethodsServiceBase {
Books books = Books();
@override
Future<Book> createBook(ServiceCall call, Book request) async {
var book = Book();
book.title = request.title;
book.id = request.id;
books.books.add(book);
return book;
}
@override
Future<Books> getAllBooks(ServiceCall call, Empty request) async {
return books;
}
@override
Future<Book> getBook(ServiceCall call, BookId request) async {
var book = books.books.firstWhere((book) => book.id == request.id);
return book;
}
@override
Future<Empty> deleteBook(ServiceCall call, BookId request) async {
books.books.removeWhere((book) => book.id == request.id);
return Empty();
}
@override
Future<Book> editBook(ServiceCall call, Book request) async {
var book = books.books.firstWhere((book) => book.id == request.id);
book.title = request.title;
return book;
}
}
Future<void> main(List<String> args) async {
final server = Server(
[BookMethodsService()],
const <Interceptor>[],
CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]),
);
await server.serve(port: 50051);
print('Server listening on port ${server.port}...');
}
复制代码
多么大块的代码啊!它看起来令人生畏,但它比你想象的要简单。
第一部分是导入所需的文件。我们导入了grpc
代码和grpc
Dart代码。我们导入了book.pb.dart
和book.pbgrpc.dart
文件,因为我们需要其中的类。
下面,我们在BookMethodsService
中扩展了BookMethodsServiceBase
接口,为BookMethods
服务中的所有方法提供了实现。
在BookMethodsService
类中,我们覆盖了所有的方法以提供它们的实现。注意方法中的两个参数。第一个参数,ServiceCall call
,包含请求的元信息。第二个参数持有被发送的信息,也就是rpc
方法将接受的数据类型作为参数。
Books books = Books();
复制代码
上面的命令设置了一个books
数组。
在createBook
方法中,我们创建了一个新的Book
,设置了id
,title
,并将其添加到books
变量中的books
数组。
在getAllBooks
方法中,我们只是返回了books
变量。
在getBook
方法中,我们从BookId request
对象中获取了ID,并使用List#firstWhere
方法从books
数组中获取了书,然后返回。
在deleteBook
中,我们从BookId request
中获取了bookID,并使用它作为游标,使用List#removeWhere
方法从books
数组中删除书。
在editBook
方法中,request
参数包含Book
信息。我们从books
数组中检索图书,并将其title
属性值编辑为request
arg中发送的值。
最后,我们在main
函数中设置了服务器。我们将数组中的BookMethodsService
实例传递给Server
构造函数。然后,我们调用serve
方法,在端口50051
启动服务器。
现在让我们来构建客户端。
构建一个gRPC客户端
在bin
文件夹中创建一个client.dart
文件。
touch bin/client.dart
复制代码
打开它并粘贴以下代码。
import 'package:grpc/grpc.dart';
import './../book.pb.dart';
import './../book.pbgrpc.dart';
class Client {
ClientChannel channel;
BookMethodsClient stub;
Future<void> main(List<String> args) async {
channel = ClientChannel('localhost',
port: 50051,
options: // No credentials in this example
const ChannelOptions(credentials: ChannelCredentials.insecure()));
stub = BookMethodsClient(channel,
options: CallOptions(timeout: Duration(seconds: 30)));
try {
//...
var bookToAdd1 = Book();
bookToAdd1.id = 1;
bookToAdd1.title = "Things Fall Apart";
var addedBook1 = await stub.createBook(bookToAdd1);
print("Added a book: " + addedBook1.toString());
var bookToAdd2 = Book();
bookToAdd2.id = 2;
bookToAdd2.title = "No Longer at Ease";
var addedBook2 = await stub.createBook(bookToAdd2);
print("Added a book: " + addedBook2.toString());
var allBooks = await stub.getAllBooks(Empty());
print(allBooks.books.toString());
var bookToDel = BookId();
bookToDel.id = 2;
await stub.deleteBook(bookToDel);
print("Deleted Book with ID: " + 2.toString());
var allBooks2 = await stub.getAllBooks(Empty());
print(allBooks2.books);
var bookToEdit = Book();
bookToEdit.id = 1;
bookToEdit.title = "Beware Soul Brother";
await stub.editBook(bookToEdit);
var bookToGet = BookId();
bookToGet.id = 1;
var bookGotten = await stub.getBook(bookToGet);
print("Book Id 1 gotten: " + bookGotten.toString());
} catch (e) {
print(e);
}
await channel.shutdown();
}
}
main() {
var client = Client();
client.main([]);
}
复制代码
我们导入了grpc.dart
包以及book.pb.dart
和book.pbgrpc.dart
文件。我们创建了一个类Client
类。我们有一个BookMethodsClient stub
;stub
将保存BookMethodsClient
实例,在这里我们可以调用BookMethods
服务方法,以便在服务器中调用它们。
在main
方法中,我们创建了一个ClientChannel
实例,同时也创建了一个BookMethodsClient
实例,将ClientChannel
实例传给它的构造函数。BookMethodsClient
使用该实例来获取配置–例如,gRPC服务器将通过哪个端口到达。在我们的例子中,它是50051
和超时时间。
在try
语句体中,我们调用了我们的gPRC方法。首先,我们创建了一本标题为 “Things Fall Apart “的书,并给它分配了一个ID:1
。我们在stub
中调用了createBook
方法,将Book
实例bookToAdd1
作为参数传入该方法。这将在服务器中用addToAdd1
对象调用createBook
方法。
接下来,我们创建了一个新的图书实例,”No Longer at Ease”,ID为2
,并调用createBook
方法,传入图书实例。这就远程调用了gRPC服务器中的createBook
方法,并创建了一本新书。
我们调用getAllBooks
方法来获取服务器上的所有书籍。
接下来,我们建立了一个BooKId
对象,将其id设置为2
。然后,我们调用deleteBook
方法,
,传入BookId
对象。这就从服务器上删除了id为2
(”No Longer at Ease”)的书。
注意我们在哪里编辑一本书。我们创建了一个ID为1
,书名为 “Beware Soul Brother “的BookId
实例。我们想把ID为1
的书名改为 “Beware Soul Brother”,而不是 “Things Fall Apart”。所以我们调用editBook
方法,传入BookId
。
最后,我们使用它的ID来检索一本特定的书。我们创建了一个BookId
实例,其id
设置为1
。这意味着我们想获得ID为1
的书,它代表了 “Beware Soul Brother “这本书。因此,我们调用getBook
方法,传入BookId
实例。返回的结果应该是一个标题为 “当心灵魂兄弟 “的Book
对象。
做完这一切后,通过从其channel
实例中调用ClientChannel
中的shutdown
方法来关闭该通道。
测试服务器
现在是测试一切的时候了。首先,运行服务器。
➜ dart_grpc dart bin/dart_grpc.dart
Server listening on port 50051...
复制代码
打开另一个终端,运行客户端。
➜ dart_grpc dart bin/client.dart
Added a book: id: 1
title: Things Fall Apart
Added a book: id: 2
title: No Longer at Ease
[id: 1
title: Things Fall Apart
, id: 2
title: No Longer at Ease
]
Deleted Book with ID: 2
[id: 1
title: Things Fall Apart
]
Book Id 1 gotten: id: 1
title: Beware Soul Brother
➜ dart_grpc
复制代码
就是这样–我们的gRPC服务器正在按计划工作
这个例子的完整源代码可以在GitHub上找到。
总结
我们在本教程中涵盖了很多内容。我们首先介绍了gRPC,并解释了它从协议缓冲区到客户端的工作原理。
接下来,我们演示了如何为协议缓冲区编译器安装工具和插件。这些都是用来从proto定义中生成Dart源代码的。之后,我们走过了在Dart中创建一个实际的gRPC服务,建立一个gRPC客户端,并从客户端调用方法的过程。最后,我们测试了一切,发现它工作得很好。
gRPC是非常强大的,你可以通过自己玩耍发现更多的东西。本教程中的例子应该能让你打下坚实的基础。
The postHow to build a gRPC server in Dartappeared first onLogRocket Blog.