[English version](/docs/docs-07-srpc-http.md) ## 07 - 使用SRPC、TRPC、Thrift发送Http **SRPC**支持**HTTP**协议,只要把**idl**的内容作填到**HTTP**的**body**中,并且在**header**里填上**idl**的类型(**json**/**protobuf**/**thrift**),就可以与其他框架通过**HTTP**协议互通,由此可以实现跨语言。 - 启动**SRPCHttpServer**/**TRPCHttpServer**/**ThriftHttpServer**,可以接收由任何语言实现的HTTP client发过来的请求; - 启动**SRPCHttpClient**/**TRPCHttpClient**/**ThriftHttpClient**,也可以向任何语言实现的Http Server发送请求; - **HTTP header**:`Content-Type`设置为`application/json`表示json,`application/x-protobuf`表示protobuf,`application/x-thrift`表示thrift; - **HTTP body**: 如果body中涉及**bytes**类型,**json**中需要使用**base64**进行encode; ### 1. 示例 想实现**SRPCHttpClient**,可以把[tutorial-02-srpc_pb_client.cc](https://github.com/sogou/srpc/blob/master/tutorial/tutorial-02-srpc_pb_client.cc)或者[tutorial-09-client_task.cc](https://github.com/sogou/srpc/blob/master/tutorial/tutorial-09-client_task.cc)中的`SRPCClient`改成`SRPCHttpClient`即可。 在项目的[README.md](/docs//README_cn.md#6-run)中,我们演示了如何使用**curl**向**SRPCHttpServer**发送请求,下面我们给出例子演示如何使用**python**作为客户端,向**TRPCHttpServer**发送请求。 **proto文件:** ```proto syntax="proto3"; // proto2 or proto3 are both supported package trpc.test.helloworld; message AddRequest { string message = 1; string name = 2; bytes info = 3; int32 error = 4; }; message AddResponse { string message = 1; }; service Batch { rpc Add(AddRequest) returns (AddResponse); }; ``` **python客户端:** ```py import json import requests from base64 import b64encode class Base64Encoder(json.JSONEncoder): def default(self, o): if isinstance(o, bytes): return b64encode(o).decode() return json.JSONEncoder.default(self, o) headers = {'Content-Type': 'application/json'} req = { 'message': 'hello', 'name': 'k', 'info': b'i am binary' } print(json.dumps(req, cls=Base64Encoder)) ret = requests.post(url = "http://localhost:8800/trpc.test.helloworld.Batch/Add", headers = headers, data = json.dumps(req, cls=Base64Encoder)) print(ret.json()) ``` ### 2. 请求路径拼接 [README.md](/docs//README_cn.md#6-run)中,我们可以看到,路径是由service名和rpc名拼接而成的。而对于以上带package名 `package trpc.test.helloworld;`的例子, package名也需要拼接到路径中,**SRPCHttp** 和 **TRPCHttp** 的拼接路径方式并不一样,而**ThriftHttp**由于SRPC的thrift不支持多个service所以无需拼接任何路径。 我们以**curl**为例子: 与**SRPCHttpServer**互通: ```sh curl 127.0.0.1:8811/trpc/test/helloworld/Batch/Add -H 'Content-Type: application/json' -d '{...}' ``` 与**TRPCHttpServer**互通: ```sh curl 127.0.0.1:8811/trpc.test.helloworld.Batch/Add -H 'Content-Type: application/json' -d '{...}' ``` 与**ThriftHttpServer**互通: ```sh curl 127.0.0.1:8811 -H 'Content-Type: application/json' -d '{...}' ``` ### 3. HTTP状态码 SRPC支持server在`process()`中设置状态码,接口为**RPCContext**上的`set_http_code(int code)`。只有在框架能够正确处理请求的情况下,该错误码才有效,否则会被设置为框架层级的错误码。 **用法:** ~~~cpp class ExampleServiceImpl : public Example::Service { public: void Echo(EchoRequest *req, EchoResponse *resp, RPCContext *ctx) override { if (req->name() != "workflow") ctx->set_http_code(404); // 设置HTTP状态码404 else resp->set_message("Hi back"); } }; ~~~ **CURL命令:** ~~~sh curl -i 127.0.0.1:1412/Example/Echo -H 'Content-Type: application/json' -d '{message:"from curl",name:"CURL"}' ~~~ **结果:** ~~~sh HTTP/1.1 404 Not Found SRPC-Status: 1 SRPC-Error: 0 Content-Type: application/json Content-Encoding: identity Content-Length: 21 Connection: Keep-Alive ~~~ **注意:** 我们依然可以通过返回结果的header中的`SRPC-Status: 1`来判断这个请求在框架层面是正确的,`404`是来自server的状态码。 ### 4. HTTP Header 用户可以通过以下三个接口来设置/获取http header: ~~~cpp bool get_http_header(const std::string& name, std::string& value) const; bool set_http_header(const std::string& name, const std::string& value); bool add_http_header(const std::string& name, const std::string& value); ~~~ 对于**server**来说,这些接口在`RPCContext`上。 对于**client**来说,需要通过`RPCClientTask`设置**请求上的http header**、并且在回调函数的`RPCContext`上**获取回复上的http header**,用法如下所示: ~~~cpp int main() { Example::SRPCHttpClient client("127.0.0.1", 80); EchoRequest req; req.set_message("Hello, srpc!"); auto *task = client.create_Echo_task([](EchoResponse *resp, RPCContext *ctx) { if (ctx->success()) { std::string value; ctx->get_http_header("server_key", value); // 获取回复中的header } }); task->serialize_input(&req); task->set_http_header("client_key", "client_value"); // 设置请求中的header task->start(); wait_group.wait(); return 0; } ~~~ ### 5. IDL传输格式问题 如果我们填写的是Protobuf且用的标准为proto3,每个域由于没有optional和required区分,所以都是带有默认值的。如果我们设置的值正好等于默认值,则proto3不能识别为被set过,就不能被序列化的时候发出。 在protobuf转json的过程中,SRPC在**RPCContext上**提供了几个接口,支持 [JsonPrintOptions](https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.util.json_util#JsonPrintOptions) 上的功能。具体接口与用法描述可以查看:[rpc_context.h](/src/rpc_context.h) **示例:** ```cpp class ExampleServiceImpl : public Example::Service { public: void Echo(EchoRequest *req, EchoResponse *resp, RPCContext *ctx) override { resp->set_message("Hi back"); resp->set_error(0); // 0是error类型int32在proto3中的默认值 ctx->set_json_always_print_primitive_fields(true); // 带上所有原始域 ctx->set_json_add_whitespace(true); // 增加json格式的空格 } }; ``` **原始输出:** ```sh {"message":"Hi back"} ``` **通过RPCContext设置过json options之后的输出:** ```sh { "message": "Hi back", "error": 0 } ```