陈蔡悲欢

引言:
本文是大概14年前的2000年时,在高中读到的一篇好文,一直保存到现在,于是今天发布到我的博客,希望能被更多的朋友阅读和传承。

陈蔡悲欢

古老富饶的陈蔡静静地卧在中原大地的怀抱。温柔多情的颍河水从鸿蒙中流出,从这块黄土地上款款流过,流淌着时间的酸甜苦辣,流尽了人生的悲欢离合;从西到东,奏响了一曲雄壮的历史悲歌……

南风悠悠地吹起来了。

雪白的奶汁把麦穗儿灌满,便凝固成丰收的希冀。竖直的麦秆是农民立体的夙愿。麦子几经风风雨雨日晖月华的漂洗,岁月之波荡成纯洁而醇香的金黄。它穿越天地间一片混混沌沌,飞向晴朗,摇响了一片耳熟的乡音。春老夏续,初夏的音符奏响了农村丰收曲的旋律。

太阳,容光焕发地走向北回归线。农民挥汗如雨,去收割饱满的麦子,一如收割沉甸甸的理想,收获岁月丰厚的馈赠。农民脊背上的汗水,被很快地蒸发掉,留在衣服上一圈圈不规则的白色痕迹,沿着脊梁的走向形成一片片形状不一的盐碱滩。皮肤,被太阳涂抹成凝重的古铜色。

20

麦场上,吱吱呀呀的石碾声从农民的记忆深处响起,隐隐约约碾过了几千年。沧桑巨变,而牛依然默默地咀嚼着历史的沉重负荷和生活的酸甜苦辣,以勤劳和诚恳哺育了人类几千年。

现代文明在麦场上奔驰着,隆隆的声音向农民发表着季节的宣言;脱粒机大口大口地吞进麦秧,向农民宣泄着季节的狂欢。老人蹒跚的脚步和孩子稚气的脸都在麦场上重叠着,形成一个美妙的组合。太阳在天空演绎着一个远古的童话,大地在久久地回味着这丰硕的收获。欢歌笑语编织着一个丰收季节的古老传说……

22

夕阳西下。

晚霞洒满西方的天空。麦场上空一条条麦子被扬起的圆弧,于晚霞映衬成细美的图景。风,从农民心灵的田野吹过,吹散往日的艰辛,吹去往日的期盼和沉默,吹去康尘,留下颗粒饱满的麦子——这多少风雨中长成的精华。

一位农民虔诚地捧起一捧麦子,眼里饱含激动的泪花。他山一般地半跪着,夕阳拈一片透明的光将他定格,久久地,定格成一尊力的雕塑。他一次又一次地凝视着这汗水和心血的结晶,脸上那丰收的豪情随手中的麦子一起落下,久久地回荡在古老的大地上……

暮色溶溶,人间重复着这收获的神圣主题。

21

原始的播种工具,农民用一支削尖的棍掘开丰收的巢穴,掘开时光深埋的沉寂。而后小心翼翼地放进两粒种子,用手埋下绿色的希望。背弯如弓,犹如历史凝成的象形文字,闪耀这生命的神光。

五千年的苍苍岁月和那流溢清辉的黄皮肤月亮从这片黄土地上一晃而过,这片依然贫瘠的土地上生长了多少欢欣和苦涩啊!

无论收获与否,龙的传人一如既往地播种着,历史的霜霜雪雪抹去了他们的生命,他们却坚持不懈地播种着,用泪,用血,用那隔世的执着……

23

当绿色的希望摇曳成岁月的金黄,辛勤的农民来不及找一片绿荫,便有掮起早秋的犁铧。

玉米杆不失时机地伸开锯似的叶子,玉米攒动着,蜿蜒成一片难以忍受的疼痛。只有大豆清闲地做着圆圆的梦,幻化成清晨七彩的露珠。

用那双点缀着血泡的手扶起那被磨砺了两千年,闪射出远古的风雨和现代色泽的犁,甩响鞭子吆喝走那沉默了几千年的牛,翻过那层孕育丰收的土地,摇响开元的古耧,撒落下现代科学的结晶,合着农民的艰辛和欢乐一同在墒田中睡去,在祖先瘦瘦的梦中萌芽……

农民,这些生活在阴历中的农民,用那被暴晒的日趋弯曲的脊梁托起了一个民族的骄傲。背负历史,在人生的道路上艰难地跋涉着。将自己成吨的汗水连同一颗淳朴的心,拌着风月霜雪和日月星辰,日复一日年复一年地浇灌着生他们养他们埋葬他们的黄土地,用那日趋干枯的心血谱写了一曲奉献者的强音!

这就是创造了和创造着中国历史的农民!

Posted in 其他 | 陈蔡悲欢已关闭评论

云归何处

引言:
2007过年回家用电脑,想找本书当作鼠标垫,偶然间发现一本1993年的中学生作文选,其中的这篇 <<云归何处>> 很是欢喜,21年前的文章,现在读起来还是感觉很不错!特此转载在我的博客,料想这篇文章的作者现在应该也已经人到中年了,数年后,再读此文,使人不禁感叹:光阴如梭,岁月如歌,时光一去不复云归何处

云归何处

维扬城内……有‘天下第五泉’,乾隆四十三年,泉内生一股轻烟,香泽幽远,雾滃缭绕,片刻间化作祥云一朵,缓缓升空,放五色霞光,邑人皆见,时许飞去……
—–录自<<琼瑶集>>
19

光阴荏苒,岁月匆匆,天上一夕会,人间百余年。遍游三山五岳,琼楼玉宇,无处是家,也曾在无垠的星空中自由地来去,也曾在浩瀚的天体中寻找自己的轨迹,搁不下,遣不去的,是那份幽幽的乡思,默默地乡情。

推云路,隐霞光,下碧空,越东海,认准那春风十里,烟花三月的鹤游之地,我归来了!细凝眸,却见那绿树已不再成荫,那红桃已不再艳丽,那琼花已不再盛开…… 目光所及,处处凋残;香火不断的观音山,如今炉灰冰冷,曲水流芳的护城河,如今淤塞灰暗;端庄雅丽的画舫佳阁,如今破烂不堪;曲径通幽,藏明园,蕴明珠的深深小巷,如今鸡犬喧闹,桑麻盈目,贴满了“打倒走资派”,“无产阶级文化大革命万岁”的标语……此情此景,不由有一首歌吹自心底,缓缓流过:“……念桥边红药,年年知为谁生?”

走一走故穴—五泉佳胜,泉水不复可见,当年那灵蕴与风情已寥寥难觅。我不禁一声轻吟,泪雨份扬,唯听得墙外鉴真堂内钟磬梵鼓,一声声栖鸟停云……

青马传讯,扬州被列为神州首批历史文化名城之一,我不禁怦然心动。几年来,岩穴静修无绪,空寂漂泊无依。乍闻佳音,顿续旧梦,顾不上多整一整云髻,多舒一舒云袖,顺着熟悉的旧路,去寻找一个失落在明月乡里的梦。

