Java程序员的现代RPC指南

标签: java 程序员 现代 | 发表时间:2015-12-05 11:37 | 作者:dc_726
出处:http://blog.csdn.net

Java程序员的现代RPC指南


1.前言

1.1 RPC框架简介

最早接触RPC还是初学Java时,直接用Socket API传东西好麻烦。于是发现了JDK直接支持的RMI,然后就用得不亦乐乎,各种大作业里凡是涉及到分布式通信的都用RMI,真是方便。后来用上了Spring,发现Spring提供了好多Exporter,可以无侵入地将一个POJO暴露为RPC服务。

接触了这么多RPC框架后,发现当时公司内部自己实现了一套支持压缩、加密等附加功能的RPC基础框架,于是就读了一下源码,发现原来自己实现个简单的RPC挺简单啊,选好序列化框架后用反射为服务接口生成存根就行了。 核心技术就是:序列化和动态反射

前一阵子又接触到了多语言支持的RPC框架。其实传统的方式也是能支持多种编程语言的,只要序列化框架为多种语言都提供了版本支持,那么序列化后使用相同的网络协议传输就能实现跨语言的RPC了,这也是最轻量级的自制RPC了,灵活但是手动工作量比较大。再就是重量级的SOAP WebService或简单方便的REST,后者一般采用JSON格式携带数据,最典型的场景就是前后台的服务调用,从JS到Java的RPC。

本文要重点介绍的则是另外一套RPC框架。 相比一般的RPC框架来说,它能够支持多种语言间的RPC;相比WebService,它却没有SOAP那么重量级,又比轻量级的REST高效。对于组件之间需要频繁通信、又对性能要求较高的分布式系统来说,它是不错的解决方案。

1.2 Protobuf vs. Thrift

Protobuf全名为Protocol Buffer,是Google推出的支持多语言的RPC基础设施。通过自定义语言无关的IDL文件和Protoc代码生成器达到跨语言RPC通信的目的。但也正因为跨语言,与我们仅限于Java的那些RPC框架相比稍显复杂一些。之前研究Protobuf序列化性能时,也正因这一点而采用了Java简化版Protostuff,详情请见 《序列化战争:主流序列化框架Benchmark》

Thrift是Facebook推出的RPC框架,与PB相比提供了内建的RPC支持,而PB开源版里并没有RPC功能(也是后面实践时才发现的)。Thrift的RPC提供了多种网络模式和序列化的选项,可以根据不同场景来灵活搭配使用。

关于Protobuf、Thrift以及本文未涉及的Avro,在《大数据日知录》里有具体的比较,感兴趣的可以参考一下。

1.3 核心技术

前面说了传统RPC框架的核心技术,“现代”RPC为了支持多语言所以稍显复杂一点儿。核心技术主要有: IDL、代码生成器、序列化、RPC

  • 在IDL文件中通过不与具体编程语言相关的语法定义通信类
  • 利用代码生成器生成出Java语言对应的代码
  • 引入框架的运行时Jar包,获得序列化、RPC等能力

所以前两者决定了框架对不同编程语言的支持能力,而后两者决定了运行时的调用性能。


2.Maven集成

不管使用哪种框架,既然涉及到了代码生成,那就要想法与项目构建的过程结合到一起。这里以Java项目最常用的Maven为例,看一下如何将代码生成器与Maven相结合,并且有哪些注意事项。

2.1 Compile阶段

按照我们的设想,代码生成过程应该在编译阶段,这样生成的代码就能跟已有代码一起被编译、打包、发布,两者没有什么差别。一旦修改了IDL,直接编译就能看到最新生成的代码了,这就是我们想要的效果。

2.2 Ant集成插件

因为有些框架并不提供专门的插件,所以与Maven最简单通用的集成方法就是采用maven-antrun-plugin插件。此插件可以执行任意命令,标准写法如下:

      <build>
        <plugins>
            <plugin>
                <artifactId>maven-antrun-plugin</artifactId>
                <executions>
                    <execution>
                        <id>generate-sources</id>
                        <phase>generate-sources</phase>
                        <configuration>
                            <tasks>
                                <exec executable="xxx.exe">
                                    <arg value="arg1 arg2 ..."/>
                                </exec>
                            </tasks>
                            <sourceRoot>target/generated-sources</sourceRoot>
                        </configuration>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

3.Protobuf实战

3.1 编写IDL

语法非常简单,其中java_generic_services选项决定是否生成Service类,默认不生成,据说是不建议使用。

  package com.test;

option java_generic_services = true;

message Request
{
    required int32 type = 1;
}

message Response
{
    required int32 cpu = 1;
    required int32 memorySize = 2;
}

service AgentService
{
    rpc detectHardware(Request) returns (Response);
}

3.2 Protoc编译

