ICE 介绍与开发入门

说明:

本文是在2011年1月面试淘宝时,一位大佬让我写给他看的,后来2月份顺便就整理成了文档,分享在CU了,今天有空,就把它转到我的博客。
欢迎读者朋友补充并指出错误,转载时请注明作者以及出处。
本文是2011-2-16完成的,我于2011-2-21入职,时隔3三再次翻出该文,重新温习下以前的知识。

1简介

本文的主旨在对ICE(Internet Communications Engine)做简要的功能介绍,说明其在应用开发中的优点和特性,并为初次接触ICE的开发人员提供一个详细的开发入门的例子,关于ICE开发环境的搭建和更多细节,请参考附录提供的链接资源.

1.1什么是ICE

Zeroc的主页(http://www.zeroc.com/)对ICE进行了简单的介绍:
ICE(Internet Communications Engine)是一种面向对象的中间件平台,主要用于网络通讯。它为面向对象的“客户端-服务器”模型的应用提供了一组很好的工具和API接口。目前在全世界被应用于很多项目之中。ICE中间件号称标准统一,开源,跨平台,跨语言,分布式,安全,服务透明,负载均衡,面向对象,性能优越,防火期穿透,通讯屏蔽。因此相比Corba,DCOM,SOAP,J2EE等的中间件技术,自然是集众多优点于一身,而却没有他们的缺点。

1.2 ICE中间件的特点

ICE作为一款通讯中间件而言,从软件开发的角度来说,有以下显而易见的特点(优点):

支持多种不同语言之间的通讯程序设计

目前支持的语言有:C++,.NET, Java,Python, Object-c, ruby和 php. 也就是说,通过ICE的标准slice语言规定的接口,再结合slice2XXX(XXX=cpp, java, php…子类)命令的执行,java开发人员就可以很方便的和C++开发人员或者python开发人员,通过一组定好的接口进行通讯程序的开发,从而实现功能。跨越不同语言之间沟通,唯一要做的就是继承slice2XX生成的XX语言的基类,然后实现虚函数就行.

支持不同操作系统程序设计

如果你要开发一个基于标准socket的通讯应用,比如windows发送数据,unix接收数据,你的windows平台程序开发人员必须熟知windows下socket开发方法,unix平台程序开发人员必须熟知unix下socket开发方法,这样对开发人员来说,未免要求多了些。如果采用ICE框架进行开发的话,你的开发人员就能轻松多了:因为他根本不需要关注更平台相关的东西,他唯一要做的就是按照标准的C++语法写符合逻辑的代码,然后在链接时链接上ICE在该平台的库文件就行—一切跟平台相关的东西,Zeroc的精英们已经在ICE中为您实现好了。

灵活支持多种网络技术实现

使用标准的socket实现时,你所采用的TCP协议想改为UDP协议时,需要修改大量的代码。而使用ICE的话,你只需要该程序传递一个不同的配置即可(实际上是把配置中的TCP改成UDP即可)。
结合以上主要的三个特点,个人认为ICE很好的实现了应用系统中通讯部分的灵活设计,使用ICE进行单机上或者主机之间进程的数据通讯,完全将通讯的实现透明化了,开发人员可以把注意力集中放在业务上,而不必关注通讯细节,从而节省时间,提高工作效率.

1.3 使用ICE进行开发的步骤

基于ICE的应用,在实现通讯时,支持与标准SOCKET相同的输入参数,即: 支持IP地址和端口的配置,支持连接超时。另外又增加了一个很重要的成员(proxy string):在服务器和客户端之间,通过一个字符串,来区分相同和不同的应用,相同字符串的应用程序才能够互通; 最后ICE还支持以程序参数或者配置文件的方式的来配置它的众多特性,了解详情可以参考ICE的开发手册和官方文档。
使用ICE中间件进行开发的一半步骤如下(我们以C++语言为例,其它语言大致一样):

使用slice语言编写通讯时使用到的数据结构和接口定义

Slice语言是用来定义客户端-服务器之间程序的公用数据结构和接口函数的,个人理解是:用一种第三方语言来定义两种或者多种程序开发语言之间的消息接口,是为了方便实现多语言数据互通,不仅是在最终的可执行程序上,而主要是在程序代码上,通过第三方语言结合第三方工具,为多种语言的程序生成一些公共的基类和功能实现代码。关于slice语言的语法介绍,请参考
http://masterkey.javaeye.com/blog/184064
这篇文章.

使用slice2XXX命令来生成基础代码

当使用slice编写完ice文件后,比如我们存为filetrans.ice这个文件。然后就要根据开发人员使用的语言来调用slice2XX(XX可以为cpp, java, py,rb等等)命令生成对应语言的基础代码了。
比如你的客户端和服务器端使用的开发语言都是C++,那么,只需要运行命令:

slice2cpp filetrans.ice

即可生成对应的代码 filetrans.cpp 和 filetrans.h。如果你的客户端或者服务端程序是使用java语言开发的,只需要调用生成java代码的命令:

slice2java filetrans.ice

即可,会生成相应的java代码包.

客户端代码编写

客户端代码的开发相对来说比较容易,在原来生成代码的基础上,再增加一个包含main函数的主线程代码,在其中按照ICE客户端代码一般的编写方式编写即可,常见的流程是,初始化ICE,创建连接对象,调用发送接口。就完成了。比如本文中的客户端代码main函数代码片段如下:

Ice::CommunicatorPtr ic = Ice::initialize (argc, argv);
FileMessage msg; // request message
try{
char szproxy [PROXY_LEN] = {0}; // what we use to identify a connection
sprintf (szproxy, “filetrans:tcp -h %s -p 12345 -t 2000”, argv[1]);
Ice::ObjectPrx base = ic->stringToProxy(szproxy);
filetrans::FileMessageHandlerPrx client =
FileMessageHandlerPrx::checkedCast(base);
if(!client)
{
throw “invalud proxy:ComPlusBasePrx::checkedCast(base)”;
}
msg.head.filename = argv[2]; // get file name
cout << "request file '" << argv[2] << "' from host " << argv[1] << endl; // try to request the file FileMessage ret_msg = client->ProcessMessage(msg);


如果是带返回值的接口,还应该对返回值加以处理。(正如本文中后面的例子一样)。

服务端代码编写

服务端一般的写法是,创建一个新的类,继承生成的基础代码中的接口类,然后在其中实现我们在ice文件中定义的接口对应的虚函数即可,要注意:实现这个虚函数的过程,即是完成消息处理的过程,因此,所有跟应用层业务相关的东西都要在此处理函数中考虑到。当客户端发送的消息到达服务器程序后,该函数会被自动调用去处理消息.

程序编译

客户端和服务器端程序的编译链接都需要ICE的开发库文件和动态库的支持。对于工程代码本身而言,客户端程序的链接往往由两部分构成:自己编写的发送部分代码编译生成的对象文件和ICE生成的代码生成的对象文件;而服务器端程序的链接则还需要新增子类即消息处理部分代码编译后生成的对象文件.比如,从本文中例子程序的Makefile的内容便可以看到这点:


CFLAG=-c -Wall -I./ -I./ice/include/ -ftemplate-depth-128
SERVER_OBJS=MessageHandler.o filetrans.o
CLI_OBJS=filetrans.o
DFLAG=ice/lib/libIce.so.3.3.1 ice/lib/libIceUtil.so.3.3.1
CC=c++
default:
$(CC) $(CFLAG) server.cpp
$(CC) $(CFLAG) client.cpp
$(CC) $(CFLAG) MessageHandler.cpp
$(CC) $(CFLAG) filetrans.cpp
$(CC) -o server server.o $(SERVER_OBJS) $(DFLAG)
$(CC) -o client client.o $(CLI_OBJS) $(DFLAG)

以上简单的五个步骤描述了ICE开发的一般过程,第二章我们将结合一个具体的例子来让给您更深刻的了解ICE的开发过程。

2 ICE开发举例

2.1文本文件下载器简介

限于篇幅问题,在此不对ICE的细节做过多说明,仅仅通过一个简单的文本文件下载器的开发过程来说明使用ICE开发网络应用的一般步骤和工作内容.

该应用实现以下简单的功能:

服务器端程序server作为文件服务器,客户端向服务器端请求一个文件,服务器端程序收到消息后返回文件给客户端。服务器和客户端采用C++语言开发(如果您更擅长python或者java语言的话,可以尝试将客户端或者服务端程序用别的语言来实现).

2.2开发过程

基于ICE的应用中,服务端和客户端都享有相同的通讯数据结构和函数接口,如上文提到的,服务端需要新建一个类,这个类继承基础代码文件中的基类, 并且需要在新类中实现消息处理的虚函数,以完成对消息的实际处理.而客户端在填写完要发送的对象后,调用定义的接口进行处理,数据就会被传送到服务端,接着,服务端定义的与客户端调用同名的处理函数便会被调用,从而完成数据从客户端到服务端的传送和处理.

编写ice文件

基于ICE的应用需要开发人员通过一个slice语言编写的文件来定义“客户端-服务器端”通讯使用的数据结构和调用方法。数据结构描述了通讯时用到的对象,包含哪些跟业务相关的属性,方法一般是约定的描述该业务流程的一个函数,方法的参数或者返回值大多是该ICE文件中定义的对象。

根据本文的下载器的功能需求,定义如下ice文件,filetrans.ice


#ifndef CME_ICE
#define CME_ICE
module filetrans
{

sequence StrArray; //字符串数组
struct msghead //消息头
{
int result; //操作结果
string filename; //文件名
};
struct FileMessage
{
msghead head; //控制信息,请求文件名和返回结果
StrArray data; //请求文件返回的内容
};
interface FileMessageHandler //服务器和客户端之间的调用接口类
{
idempotent FileMessage ProcessMessage(FileMessage msg); //实际调用接口
};
};

定义这样的ICE文件,需要简述一下程序调用的流程即可理解:

客户端程序创建连接对象,填写服务端的IP地址,端口和协议值,以及连接超时等设置信息. 然后,定义一个 FileMessage 对象fm, 填充请求的文件名称. 如果客户端连接到服务器成功的话,客户端调用方法 ProcessMessage如下:

FileMessage req_msg;
req_msg.head.filename = “1.txt”;
FileMessage ret_msg = ProcessMessage(req_msg);

此时,服务端的ProcessMessage函数就被调用了,服务端开发人员在新的类的ProcessMessage函数中如下实现:

FileMessage ProcessMessage(FileMessage msg)
{
string name = msg.head.filename;
FileMessage ret;

//然后尝试读取name文件,并且
//把文件内容按行读出来,push_back到ret的data数组中去。
//结尾
return ret;
}

这时,客户端的调用便会返回,从返回的ret_msg中便可解析出文件的内容了.

生成对应语言的代码

ICE最方便之处,在于它通过ICE文件生成不同语言的代码,把通讯和方法调用的实现都在ice的链接库和生成的代码中完成了。因此,在开发ICE应用时,除了连接它提供的动态库之外,还应该在我们的工程代码中加入ICE文件生成的对应开发语言的代码,比如C++开发人员,需要生成的C++代码,java开发人员需要生成的java代码。方法如下:

生成对应的C++的基础代码,用ICE自带的工具:

Slice2cpp filetrans.ice

便看到了filetrans.h和filetrans.cpp

客户端开发

客户端代码的结构在上文中已经说过了,最后要注意的就是客户端代码中有些对象的类型命名,是ICE生成的,初次应用ICE的开发人员可能会感到迷惑,知道它的来源就容易理解了。一般来说,都是根据ice文件添加后缀生成的。比如本文中的例子中的一个类型:
FileMessageHandlerPrx 来源于filetrans.ice中的interface FileMessageHandler

以上是客户端的完整代码.

服务端开发

服务端的开发就稍微麻烦些,因为我们要创建一个新的类,继承ICE生成文件中的基类,并且实现继承类中的虚函数。具体步骤如下:

创建新的Class 名为 CMessageHandler,继承inerface对应的类FileMessageHandler,如下:


class CMessageHandler: public FileMessageHandler
{
public:
CMessageHandler();
virtual ~CMessageHandler();
::filetrans::FileMessage ProcessMessage(const ::filetrans::FileMessage& msg, const Ice::Current &);
};

在CPP代码中实现消息处理函数, 具体实现参见附件中MessageHandler.cpp文件。
主线程代码(server.cpp

int main(int argc, char* argv[])
{

Ice::CommunicatorPtr m_ic;
try
{
char szProxy[128] = {0};
sprintf(szProxy, “%s -p %d”, “tcp”, 12345); //协议,监听端口
m_ic = Ice::initialize(argc, argv);
Ice::ObjectAdapterPtr adapter =
m_ic->createObjectAdapterWithEndpoints(“filetrans”, szProxy);
CMessageHandler * pH = new CMessageHandler;
Ice::ObjectPtr object = pH;

adapter->add(object, m_ic->stringToIdentity(“filetrans”));
adapter->activate();
cout << argv[0] << "start running..." << endl; m_ic->waitForShutdown();//等待退出,进入阻塞状态
}
catch (const Ice::Exception & e)
{
cerr << e << endl; } catch (const char * msg) { cout << msg << endl; } return 0;
当程序编译通过,运行后,从m_ic->waitForShutdown()主线程阻塞,进程调度相当于交给ICE底层库,成为一个消息驱动型的程序,到来一个客户端的FileMessage消息时,CMessageHander类的ProcessMessage方法便会被调用。

编译运行

如果正确安装了ICE的开发库,根据您所安装ICE的位置修改附录中代码目录下的Makefile,然后运行make进行编译,如果顺利通过的话,就可以进行运行测试了。

启动server程序./server

然后运行客户端程序,比如 ./client 192.168.0.1111 Test.txt

客户端就会向运行在192.168.0.111主机上的服务端请求Test.txt,如果服务器不能连接或者文件不存在,客户度会打印异常信息。如果文件获取成功,客户端会打印文件内容.

3总结

本文只是对ICE做了一些简单的介绍,和应用开发的HOWTO指导,如果您想了解更多关于ICE的特性和技术细节,请咨询ICE的官方网站和手册。

4附录

zeroc官方主页

Zeroc主页

ICE开发环境搭建

请参考我的这篇文章”给工作内容与通讯有关的朋友推荐下zeroc的ICE中间件

参考文章

Zeroc-ICE之旅

5示例程序说明

开发环境

RedHat As5 kernel 2.6.18-8.el5(32位)
gcc version 4.1.1 20070105

文件与代码

ice_ex目录包含内容如下:

Makefile MessageHandler.h filetrans.cpp filetrans.ice server.cpp
MessageHandler.cpp client.cpp filetrans.h ice

其中ice目录中包含了ice开发库的头文件目录include和动态库目录lib,限于动态库文件太大(三十几兆),只保留了include中的内容。

附件代码在此:ice_ex[1].tar

This entry was posted in C/C++编程. Bookmark the permalink. Follow any comments here with the RSS feed for this post. Both comments and trackbacks are currently closed.