归途匆匆,匆匆中又步履迟迟,不知今日扬州是一番什么景象?生恐心儿又一次受到刺激。风一程,雨一程,终于又回到朝夕萦心的的绿杨城郭,按下云头,只见全城一派生气,男女老少,开塘的开塘,种树的种树,挖渠的挖渠……观音山内,大小菩萨刚刚“坐定”,正待塑金;小秦淮河畔,河道已疏浚拓宽,唯缺一眼活水;仿古的三元路建筑群正在打桩作基……巍峨的新装与低矮的旧貌参差不齐,……一切的一切,都在我没有准备的意识中,热闹起来—这是春天来了。

于是,想做一个不惊醒梦的寻找,让自己在乡情的温柔中轻轻摇曳。我的心萌动了,我感觉到生命的又一次涨潮;终于,我苏醒又一次的渴望。

正欲跨过大宏桥去往当年的第五泉,忽有仙友飞来,盛情相邀去俯看截断万里长江的的葛洲坝工程,盛世奇观,佳意难却,途中,我暗想:故土,故土,但愿再见时,能捧一泓溪水润湿一两份欣喜,悄悄吐露一两个灵感吧!

葛洲坝上推祥云,布瑞雨,倏忽数年,忽听说扬州发了水灾,一场大水淹没了房屋,农田,牲口…….令我心急如焚。是不是我的希望就此沉入滔滔的水底了?

瑶光电射,转眼间已是广陵旧居,凌空下视—-只觉得是个奇迹: 小桥流水,雕梁画栋,曲水环绕,石舫幽雅,游人如云,绿树成荫,亭台楼榭,琼花满枝,殿宇梵唱,银杏累累……,好一派冶华绝色的风姿!你看那千余年来一直在撩拨人心的,具有清幽恬静意境的二十四桥,在四桥烟雨中,在春台明月下,在香海慈云间,荷浦熏风,碧玉交流,临水红霞,万松叠翠,可谓“两堤花柳全依水,一路楼台直到山”。想必当年隋炀帝在月夜所携二十四名吹箫美女的容貌精华已全然集中于这一处了。

再看那曾多次毁于兵火的观音山,摘星楼外游天池,紫竹园内存鱼洗,还有那迷楼的宫深幽折,千门万户,意夺神迷。每届六月,秉烛进香,虔诚许愿的人流更是恒久不息。

世外桃源,竹西佳处,与前者相比则是玲珑,悠静,“二十年前载酒瓶,春风依醉竹西亭,而今再种扬州竹,依旧淮南一片青。”那竹西精舍,月明桥,凉亭都是不凡而又精秀的好去处。古运河畔,这片悠悠的风情在悠悠的岁月中滴下了宁静的和谐之美。

独自漫步于小秦淮河,这里,白石栏杆齐列,初蕾夹新絮,别有一种脱俗宁静的美,秦淮河水悠悠地流,淌进了桨声里的灯影。流水涓涓,拱桥如眉,树影寂寂花悄悄。时近黄昏,天际如染。金色洒在河里,不远处是缤纷各异的色彩—那是灯会。

人海如流,笙歌鼎沸。点点灯火,闪烁着,跳跃着,每一盏灯都是一个祝福,每一道光都是一份温馨。我感慨万千,在这红的热烈,黄的朦胧,紫的忧郁,蓝的仿佛,绿的喜悦之中,融入了扬州几番兴衰,几步枯荣,几经磨难,几度繁华。

漂泊终于过去,绮梦近在眼前,刹那间,我放出蕴蓄百年的霞光,腾身而下,融入主灯的异彩之中,给古城留一段永恒的神话……
,每个人都应该珍惜当前的幸福生活。

Posted in 其他 | 云归何处已关闭评论

低调做人,厚道处世

低调做人,厚道处事不仅是一种境界、一种风范,更是一种思想、一种哲学。
18

 1 地低成海,人低成王

地不畏其低,方能聚水成海,人不畏其低,方能孚众成王。世间万事万物皆起于低,成之于低,低是高的发端与缘起,高是低的婵变与演绎。低调做人正是一种终成其高,必成大器的哲学。谙通此一哲学的人方为大智之人、方成大价之身。

2 圣者无名,大者无形
真正的圣人最终会修炼到无我的状态,无我者何顾其名哉!真正的大者最终会演化为混沌的状态,混沌者何存其形哉!人之圣,其名奄奄乎成其道;天之大,其行浩浩乎成其理。低调做人的道理正在于此,遁其名,隐其形,方为至圣,方为至大,方为永恒。

3 鹰立如睡,虎形似病
鹰者天之雄,虎者地之威,如此雄威的动物却常常扮作一副恹恹欲睡的病态,从而使猎物放松对它的警惕,待猎物接近时再雄威大作,捕之食之。在现实生活中,常见弱者好趁强施威,而强者却多装熊示弱——看来低调做人更像施强者的哲学,这套哲学不止是一种自我保全和自我防守的智慧,更是一种谋求生存和伺机攻敌的武器!

4 贵而不显,华而不炫
荣华富贵几乎是每个人的向往的。但贵而不显,华而不炫却不是每个人都能做到的。低调正是这些已获得荣华富贵者的做人哲学。富贵固然令人倾慕,但自古富贵险中求,富贵者常在风头浪尖上;荣华固然令人向往,但自古荣华难常在,荣华者多在枝头惹眼处。富贵荣华者只有深谙低调做人之道,才能更好的成就自己和保全自己

5 才高而不自诩,位高而不自傲
才高是件大好事,但如果自诩自夸,就会自损其才,自伤其能;位高是件大好事,但如果自骄自傲。就会自贬其尊,自嫡其位。所以,无论何时何地,都应该保持谦逊,放低姿态,勤勉做事,低调做人。自诩与自傲是自轻自贱的表现,只有低调做人才是自珍自重的大理。

6 韬光养晦,深藏不露
永远不要暴露自己的目标,不要轻易亮出自己的底牌,不要让自己的锋芒在别人的眼前晃动。人生好比一场战斗,要学会隐藏自己,埋伏自己,只有学会防守,让自己首先获得保全,才能在帷幄中运筹进攻的策略和等待进攻的时机。而低调做人正是一种隐藏自己的保护色和遮阳伞。

7 人世多繁杂,做人不能太单纯
要想在繁杂的社会里生存和发展,首先必须学会做人之道。成功的机会对于每个人都是均等的,惟一能胜过别人,与众不同的地方就是做人的智慧。会做人的人能在人世繁杂中游刃有余,这样的让人拥有非凡的的心机和智慧。而做人太单纯的人,不懂得保全自己的实力,就会吃亏。做人不能太单纯,不是要你虚伪与狡诈,而是让你了解做人的智慧和策略。

8 做人要留退路,做事要留余地
做人难,难做人。这是千百年来一直让人们困惑的问题。事实上,做人真的有那么难吗?其实不然,只要我们存有宽广之心,做人不要做的太绝,做事不要穷追不舍,你会发现,脚下的路其实很平坦。人不是生活在一时一刻。也不是与人只有一次接触,聪明的人懂得给自己留退路,懂得给他人留余地。表面上是宽容了别人,而实际上也是在为自己铺路,否则,就会陷入死胡同,使自己无出路,后无退路。

9 成熟而不世故,小糊涂中有大聪明
做人的成熟是人生的一种气质,而世故则是人生的一种疾病。成熟者对事物洞明,敢作敢当,有“舍我其谁”的气概,往往小事糊涂,大事清楚。世故者则游戏人生,奉行的是滑头主义和混世方略,专搞中庸,惯于骑墙。世故的人在交往中被人们认为是聪明,实则不然,这恰恰是愚蠢的表现。他们让人不可靠近,不敢靠近,不愿意靠近,实际上这才是做人最大的失败。