Windows版预编译好的protoc.exe支持C++,Java,Python三种最常用的语言,如果你只使用这几种语言的话那就很简单了。之所以把Protobuf相关文件都放到src/protobuf而非src/main/resources下是因为: src/main/resources里东西默认会被包含到最终的jar里。如果我们不想把protoc.exe和一堆.proto文件打到jar包里发布的话,要么加一个Maven的拷贝filter,或者像本文的方法将Protobuf相关文件都放到src/protobuf下。

      <build>
        <plugins>
            <plugin>
                <artifactId>maven-antrun-plugin</artifactId>
                <executions>
                    <execution>
                        <id>generate-sources</id>
                        <phase>generate-sources</phase>
                        <configuration>
                            <tasks>
                                <exec executable="src/protobuf/protoc.exe" failonerror="true">
                                    <arg value="--java_out=src/main/java"/>
                                    <arg value="src/protobuf/idl/*.proto"/>
                                </exec>
                            </tasks>
                            <sourceRoot>target/generated-sources</sourceRoot>
                        </configuration>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

3.3 使用源码

以下是利用Protobuf生成的代码进行序列化和反序列化的示例。由于缺少RPC功能,所以也只能测试一下序列化功能了。

  public class PbRpcTest {

    public static void main(String[] args) throws InvalidProtocolBufferException {
        // Build request
        Agent.Request.Builder reqBuilder = Agent.Request.newBuilder();
        reqBuilder.setType(1);
        Agent.Request req = reqBuilder.build();
        System.out.println(req);

        // Parse from bytes
        byte[] bytes = req.toByteArray();
        Agent.Request req2 = Agent.Request.parseFrom(bytes);
        System.out.println(req2.getType());
    }
}

4.Thrift

4.1 编译IDL

与Protobuf的IDL相似,Thrift的IDL也很简单。

  namespace java com.test

service AgentService {
    string detectHardware()
}

4.2 Java

Thrift支持多种网络和序列化模式,这里采取最简单的同步阻塞和二进制序列化的方式。

  public class RpcClientTest {

    public static void main(String[] args) throws TException {
        TSocket transport = new TSocket("127.0.0.1", 8090);
        TProtocol protocol = new TBinaryProtocol(transport);
        AgentService.Client client = new AgentService.Client(protocol);
        transport.open();

        System.out.println(client.detectHardware());
    }
}

public class RpcServerTest {

    public static void main(String[] args) throws TTransportException {
        AgentService.Processor<AgentService.Iface> processor = new AgentService.Processor<AgentService.Iface>(
                new AgentServiceImpl()
        );

        TServerSocket transport = new TServerSocket(8090);
        TServer.Args tArgs = new TServer.Args(transport);
        tArgs.processor(processor);
        tArgs.protocolFactory(new TBinaryProtocol.Factory());

        TServer server = new TSimpleServer(tArgs);
        server.serve();
    }
}

public class AgentServiceImpl implements AgentService.Iface {

    @Override
    public String detectHardware() throws TException {
        return "hello";
    }
}

4.3 Python

Python要想运行时支持Thrift,也需要安装相应的 插件。我是在Windows下的Cygwin中完成安装的,然后在cmd中执行却报错还是没找到thrift模块,结果发现是cmd和Cygwin默认执行的Python版本不一样。汗,之前可能2和3都装了忘记了,生成的代码用Python 3运行的话会有问题:

  $ tar -xzvf thrift-0.9.3.tar.gz
$ cd thrift-0.9.3/
$ python setup.py install

注意一定要指定IP地址,否则Java客户端调用Python服务端时会报”Connection refused”错误,详见 Stackoverflow上的问题解答

  import sys, glob
sys.path.append('gen-py')
#sys.path.insert(0, glob.glob('../../lib/py/build/lib*')[0])

from agent import AgentService
from agent.ttypes import *

from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TServer

class AgentServiceHandler:
    def __init__(self):
        print('init')

    def detectHardware(self):
        print('detect!')
        return 'hello~~~'

handler = AgentServiceHandler()
processor = AgentService.Processor(handler)
transport = TSocket.TServerSocket(host="127.0.0.1", port=8090)
tfactory = TTransport.TBufferedTransportFactory()
pfactory = TBinaryProtocol.TBinaryProtocolFactory()

server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)
print('Starting the server...')
server.serve()
print('done.')

这就是目录结构,现在就可以启动服务端,客户端仍然用之前的Java客户端,调用成功!

  $ tree py-demo/ -I "*.pyc"
