Grpc, a framework different from REST !|Python 主題月

本文正在參加「Python主題月」,詳情查看活動連接java

GRPC

什麼是GRPC?

隨着微服務的流行,GRPC的發展勢頭也愈來愈盛。咱們的後端項目,可能有不少微服務,而這些服務也許是有不少語言寫的。好比身份驗證的服務是Java,而消息通知的服務是Python,其餘業務使用C等等。既然是微服務,咱們這些服務之間獲取還須要互相調用來協做完成業務,那麼不一樣語言之間如何調用呢?GRPC就是這樣的一種通訊工具幫助你,讓不一樣語言的程序能夠互相交流。不須要你去處理語言之間的障礙,讓你看起來就像調用本身寫的函數同樣簡單。node

GRPC顧名思義就是遠程調用,爲何呢?來自官方文檔的一句話:gRPC Remote Procedure Calls。python

GRPC與REST的區別

  • REST是基於http/1.1,而GRPC是基於http/2。GRPC相對於REST要快不少。
  • 消息傳輸上,REST是JSON/XML,而GRPC是Protobuf,以二進制的形式傳輸,因此相對於JSON/XML要小不少。
  • GRPC API接口是很是嚴格的,必須明確的在proto文件中定義,REST則無需這樣作。
  • GRPC代碼的生成可使用協議緩衝區編譯器自動生成在GRPC項目內部,而REST須要藉助三方工具(Swagger、OpenAPI)
  • GRPC能夠通訊流是雙向的,而REST是單向的。
  • REST支持瀏覽器,而GRPC不支持,因此目前RPC 最經常使用的場景是 IOT 等硬件領域。

使用GRPC的前提

你必須知道如下幾個概念git

  • Protocol Buffers
  • 簡單流(Unary RPC)
  • 客戶端流(Server streaming RPC)
  • 服務端流(Client streaming RPC)
  • 雙向流(Bidirectional streaming RPC)

GRPC in Python

下面以簡單流爲例,其餘流可參考官方代碼,各類語言的都能在這個倉庫找到,routeguide這個示例包含了全部類型的grpc服務。使用注意:使用python開發前你必須安裝grpcio-tools、grpcio。github

python -m pip install grpcio
python -m pip install grpcio-tools
複製代碼

一、編寫proto文件

首先根據Protocol Buffers文件,來定義約束咱們的服務。咱們仍是以咱們最多見的helloworld爲例。在項目根目錄下建立文件夾protos,並在該文件夾下建立helloworld.proto文件,內容以下。spring

syntax = "proto3";
// To be compatible with Java configuration, it does not work in Python
// If false, only a single .java file will be generated for this .proto file.
// If true, separate .java files will be generated for each of the Java classes/enums/etc.
option java_multiple_files = true;
// The package you want to use for your generated Java/Kotlin classes.
option java_package = "com.wangscaler.examples.helloworld";
// The class name (and hence the file name) for the wrapper Java class you want to generate.
option java_outer_classname = "HelloWorldProto";
​
​
package helloworld;
// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  // Sends another greeting
  rpc SayAuthor (AuthorRequest) returns (AuthorReply) {}
}
​
// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}
​
// The request message containing the user's name.
message AuthorRequest {
  string name = 1;
  int32 age = 2;
}
​
// The response message containing the greetings
message HelloReply {
  string message = 1; } // The response message containing the greetings message AuthorReply {
  string message = 1;  int32 code = 2; } 複製代碼

咱們定義了一個Greeter服務,而且這個服務提供了兩個接口SayHello和SayAuthor。分別給這兩個接口的請求參數和響應參數作了限制。後端

不一樣的是SayHello的入參是一個字符串類型的,響應參數也是一個字符串類型的;而AuthorReply入參多了一個int類型的age,而響應參數也多了個int類型的code。瀏覽器

二、自動生成代碼

在項目根目錄下執行springboot

python -m grpc_tools.protoc -I./protos --python_out=. --grpc_python_out=. ./protos/helloworld.proto
複製代碼