Posted in 励志 | 1 Comment

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

Posted in C/C++编程 | ICE 介绍与开发入门已关闭评论

libpcap学习之一:ip,tcp和udp协议

这个例子使用libpcap解析网络报文,打印tcp/udp协议的内容。
需要注意的是,这个程序的编译需要libpcap-devel支持。
如果通过命令:

rpm -qa | grep pcap

查看不到 libpcap-devel的话,就需要

sudo yum install libpcap-devel -y

来安装这个包。
然后下载附件的源码
pcap1(点击即可直接下载)
解压缩,进入目录中,直接运行

make

即可完成编译,生成的二进制程序名称为 “a”
如下运行:

sudo ./a bond0 (或者eth0等)

可以看到解析到的报文,输出如下:

Device: bond0
Number of packets: 10
Filter expression: udp

Packet number 1:
From: 0.0.0.0
To: 255.255.255.255
Protocol: UDP
Src port: 68
Dst port: 67
udp length:286
udp sum:17843
Payload (278 bytes):
00000 01 01 06 00 5e 2a 4d cd 00 00 80 00 00 00 00 00 ….^*M………
00016 00 00 00 00 00 00 00 00 00 00 00 00 00 21 5e 2a ………….!^*
00032 4d ec 00 00 00 00 00 00 00 00 00 00 00 00 00 00 M……………
00048 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00064 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00096 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00112 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00128 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00144 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00176 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00192 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00208 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
00224 00 00 00 00 00 00 00 00 00 00 00 00 63 82 53 63 …………c.Sc
00240 35 01 01 33 04 ff ff ff ff 0c 08 36 34 39 41 41 5..3…….649AA
00256 33 32 31 37 03 01 03 06 ff 00 00 00 00 00 00 00 3217…………
00272 00 00 00 00 00 00 ……

Packet number 2:
From: 10.232.2.102
To: 10.232.2.254
Protocol: UDP
Src port: 38880
Dst port: 53
udp length:46
udp sum:41840
Payload (38 bytes):
00000 d6 a9 01 00 00 01 00 00 00 00 00 00 04 70 6f 70 ………….pop
00016 33 0b 61 6c 69 62 61 62 61 2d 69 6e 63 03 63 6f 3.alibaba-inc.co
00032 6d 00 00 01 00 01 m…..

Packet number 3:
From: 10.232.2.254
To: 10.232.2.102
Protocol: UDP
Src port: 53
Dst port: 38880
udp length:214
udp sum:60805
Payload (206 bytes):
00000 d6 a9 81 80 00 01 00 01 00 04 00 04 04 70 6f 70 ………….pop
00016 33 0b 61 6c 69 62 61 62 61 2d 69 6e 63 03 63 6f 3.alibaba-inc.co
00032 6d 00 00 01 00 01 c0 0c 00 01 00 01 00 00 00 e9 m……………
00048 00 04 2a 78 51 1b c0 11 00 02 00 01 00 00 02 02 ..*xQ………..
00064 00 15 04 6e 73 68 7a 0d 61 6c 69 62 61 62 61 6f …nshz.alibabao
00080 6e 6c 69 6e 65 c0 1d c0 11 00 02 00 01 00 00 02 nline………..
00096 02 00 07 04 6e 73 70 32 c0 47 c0 11 00 02 00 01 ….nsp2.G……
00112 00 00 02 02 00 06 03 6e 73 38 c0 47 c0 11 00 02 …….ns8.G….
00128 00 01 00 00 02 02 00 06 03 6e 73 70 c0 47 c0 76 ………nsp.G.v
00144 00 01 00 01 00 01 bf b5 00 04 6e 4b d9 03 c0 88 ……….nK….
00160 00 01 00 01 00 01 c0 07 00 04 6e 4b d9 02 c0 42 ……….nK…B
00176 00 01 00 01 00 01 c0 07 00 04 79 00 15 04 c0 63 ……….y….c
00192 00 01 00 01 00 01 c0 07 00 04 79 00 15 03 ……….y…

注意
(1): 协议类型
也可以把过滤器字符串从ip修改为tcp或者udp,比如你可以把代码行:

char filter_exp[] = “ip”;

修改为:

char filter_exp[] = “udp”;

这样,就只会过滤指定协议的报文到回调函数 got_packet。

(2): 处理包个数
在代码

pcap_loop (handle, -1, got_packet, NULL);

中,当第二个参数为正数num时,处理到num个包就会退出,而为-1时,就会一直处理下去,这点要注意下。
(3): 核心代码片段
这个例子的核心函数是:

void
got_packet (u_char * args, const struct pcap_pkthdr *header,
const u_char * packet);

他是注册到pcap的回调函数,用于处理经过规则 filter_exp 过滤后的报文。
看这个函数之前,先看看代码中的几个数据结构定义。
首先是MAC协议头和IP协议头定义:

/* Ethernet header */
struct sniff_ethernet
{
u_char ether_dhost[ETHER_ADDR_LEN]; /* destination host address */
u_char ether_shost[ETHER_ADDR_LEN]; /* source host address */
u_short ether_type; /* IP? ARP? RARP? etc */
};
/* IP header */
struct sniff_ip
{
u_char ip_vhl; /* version << 4 | header length >> 2 */
u_char ip_tos; /* type of service */
u_short ip_len; /* total length */
u_short ip_id; /* identification */
u_short ip_off; /* fragment offset field */
#define IP_RF 0x8000 /* reserved fragment flag */
#define IP_DF 0x4000 /* dont fragment flag */
#define IP_MF 0x2000 /* more fragments flag */
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
u_char ip_ttl; /* time to live */
u_char ip_p; /* protocol */
u_short ip_sum; /* checksum */
struct in_addr ip_src, ip_dst; /* source and dest address */
};

如果你还看不懂这两个数据结构的定义,那么,迅速去百度下tcp/ip协议图示,先弄明白MAC/IP层协议包含了哪些元素。或者安装个wireshark或者tcpdump抓包看下。最好的方式就是在windows下用wireshark抓取一个报文看下结构,另外,也可以阅读《TCP/IP第一卷:协议》来掌握这部分知识。

第二个数据结构是udp协议头,在原来的开源代码中是没有sniff_udp结构定义的,不过参考ip和tcp结构的定义,以及TCP/IP协议结构图,
就可以定义出sniff_udp的结构,如下:

struct sniff_udp
{
uint16_t sport; /* source port */
uint16_t dport; /* destination port */
uint16_t udp_length;
uint16_t udp_sum; /* checksum */
};

udp协议是比较简单的,tcp结构由于其协议复杂,数据结构也就复杂多了,定义如下:

struct sniff_tcp
{
u_short th_sport; /* source port */
u_short th_dport; /* destination port */
tcp_seq th_seq; /* sequence number */
tcp_seq th_ack; /* acknowledgement number */
u_char th_offx2; /* data offset, rsvd */
#define TH_OFF(th) (((th)->th_offx2 & 0xf0) >> 4)
u_char th_flags;
#define TH_FIN 0x01
#define TH_SYN 0x02
#define TH_RST 0x04
#define TH_PUSH 0x08
#define TH_ACK 0x10
#define TH_URG 0x20
#define TH_ECE 0x40
#define TH_CWR 0x80
#define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)
u_short th_win; /* window */
u_short th_sum; /* checksum */
u_short th_urp; /* urgent pointer */
};

