bboyjing's blog

尝试ZeorC ICE之【在线预定图书项目(一)】

从本章开始,我们以《ZeroC Ice权威指南》中提供的在线预定图书为例,来深入学习Ice的使用。

RPC调用详解

首先还是新建个项目ice_book,步骤就不说了,已经建过两个项目了,有经验了。在slice文件夹中新建Service.ice文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
[["java:package:cn.didadu.generate"]]
module book {
struct Message {
string name;
int type;
bool valid;
double price;
string content;
};
interface OnlineBook {
Message bookTick(Message message);
};
};

本例中我们创建了一个结构体Message作为参数和返回值,还有OnlineBook接口,该接口只有一个bookTick()方法。我们可以看下结构体Message的定义,涵盖了布尔类型、数值类型、整数类型以及字符串,目的是尽可能接近现实业务的需求;另外返回值Message也是为了测试RPC通信。然后在ice_book项目根目录执行gradle build,一共生成了12个java文件,下面挑重点来看下:

  • _OnlineBookOperationsNC和_OnlineBookOperations这两个姐妹接口是OnlineBook的具体业务方法定义的接口,不同的是_OnlineBookOperations在方法签名中增加了Ice的内部对象Current,这个Current对象包括了当前调用的网络连接等上下文信息。

    1
    2
    3
    public interface _OnlineBookOperations{
    Message bookTick(Message message, Ice.Current __current);
    }
  • _OnlineBookDisp作为实现了OnlineBook接口的抽象类,完成了基本的RPC调用过程,其中Disp是Dispatch的缩写,即调用分发。我们来看下核心代码。这里有一个小问题,我们知道Java平台采用大端字节序,而Ice采用小端字节序,转换的时候会有额外的开销,照官方说法在整个请求来看这个开销可以忽略不计,但感觉略有点儿不爽。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    /**
    *
    * @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的远程对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static final String[] __ids = {
// 所有的Ice Object都有此ID
"::Ice::Object",
// OnlineBook的真正ID
"::book::OnlineBook"
};
// 二分查找是否是某个远程对象
public boolean ice_isA(String s) {
return java.util.Arrays.binarySearch(__ids, s) >= 0;
}
// 默认ID是::book::OnlineBook
public String ice_id() {
return __ids[1];
}

  • OnlineBookPrxHelper负责客户端调用逻辑,Ice的版本比作者写书的时候新了许多,客户端的调用略有改动,这里先不剖析了,等项目完成,我们单步debug下看看。

了解IceBox

简单地说,IceBox就好像是一个Tomcat,我们只要写N个Ice服务的代码,用一个装配文件定义需要加载的服务列表,服务的启动参数,启动次序等必要信息,然后启动IceBox,我们的应用系统就能够正常运行了。
要将一个Ice服务纳入到IceBox中,我们需要引入icebox包,改下build.gradle文件:

1
2
3
4
5
6
7
8
9
10
ext {
iceVersion = "3.6.3"
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.11'
compile group: 'com.zeroc', name: 'ice', version: iceVersion
compile group: 'com.zeroc', name: 'icebox', version: iceVersion
compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.24'
}

然后让我们的服务实现类继承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的两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void start(String name, Communicator communicator, String[] args) {
// 创建ObjectAdapter,这里和service同名
_adapter = communicator.createObjectAdapter(name);
// 创建servant,同样和service同名
_adapter.add(this, communicator.stringToIdentity(name));
// 激活servant
_adapter.activate();
logger.info("Service " + name + " started.");
}
@Override
public void stop() {
// 销毁ObjectAdapter
_adapter.destroy();
logger.info("Service " + _adapter.getName() + " stopped.");
}

看到这里,有两个疑问:

  1. 每个ObjectAdapter只绑定一个Servant,有没有浪费资源的嫌疑?因为我稍微修改了下ice_better_hello项目,一个ObjectAdapter是可以绑定多个Servant的,具体步骤就不贴出来了,很简单,部分引用代码如下:

    1
    2
    adapter.add(new HelloImpl(), Ice.Util.stringToIdentity("hello"));
    adapter.add(new GoodbyeImpl(), Ice.Util.stringToIdentity("bye"));
  2. 如果每个Service都需要实现IceBox.Service,并且所有的类这两个方法的实现代码都一样,那岂不是很蛋疼。

这两个问题我们先保留,等后面看有没有合理的解决方案。
在看下继承_OnlineBookDisp的实现逻辑:

1
2
3
4
5
@Override
public Message bookTick(Message message, Current __current) {
logger.info("call bookTick : " + message.toString());
return message;
}

就这么短短的一行代码,又有一个疑问了,如果我们需要重写Message的toString()方法,可以去改java代码,但是项目重新编译一下,生成的*.java会被覆盖,照这么看,如果自动生成的代码有任何修改,后果都是灾难性的。不过这也是可以理解的,毕竟是自动生成的代码,谁让你去改的呢。但是,从结果来看,总归是不灵活,同样保留疑问吧。

配置log4j

OnlineBookService中用到了log4j,配置一下,要不然日志没法输出,在resources目录下新建log4j.properties,随便找个例子配置下,目前能打印出日志就行了:

1
2
3
4
5
log4j.rootLogger=debug, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# Pattern to output the caller's file name and line number.
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n

配置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中。

这么多配置也只是不完全列举,有点懵逼。我们先配置几个必须项,尝试着跑跑看:

  1. 在resources目录下新建config.icebox文件:

    1
    2
    IceBox.PrintServicesReady=MyAppIceBox
    IceBox.Service.OnlineBook=cn.didadu.service.OnlineBookService --Ice.Config=config.service
  2. 在resources目录下新建config.server文件:

    1
    OnlineBook.Endpoints=tcp -h localhost -p 10000

运行IceBox

命令行方式

首先需要在build.gradle中添加如下内容,打成fat jar,要不然没法执行:

1
2
3
4
5
6
7
8
jar {
manifest {
attributes("Main-Class": "IceBox.Server")
}
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
}
}

在项目根目录执行如下命令:

1
2
3
4
5
gradle build
java -jar build/libs/ice_book-1.0.0-SNAPSHOT.jar --Ice.Config=config.icebox
// 成功输出
INFO [main] (OnlineBookService.java:28) - Service OnlineBook started.
MyAppIceBox ready

图形界面

在开发过程中,还是图形来的方便,在IDEA中新建一个Run/Debug Configurations,配好之后点击Run就可以了。
zeroc_ice_7

这一章节就写到这里了,我们跑通了一个简单的IceBox,在查资料的过程中找到一个小伙伴的Github,推荐一下。