執行完以後,會在項目根路徑下生成helloworld_pb2.py、helloworld_pb2_grpc.py兩個python文件。bash

三、開發服務端

定義Greeter去繼承helloworld_pb2_grpc.GreeterServicer,重寫父類的SayHello、SayAuthor來實現咱們的業務。

from concurrent import futures
import logging
import grpc
import helloworld_pb2
import helloworld_pb2_grpc
​
​
class Greeter(helloworld_pb2_grpc.GreeterServicer):
​
    def SayHello(self, request, context):
        print("Get a message from %s client" % request.name)
        return helloworld_pb2.HelloReply(message='Hello world, %s client !' % request.name)
​
    def SayAuthor(self, request, context):
        print("Hi author(%(name)s), your age is %(age)d" % {"name": request.name, "age": request.age})
        return helloworld_pb2.AuthorReply(
            message='Hello, %s ! ' % request.name, code=0)
​
​
def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()
​
​
if __name__ == '__main__':
    logging.basicConfig()
    serve()
​
​
複製代碼

四、開發客戶端(python)

import logging
import grpc
import helloworld_pb2
import helloworld_pb2_grpc
​
​
def run():
    # NOTE(gRPC Python Team): .close() is possible on a channel and should be
    # used in circumstances in which the with statement does not fit the needs
    # of the code.
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = helloworld_pb2_grpc.GreeterStub(channel)
        hello_response = stub.SayHello(helloworld_pb2.HelloRequest(name='python'))
        author_response = stub.SayAuthor(helloworld_pb2.AuthorRequest(name='scaler', age=18))
    print("Greeter client received: " + hello_response.message)
    print("Greeter client received message: %(message)s and received code: %(code)d !" % {
        "message": author_response.message,
        "code": author_response.code})
​
​
if __name__ == '__main__':
    logging.basicConfig()
    run()
​
複製代碼

執行以後控制檯打印的消息以下:

Greeter client received: Hello world, python client !
Greeter client received message: Hello, scaler !  and received code: 0 !
複製代碼

而咱們服務器端的打印消息以下:

Get a message from python client
Hi author(scaler), your age is 18
複製代碼

GRPC Java client

一開始咱們就說了,GRPC能夠兼容多語言的調用,因此咱們的java客戶端也是能夠調用的上面的python的服務器端的SayHello和SayAuthor接口。

一、建立Maven項目

自行使用IDEA建立,不過多介紹。

二、複製上述的proto文件

在maven項目的src/main文件夾下建立proto文件夾,並將上述python中建立的proro文件,複製到這個文件夾下。

三、自動生成代碼

使用mvn compile命令,將在target/generated-sources/protobuf/grpc-javatarget/generated-sources/protobuf/java生成咱們須要的文件。

文件的包名就是咱們proto文件中指定的java_package。

四、開發客戶端(java)

在src/main/java下建立包和java_package一致,即和生成的代碼的包名保持一致。

package com.wangscaler.examples;
​
import io.grpc.Channel;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
​
import java.text.MessageFormat;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
​
/** * @author WangScaler * @date 2021/7/22 16:36 */
​
​
public class Client {
    private static final Logger logger = Logger.getLogger(Client.class.getName());
    private final GreeterGrpc.GreeterBlockingStub blockingStub;
​
    public Client(Channel channel) {
        blockingStub = GreeterGrpc.newBlockingStub(channel);
    }
​
    public void greet(String name) {
        logger.info("Will try to greet " + name + " ...");
        HelloRequest helloRequest = HelloRequest.newBuilder().setName(name).build();
        AuthorRequest authorRequest = AuthorRequest.newBuilder().setName("wangscaler").setAge(18).build();
        HelloReply helloResponse;
        AuthorReply authorResponse;
        try {
            helloResponse = blockingStub.sayHello(helloRequest);
            authorResponse = blockingStub.sayAuthor(authorRequest);
        } catch (StatusRuntimeException e) {
            logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
            return;
        }
        logger.info("Greeter client received: " + helloResponse.getMessage());
        logger.info(MessageFormat.format("Greeter client received message: {0} and received code: {1} ! ", authorResponse.getMessage(), authorResponse.getCode()));
    }
​
    public static void main(String[] args) throws Exception {
        String user = "java";
        String target = "localhost:50010";
        if (args.length > 0) {
            if ("--help".equals(args[0])) {
                System.err.println("Usage: [name [target]]");
                System.err.println("");
                System.err.println(" name   The name you wish to be greeted by. Defaults to " + user);
                System.err.println(" target The server to connect to. Defaults to " + target);
                System.exit(1);
            }
            user = args[0];
        }
        if (args.length > 1) {
            target = args[1];
        }
​
        ManagedChannel channel = ManagedChannelBuilder.forTarget(target)
                .usePlaintext()
                .build();
        try {
            Client client = new Client(channel);
            client.greet(user);
        } finally {
            channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
        }
    }
}
​
複製代碼