然后,mac头,ip头,tcp头和udp头的解析就比较容易看懂了:

/* define ethernet header */
ethernet = (struct sniff_ethernet *) (packet);

/* define/compute ip header offset */
ip = (struct sniff_ip *) (packet + SIZE_ETHERNET);

/* define/compute tcp header offset */
tcp = (struct sniff_tcp *) (packet + SIZE_ETHERNET + size_ip);
udp = (struct sniff_udp *) (packet + SIZE_ETHERNET + size_ip);

tcp和udp的payload也是根据协议来计算的:

/* define/compute tcp payload (segment) offset */
payload = (u_char *) (packet + SIZE_ETHERNET + size_ip + size_tcp);

/* compute tcp payload (segment) size */
size_payload = ntohs (ip->ip_len) – (size_ip + size_tcp);

payload = (u_char *) (packet + SIZE_ETHERNET + size_ip + 8);
size_payload = ntohs (ip->ip_len) – (size_ip + 8);

最后是打印payload的代码片段:

void
print_payload (const u_char * payload, int len)
{

int len_rem = len;
int line_width = 16; /* number of bytes per line */
int line_len;
int offset = 0; /* zero-based offset counter */
const u_char *ch = payload;

if (len <= 0) return; /* data fits on one line */ if (len <= line_width) { print_hex_ascii_line (ch, len, offset); return; } /* data spans multiple lines */ for (;;) { /* compute current line length */ line_len = line_width % len_rem; /* print line */ print_hex_ascii_line (ch, line_len, offset); /* compute total remaining */ len_rem = len_rem - line_len; /* shift pointer to remaining bytes to print */ ch = ch + line_len; /* add offset */ offset = offset + line_width; /* check if we have line width chars or less */ if (len_rem <= line_width) { /* print last line and get out */ print_hex_ascii_line (ch, len_rem, offset); break; } } return; }
整个程序基本上就分析完了。

Posted in 网络技术 | libpcap学习之一:ip,tcp和udp协议已关闭评论

rpm文件认知与格式剖析

说明:未经作者同意,不得转载本站文章
前言:

2012年9月,我有幸参加了IT168在北京举办的系统架构师大会,并在该会议上做了一篇关于《软件管理平台设计》的演讲,PPT下载在这里:
PPT下载地址在这里
在大会结束后,为了不使得这个讨论话题只成为一次演讲,而能将其延伸下去,我便又在 chinaunix 发表了三篇关于软件管理的文章,本文就是其中的第一篇。

本文最早发布在 chinaunix 社区的“架构设计”板块,后来经过系统化的整理后作为基础部分纳入了《linux软件管理平台设计与实现》这本书中。对本书感兴趣的读者可以搜索相关文章或者购买本书阅读。闲话少说,转入正文。

从文章的标题和PPT的内容,可以知道,本文要说的对象是 rpm 文件和 yum 服务,本人不太喜欢那种教材方式的介绍,而是喜欢从个人的最直观理解去阐述,叙说一个事情或者一门技术,那么,本文采用的就是这种书写方式。

RPM文件

首先说说 RPM 文件,百度搜索下 “rpm”,可以看到他的英文名字是: RedHat Package Manager, 你可以认为 rpm 是一种软件包管理方式,或者是一个软件包管理工具。提到软件包管理,就赘述几句,什么是软件包呢?

我对“软件包”的理解是:最原始的包是一堆需要运行的程序的集合,可能还需要加上一些配置文件,动态库之类,就构成了软件包。比如,
你把自己写好的脚本或者 C 代码编译生成的二进制文件,加上依赖的某些 so 文件和 conf 文件,存储在一个目录中,叫做 execute, 那么,这时就可以称呼 execute 为

一个软件包了。

有人可能说:这怎么能叫作软件包呢? 朋友,让我们仔细想想,它(execute)确实应该是一个软件包,它包含了要运行的应用需要的基本东西:执行程序,配置,依赖库等,这时,他不是软件包还能是什么? 只不过这个软件包看起来有些原始或者简单而已。

有了基本的软件包-一个目录中存放文件的集合,我们就会想着更高级格式的软件包,比如对 execute 进行压缩后,也是一个软件包,通过 tar 或者 gzip 得到 .tar.gz, .rar 或者 .zip 格式的文件,你就获得了一个较为高级的软件包了,为什么称其为“高级”软件包呢?因为它把程序和配置变成了一个单一的文件,这样就方便拷贝 了,另外压缩文件格式的软件包,也节省了磁盘空间占用和网络数据传输的量。在我之前从事3年的单位,我们所在项目组的程序基本都是通过这种方式发布的,每次发布程序,都是把动态库,二进制程序和配置文件压缩成一个 tgz 文件,拷贝到需要运行的 2000 多台机器上,解压到指定目录下,然后直接运行即可。可以看到,这种软件包组织方式使用起来已经比较方便了。

有了 tgz/rar/zip 格式的软件包就够了吗?可能对于某些小型应用确实够了,但是,日常工作中我们经常还会碰到下面这些问题:

(1): 想要查某个软件包的信息,比如谁制作的,什么时候制作的,以及描述信息?
(2): 给软件包带上一些特殊功能,除了文件拷贝功能外,还要有配置文件生成,安装服务,执行命令等操作,压缩文件怎么用?
(3): 软件包版本升级时,通过压缩文件怎么做?

除了以上这3点,应该还有其他方面的问题,你应该都会遇到或者思考到。再思考下。。。压缩文件格式的软件包确实存在这些功能上的不足,有没有一种格式的软件包,除了压缩存储之外,还能实现更多的功能呢?
答案是肯定的。

rpm 就是具有上面提到的诸多功能的一种包组织方式。当然还有其他方式的高级软件包格式,不过本文和后续的文章中只对 rpm 这种格式的软件包进行讨论。

先来看看这种高级软件包的特点(或者功能)吧:

(1): 压缩数据存储。
(2): 文件安装到系统中指定位置。
(3): 配置文件生成。
(4): 系统服务注册。
(5): 软件依赖检查。

除了(1)和(2),后面这三个功能,大概是压缩格式软件包都不具有的功能吧。

压缩存储

这是所有软件包的基本功能,比如,我这里有个目录是5.2M字节, 用它制作好的 rpm文件 只有2.4M大小。

文件安装

这个功能同样是软件包的基本功能,运行命令:

rpm -qpl ./cmeguard-1.1.2-34.i386.rpm

能够看到如下输出:

/etc/init.d/cmeguard
/usr/local/cmeguard/bin/auto_update
/usr/local/cmeguard/bin/cmeguard
/usr/local/cmeguard/bin/cmesync
/usr/local/cmeguard/bin/daemon
/usr/local/cmeguard/bin/genfinger
/usr/local/cmeguard/bin/run
/usr/local/cmeguard/bin/sync_plug
/usr/local/cmeguard/bin/sync_plug_back
/usr/local/cmeguard/conf/cmeguard.conf
/usr/local/cmeguard/conf/cmeproxy.conf
/usr/local/cmeguard/conf/cmesync.conf
/usr/local/cmeguard/conf/error.html
/usr/local/cmeguard/conf/mime.types
/usr/local/cmeguard/data
/usr/local/cmeguard/db
/usr/local/cmeguard/finger
/usr/local/cmeguard/lib
/usr/local/cmeguard/log
/usr/local/cmeguard

