从本章开始,我们以《ZeroC Ice权威指南》中提供的在线预定图书为例,来深入学习Ice的使用。
RPC调用详解
首先还是新建个项目ice_book,步骤就不说了,已经建过两个项目了,有经验了。在slice文件夹中新建Service.ice文件:
本例中我们创建了一个结构体Message作为参数和返回值,还有OnlineBook接口,该接口只有一个bookTick()方法。我们可以看下结构体Message的定义,涵盖了布尔类型、数值类型、整数类型以及字符串,目的是尽可能接近现实业务的需求;另外返回值Message也是为了测试RPC通信。然后在ice_book项目根目录执行gradle build
,一共生成了12个java文件,下面挑重点来看下:
_OnlineBookOperationsNC和_OnlineBookOperations这两个姐妹接口是OnlineBook的具体业务方法定义的接口,不同的是_OnlineBookOperations在方法签名中增加了Ice的内部对象Current,这个Current对象包括了当前调用的网络连接等上下文信息。
123public interface _OnlineBookOperations{Message bookTick(Message message, Ice.Current __current);}_OnlineBookDisp作为实现了OnlineBook接口的抽象类,完成了基本的RPC调用过程,其中Disp是Dispatch的缩写,即调用分发。我们来看下核心代码。这里有一个小问题,我们知道Java平台采用大端字节序,而Ice采用小端字节序,转换的时候会有额外的开销,照官方说法在整个请求来看这个开销可以忽略不计,但感觉略有点儿不爽。
123456789101112131415161718192021222324252627/**** @param __obj* @param __inS 代表当前RPC请求的网络通道* @param __current* @return*/public static Ice.DispatchStatus ___bookTick(OnlineBook __obj,IceInternal.Incoming __inS,Ice.Current __current) {__checkMode(Ice.OperationMode.Normal, __current.mode);// 从当前网络通道中读取RPC方法传入的参数IceInternal.BasicStream __is = __inS.startReadParams();Message message = null;// 执行反序列化,将网络字节流变为具体的Java对象message = Message.__read(__is, message);__inS.endReadParams();// 调用用户实现的OnlineBook的具体业务接口,即_OnlineBookOperationsNC的实现方法Message __ret = __obj.bookTick(message, __current);// 将结果回写到RPC请求的网络应答报文中IceInternal.BasicStream __os = __inS.__startWriteParams(Ice.FormatType.DefaultFormat);// 序列化调用结果,写入网络通道,等待发送给客户端Message.__write(__os, __ret);__inS.__endWriteParams(true);// 完成调用return Ice.DispatchStatus.DispatchOK;}
另外,我们看下Ice Object的Identity相关代码,从其代码看,一个Ice Object可以绑定多个ID,但是其中只有一个是最确切的真正的ID,此ID用来查找和定位RPC的远程对象:
- OnlineBookPrxHelper负责客户端调用逻辑,Ice的版本比作者写书的时候新了许多,客户端的调用略有改动,这里先不剖析了,等项目完成,我们单步debug下看看。
了解IceBox
简单地说,IceBox就好像是一个Tomcat,我们只要写N个Ice服务的代码,用一个装配文件定义需要加载的服务列表,服务的启动参数,启动次序等必要信息,然后启动IceBox,我们的应用系统就能够正常运行了。
要将一个Ice服务纳入到IceBox中,我们需要引入icebox包,改下build.gradle文件:
然后让我们的服务实现类继承IceBox的Service接口即可,此接口有两个方法:
- void start(String name, Ice.Communicator communicator, String[] args):服务启动方法
- void stop();服务停止方法
实际上,IceBox定义的这个Service接口是一个标准的服务生命周期管理接口,具体使用方法后面会涉及到。
实现OnlineBook服务
理解了IceBook基本知识后,我们来实现OnlineBook服务,新建cn.didadu.service.OnlineBookService.java,我们先看下实现IceBox.Service的两个方法:
看到这里,有两个疑问:
每个ObjectAdapter只绑定一个Servant,有没有浪费资源的嫌疑?因为我稍微修改了下ice_better_hello项目,一个ObjectAdapter是可以绑定多个Servant的,具体步骤就不贴出来了,很简单,部分引用代码如下:
12adapter.add(new HelloImpl(), Ice.Util.stringToIdentity("hello"));adapter.add(new GoodbyeImpl(), Ice.Util.stringToIdentity("bye"));如果每个Service都需要实现IceBox.Service,并且所有的类这两个方法的实现代码都一样,那岂不是很蛋疼。
这两个问题我们先保留,等后面看有没有合理的解决方案。
在看下继承_OnlineBookDisp的实现逻辑:
就这么短短的一行代码,又有一个疑问了,如果我们需要重写Message的toString()方法,可以去改java代码,但是项目重新编译一下,生成的*.java会被覆盖,照这么看,如果自动生成的代码有任何修改,后果都是灾难性的。不过这也是可以理解的,毕竟是自动生成的代码,谁让你去改的呢。但是,从结果来看,总归是不灵活,同样保留疑问吧。
配置log4j
OnlineBookService中用到了log4j,配置一下,要不然日志没法输出,在resources目录下新建log4j.properties,随便找个例子配置下,目前能打印出日志就行了:
配置IceBox
IceBox配置文件分为两部分:一部分是与具体服务定义相关的配置;另一部分是共有属性的定义。下面我们来详细看看:
- 在所有的服务的初始化完成之后,服务管理器将打印”token ready”。如果有脚本想要等待所有服务准备就绪,则这个特性很有用,比如:IceBox.PrintServicesReady=MyAppIceBox,会打印”MyAppIceBox ready”
- 当存在多个Ice服务时,通常它们之间有先后启动的顺序问题,我们通过下面的参数配置可以确定这些服务的启动先后顺序:IceBox.LoadOrder=server1,server2,server34
- 优化本地服务之间的调用的重要参数UseSharedCommunicator,值为1表示开启优化,下面假设Hello和Printer的两个服务存在调用关系,又部署在一个IceBox实例中,则定义两者使用同一个Communicator对象:IceBox.UseSharedCommunicator.Hello=1、IceBox.UseSharedCommunicator.Printer=1
- Ice.MessageSizeMax=2048:最大消息包的字节数
- Ice.Trace.Network=1:开启网络时间相关的日志追踪
- Ice.Trace.ThreadPool=1:开启线程池时间的日志追踪
- Ice.Trace.Locator=1:开启对Locator对象的日志追踪
- IceBox还有个管理服务组件,使之能够被远程控制,为了安全期间,管理服务组件默认是关闭的,可以通过Ice.Admin.Endpoints=tcp -p 9996 -h localhost开启,关于这一点,我们后面再看可以怎么使用
- 定义具体服务的相关参数,对于每个IceBox服务,需要这样定义:IceBox.Service.name=entry_point [–key=value] [args],配置项含义如下:
- name定义了service的名字,作为start方法的name参数,必须唯一
- entry_point是上面的service的完整类名,必须在classpath中可以找到
- [–key=value]作为property属性,用于构造该服务的communicator,这里也可以用–Ice.Config=xxx.cfg的方式独立配置
- [args]作为参数传入start方法的String[] args中。
这么多配置也只是不完全列举,有点懵逼。我们先配置几个必须项,尝试着跑跑看:
在resources目录下新建config.icebox文件:
12IceBox.PrintServicesReady=MyAppIceBoxIceBox.Service.OnlineBook=cn.didadu.service.OnlineBookService --Ice.Config=config.service在resources目录下新建config.server文件:
1OnlineBook.Endpoints=tcp -h localhost -p 10000
运行IceBox
命令行方式
首先需要在build.gradle中添加如下内容,打成fat jar,要不然没法执行:
在项目根目录执行如下命令:
图形界面
在开发过程中,还是图形来的方便,在IDEA中新建一个Run/Debug Configurations,配好之后点击Run就可以了。
这一章节就写到这里了,我们跑通了一个简单的IceBox,在查资料的过程中找到一个小伙伴的Github,推荐一下。