图2-21 对比HTTP/1.1和HTTP/2的访问性能
我们都知道,在HTTP/1.x的协议中,浏览器在同一时间对同一域名下的请求数量是有限制的,这会导致大量并发请求阻塞,这个问题也被称为线端阻塞(head-of-line blocking)。同一域名下浏览器支持的连接数,如表2-4所示。HTTP/1.1对不同浏览器连接数的限制不同,很多互联网公司为了解决这个问题,做了大量优化,包括建立多域名,通过CDN缓存大量静态资源等。
HTTP/2是基于二进制协议的,与HTTP/1.x这样的文本协议相比,显然二进制协议性能更高。另外HTTP/2使用报头压缩,降低了网络开销。HTTP/2将HTTP协议通信分解为二进制编码帧的交换,这些帧对应着特定数据流中的消息。所有这些消息都在一个TCP连接内复用,这就是HTTP/2的多路复用机制(Multiplexing)。Benjamin在2015年写的一篇文章中描述了一个简单的例子,如果只请求3个资源,从Web页面开始渲染到加载结束,HTTP/2比HTTP/1.1节省不少时间,如图2-22所示。
表2-4 同一域名下浏览器支持的连接数
图2-22 HTTP/1.1和HTTP/2调用流程对比
HTTP/2完全兼容HTTP/1.1的语义,HTTP/2和HTTP/1.1的大部分高级语法(例如方法、状态码、头字段和URI)都是相同的。
HTTP/1.1如果要实现长连接,需要设置Connection:keep-alive来控制长连接时间,超时就断开TCP连接。只有在客户端发起请求的时候,服务器端才会响应。所以就算一直给服务器发送心跳包以维持长连接,也不能用来推送,只有客户端不断发起请求给服务器端,服务器才会响应,这就是pull轮询的方式。
HTTP/2引入服务端推送模式,即服务端向客户端发送数据,如图2-23所示。服务器可以对一个客户端请求发送多个响应,HTTP/2打破了严格的请求-响应语义,支持一次请求-多次响应的形式。由于现如今的Web界面丰富多彩,加载的资源往往非常多,服务端实际上已经知道要推送什么内容,但HTTP/1.x的语义只支持客户端发起请求、服务端响应数据。HTTP/2改变了这种模式,只需要客户端发送一次请求,服务端便把所有的资源都推送到客户端。服务器推送的缺点是,在客户端已经缓存了资源的情况下可能会有冗余。这个问题可以通过服务器提示(Server Hint)解决。
图2-23 HTTP/2推送模式
HTTP/2和Protobuf的组合——gRPCgRPC源于被称为Stubby的Google内部项目,Google内部大量使用Stubby进行服务间通信。作为gRPC的前身,Stubby大量依赖Google的其他基础服务,所以不太方便开放出来给社区使用。随着HTTP/2的逐步成熟,2015年初Google开源了gRPC框架。截至2017年12月,gRPC已经发布了1.7.3版本,并且被CNCF(云原生计算基金会)所收录。gRPC在ETCD/Kubernetes上得到了大量使用。
gRPC是基于HTTP/2设计的,因此也继承了HTTP/2相应的诸多特性,这些特性使得其在移动设备上表现得更好,更节省空间、更省电。gRPC目前提供的C、Java和Go语言版本分别是grpc、grpc-java、grpc-go,其中C版本支持C、C 、Node.js、Python、Ruby、Objective-C、PHP和C#。
说了这么多,gRPC到底能够给我们提供哪些优势呢?
· gRPC默认使用Protobuf进行序列化和反序列化,而Protobuf是已经被证明的高效的序列化方式,因此,gRPC的序列化性能是可以得到保障的。
· gRPC默认采用HTTP/2进行传输。HTTP/2支持流(streaming),在批量发送数据的场景下使用流可以显著提升性能——服务端和客户端在接收数据的时候,可以不必等所有的消息全收到后才开始响应,而是在接收到第一条消息的时候就可以及时响应。例如,客户端向服务端发送了一千条update消息,服务端不必等到所有消息接收完毕才开始处理,而是一边接收一边处理。这显然比以前的类HTTP 1.1的方式提供的响应更快、性能更优。gRPC的流可以分为三类:客户端流式发送、服务器流式返回,以及客户端/服务器同时流式处理,也就是单向流和双向流。在我写这本书的时候,Dubbo 3.0正在酝酿中,其中一个显著的变化是新版本将以streaming为内核,而不再是2.0时代的RPC,目的是去掉一切阻塞。
· 基于HTTP/2协议很容易实现负载均衡及流控的方案,可以利用Header做很多事情。
同时,gRPC也不是完美的。相比于非IDL描述的RPC(例如Hession、Kyro)方式,定义proto文件是一个比较麻烦的事情,而且需要额外安装客户端、插件等。另外HTTP/2相比于基于TCP的通信协议,性能上也有显著的差距。
下面通过一个简单的例子来理解一下gRPC的使用方式。假设我们要开发电商中的产品服务,通过id获取产品的信息,主要步骤及实现代码如下。
(1)定义proto文件。
syntax = "proto3";//声明支持的版本是proto3
option java_multiple_files = true;//以外部类模式生成
option java_package = "com.cloudnative.grpc";//声明包名,可选
option java_outer_classname="ProductProtos";//声明类名,可选
message ProductRequest{
int32 id = 1;
}
message ProductResponse {
int32 id = 1;
string name = 2;
string price = 3;
}
service ProductService{
rpc GetProduct(ProductRequest) returns(ProductResponse);
}
(2)生成相关类。可以采用Protobuf中介绍的方法,在命令行执行protoc生成相关代码。如果使用Maven,则可以通过Maven插件实现。
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<protocArtifact>com.google.protobuf:3.5.1:exe:${os.detected.classifier} </protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.8.0:exe:${os.detected. classifier}</pluginArtifact>
<protocExecutable>/usr/local/bin/protoc</protocExecutable>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
在pom.xml中配置,并且执行mvn compile命令会在target/generated-sources中生成相关类,可以将相关类移到src /main/java目录下备用。
(3)服务端实现代码。一是,实现ProductService。
public class ProductService extends ProductServiceGrpc.ProductServiceImplBase{
private static final Logger logger = Logger.getLogger(GRPCServer.class.getName());
@Override
public void getProduct(ProductRequest request, StreamObserver<ProductResponse> responseObserver) {
logger.info("接收到客户端的信息:" request.getId());
ProductResponse responsed;
if (111==request.getId()){
responsed=ProductResponse.newBuilder().setId(111).setName ("dddd").build();
}else {
responsed=ProductResponse.newBuilder().setId(0).setName("---").build();
}
responseObserver.onNext(responsed);
responseObserver.onCompleted();
}
}
二是,实现server代码。
public class GRPCServer{
private static final Logger logger = Logger.getLogger(GRPCServer.class.getName());
private final int port;
private final Server server;
public GRPCServer(int port){
this.port=port;
this.server = ServerBuilder.forPort(port)
.addService(new ProductService())
.build();
}
/** Start serving requests. */
public void start() throws IOException {
this.server.start();
logger.info("Server started, listening on " port);
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
// Use stderr here since the logger may has been reset by its JVM shutdown hook.
logger.info("*** shutting down gRPC server since JVM is shutting down");
GRPCServer.this.stop();
logger.info("*** server shut down");
}
});
}
/** Stop serving requests and shutdown resources. */
public void stop() {
if (server != null) {
server.shutdown();
}
}
/**
* Await termination on the main thread since the grpc library uses daemon threads.
*/
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
/**
* Main method. This comment makes the linter happy.
*/
public static void main(String[] args) throws Exception {
GRPCServer server = new GRPCServer(8888);
server.start();
server.blockUntilShutdown();
}
}
(4)客户端实现代码。
public class GRPCClient {
private static final Logger logger = Logger.getLogger(GRPCServer.class.getName());
public static void main(String[] args) {
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8888)
.usePlaintext(true)
.build();
ProductServiceGrpc.ProductServiceBlockingStub blockStub=ProductServiceGrpc.newBlockingStub(channel);
ProductResponse response=blockStub.getProduct(ProductRequest.newBuilder().setId(111).build());
logger.info(response.getName());
response=blockStub.getProduct(ProductRequest.newBuilder().setId(2).build());
logger.info(response.getName());
}
}
上面是一个简单的实现,关于流式RPC可以参考官方的例子。
来源 公众号 技术琐话 | 王启军
如有侵权请联系删除