上面看到的文件列表就是将来会安装的文件列表,如果你用默认安装命令:

rpm -ivh cmeguard-1.1.2-34.i386.rpm

安装这个包的话,就会在你的系统上找到上面列表中对应的所有文件,而且最少是这些文件(因为安装时可能会产生新的文件,这个后面会说)。

配置文件产生

配置文件既可以通过安装列表中的文件来生成,又可以通过安装过程中的脚本来生成。

服务注册

如果你用rpm安装过 apache, mysql-server 等常见的软件,应该会知道,安装完成后,会对应在 /etc/init.d/ 目录下创建一个 httpd 或者 mysqld 的可执行文件,这个文件按照标准的自启动脚本格式书写的(参考chkconfig),当系统以对应的模式启动时,被安装导系统中的服务程序就会运行起来。
比如,运行命令:

rpm –nosignature -qpl ./vsftpd-2.0.5-10.el5.i386.rpm | grep init

输出如下:

/etc/rc.d/init.d/vsftpd

能够看到:

/etc/rc.d/init.d/vsftpd

这个文件被安装到了系统中,这时,你就可以通过命令:

service vsftpd start/stop/restart/status

来控制 ftp 服务的启动和停止了。

软件依赖检查

我们开发的程序很少是单独运行的,大多数都会依赖其它软件,比如你开发的数据库处理程序可能需要 libmysql,网络报文处理程序需要libpcap这个包的支持,这时,为了保证你的软件安装后能正常运行,而且在安装时就能够检查环境是否就绪,就可以通过软件依赖的方式来实现(当然,有人可能会问怎么实现这些呢,这个在后面文章的spec 语法中会详细说明)。

还是举个例子,比如有个 rpm 叫作:

test_rpm-1.1.1-21.i386.rpm

首先我们看下它 require了 哪些咚咚,
运行命令:

rpm -qp test_rpm-1.1.1-21.i386.rpm –requires

输出如下:

test__require_pkg
ruby-libs
/bin/sh
/bin/sh
rpmlib(PayloadFilesHavePrefix) >= 4.0-1
rpmlib(CompressedFileNames) >= 3.0.4-1

能够看到它依赖了这些组件或者包。
然后尝试安装下这个软件包,运行命令:

rpm -ivh test_rpm-1.1.1-21.i386.rpm

提示如下:

error: Failed dependencies:
test__require_pkg is needed by test_rpm-1.1.1-21.i386

能够看到,系统缺少 “test__requre_pkg” 这个包,因此 test_rpm-1.1.1-21.i386.rpm
这个包是不能安装成功的,这样的逻辑是合理的。虽然我们通过如下命令查看系统安装的包

rpm -qa ruby-libs

能够看到依赖之一的 “ruby-libs” 已经安装好了,但是在每一个rpm的依赖列表中,他们彼此是缺一不可的,或者说他们是”与”的关系。

好了,rpm 的五个特性,暂时就说到这里。
再来少许赘言对上面起初提的问题进行回答: rpm 是怎样在压缩存储之外又能做到其它功能的呢,比如安装服务,执行某些命令,打印信息,发邮件,检查依赖包,还有升级时做版本检查。这些都怎么实现的?

在此只进行简单的解释,细节留待 spec 文件那节再说。
rpm 其中一个功能就是对脚本( scripts) 的支持,除了文件压缩存储,它支持在安装软件或者卸载软件的过程中(确切点说,是这个过程的开始,进行和结束后这些不同时间点),执行一些命令,常用的脚本有:

post install
pre install
post uninstall
pre uninstall

从字面意思就能看到其作用,也就是说rpm能够在安装前,安装完,卸载前,卸载完执行某些脚本,这就为扩展软件包的功能提供了极大的空间。
我们看一个例子,运行命令:

pm -qp test_rpm-1.1.1-21.i386.rpm –scripts

输出如下:

preinstall scriptlet (using /bin/sh):
echo “pre install scripts by duanjigang”
postinstall scriptlet (using /bin/sh):
echo “post install scripts by duanjigang”
preuninstall scriptlet (using /bin/sh):
#!/bin/bash
echo “pre uninstall by duanjigang”
postuninstall scriptlet (using /bin/sh):
#!/bin/bash
echo “post uninstall by duanjigang”

能够看到这个软件包有 5 个 scripts 在 rpm 中附带着,分别在安装前后,卸载前后,build 这五个点执行,这样,你就可以通过这些 scripts 来实现想要的功能了。
关于 scripts 在那些不同点执行,有什么效果,后面会详述。

还有就是 rpm 怎么做到版本的控制和更新的?
通过运行命令:

rpm -qpi test_rpm-1.1.1-21.i386.rpm

能够看到输出:

Name : test_rpm
Version : 1.1.1
Release : 21

可以得知 该软件的版本为1.1.1, Release 号是21,因此它的包名默认就是“test_rpm-1.1.1-21”。
当 version 进行升级,或者 Release 号进行升级后,新版本的 RPM 就可以在老的版本上做升级了,而且 rpm 安装命令会检查版本号,然后确定是否能够升级。
有时候,在制作rpm包时,有的人会采用 spec中的 “Epoch” 这个字段来控制版本,不过这个字段却是一个神秘数字,他会加在版本的前面,用冒号隔开,比
如 “99:1.1.1-21” 这样的一个版本字符串,和 “1.2.3-xx”
来比较,前者始终比后者版本高,后者版本的包就没法升级前者了,当然这么做并不全是为了控制版本升级,主要目的还是做特殊标识的。
rpm 的基础介绍部分先到这里。

rpm 文件格式

做过网络程序开发或者熟悉 wireshark (或者 tcpdump)的同学肯定都对网络报文文件比较熟悉,我们都知道,网络报文文件都有固定的格式,或者我觉得叫“协议”更容易理解。
这些协议是人为定下来的,告知大家:我们这个文件前面多少比特写什么内容,中间哪个数据结构表示什么意思,后面那个字段又代表什么。。总之,通过这么一种 通告的方式向众生告知,如果你要参与到这种格式的数据(或者文件)中来,就必须按照我说的格式(协议)去读写,只有这样,才能够读取到正确的数据,或者 写成一个正确格式(能被别人识别)的文件。

报文文件为大家熟知,我们就从报文文件的例子开始。下面是用 wireshark 抓取的一个数据包,用 wireshark 打开并查看该文件,能够清晰看到按照报文文件协议解析后的数据包文件的各个字段:

链路层
IP层
TCP层
FTP层

每层的各个字段都能看到。如下图所示:

点击查看原图

TCP/IP协议层说明图

之所以 wireshark 能够展示出来报文的每个字段,因为它是按照网络报文的协议格式对该文件进行了解析,对于 rpm 文件而言,存储原理也是一样的,知晓了文件的数据格式,然后逐个解析,就能拿到你想要的信息,rpm 命令中的大部分功能,都是这样实现的。