py-demo/
|-- agent.thrift
|-- gen-py
|   |-- __init__.py
|   `-- agent
|       |-- __init__.py
|       |-- __pycache__
|       |-- AgentService.py
|       |-- AgentService-remote
|       |-- constants.py
|       `-- ttypes.py
|-- server.py
`-- thrift-0.9.3.exe

$ python server.py
init
Starting the server...
detect!

4.总结

与直接使用Java其他RPC框架相比的确麻烦了一些,例如Spring中就自带了一些Exporter可以无侵入的实现RPC服务。但熟悉了Protobuf和Thrift以后发现实际上还是挺方便的,而且Windows版预编译好的Protoc支持C++,Java,Python三种最常用的语言,Thrift则支持几乎主流的各种语言,足够我们使用了。

参考资料:

  1. Protobuf语言指南
  2. Thrift入门及Java实例演示
作者:dc_726 发表于2015/12/5 11:37:55 原文链接
阅读:65 评论:0 查看评论

相关 [java 程序员 现代] 推荐:

Java程序员的现代RPC指南

- - CSDN博客推荐文章
Java程序员的现代RPC指南. 最早接触RPC还是初学Java时,直接用Socket API传东西好麻烦. 于是发现了JDK直接支持的RMI,然后就用得不亦乐乎,各种大作业里凡是涉及到分布式通信的都用RMI,真是方便. 后来用上了Spring,发现Spring提供了好多Exporter,可以无侵入地将一个POJO暴露为RPC服务.

Java程序员常用工具集

- - BlogJava-庄周梦蝶
    我发现很多人没办法高效地解决问题的关键原因是不熟悉工具,不熟悉工具也还罢了,甚至还不知道怎么去找工具,这个问题就大条了. 我想列下我能想到的一个Java程序员会用到的常用工具. 1.IDE: Eclipse或者 IDEA,熟悉尽可能多的快捷键,《 Eclipse常见快捷键列表》. (1) Findbugs,在release之前进行一次静态代码检查是必须的.

Java系统程序员修炼之道

- - 博客 - 伯乐在线
从2002开始接触Java学会HelloWorld这么经典的程序到如今不知不觉已经十年啦,十年中亲耳听到过不少大牛的演讲,见到过项目中的神人在键盘上运指如飞的编程速度,当时就被震撼了. 当编程越来越成体力活,我们还能有自己的思想,还能修炼为Java系统级别的 程序员嘛. 学习与修炼以下知识与技能,帮你早日达成愿望.

Java面试题:多线程,作为Java程序员你不得不懂

- sun - IT程序员面试网
线程:是指进程中的一个执行流程. 线程与进程的区别:每个进程都需要操作系统为其分配独立的内存地址空间,而同一进程中的所有线程在同一块地址空间中工作,这些线程可以共享同一块内存和系统资源. 创建线程有两种方式,如下: 1、 扩展java.lang.Thread类 2、 实现Runnable接口 Thread类代表线程类,它的两个最主要的方法是: run()——包含线程运行时所执行的代码 Start()——用于启动线程.

25个让Java程序员更高效的Eclipse插件

- - 博客 - 伯乐在线
Eclipse提供了一个可扩展插件的开发系统. 这就使得Eclipse在运行系统之上可以实现各种功能. 这些插件也不同于其他的应用(插件的功能是最难用代码实现的). 拥有合适的Eclipse插件是非常重要的,因为它们能让Java开发者们无缝的开发基于J2EE和服务的应用程序. Eclipse的插件也能帮助他们开发不同应用架构上的程序.

java程序员必知的8大排序

- - CSDN博客推荐文章
   (1)基本思想:在要排序的一组数中,假设前面(n-1)[n>=2] 个数已经是排. 好顺序的,现在要把第n个数插到前面的有序数中,使得这n个数. 如此反复循环,直到全部排好顺序. //将大于temp的值整体后移一个单位. 2,           希尔排序(最小增量排序). (1)基本思想:算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序.

Java程序员不该有的6种陋习

- - BlogJava-首页技术区
只有在学会处理异常之后,我们才能说自己是一个合格的java程序员. 只有在摆脱了以下六种异常处理的陋习之后,才能威慑一下刚毕业的小菜鸟.   现在就来测试一下大家对异常的掌握程度. 不用担心,事实上,这些不合理的设计很容易看出来. 那么,以下六种不合理的代码,大家能看出每一种的问题出在哪儿吗.   + ",姓名:" + rs.getString("name"));.

Java程序员应该知道的10个调试技巧

- - 博客 - 伯乐在线
摘要:调试不仅可以查找到应用程序缺陷所在,还可以解决缺陷. 对于Java 程序员来说,他们不仅要学会如何在Eclipse里面开发像样的程序,更需要学会如何调试程序. 本文介绍了Java程序员必知的10个调试技巧,保证让你受益匪浅. 调试可以帮助识别和解决应用程序缺陷,在本文中,作者将使用大家常用的的开发工具Eclipse来调试Java应用程序.

【外刊IT评论网】Java程序员的堕落

- - 外刊IT评论网
我曾在多个场合撰文说过,现在培训出来的企业Java开发兵团只知道使用某种框架,但完全不知道这些框架的底层原理. 今天早上,在依次对三个职位的应聘者面试中,我又再次见到了这种情形. 我们的顾问公司收到了一个客户的招聘需求,要一个经验丰富并通晓SQL的Java开发人员. 我们公司在这个客户那里享有非常好的声誉,于是我们从招聘代理公司要来了一批简历,开始搜寻合适的目标.

从程序员到CTO的Java技术路线图

- - ITeye博客
 总感觉需要提升自己,也摸索了一下路子. 但有如此清晰的指示图供参考还是非常不错的,不是需要我们完全掌握,只是扩展思维.               反射、泛型、注释符、自动装箱和拆箱、枚举类、可变.               参数、可变返回类型、增强循环、静态导入.         核心编程.              IO、多线程、实体类、.