gRPC基于Golang和Java的简单实现

原文连接: 一文了解RPC以及gRPC基于Golang和Java的简单实现

一:什么是RPC

  • 简介:RPC:Remote Procedure Call,远程过程调用。简单来说就是两个进程之间的数据交互。正常服务端的接口服务是提供给用户端(在Web开发中就是浏览器)或者自身调用的,也就是本地过程调用。和本地过程调用相对的就是:假如两个服务端不在一个进程内怎么进行数据交互?使用RPC。尤其是现在微服务的大量实践,服务与服务之间的调用不可避免,RPC更显得尤为重要。

  • 原理:计算机的世界中不管使用哪种技术,核心都是对数据的操作。RPC不过是将数据的操作垮了一个维度而已。解决的问题本质上只是数据在不同进程间的传输。说的再多一些,就要了解网络模型的知识,七层也好,四层五层也罢。这个不是本文的重点。我们所说的RPC一般是指在传输层使用TCP协议进行的数据交互,也有很多基于HTTP的成熟框架。

    盗用网络上一张图片说明:

    gRPC流程

    上图描述了一个RPC的完整调用流程:

    1:client向client stub发起方法调用请求。

    2:client stub接收到请求后,将方法名,请求参数等信息进行编码序列化。

    3:client stub通过配置的ip和端口使用socket通过网络向远程服务器server发起请求。

    4:远程服务器server接收到请求,解码反序列化请求信息。

    5:server将请求信息交给server stub,server stub找到对应的本地真实方法实现。

    6:本地方法处理调用请求并将返回的数据交给server stub。

    7:server stub 将数据编码序列化交给操作系统内核,使用socket将数据返回。

    8:client端socket接收到远程服务器的返回信息。

    9:client stub将信息进行解码反序列化。

    10:client收到远程服务器返回的信息。

    上图中有一个stub(存根)的概念。stub负责接收本地方法调用,并将它们委托给各自的具体实现对象。server端stub又被称为skeleton(骨架)。可以理解为代理类。而实际上基于Java的RPC框架stub基本上也都是使用动态代理。我们所说的client端和server端在RPC中一般也都是相对的概念。

    而所谓的RPC框架也就是封装了上述流程中2-9的过程,让开发者调用远程方法就像调用本地方法一样。


二:常用RPC框架选型

  • Duboo:

    阿里开源的基于TCP的RPC框架,基本上是国内生产环境应用最广的开发框架了。使用zookeeper做服务的注册与发现,使用Netty做网络通信。遗憾的是不能跨语言,目前只支持Java。

  • Thrift:

    Facebook开源的跨语言的RPC框架,通过IDL来定义RPC的接口和数据类型,使用thrift编译器生成不同语言的实现。据说是目前性能最好的RPC框架,只是暂没使用过。

  • gRPC:

    这个是我们今天要聊的重点。gRPC是Google的开源产品,是跨语言的通用型RPC框架,使用Go语言编写。 Java语言的应用同样使用了Netty做网络通信,Go采用了Goroutine做网络通信。序列化方式采用了Google自己开源的Protobuf。请求的调用和返回使用HTTP2的Stream。

  • SpringCloud:

    SpringCloud并不能算一个RPC框架,它是Spring家族中一个微服务治理的解决方案,是一系列框架的集合。但在这个方案中,微服务之间的通信使用基于HTTP的Restful API,使用Eureka或Consul做服务注册与发现,使用声明式客户端Feign做服务的远程调用。这一系列的功能整合起来构成了一套完整的远程服务调用。

如何选择:

如果公司项目使用Java并不牵扯到跨语言,且规模并没有大到难以治理,我推荐Dubbo。如果项目规模大,服务调用错综复杂,我推荐SpringCloud。

如果牵扯到跨语言,我推荐gRPC,这也是目前我司的选择。即使Thrift性能是gRPC的2倍,但没办法,它有个好爹,现在我们的开发环境考虑最多的还是生态。


三:gRPC的原理

一个RPC框架必须有两个基础的组成部分:数据的序列化和进程数据通信的交互方式。

对于序列化gRPC采用了自家公司开源的Protobuf。什么是Protobuf?先看一句网络上 大部分的解释:

Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。

上句有几个关键点:它是一种数据存储格式,跨语言,跨平台,用于通讯协议和数据存储。

