服务提供者如何发布一个服务? 服务消费者如何引用这个服务? 具体来说,就是这个服务的接口名是什么? 调用这个服务需要传递哪些参数? 接口的返回值是什么类型?
RESTful API首先来说说RESTful API的方式,主要被用作HTTP或者HTTPS协议的接口定义,即使在非微服务架构体系下,也被广泛采用。
以开源服务化框架Motan发布RESTful API为例发布三个RESTful格式的API声明
具体的服务实现如下:
服务提供者这一端通过部署代码到Tomcat中,并配置Tomcat中如下的web.xml,就可以通过servlet的方式对外提供RESTful API。
这样服务消费者就可以通过HTTP协议调用服务了,因为HTTP协议本身是一个公开的协议,对于服务消费者来说几乎没有学习成本,所以比较适合用作跨业务平台之间的服务协议。比如你有一个服务,不仅需要在业务部门内部提供服务,还需要向其他业务部门提供服务,甚至开放给外网提供服务,这时候采用HTTP协议就比较合适,也省去了沟通服务协议的成本。
主要三步:
-
服务提供者定义接口,并实现接口
-
服务提供者进程启动时,通过加载server.xml配置文件将接口暴露出去,开启8002端口监听。
-
服务消费者进程启动时,通过加载client.xml配置文件来引入要调用的接口
服务消费者要想调用服务,就必须在进程启动时,加载配置client.xml,引用接口定义,然后发起调用。
-
client.xml
服务消费者启动时,加载client.xml
就这样,通过在服务提供者和服务消费者之间维持一份对等的XML配置文件,来保证服务消费者按照服务提供者的约定来进行服务调用。在这种方式下,如果服务提供者变更了接口定义,不仅需要更新服务提供者加载的接口描述文件server.xml,还需要同时更新服务消费者加载的接口描述文件client.xml。
一般是私有RPC框架会选择XML配置描述接口,因为私有RPC协议的性能要比HTTP协议高,所以在对性能要求比较高的场景下,采用XML配置的方式比较合适。
但这种方式对业务代码侵入性比较高,XML配置有变更的时候,服务消费者和服务提供者都要更新,所以适合公司内部联系比较紧密的业务之间采用。如果要应用到跨部门之间的业务调用,一旦有XML配置变更,需要花费大量精力去协调不同部门做升级工作。
在我经历的实际项目里,就遇到过一次底层服务的接口升级,需要所有相关的调用方都升级。所以对于XML配置方式的服务描述,一旦应用到多个部门之间的接口格式约定,如果有变更,最好是新增接口,不到万不得已不要对原有的接口格式做变更。
IDL文件接口描述语言(interface description language),通过一种中立的方式来描述接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。比如你用Java语言实现提供的一个服务,也能被PHP语言调用。
也就是说IDL主要是用作跨语言平台的服务之间的调用,有两种最常用的IDL
- Facebook开源的Thrift协议
- Google开源的gRPC协议
以gRPC协议为例使用IDL文件方式来描述接口。
gRPC协议使用Protobuf简称proto文件来定义接口名、调用参数以及返回值类型。
比如文件helloword.proto定义了一个接口SayHello方法,它的请求参数是HelloRequest,它的返回值是HelloReply。
// The greeter service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} rpc SayHelloAgain (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; }
假如服务提供者使用的是Java语言,那么利用protoc插件即可自动生成Server端的Java代码。
private class GreeterImpl extends GreeterGrpc.GreeterImplBase { @Override public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) { HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } @Override public void sayHelloAgain(HelloRequest req, StreamObserver<HelloReply> responseObserver) { HelloReply reply = HelloReply.newBuilder().setMessage("Hello again " + req.getName()).build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } }
假如服务消费者使用的也是Java语言,那么利用protoc插件即可自动生成Client端的Java代码。
public void greet(String name) { logger.info("Will try to greet " + name + " ..."); HelloRequest request = HelloRequest.newBuilder().setName(name).build(); HelloReply response; try { response = blockingStub.sayHello(request); } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); return; } logger.info("Greeting: " + response.getMessage()); try { response = blockingStub.sayHelloAgain(request); } catch (StatusRuntimeException e) { logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); return; } logger.info("Greeting: " + response.getMessage()); }
gRPC协议的服务描述是通过proto文件来定义接口,然后再使用protoc来生成不同语言平台的客户端和服务端代码,从而跨语言服务调用。
在描述接口定义时,IDL文件需要对接口返回值进行详细定义。如果接口返回值的字段比较多,并且经常变化时,采用IDL文件方式的接口定义就不太合适了。一方面可能会造成IDL文件过大难以维护,另一方面只要IDL文件中定义的接口返回值有变更,都需要同步所有的服务消费者都更新,管理成本太高。
总结通常情况下,如果只是企业内部之间的服务调用,并且都是Java,选择XML配置最简单。 如果企业内部存在多个服务,并且服务采用的是不同语言平台,建议使用IDL文件方式进行描述服务。 如果还存在对外开放服务调用的情形的话,使用RESTful API方式则更加通用。