執行以後控制檯打印的消息以下:

信息: Will try to greet java ...
七月 22, 2021 5:00:17 下午 com.wangscaler.examples.Client greet
信息: Greeter client received: Hello world, java client !
七月 22, 2021 5:00:17 下午 com.wangscaler.examples.Client greet
信息: Greeter client received message: Hello, wangscaler !  and received code: 0 ! 
複製代碼

而咱們python服務器端的打印消息以下:

Get a message from java client
Hi author(wangscaler), your age is 18
複製代碼

Cool!咱們的Java客戶端像調用本身內部的函數同樣,調用了遠程的python服務器上的方法。這就是GRPC的強大之處。微服務之間的調用就像這同樣輕鬆簡單。

使用SSL身份驗證

一、生成根證書和私鑰

使用openssl生成ssl證書。

openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt
複製代碼

注意:執行命令以後須要輸入你的相關信息,若是你是在ssl本地測試,切記CN的值爲localhost,此時你的客戶端才能夠經過localhost訪問你的服務器。

二、修改服務器端

from concurrent import futures
import logging
import grpc
import helloworld_pb2
import helloworld_pb2_grpc
​
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
​
​
class Greeter(helloworld_pb2_grpc.GreeterServicer):
​
    def SayHello(self, request, context):
        print("Get a message from %s client" % request.name)
        return helloworld_pb2.HelloReply(message='Hello world, %s client !' % request.name)
​
    def SayAuthor(self, request, context):
        print("Hi author(%(name)s), your age is %(age)d" % {"name": request.name, "age": request.age})
        return helloworld_pb2.AuthorReply(
            message='Hello, %s ! ' % request.name, code=0)
​
​
def serve():
    with open('server.key', 'rb') as f:
        private_key = f.read()
    with open('server.crt', 'rb') as f:
        certificate_chain = f.read()
    server_credentials = grpc.ssl_server_credentials(
        ((private_key, certificate_chain,),))
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    server.add_secure_port('[::]:50051', server_credentials)
    server.start()
    server.wait_for_termination()
​
​
if __name__ == '__main__':
    logging.basicConfig()
    serve()
​
複製代碼

至此咱們的GRPC,就加入了SSL身份驗證。

三、修改客戶端(python)

咱們使用以前的客戶端去鏈接,發現鏈接失敗,以下圖所示。

image-20210727092307787.png 接下來,修改以下:

首先將openssl生成的server.key和server.crt複製到項目的根路徑下。最後修改代碼以下:

import logging
import grpc
import helloworld_pb2
import helloworld_pb2_grpc
​
​
def run():
    with open('server.crt', 'rb') as f:
        trusted_certs = f.read()
    credentials = grpc.ssl_channel_credentials(root_certificates=trusted_certs)
    with grpc.secure_channel('localhost:50051', credentials) as channel:
        stub = helloworld_pb2_grpc.GreeterStub(channel)
        hello_response = stub.SayHello(helloworld_pb2.HelloRequest(name='python'))
        author_response = stub.SayAuthor(helloworld_pb2.AuthorRequest(name='scaler', age=18))
    print("Greeter client received: " + hello_response.message)
    print("Greeter client received message: %(message)s and received code: %(code)d !" % {
        "message": author_response.message,
        "code": author_response.code})