这么看和我们熟悉的JSON类似,但其实着重点有些本质的区别。JSON主要是用于数据的传输,因为它轻量级,可读性好,解析简单。Protobuf主要是用于跨语言的IDL,它除了和JSON、XML一样能定义结构体之外,还可以使用自描述格式定于出接口的特性,并可以使用针对不同语言的protocol编译器产生不同语言的stub类。所以天然的适用于跨语言的RPC框架中。

而关于进程间的通讯,无疑是Socket。Java方面gRPC同样使用了成熟的开源框架Netty。使用Netty Channel作为数据通道。传输协议使用了HTTP2。

通过以上的分析,我们可以将一个完整的gRPC流程总结为以下几步:

  • 通过.proto文件定义传输的接口和消息体。

  • 通过protocol编译器生成server端和client端的stub程序。

  • 将请求封装成HTTP2的Stream。

  • 通过Channel作为数据通信通道使用Socket进行数据传输。


四:代码的简单实现

概念永远都是枯燥的,只有实战才能真正理解问题。下面我们使用代码基于以上的步骤来实现一个简单gRPC。为了体现gRPC跨语言的特性,这次我们使用两种语言:Go实现server端,Java作为client端来实现。

1:安装Protocol Buffers,定义.proto文件

登录Google的 github下载对应Protocol Buffers版本。

安装完成后当我们执行protoc命令如果返回如下信息说明安装成功。

protoc

下面我们定义一个simple.proto文件,这也是后续我们实现gRPC的基础

syntax = "proto3"; //定义了我们使用的Protocol Buffers版本。

 //表明我们定义了一个命名为Simple的服务(接口),内部有一个远程rpc方法,名字为SayHello。
 //我们只要在server端实现这个接口,在实现类中书写我们的业务代码。在client端调用这个接口。
 service Simple{
    rpc SayHello(HelloRequest) returns (HelloReplay){}
 }

 //请求的结构体
 message HelloRequest{
     string name = 1;
 }
 //返回的结构体
 message HelloReplay{
     string message = 1;
 }

通过上面的注释可以看出此文件是一个简单的RPC远程方法描述。

2:使用Golang实现sever端

根据官方文档使用如下命令安装针对Go的gRPC:

$ go get -u google.golang.org/grpc

但是由于我们有伟大的长城,一般这条命令都不会下载成功。但Google的文件一般都会在github存有一份镜像。我们可以使用如下命令:

$ go get -u github.com/grpc/grpc-go

随后将下载的文件夹重命名为go,并放入一个新建的google.golang.org的文件夹中。🤷‍♀️

当我们安装完gRPC并定义好了远程接口调用的具体信息后,我们要使用protocol编译器生成我们的stub程序。

我们安装的Protocol Buffers是用来编译我们的.proto文件的,但是编译后的文件是不能被Java、C、Go等这些语言使用。Google针对不同的语言有不同的编译器。本次我们使用Golang语言,所以要安装针对Golang的编译器,根据官方提供的命令执行:

$ go get -u github.com/golang/protobuf/protoc-gen-go

但有可能我们会下载不成功,因为这个会依赖很多Golang的类库,这些类库和上面安装gRPC一样,鉴于墙的原因,还要执行一系列繁琐的改文件夹的步骤。但这个不是我们的重点,就不细说了。

安装成功之后我们就可以建立Go的project了。

本次我们建立一个grpc-server的项目,然后将前面写的simple.proto放入项目proto的package中。

随后在项目的目录下使用命令行执行如下命令:

protoc -I grpc-server/ proto/simple.proto --go_out=plugins=grpc:simple

这样就将simple.proto编译成了Go语言对应的stub程序了。

随后我们就可以写我们server端的代码了:main.go。

package main

import (
    "context"
    "grpc-server/proto"
    "fmt"
    "net"
    "log"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)

const(
    port = ":50051"
)

type server struct{}

func (s *server) SayHello(ctx context.Context,req *simple.HelloRequest) (*simple.HelloReplay, error){

    fmt.Println(req.Name)

    return &simple.HelloReplay{Message:"hello =======> " + req.Name},nil
}

func main(){
    lis,err := net.Listen("tcp",port)

    if err != nil {
        log.Fatal("fail to listen")
    }

    s := grpc.NewServer()

    simple.RegisterSimpleServer(s,&server{})

    reflection.Register(s)

    if err:= s.Serve(lis);err != nil{
        log.Fatal("fail to server")
    }
}

以上的代码都是模板代码,main函数是socket使用Go的标准实现。作为开发者我们只关注远程服务提供的具体接口实现即可。

最终我们的项目目录是这样的:

go-server