下面,我们看看 rpm 的文件格式。
就像报文文件由 mac 层,ip 层,tcp/udp/tcp层,http/snmp 层这些信息块组成一样,一个 rpm 文件由一下几种数据块组成:
lead
signature
header
archive
在 一个 rpm 文件中,上面这四种数据元,会被包含一个或者多个,其中每个数据元中又有自己的数据格式,这样,一层层的存储协议,就构成了一个 rpm 文件。

首先看 Lead 信息。

rpm-devel 这个包中 的文件

/usr/include/rpm/rpmlib.h

中对 rpmlead 是如是定义的:

unsigned char magic[4];
unsigned char major;
unsigned char minor;
short type;
short archnum;
char name[66];
short osnum;
short signature_type; /*!< Signature header type (RPMSIG_HEADERSIG) */
/*@unused@*/ char reserved[16]; /*!< Pad to 96 bytes — 8 byte aligned! */

通过字面意思大概能看到其中的意思,这个结构体中包含了一个软件包的名字,适合安装的操作系统类型, 适合的平台信息,包类型(二进制包还是源码包)。

前四个字节 magic,作为标识符, 表示这个文件是否是rpm文件,file 命令和 rpm 命令都是靠这四个字节来判断文件类型的
比如运行命令:

file test-rpm-1.1.1-15.x86_64.rpm

输出如下信息:

test-rpm-1.1.1-15.x86_64.rpm: RPM v3 bin i386 test-rpm-1.1.1-15

这个文件类型的识别方法,就是从 lead 中获取magic 的值来判断的。

===============================================================================================
目前,rpm文件中这个magic 数组的值是 “edab eedb”, 可以通过 ultraedit 打开一个RPM 文件 查看前四个字节,如图示:

1

接下来两个字节 major 和 minor 用来标识 rpm 文件的版本(是rpm格式的版本,而不是rpm包的版本),这个和 TCP/IP 协议中的 version 字段作用类似。

在 rpm 文件中能看到的大多数version的值都是

major = 3
minor = 0

也就是 3.0 版本的 RPM 文件,在上图中可以看到这一点。

接下来是 rpm 文件的类型字段 type,0 表明是二进制 rpm 文件,1 表明是源码文件。

下来的 archnum 标识软件包将要安装的平台架构信息,1 标识 i368, 在最新的 rpm version 3.0 中看到的 这个字段在 x86_64, noarch 和 i386 中都是0,可能已经不用这个字段了,而是采用header来存储。

66个字符的 name 用来存储软件包的名称。
osnum 是标识 操作系统的, 1 标识是 Linux,2 是 IRIX,这些对应的常量定义能在文件

/usr/lib/rpm/rpmrc

os_canon: Linux: Linux 1
os_canon: IRIX: Irix 2
# This is wrong
os_canon: SunOS5: solaris 3
os_canon: SunOS4: SunOS 4
os_canon: AmigaOS: AmigaOS 5
os_canon: AIX: AIX 5
os_canon: HP-UX: hpux10 6
os_canon: OSF1: osf1 7
os_canon: osf4.0: osf1 7
os_canon: osf3.2: osf1 7
os_canon: FreeBSD: FreeBSD 8
os_canon: SCO_SV: SCO_SV3.2v5.0.2 9
os_canon: IRIX64: Irix64 10
os_canon: NEXTSTEP: NextStep 11
os_canon: BSD_OS: bsdi 12
os_canon: machten: machten 13
os_canon: CYGWIN32_NT: cygwin32 14
os_canon: CYGWIN32_95: cygwin32 15
os_canon: UNIX_SV: MP_RAS: 16
os_canon: MiNT: FreeMiNT 17
os_canon: OS/390: OS/390 18
os_canon: VM/ESA: VM/ESA 19

signature_type 字段标识了下一个 数据块 signature 的类型,在 rpm version 3.0 中,这个变量的值是 5.

头结构体 header structure

从 上面 讲到的 lead 结构体可以知道,lead 这个结构体从编程角度来来讲,很好用,要读取成员,你只需要用

pointer->name

即可获取到 rpm 的名字,但是,我们很容易就会发现,name 这个数组的长度只能容纳 66 个字符,如果包的名字长度超过66个字符,怎么办呢?
有的开发人员可能会说:我把 name 长度改成 100,256, 这样是能处理大多数包,但是会带来两个问题:

(1): name 长度修改了,重新生成的 rpm 命令,会去按照新的格式读取rpm文件,这样,新版的rpm命令就不能读取老的格式的 rpm 文件了。
(2): 老版本的rpm 命令,不能正确读取新版本的rpm文件。

因此,要解决此类问题,就需要将数据的存储和读取规范化,或者说,需要把跟业务相关的数据在协议中弱化,在协议中只见协议,不见业务相关的数据。
我想,这也是软件设计之道吧,工具和业务分开,才能灵活扩展,特别是通讯程序之类的应用,尤为重视这一点。

rpm 文件中为了解决数据统一读取和存储的需求,引入了 头结构体 (header structure),在一个文件中,可以有一个或者多个 header structure, 而在 rpm 文件中
只有两个header structure,一个是 signature 数据块,一个是header 数据块。
每一个 header structure 包含三部分内容:

第一部分叫做: header structure 的头(header), 用来标识一个header structrure 的起始位置,header structure 的大小,以及它包含的数据条目数。
第二部分紧跟 header structure header,叫索引 (index) ,index 包含了多个index条目,其中每个 index 条目都是对一块数据的描述,每个index告诉你它指向的数据是什么样子的,在哪里存储着,根据 index 就能获取到这个index对应的数据。
第三部分是存储字段,叫作 store, 存储了 index 描述的数据。

请注意,这里之所以引入 对 header structure 的介绍, 是因为在新版本的 rpm 中,header structure 已经在使用了,为了保持文章逻辑的自然逻辑性(从lead发展到header structure),我们专门花一些篇幅来介绍header structure。

继续回到 rpm 文件后面数据块的介绍部分:signature 和 header,看看这两个 header structure 是怎样的。

在一个 rpm 文件中,每个 header structure 开始都是用三个字节的神秘数字开始 “8E AD E8”,

而且前文,我已经介绍过了
,每个 rpm 有两个 header structure– signature 和 header, 因此,不妨打开一个rpm文件看看,以下面的 rpm 为例, 如图:
2
能够看到这两个神秘数字,也就是说,我们的两个 header structure 都被找到了。
接着往下分析。

header structure 之 header

在 三字节的神秘数字之后,是 1 个字节的版本号(为1),然后是4个字节的保留字,在保留字之后,是4个字节的整数, 表示在该header structure 中有多少个索引项,也就是说有多少个index, 接下来的4个字节整数,标识在该header structure 中有多少字节的数据。 这些,在上图都能看到。

header structure 已经很清楚了,但是还有两个概念没清楚,一个是 index, 一个是 store,接下来,我们来剖析这两个咚咚。

header structure 之 index

每个 index 有 16个 字节的存储空间,前四个字节整数是一个 Tag,表明该 index 指向的数据是什么类型的,关于这段描述,原文是:

The first four bytes contain a tag — a numeric value that identifies what type of data is pointed to by the entry. The tag values change according to the header structure’s position in the RPM file