​
​
if __name__ == '__main__':
    logging.basicConfig()
    run()
複製代碼

再次運行客戶端,控制檯正常打印

Greeter client received: Hello world, python client !
Greeter client received message: Hello, scaler !  and received code: 0 !
複製代碼

四、修改客戶端(java)

修改以前測試鏈接。報錯以下:

RPC failed: Status{code=UNAVAILABLE, description=Network closed for unknown reason, cause=null}
複製代碼

修改以下,將openssl生成的server.crt複製到項目路徑下,能找到就能夠,不復制也行。

package com.wangscaler.examples;
​
import io.grpc.Channel;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NegotiationType;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
​
import javax.net.ssl.SSLException;
import java.io.File;
import java.text.MessageFormat;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
​
/** * @author WangScaler * @date 2021/7/22 16:36 */
​
​
public class Client {
    private static final Logger logger = Logger.getLogger(Client.class.getName());
    private final GreeterGrpc.GreeterBlockingStub blockingStub;
​
    public Client(Channel channel) {
        blockingStub = GreeterGrpc.newBlockingStub(channel);
    }
​
    public void greet(String name) {
        logger.info("Will try to greet " + name + " ...");
        HelloRequest helloRequest = HelloRequest.newBuilder().setName(name).build();
        AuthorRequest authorRequest = AuthorRequest.newBuilder().setName("wangscaler").setAge(18).build();
        HelloReply helloResponse;
        AuthorReply authorResponse;
        try {
            helloResponse = blockingStub.sayHello(helloRequest);
            authorResponse = blockingStub.sayAuthor(authorRequest);
        } catch (StatusRuntimeException e) {
            logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
            return;
        }
        logger.info("Greeter client received: " + helloResponse.getMessage());
        logger.info(MessageFormat.format("Greeter client received message: {0} and received code: {1} ! ", authorResponse.getMessage(), authorResponse.getCode()));
    }
​
    private static SslContext buildSslContext(String trustCertCollectionFilePath) throws SSLException {
        SslContextBuilder builder = GrpcSslContexts.forClient();
        if (trustCertCollectionFilePath != null) {
            builder.trustManager(new File(trustCertCollectionFilePath));
        }
        return builder.build();
    }
​
    public static void main(String[] args) throws Exception {
        String user = "java";
        String target = "localhost:50010";
        if (args.length > 0) {
            if ("--help".equals(args[0])) {
                System.err.println("Usage: [name [target]]");
                System.err.println("");
                System.err.println(" name   The name you wish to be greeted by. Defaults to " + user);
                System.err.println(" target The server to connect to. Defaults to " + target);
                System.exit(1);
            }
            user = args[0];
        }
        if (args.length > 1) {
            target = args[1];
        }
        String[] targets = new String[2];
        targets = target.split(":");
        String host = targets[0];
        int port = Integer.parseInt(targets[1]);
        SslContext sslContext = Client.buildSslContext("D://springboot/test-grpc/src/main/java/com/wangscaler/examples/server.crt");
        ManagedChannel channel = NettyChannelBuilder.forAddress(host, port)
                .negotiationType(NegotiationType.TLS)
                .sslContext(sslContext)
                .build();
        try {
            Client client = new Client(channel);
            client.greet(user);
        } finally {
            channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
        }
    }
}
​
複製代碼

再次鏈接。控制檯正常的調用到服務器端的函數。

信息: Will try to greet java ...
七月 27, 2021 9:38:51 上午 com.wangscaler.examples.Client greet
信息: Greeter client received: Hello world, java client !
七月 27, 2021 9:38:51 上午 com.wangscaler.examples.Client greet
信息: Greeter client received message: Hello, wangscaler !  and received code: 0 ! 
複製代碼

參考信息

相關文章
相關標籤/搜索