就这样一个使用Go语言实现的最简单server端就完成了。

3:使用Java实现client端

相对来说Java实现就简单一些,首先我们可以使用熟悉的Maven插件进行stub代码的生成。

新建一个grpc-client的父项目,两个子项目:client和lib。lib用于stub程序的代码生成。

lib项目编辑pom.xml,添加gRPC针对Java的插件编译器:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.js</groupId>
    <artifactId>grpc-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>client</module>
    </modules>

    <name>grpc-client</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <grpc.version>1.13.1</grpc.version>
        <springboot.version>2.0.4.RELEASE</springboot.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
                <version>${springboot.version}</version>
            </dependency>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-netty</artifactId>
                <version>${grpc.version}</version>
            </dependency>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-protobuf</artifactId>
                <version>${grpc.version}</version>
            </dependency>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-stub</artifactId>
                <version>${grpc.version}</version>
            </dependency>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${springboot.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

将定义好的simple.proto文件拷贝项目proto的package下。随后右键:Run Maven——compile。

maven

生成完成后将target下图中的两个文件拷贝到client项目目录中。

target

之后就是编写我们的业务代码进行gRPC的远程调用了。本次我们写一个简单的web程序模拟远程的调用。

定义一个class:SimpleClient:

package org.js.client.grpc;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

import java.util.concurrent.TimeUnit;

/**
 * @author JiaShun
 * @date 2018/8/11 12:11
 */
public class SimpleClient {
    private final ManagedChannel channel;
    private final SimpleGrpc.SimpleBlockingStub blockingStub;
    public SimpleClient(String host, int port){
        this(ManagedChannelBuilder.forAddress(host, port).usePlaintext());
    }

    private SimpleClient(ManagedChannelBuilder<?> channelBuilder){
        channel = channelBuilder.build();
        blockingStub = SimpleGrpc.newBlockingStub(channel);
    }

    public void shutdown()throws InterruptedException{
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }

    public String sayHello(String name){
        SimpleOuterClass.HelloRequest req = SimpleOuterClass.HelloRequest.newBuilder().setName(name).build();
        SimpleOuterClass.HelloReplay replay = blockingStub.sayHello(req);
        return replay.getMessage();
    }
}

基本都是模板代码。下面再编写一个简单的web请求:

controller代码:

package org.js.client.controller;

import org.js.client.service.IHelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author JiaShun
 * @date 2018/8/10 22:20
 */
@RestController
public class HelloController {
    @Autowired
    private IHelloService helloService;

    @GetMapping("/{name}")
    public String sayHello(@PathVariable String name){
        return helloService.sayHello(name);
    }
}

service实现类:

package org.js.client.service;

import org.js.client.grpc.SimpleClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

/**
 * @author JiaShun
 * @date 2018/8/10 22:22
 */
@Service
public class HelloServiceImpl implements IHelloService{
    private Logger logger = LoggerFactory.getLogger(HelloServiceImpl.class);
    @Value("${gRPC.host}")
    private String host;
    @Value("${gRPC.port}")
    private int port;

    @Override
    public String sayHello(String name) {
        SimpleClient client = new SimpleClient(host,port);
        String replay = client.sayHello(name);
        try {
            client.shutdown();
        } catch (InterruptedException e) {
            logger.error("channel关闭异常:err={}",e.getMessage());
        }
        return replay;
    }

}

就这么简单。

随后我们测试一下:

分别启动Go server端,Java client端。

gRPC-start

访问:http://localhost:8080/jiashun

gRPC-test

可以发现server端打印出了client端的请求,client端也收到了server端的返回。

完整代码:

server:https://github.com/jia-shun/grpc-server

client:https://github.com/jia-shun/grpc-client

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • GRPC是基于protocol buffers3.0协议的. 本文将向您介绍gRPC和protocol buffe...
    二月_春风阅读 17,982评论 2 28
  • 福贵是从败家后才开始真正意义上的活着的。 我常常喜欢用辩证的方法去看问题。比如幸与不幸。你看,福贵...
    杨沐云舒阅读 812评论 3 10
  • 标签放在<>中 是闭合的 有一个或多个属性 标签和属性名常用小写 常用属性 id 规定了属性在界面中唯一的标识 ...
    小九喵喵阅读 264评论 0 0
  • 开始于晚上公司的聚餐. 大家随意的聊着聊着,突然陷入了一种尴尬的情形. 我无法融入其中,无论是什么话题,我都觉得自...
    李方鸣阅读 237评论 0 0