这个很容易让人误解为是,这个类型是 整形,字符串,或者数组,其实不是,这里的 “TYPE” 其实就是我们通常说的变量名称, 包括了通过命令

rpm -qpi xxoo.rpm

能够看到的所有信息。
Tag 的值定义列表如下:

#define RPMTAG_NAME 1000
#define RPMTAG_VERSION 1001
#define RPMTAG_RELEASE 1002
#define RPMTAG_SERIAL 1003
#define RPMTAG_SUMMARY 1004
#define RPMTAG_DESCRIPTION 1005
#define RPMTAG_BUILDTIME 1006
#define RPMTAG_BUILDHOST 1007
#define RPMTAG_INSTALLTIME 1008
#define RPMTAG_SIZE 1009

接下来的4个字节整数(4-7),才是前4个字节代表的变量的类型,整数啊,字符串之类。。
定义如下(在文件 /usr/include/rpm/header.h 中能看到):

typedef enum rpmTagType_e {
#define RPM_MIN_TYPE 0
RPM_NULL_TYPE = 0,
RPM_CHAR_TYPE = 1,
RPM_INT8_TYPE = 2,
RPM_INT16_TYPE = 3,
RPM_INT32_TYPE = 4,
/* RPM_INT64_TYPE = 5, —- These aren’t supported (yet) */
RPM_STRING_TYPE = 6,
RPM_BIN_TYPE = 7,
RPM_STRING_ARRAY_TYPE = 8,
RPM_I18NSTRING_TYPE = 9
#define RPM_MAX_TYPE 9
} rpmTagType;

STRNG_TYPE 是空字符结束的字符串
STRING_ARRAY_TYPE 是 STRING_TYPE 的集合。

接下来的 4字节的整数,是该 index 对应的数据在 store 段的偏移。
最后12-15这4字节的整数,表明了该index指向的数据,有多个数据据条目,主要用于 STRING 和 STRING_ARRY 类型,STRING 的话,是1,STRING_ARRY就是STRING的个数。

header structure 之store

store 就是 该 header structure 中数据存储的地方,有几个点要注意:

(1): 每个 STRING 类型,都以 空字符结尾。
(2): 整数存储都是按照它的自然边界存储的。 也就是说,64位的用8个字节存储,16位的用2个字节存储,32位的用4个字节存储等等。
(3): 所有数据都是网络字节序存储的。

接着来看由 header structure 组成的 rpm 文件中的两个要素:
signature 和 Header
signature 和 header 的本质都是 header structure,所以将其放在一起来介绍。

rpm 中的第一个header structure是signature。在signature中存储了 rpm 包的校验信息,如 md5sum、sha1值等,这个header structure的头信息中有5个index, 如下所示:

00006-00000096: 8E AD E8 01 00 00 00 00 00 00 00 05 00 00 00 54 ;
00007-00000112: 00 00 00 3E 00 00 00 07 00 00 00 44 00 00 00 10 ;
00008-00000128: 00 00 01 0D 00 00 00 06 00 00 00 00 00 00 00 01 ;
00009-00000144: 00 00 03 E8 00 00 00 04 00 00 00 2C 00 00 00 01 ;
00010-00000160: 00 00 03 EC 00 00 00 07 00 00 00 30 00 00 00 10 ;
00011-00000176: 00 00 03 EF 00 00 00 04 00 00 00 40 00 00 00 01 ;

===========================================================================

第一行是 标准头,从第二行开始是 5 个 index, 每行一个index,每个 index 项都是4个32位的整数,分别是:

TAG (0-3)
TYPE (4-7)
OFFSET (8-11)
COUNT(12-15)

我们来看下这5个index。
signature中的TAG名称和整数对应表可以在文件


/usr/include/rpm/rpmlib.h

中找到:


/** \ingroup signature
* Tags found in signature header from package.
*/
enum rpmtagSignature {
RPMSIGTAG_SIZE = 1000, /*!< internal Header+Payload size in bytes. */ RPMSIGTAG_LEMD5_1 = 1001, /*!< internal Broken MD5, take 1 @deprecated legacy. */ RPMSIGTAG_PGP = 1002, /*!< internal PGP 2.6.3 signature. */ RPMSIGTAG_LEMD5_2 = 1003, /*!< internal Broken MD5, take 2 @deprecated legacy. */ RPMSIGTAG_MD5 = 1004, /*!< internal MD5 signature. */ RPMSIGTAG_GPG = 1005, /*!< internal GnuPG signature. */ RPMSIGTAG_PGP5 = 1006, /*!< internal PGP5 signature @deprecated legacy. */ RPMSIGTAG_PAYLOADSIZE = 1007,/*!< internal uncompressed payload size in bytes. */ RPMSIGTAG_BADSHA1_1 = RPMTAG_BADSHA1_1, /*!< internal Broken SHA1, take 1. */ RPMSIGTAG_BADSHA1_2 = RPMTAG_BADSHA1_2, /*!< internal Broken SHA1, take 2. */ RPMSIGTAG_SHA1 = RPMTAG_SHA1HEADER, /*!< internal sha1 header digest. */ RPMSIGTAG_DSA = RPMTAG_DSAHEADER, /*!< internal DSA header signature. */ RPMSIGTAG_RSA = RPMTAG_RSAHEADER /*!< internal RSA header signature. */ };

从上面的五个index中可以看到,例子中打开的rpm文件的signature中的index的TAG取值分别如下(第7行到11行)。
第一个index的TAG是62(十六进制00 3E),对应的TAG为HEADER_SIGNATURES。
第二个index的TAG是269 (十六进制01 0D), 对应的TAG为RPMTAG_SHA1HEADER。
第三个index的TAG是1000(十六进制03 E8),对应的TAG为RPMSIGTAG_SIZE。
第四个index的TAG是1004(十六进制03 EC),对应的TAG为RPMSIGTAG_MD5。
第五个index的TAG是1007(十六进制 03 EF),对应TAG为RPMSIGTAG_PAYLOADSIZE。

事实上,大多数rpm 文件的这5个index 都是相同的(为甚还需要研究,目前看到现象是这样的)。

signature这个header structure 存储了软件包的校验信息等相关数据,而name,version等字段都不在这个数据块中存储。因此,我们在此只通过实际数据来验证

rpm 软件包的组成是否与协议描述一致即可,而不必关心每个TAG的作用。对这块感兴趣的读者可以参考相关文档。

===============================================================

图6和图7所示是两个 rpm 的signature段截图。
6
图6 rpm 文件格式(一)
7
图7 rpm 文件格式(二)
图6所示 rpm 的signature段有5个index,数据长度为0x54。我们可以在图中按照协议去计算,向后跳过5个index (5个16字节),然后再数0x54个字节,store段结束。因为是8字节对齐,所以header段的header structure从4字节之后开始。
图7所示情况也是如此,它的signature有7个index,因此,先跳过7个index到达store段,store段的数据为0xD8个字节,所以再向后统计0xD8个字节,到“00 10”处结束,紧接着就是它的header段了。

