背景

在平常的业务开发中遇到了两个场景:

1.由于业务用的rpc框架是thrift,代码也是都是用thrift在写,有一天突然接到个需要前端要用http访问接口的需求,于是花了几天时间把所有的thrift接口又用Controller封装一层。由于跨语言,且对方不使用thrift,就需要你提供Http接口

2.写完thrift为了自测,需要再写个TestController验证代码是否正确,整个流程是否跑通,非常麻烦。

这两个场景大家遇到的比较多,所以要是能一写完thrift接口就能直接转换为http接口,那样就好了。

放眼整个互联网中,在互联网快速迭代的大潮下,越来越多的公司选择nodejs、django、rails这样的快速脚本框架来开发web端应用,而对于我们来说公司选择的后端语言是Java,这就产生了大量的跨语言的调用需求。其实对于thrift来说是支持很多语言的,但是给每次给其他语言开发都需要开发对应的客户端,并且还有很多rpc框架并不是像thrift一样支持这么多语言的,所以现在微服务都推出了service mesh(http://www.servicemesh.cn/),但是这个依然很新,有需要尝试的其实可以起尝试一下。http、json是天然合适作为跨语言的标准,各种语言都有成熟的类库,所以如何把像thrift这种tcp rpc框架转换成http,对于多语言支持是比较重要的。

RESTful or JSONRPC

RESTful

最开始想的是如何把thrift接口映射成RESTful,因为这个更加符合互联网http的标准,但是TCP rpc 对比RESTful有根本的区别,RESTful的核心是资源,并且利用Http协议中的各种方法GET,POST,OPTION等等对资源进行操作,如果想把thrift每个接口一一映射上,这个难度有点大,毕竟两个产生不出来任何关联,这个时候就需要每个接口进行配置映射,起成本不亚于我重写一套Controller了,所以RESTful这个方案基本被否决了。

JSONRPC

JSON-RPC是一个无状态且轻量级的远程过程调用(RPC)协议。它允许运行在基于socket,http等诸多不同消息传输环境的同一进程中。

JSONRPC本质上也是个RPC,定位和thrfit类似,不需要进行过多的协议映射。所以我们选择了使用JSONRPC,进行Http的转换。

JSONRPC请求对象

发送一个请求对象至服务端代表一个rpc调用, 一个请求对象包含下列成员:

jsonrpc

指定JSON-RPC协议版本的字符串,必须准确写为“2.0”

method

包含所要调用方法名称的字符串,以rpc开头的方法名,用英文句号(U+002E or ASCII 46)连接的为预留给rpc内部的方法名及扩展名,且不能在其他地方使用。

params

调用方法所需要的结构化参数值,该成员参数可以被省略。

id

已建立客户端的唯一标识id,值必须包含一个字符串、数值或NULL空值。如果不包含该成员则被认定为是一个通知。该值一般不为NULL[1],若为数值则不应该包含小数[2]。

服务端必须回答相同的值如果包含在响应对象。 这个成员用来两个对象之间的关联上下文。

[1] 在请求对象中不建议使用NULL作为id值,因为该规范将使用空值认定为未知id的请求。另外,由于JSON-RPC 1.0 的通知使用了空值,这可能引起处理上的混淆。

[2] 使用小数是不确定性的,因为许多十进制小数不能精准的表达为二进制小数。

通知

没有包含“id”成员的请求对象为通知, 作为通知的请求对象表明客户端对相应的响应对象并不感兴趣,本身也没有响应对象需要返回给客户端。服务端必须不回复一个通知,包含那些批量请求中的。

由于通知没有返回的响应对象,所以通知不确定是否被定义。同样,客户端不会意识到任何错误(例如参数缺省,内部错误)。

参数结构

rpc调用如果存在参数则必须为基本类型或结构化类型的参数值,要么为索引数组,要么为关联数组对象。

  • 索引:参数必须为数组,并包含与服务端预期顺序一致的参数值。

  • 关联名称:参数必须为对象,并包含与服务端相匹配的参数成员名称。没有在预期中的成员名称可能会引起错误。名称必须完全匹配,包括方法的预期参数名以及大小写。

响应对象

当发起一个rpc调用时,除通知之外,服务端都必须回复响应。响应表示为一个JSON对象,使用以下成员:

jsonrpc

指定JSON-RPC协议版本的字符串,必须准确写为“2.0”

result

该成员在成功时必须包含。

当调用方法引起错误时必须不包含该成员。

服务端中的被调用方法决定了该成员的值。

error

该成员在失败是必须包含。

当没有引起错误的时必须不包含该成员。

该成员参数值必须为5.1中定义的对象。

id

该成员必须包含。

该成员值必须于请求对象中的id成员值一致。

若在检查请求对象id时错误(例如参数错误或无效请求),则该值必须为空值。

响应对象必须包含result或error成员,但两个成员必须不能同时包含。

错误对象

当一个rpc调用遇到错误时,返回的响应对象必须包含错误成员参数,并且为带有下列成员参数的对象:

code

使用数值表示该异常的错误类型。 必须为整数。

message

对该错误的简单描述字符串。 该描述应尽量限定在简短的一句话。

data

包含关于错误附加信息的基本类型或结构化类型。该成员可忽略。 该成员值由服务端定义(例如详细的错误信息,嵌套的错误等)。

JsonRpc4j

jsonRpc4j是一款用Java语言实现的JSONRPC的框架,使用JackSon进行JSON解析。他的github地址为:https://github.com/briandilley/jsonrpc4j

在jsonRpc4j中他可以处理HTTP Server (HttpServletRequest \ HttpServletResponse),所以能够帮助我们很快的构建httpserver,使用JsonRpc4j很简单:

ObjectMapper mapper = new ObjectMapper();JsonRpcServer skeleton = new JsonRpcServer(mapper, new DemoService(), (Class<?>) service.getClass());skeleton.handle(req, resp);

首先创建一个ObjectMapper,用于JSON的转换的,然后 把需要变成Server的Service放进JsonRpcServer,最后执行这个请求。

thrift到http

对于thrift到http是利用Serlvet加上jsonRpc4j完成关系的映射,如下图所示:

HTTP URL

http中关键在于http URL如何制定,这里URL为了简单快速明了,用以下规则:

POST: servlet-url-pattern + thriftServiceInfaceName

首先所有thrift方法公共的路径在Servlet中制定,所有/thrift/*的URL都走ThriftSerlvet

<servlet><servlet-name>thriftSerlvet</servlet-name><servlet-class>com.thrift.ThriftSerlvet</servlet-class></servlet><servlet-mapping><servlet-name>thriftSerlvet</servlet-name><url-pattern>/thrift/*</url-pattern></servlet-mapping>

我们有如下一个thrift

public class CustomerThriftServiceImpl implements customerService.Iface{@Overridepublic QueryCustomerResp queryCustomer(QueryRuleReq queryReq) throws TException {QueryCustomerResp result = new QueryCustomerResp();try {result.setCustomr(new Customer());result.setStatus(ThriftRespStatusHelper.OK);} catch (Exception e) {LOGGER.error("查询出现错误{}", e);result.setStatus(ThriftRespStatusHelper.failure("查询失败"));}return result;}}

所以我们的URL如下/thrift/customerService

ThrifSerlvet

我们所有的thrift的请求都会经过这个serlvet,然后通过其做jsonRpcServer的路由分发代码如下:

public class ThriftSerlvet extends HttpServlet {public static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";public static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";public static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";private final Map<String, JsonRpcServer> rpcServerMap = new ConcurrentHashMap<>();private Logger LOGGER = LoggerFactory.getLogger(ThriftSerlvet.class);public static final String JSON_FILTER_ID = "thriftPropFilter";@Overridepublic void init() throws ServletException {super.init();WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());Map<String, ThriftServerPublisher> publisherMap = rootContext.getBeansOfType(ThriftServerPublisher.class);if (publisherMap == null || publisherMap.size() == 0) {return;}for (ThriftServerPublisher serverPublisher : publisherMap.values()) {try {Field serviceImplField = serverPublisher.getClass().getDeclaredField("serviceImpl");serviceImplField.setAccessible(true);Object serveiceImpl = serviceImplField.get(serverPublisher);addJsonRpcServer(serveiceImpl, serverPublisher.getServiceSimpleName());} catch (Exception e) {LOGGER.error("this serverPublisher:{}, get the filed:{} has error", serverPublisher, "serviceImpl", e);}}}private void addJsonRpcServer(Object serveiceImpl, String serviceSimpleName) {serviceSimpleName = serviceSimpleName.replaceFirst(String.valueOf(serviceSimpleName.charAt(0)), String.valueOf(serviceSimpleName.charAt(0)).toLowerCase());LOGGER.info("serverPubliser");ObjectMapper mapper = new ObjectMapper();SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider();simpleFilterProvider.addFilter(JSON_FILTER_ID, new ThriftPropertiesFilter());mapper.setFilterProvider(simpleFilterProvider);mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {@Overridepublic Object findFilterId(Annotated a) {return JSON_FILTER_ID;}});JsonRpcServer rpcServer = new JsonRpcServer(mapper, serveiceImpl, serveiceImpl.getClass().getSuperclass());rpcServer.setInterceptorList(Arrays.asList(new ThriftJsonInterceptor()));rpcServerMap.put(serviceSimpleName, rpcServer);}@Overrideprotected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*");resp.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, "POST");resp.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, "*");if (req.getMethod().equalsIgnoreCase("OPTIONS")) {resp.sendError(200);} else if (req.getMethod().equalsIgnoreCase("POST")) {String uri = req.getRequestURI();String path = req.getServletPath();String serviceName = uri.substring(path.length(), uri.length()).replaceAll("/", "");JsonRpcServer rpcServer = rpcServerMap.get(serviceName);if (rpcServer == null) {resp.sendError(404);return;}rpcServer.handle(req, resp);} else {//方法不被允许resp.sendError(405);}}}

上面代码的解释过程如下:

1.init:初始化的时候我们需要把我们所有的thriftService的Bean从Spring容器中拿出来,然后对每个Service构建一个不同的JsonRpcServer,放进Map中等待service方法路由。

这里初始化有几点注意:

  • ObjectMapper我们对于输出过滤了以set开头的因为jackSon转换thrift的时候会把thrift自己生成的文件给转换出来。

  • 这里我们在spring的配置文件中要配置

<aop:aspectj-autoproxy proxy-target-class="true"/>

显示的要使用cglib,如果不指定这个默认是Jdk的代理,jdk代理的话默认就拿不到自己本来的类了,这里必须要使用cglib代理,这样通过getSuperClass即可获得自己本来的Class。

2.service就比较简单了,我们先加了允许跨域,然后指定只有POST方法才能访问。

JsonRpc4j的修改

对于这个开源项目并没有直接用他而是对他进行了修改,为什么会需要进行修改呢?

我们简单看看下面这个方法

public Person sayHello(Person person, Type type);

如果我们想用调用这个服务的话需要传入的json为:

{"jsonrpc": "2.0", "method": "sayHello", "params": \[{"age":"12","name":"lizhao"},{"type":1}\], "id": 1}

上线这个json,params参数传入的是数组,其实我们更希望传的是下面这样,因为对于这种参数需要用名字指定,才能更加可读,减少出错的概率:

{"jsonrpc": "2.0", "method": "sayHello", "params": {"person":{"age":"12","name":"lizhao"},"type":{"type":1}}, "id": 1}

但是这样传的话会报出找不到方法,jsonrpc4j官方的做法是用注解,将方法修改成:

public Person sayHello(@JsonRpcParam("person")Person person, @JsonRpcParam("type")Type type);

但是用过thrift的同学都知道,thrift的很多代码都是根据IDL生成的,这样会导致一个问题,不能使用注解,因为一旦用了注解之后下次生成会直接覆盖。所以这里我们必须要使用传入的参数的名字才行,具体修改的实现如下:

private List<JsonRpcParam> getAnnotatedParameterNames(Method method) {List<JsonRpcParam> parameterNames = new ArrayList<>();​List<Parameter> parameters = ReflectionUtil.getParameters(method);Iterator<Parameter> parameterIterator = parameters.iterator();​List<String> parameterLocalNames = ReflectionUtil.getParameterLocalNames(method);Iterator<String> parameterLocalNameIterator = parameterLocalNames.iterator();​while (parameterIterator.hasNext() && parameterLocalNameIterator.hasNext()) {parameterNames.add(getJsonRpcParamType(parameterIterator.next(), parameterLocalNameIterator.next()));}return parameterNames;}​public static List<String> getParameterLocalNames(Method method) {List<String> parameterNames = new ArrayList<>();Collections.addAll(parameterNames, PARAMETER\_NAME\_DISCOVERER.getParameterNames(method));return Collections.unmodifiableList(parameterNames);}

这里主要是使用spring中的ParameterNameDiscoverer通过字节码获取参数名字,这样我们就不需要用注解即可使用传参数名字的方式。

Swagger

Swagger是一个规范且完整的框架,提供描述、生产、消费和可视化RESTful Web Service。swagger其实不是很适合对于这种,但是也能进行生成,可以通过重写swagger-maven-plugin这个开源框架,能生成自己指定的,但是由于这个目前只用来做快速调试,swagger这部分暂时还没有计划。

总结

本次主要介绍了如何从thrfit转换为http,还有更多的细节,鉴权,分布式追踪系统埋点等等需要补充,这种方法实现http可能不是最好的,我觉得最好的还是要实现rest,毕竟rest才是互联网系统调用所认可的,但是通过这种方式了解了如何从一个协议转换成另外一个协议,补充了自己在协议转换这方面的一些空白吧。

参考文档

jsonRpc2.0规范

想与我交流请扫描下方公众号

如何把thrift rpc转换为http相关推荐

  1. 使用Thrift RPC编写程序

    http://dongxicheng.org/search-engine/thrift-rpc/ 1. 概述 本文以C++语言为例介绍了thrift RPC的使用方法,包括对象序列化和反序列化,数据传 ...

  2. thrift RPC接口请求超时

    某次client调用服务端thrift  RPC接口超时导致连接断开,但是server说自己返回数据了,然后client用tcpdump抓包发现没抓到server返回的数据,没抓到表明client没收 ...

  3. LinuxC/C++编程基础(24) 使用thrift/rpc开发简单实例(续2)

    写在前面:前面两篇文字已经把thrift/rpc的安装以及服务端的编写叙述了,这里再把客户端的编写加上 一.client.cpp文件实现,如下: #include "../gen-cpp/M ...

  4. Thrift RPC 系列教程(4)——源码目录结构组织

    Thrift 代码就是编程代码.是代码,就应该有良好的工程组织,并且,单独git仓库.版本管理,都是必不可少的. 前面我们简单总结了一些 Thrift 的一些基础知识点,但无非是一些细节层面的东西,所 ...

  5. Thrift RPC 系列教程(5)—— 接口设计篇:struct enum设计

    好的接口,如同漂亮的美女,是人都会多看一眼. 一个示例 比如,要我们设计一个 User.那很简单,典型的 class 嘛,按照 OOP 的套路走就行了,于是: struct User{1: strin ...

  6. Thrift RPC 系列教程(3)——模块化

    模块化是好事,以及,它让我联想到了 C 语言,以及它那如同平原一样的命名空间. 为什么需要模块化 所谓『模块化』,是一种很自然的事情,体现了『分而治之』的思想. 坦白来说,这是一个无需过多讨论的话题. ...

  7. 【网络】RPC通信之Apache Thrift

    文章目录 概述 RPC的概念 Thrift架构设计 一.Transports 1. Transports接口 2.End Point Transports 3.Layered Transports 4 ...

  8. Thrift架构与使用方法

    Thrift是由Facebook为"大规模跨语言服务开发"而开发的,现在是Apache软件基金会的开源项目. Thrift实现了一种接口描述语言和二进制通讯协议,用来定义和创建跨语 ...

  9. tns(thrift 分布式组件)介绍

    tns(thrift name server)是我在700Bike开发的一个thrift rpc分布式组件,可以实现高可靠.负载均衡.动态水平扩展等. 相比haproxy.zookeeper等有什么优 ...

最新文章

  1. 磁悬浮地球仪底座驱动电路板分析
  2. 那些不敢生孩子的女人,都在怕什么?
  3. 「 ThoughtWorks面试 —— 一次愉快的技术交流 | 掘金技术征文」
  4. php --魔术常量 /魔术方法
  5. 为什么自己编写的页面总是在那里抖动_别克威朗为什么销量不佳?
  6. 化妆definer是什么意思_我们为什么说隔离霜是个智商税的东东!
  7. flowable 多人签收_业务流程 BPM、工作流引擎、Flowable、Activiti
  8. 工作资讯003---甘特图
  9. CSDN上传的资源为何不能自定义下载积分?
  10. Intel APIC Configuration
  11. 浏览器点击跳转链接弹出下载框的可能原因
  12. 使用python调用微步在线接口实现自动化查询IP情报
  13. operator重载运算符
  14. android 沉浸式按钮,android – 如何完全退出沉浸式全屏模式?
  15. 存储器——Cache
  16. 如何临时删除桌面右键菜单上的登录画面修改
  17. 优化AWS使用成本系列之预留实例(RI)为您提供大幅折扣
  18. 【LeetCode】1652. 拆炸弹(C++)
  19. 图形化的dialog交互式脚本
  20. 贵州厉害的计算机学校,贵州2021年计算机学校好点的大专学校

热门文章

  1. 贪心算法 Greedy
  2. 【HLS教程】HLS入门与精通
  3. .NET Core Onvif协议C#教程系列之XiaoFeng.Onvif组件库
  4. GameFramework源码学习(一)
  5. 校验普通电话、传真号码:可以“+”开头,除数字外,可含有“-”
  6. 基于引擎开发HTML5游戏实战(一)---游戏引擎
  7. 大数据技术原理与应用 第一篇 大数据基础
  8. 结构方程模型的R语言实现
  9. 不定式作各种成份和现在分词作各种成份
  10. Dell PowerEdge T630安装GPU