2. header
header和signature一样,都是由header structure组成的,header中存储了 rpm 包的所有描述信息(不包括数据)。还以上面的 rpm 为例,我们接着分析它的header段:
首先看下header段的header(header structure的header,而不是 rpm 的header)和index内容,如图 8 所示。
8
图8 rpm文件格式(三)
从图8中可以看到,header中包含了0x31个,也就是49个index,而且它的store段占据空间为0x02F6字节,也就是758个字节。
跳过type为0x3F和0x64这两个index查看后面的index,如图9所示。
9
图9 rpm 文件格式(四)
在偏移00000320(本书凡涉及偏移地址的地方,都默认用十进制表示)处:0x03 E8 对应的TAG为十进制1000(RPMTAG_NAME),对应于RPM的软件包名称。它的类型为0X06(RPM_STRING_TYPE),偏移地址为相对store起始位置为0x02的地方,只有一个串,并且是空字符结尾。我们来计算一下:
1) 从偏移00000296处,index开始存储,有0x31个index,那么index的结束位置应该为00000296+0x31*16=00001080的前一个位置,也就是说偏移地址00001080为store的开始位置,那么偏移为0x2时,00001082处偏移就是RPM名称的存储位置,如图1-10所示。事实证明,实际数据与我们的计算的结果是一致的。
10

图 10 rpm文件格式(五)
同理,0x03E9对应的index的TAG为十进制1001(RPMTAG_VERSION),类型为字符串,偏移为0xB,起始位置为偏移00001091处,个数为0x1,它在 rpm 中的存储位置如图11所示。

11
图11 rpm 文件格式(六)

关于header中 rpm 信息的存储结构,这里就不再多举例子了,感兴趣的读者可以自己分析。
至此,signature和header的结构就介绍完成了。

rpm 之archive
header 之后是archive 字段,archive中存储了组成 RPM包的所有文件的内容,可以通过标志位“1F 8B”来找到它的起始位置。archive是通过 gzip算法压缩存储的。
为了保持内容的连续性,接着上一节head er的内容继续分析,最后一个index的内容如图12所示。
12
图12 rpm文件格式(七)
它对应的TAG为十进制1147(RPMTAG_FILECONTEXTS),类型为0x8,对应于字符串数组
(RPM_STRING_ARRAY_TYPE),偏移为0x2B0。因此,实际的存储位置为00001080 + 0x2B0 = 00001768,字符串个数为0x2个,所以,我们只需要去00001768偏移处跳过两个字符串,接着到达了00001821偏移处,如图13所示。
13
图13 rpm文件格式
从图13和上面的计算可以知道,header的store结束于00001821偏移处,而根据 rpm 的header段的头(header structure 中的header)信息可以知道:rpm的header中的store段的大小是0x02F6,为758个字节,而store的开始位置为00001080偏移处,所以,store的结束位置应该为 00001080+0x02F6=00001838处偏移的前一个位置,也就是00001837偏移处,所以,store实际存储的数据比index告诉我们的数据少了16字节.那么,这16字节的数据是怎么来的呢?经过对多个rpm文件的分析发现,在rpm文件的header块的store结束处,通常都填补了16字节的数据(这16字节的数据看上去并没有什么作用,也不是常量值),也就是从00001822到00001837这16字节的内容。
紧接着,从00001838偏移处开始,是archive的数据,关键字“1F 8B”标识了archive的开始,紧挨着的“08” 表明数据是用 gzip 的“deflation”方法压缩存储的。
如果读者想获取更多关于rpm文件数据存储的细节,可以参考 rpmbuild 或者rpm命令的源代码。

================================================

RPM解析例程
笔者基于rpm-devel 开发了一个C程序,用来读取RPM文件的基本信息并且打印到标准输出。如果你对前面章节介绍的知识已经理解并且掌握,下面这段代码就比较容易阅读了。
核心代码片段如下:


char * readHeaderString (Header header, int_32 tag_id)
{
int_32 type;
void *pointer;
int_32 data_size;
//获取header中的一个string
int header_status = headerGetEntry (header,
tag_id, &type, &pointer, &data_size);
if (header_status)
{
if (type == RPM_STRING_TYPE)
{
return pointer;
}
}
return NULL;
}
int samplerpm (const char *szrpm)
{
char g_szname[1024] = {0};
FD_t fd = Fopen (szrpm, “r”);
memset (g_szname, 0, 1024);
sprintf (g_szname, “%s”, szrpm);
fflush (stdin);
fflush (stdout);
if (!fd)
{
printf (“open file ‘%s’ failed\n”, szrpm);
return 0;
}

struct rpmlead plead;
int lead = readLead (fd, &plead);//读取lead结构体
if (lead)
{
printf (“readLead of ‘%s’ failed\n”, szrpm);
Fclose (fd);
return 0;
}
//sigType sig_type = plead.signature_type;
Header header;
//读取第一个header structure–signature
rpmRC ret = rpmReadSignature (fd, &header, plead.signature_type);
if (ret != RPMRC_OK)
{
printf (“rpmReadSignature of ‘%s’ failed\n”, szrpm);
Fclose (fd);
return 0;
}
//读取第一个header structure–header
Header newheader =
headerRead (fd, (plead.major >= 3) ? HEADER_MAGIC_YES : HEADER_MAGIC_NO);
if (!newheader)
{
printf (“headerRead of ‘%s’ failed\n”, szrpm);
Fclose (fd);
return 0;
}
//读取各个TAG
const char *name = readHeaderString (newheader, RPMTAG_NAME);
const char *version = readHeaderString (newheader, RPMTAG_VERSION);
const char *release = readHeaderString (newheader, RPMTAG_RELEASE);
const char *group = readHeaderString (newheader, RPMTAG_GROUP);
const char *packager = readHeaderString (newheader, RPMTAG_PACKAGER);
if (!group) group = “NONE_GROUP”;
if (!packager) packager = “NONE_PACKAGER”;
printf (“name:%s\nversion:%s\nrelease:%s\ngroup:%s\npackager:%s\n\n”,
name, version, release, group, packager);
Fclose (fd);
return 1;
}


可执行程序的编译方法如下:


gcc test.c -I/usr/include/rpm -lrpm -lrpmdb -lrpmio -lpopt -o test_rpm

可以通过以下方法进行测试:


./test_rpm ./mysql-server-5.0.77-4.el5_6.6.x86_64.rpm

输出如下:


name:mysql-server
version:5.0.77
release:4.el5_6.6
group:Applications/Databases
packager:Red Hat, Inc.


可以看到,test_rpm这个程序解析并且打印了指定 rpm 文件的基本信息,读者可以参考文档对该程序进行功能细化。
由于笔者并没阅读rpm命令或者rpmbuild 的源码,因此对 rpm 格式的剖析只能到此深度,不过rpmbuild的实现应该也是类似的,感兴趣的读者可以参考 rpm 的文档和源码进一步学习。

补充

rpm 结构总图
补充下后来整理的 rpm 结构总图,如下:

rpm 结构总图

rpm 结构总图

rpm 文件十六进制结构图展示
下面的三幅图是打开的一个rpm文件的十六进制内容,读者可以结合本文的内容进行查看,以加深对rpm格式的理解。
16
16
17

例子代码
附件是例子代码:
点击此处下载例子代码
2014-01-24 修改完结。

Posted in 系统管理 | rpm文件认知与格式剖析已关闭评论

西溪书社开篇

2009年建站以来,一直使用自己写的页面。

于2014年1月12日将cmesoft.com从自己开发的页面替换成专业的wordpress站点,特书此文,予以纪念!

Posted in 其他 | 2 Comments