欢迎访问年轻的天空。在您劳累之余,不妨来这里转转,这里有休闲,娱乐,以及知识,让您在轻松的同时能够有所收获,希望您能找到那片属于您自己的年轻的天空。
住房改革为何会把民众买房负担推向世界之最ZZ
一、都市新怪:七成新人买不起房 三成买房靠“啃老”
中国新一代,结婚成本“火箭般”飞向了天价
一段时间以来,在国内不少网站和论坛上,盛传着这么一件“黑色冷笑话”:生活在北京、上海、深圳、广州、杭州和南京等地的年轻职员们,有一天忽然都严肃起来,要一起做一道“解析题”,看看在各地“娶一个老婆的成本究竟是多少”。
南京人答题最快:娶一个南京老婆的成本是人民币70万。此言一出,一片哗然。南京小生的算法也很简单:房屋一套(市区80平米,均价6000元) 48万+5万元中等装修+3万元家电及家具(有部分女方以嫁妆形式出资承担)+10万元普通轿车一辆+新马泰港澳或云南、海南度蜜月1.6万+2.4万元的2年谈恋爱吃饭、娱乐和送礼费用,所有这些加在一起,不多不少正好是70万(婚宴开支和所收红包相抵,不列)。
由此推算,以男方家庭有20万的家产,他本人年收入5万来计算,最后得出结论是:讨一个南京中上条件的老婆的成本=男方倾家荡产+男人不吃不喝工作10年[(70-20)/5]。
应该说,这位年轻人的算法除了在轿车和蜜月开支可删可减外扣除10万元以外,其他近60万元基本上属于正常。尤其是占开支最大的住房一项,每平米6000元在南京城区内要买到较好的房子已属不易,而80平米的面积等有了小宝贝后连人均30平方米的小康标准也没达到。这还是一个接近中产(有20万元存款,年收入达到5万元)的家庭的算法,一般普通的家庭就更可想而知了。
北京、上海、深圳、广州和杭州等地的结婚成本的计算结果也纷纷贴到了网上,不比不知道,一比吓一跳。南京的60万“天价”竟然还是最低的。其中北京结婚的合理的成本为85万元(其中在三环外买房80平米,为64万元),上海为100万元(其中买房花去80万元)、深圳76万元(买房占其中58万元),广州为85万元(买房花去其中64万元),杭州和广州一样,总成本也是85万元,买房也是为64万元(备注:各地的买房标准和南京一样,在市区的普通地方,面积均为80平米)。
“天价结婚”高速引擎:买房
面对这样的天价结婚成本,作为“准新人”的父母和兄长辈,也许早就是“十有八晕”了。因为这和他们那个结婚的年代相比,相差的可算是地地道道的“十万八千里”。
年岁稍长的人还清楚得记得,在七十年代,结婚基本上就等于一个发喜糖和两家人在一起吃饭的钱。住房的问题,在领结婚证的时候一般单位就要帮助解决,尽管房子很小很老。所有开销在加在一起,基本上在百把元上下。只是到了七十年代后期和八十年代初,有了“三大件”(手表、自行车、缝纫机)和一些简单的家具之后,才进入了几百元时代。从八十年代中开始,随着逐年不断增加的录音机、电视机等家用电器和请客规模和档次的提升,结婚成本才开始进入千元、万元时代;那时租单位房子,房租也就几块钱。直到98年房改之前,当时一般人的结婚成本还是在几万元上下打转转。
也就是在这一代人的功夫,虽然人们的收入的水平从过去的几十元提高了现在的几千元,涨了近百倍,但结婚成本却飞涨的更快,从100多元火箭般的涨到了50多万元以上。数千倍的涨幅,归根结底还是“高房价”的“功劳”,尽管我们住房的面积只比比过去增加了1-3倍,尽管住宅的水平只有一点点的提高。
比较各地的天价结婚成本,你可以发现,除了买房外,这几个大城市的结婚的其他花费加在基本上都是在20万元上下(深圳人给红包“份量”重一些,可以为新人省掉一些,南京人的生活水平低一些,花钱自然也省了不少)。天价结婚成本的关键,还是在高昂的房价上。它占据了各大城市整个结婚成本的75%以上,高的达到80%以上。
如此高昂的结婚成本,使得社会上“黑色冷笑话”又有了“递进”的空间:现在小年轻为什么同居多,原来都是让高房价给吓得啊!事业小有成就、属于白领的“青年才俊们”都不敢结婚了,众多的打工仔就更只能搞“地下工作”了!
高房价下的“非常6+1”:七成新人不买房,三成买房靠“啃老”
当然,年轻人到了谈婚论嫁的年龄,婚总是还要结的。怎么办呢?上海市房地产交易中心下属一部门去年对该市上年12万对结婚新人的婚房进行了调查,发现有70%的新人因买不起房等原因干脆选择和父母同住,两代人挤一挤照样也要过;而另外的30%新人虽买房结婚,但80%是靠父母资助,最流行和非常普遍的方法就是“六个人买一套房”,即小俩口结婚买房的首付款由双方父母支付,他们自己则负担每月的贷款。
据了解,“六个人买一套房”的模式在全国的大中城市、尤其是像北京、上海、广州、深圳等外地大学生就业集中的大城市极为普遍。双方父母为儿女买房拿出的首付款,大多是他们多少年一点一滴积攒下来作为晚年防老之用的“养命钱”。现在为了儿女的有房结婚,只能把自己晚年一点微薄的福利“透支”出去。问题是,现在是父母为儿女“透支”养房,以后背负“按揭大山”的儿女还有能力赡养父母吗?“提前送终”会不会由“冷幽默”变成“热现实”?
二、住房负担,国际上有把“公平秤”
“房价收入比”,全球买房人通用的负担系数
城市居民中,当70%要结婚成家的年轻人买不起婚房,另外买上婚房的30%还是通过双方父母以“透支晚年福利”为代价倾囊相助才得以解决住房问题的时候,标志着城市的房价已经到了让绝大多数居民难以承受的“高危”地步。对于一个负责任的政府来说,现在已到了刻不容缓去反思和变革我们的住房政策的时候了。
看一个地方的房价是不是过高,是不是给老百姓带来了沉重的生活负担和压力,国际上有一个通行的计算模式,就是看看这个地方或城市的房价收入比究竟是多少。
“房价收入比”从狭义上来讲,就是指家庭所居房屋的价格,和这个家庭年收入之比,称为房价收入比。比如某家庭所居房屋的房价是60万元,夫妇俩年收入是12万元,房价正好为夫妇俩年收入的5倍,那么他们的房价收入比就是5。通常我们所说的房价收入比是指广义上的概念,即指一个城市的平均房价与每户居民的平均年收入之比。
那么,什么样的“房价收入比”是国际上公认的合理水平呢?通过对数十个经济发展水平不同的国家和地区主要城市居民住房房价的多年考察,世界银行和联合国人居中心分别得出 “合理的住房价格”的房价收入比应该为3-6(世界银行专家的说法为4-6)。
3到6倍的房价收入比,既然是国际上公认的让居民对住房价格可承受性的、并有利于房地产市场健康有效发展的“合理和理想的水平”,那么,具体的情况又是怎样呢?还是让我们来看看现实中的“国际惯例”。
告诉你一个真实的“房价收入比”国际惯例
今年春节,有朋友从加拿大的渥太华回来探亲。同学聚会时笔者向他们夫妇俩打听他们所在的加拿大首都渥太华的住房情况。碰巧他们不久前刚刚换了新房子。据介绍,他们的买房也十分简单,开发商从政府手中竞标买下一大块地以后,搞好景观建设和划分成不同的小块单元(大小相当于我们的一亩地左右)。买房人看中了那块单元,再从开发商提供的不同风格的住房套型中选择好自己喜欢的套型,由开发商统一组织的建筑装饰公司施工。朋友夫妇搬进新房时不需要自己再搞什么装潢,只要带着家具入住即可。朋友夫妇选择的是面积300平米左右的3层楼的独立屋(别墅),总共花费25万加元都不到。而他们俩口子一年的收入超过了10加元。也就是说,他们用了不到2.5的房价收入比在加拿大的首都买到了土地产权归己的“精装修大别墅”。
其实这样的情况在加拿大极为正常。2000年记者曾经到据称是加拿大房价最贵的城市——温哥华呆过一段时间。抽空对当地的房价专门进行了了解。当时温哥华一般中产(占总人口70——80%)所居住的townhouse 和高级公寓(介于townhouse和公寓房之间),200多平米的也就20万加元上下。而他们的平均家庭年收入在5万加元左右。就是说,在加拿大房价炒作最厉害、价格最高的的温哥华(当时该地区的房价比加国人口最多的城市多伦多还要高三分之一,而人均收入却比多伦多少三分之一)地区,房价收入比也只是在4左右(据说这两年高了一些,也是被中国去的暴发户炒上去的)。
记得当时记者到一所新的豪华小高层公寓看Open House(样板房)。150平米的面积,精致的全装修,旁边的森林公园有山有水,离Sky train(高架轻轨,相当于我们的地铁)站也只有几分钟的路程,价格却只有19万加元(当时相当于人民币95多万元)。如此条件和价格的豪宅不要说在北京、上海见不到,就是对在南京这样的二线城市条件类似的毛坯房,也有极大的冲击力。难怪一个在南京开驾校的L老板到了温哥华旅游后,什么身份也没有的他二话不说,掏出钞票就买了一套房。L老板应该感到庆幸的是,5-6年后的今天,他如果要在南京买一套条件接近当初他在温哥华所买的那套房(尽管还是毛坯房),所需投资至少比当时还要多花2-3倍。
也许有人会说,加拿大人少地多,不具“典型性”。那么,我们就拿世界上最发达的美国来说事。加州北部的旧金山和圣荷西地区,是全球著名的硅谷所在地,也是全美房价最高的地区之一。记者也多次到那儿开会、探亲。我有一亲戚十年前刚到圣荷西的时候由于工作时间不长,夫妇俩收入加在一起也就5万美元左右,低于当地平均水平。当初他们买下一栋200多平米、价格27.5万美元的独立屋时,有些银行都不愿意为他们提供贷款,认为他们的偿付能力有疑问。要知道,当时他们作为刚出道的年轻人,房价收入比也在5.5啊。现在他们的房价涨到了40多万美元左右,而他们的收入增长的更快,现在的房价收入比已经降到连3都不到。当然,当地一般人的房价收入比还是在4上下。笔者的另外一个亲友在美国东部的威尔明顿市,也是在十年前他们在当地一个风景如画的高档社区买了一栋前后花园就有数百平米260平米的独立屋(别墅),只花了22万美元。对在美国工作不久的他们来说,房价收入比在5左右。现在他们的房子升值到了35万美元2.5左右,但他们的房价收入比却降到了3以下。
不过最让笔者感叹美国房价低的,还是1999年到科罗拉多州丹佛附近一新兴的高科技小城(Sprin)时的所见所闻。当时是去看望的留学生夫妇朋友都在IT行业工作,两人收入加起来达到近9万美元,但他们买的一栋新的独立屋(别墅)200平米左右,装修好的,才14万美元。当时在笔者还没有房价收入比的概念,只觉得用一年多的工资(房价收入比为1.5)即可买上一栋装修好的别墅,实在是叫人羡慕不已啊。
这里还应该特别说明的是,美国、加拿大等发达国家不仅房价收入比要比我们低得多,更重要的是他们“房价收入比”的“含金量”要比我们中国高得多:他们是用别墅的钱和工资来比,我们也只能用公寓房价和收入相除;他们买的房是装修好的,走进去就可住,而我们拼了半天拿到的也只是毛坯房,要住进去还得花一大笔装修费;他们房子前后带大花园,有的社区还带高尔夫,不少人家还有游泳池,配套设施先进豪华,而我们的住房大部分没有一定的配套设施,很多的住房还处在“工房”的水平;最重要的,美国的人均住房的面积标准是我们的几倍,家庭新建成房屋平均面积2,114平方英尺(196.36平米),而且这还是用他们特定住宅建筑面积(Square footage)标准来计算的(接近我们的使用面积,走廊、阁楼、地下室、院子都不在计算之列),买房更不会有什么“共摊面积”。而在中国买房,计算采用的是“建筑面积”,再加上共摊面积。真正使用上的面积只能是购房面积的八成上下。就这样我们也只能用人均30平米、户均90平米的标准来测算“房价收入比”,而且这还是新的“小康标准”。如果要按欧美的标准和“含金量”来计算房价收入比,那我们的差距就会在现有的基础上还要翻上一番。
不仅地大物博的发达国家如此,而且人口密度和生活水平远高于我们的东亚国家,如韩国、日本等,他们的房价收入比也比我们要低得多。
有一段时间以来,韩国人都在抱怨他们的房价涨的太快,给国民生活带来了较大的压力。那么,韩国的房价究竟高到哪儿呢?最近韩国的国土研究院经过对上万的家庭调查研究,并和英美等国进行了比较,显示韩国国民全部收入攒六年才能买房。尤其是首都首尔地区,需要将总收入存7.7年才能购买住宅,地方大城市为3.8年,中小城市为2.6年。而在美国和英国,要购买住宅,则分别只需要2.7年和4.1年全部收入。这就是韩国老百姓喊吃不消高房价的原因。韩国人也许没想到,在中国的一些大城市,老百姓要用十几年全部收入才能买上一套“体面的住宅”。
另据介绍,根据日本官方的数据,在东京新建的专有面积75——80平米(相当于我们90——100平米的建筑面积)、精装修的3室1厅1厨1卫单元公寓,加停车位售价在3000日元左右。而普通工薪阶层家庭平均月收入为56万日元。这个号称世界上物价水平最高、人口密度也最大、房价同样也是最高的城市,工薪族的房价收入比也计算在4.5-5左右。而且东京人买的这样的公寓,一般还都附送20平米的阳台和好几个平米的走廊以及包括液晶电视等基本电器。据介绍,日本的家庭主要还是男人在外工作,如果家庭主妇也到外面做个半天兼职的话,其房价收入比可降到3左右。
当然,并不是每个国家的和城市的房价收入比能让居民满意的。有一家美国的顾问公司根据房价收入比,专门就国际房价购买力的调查做过一个调查,看看世界上哪些城市的房价让居民“最难以承受”。结果澳洲悉尼超过伦敦(6.9)、纽约(7.9)和东京,以8.5名列前茅。不过,据报道,澳洲统计局资料显示,澳洲是每0.98户家庭或2.7人就有一幢(套)住宅,平均每幢(套)住宅的居住面积是187平方米。也就是说,悉尼人是因为住在人均70平米装修好的别墅里才感觉“房价最难承受的”,尽管就这样他们的房价收入也只是8.5。
这项调查更有意思的还在于,它认为由于澳洲所有人口超过100万的城市房价收入比都在5.1以上,从而都被列入房屋购买力“极低”的范围,并由此断言“澳洲存在世界上最大的地产危机”。真不知道中国的百姓和官员看了后会有什么感触!
这项调查对我们分析房地产市场的价值还在于,它给出了具体指标,让我们可以用数据说话、更科学的分析和评价一个地方的房地产是否“健康”。比如:
当一个地方房价收入比超过5的时候,国际惯例就认为该城市房屋购买力“极低”;
当一个地方房价收入比超过6的时候,就会被国际上公认为属于房地产泡沫区;
当一个地方房价收入比超过7以后,就会被世界上公认为“国际房价最难承受地区”。
另外,在比较房价收入比的时候,还请特别注意别人人均住房分面积、档次、算法、装修和其他配套等等。
三、中国市民,背负着全球最重的买房负担
对其他国家的居民住房水平和具体负担就承受状况有了客观的了解后,回过来再看看我们国内居民、尤其是大城市居民的住房负担是否合理,就会有一个更准确的认知和定位。
就拿我们上面所举例“天价结婚成本城市”来具体分析。依据的标准稍靠“小康”。即面积为90平米普通公寓、地点处在传统上市民所居住的市城区(不是郊县和新开发区)、三口之家(父母工作、子女上学)住一套房。就可以破解中国的大中城市的高房价究竟有多可怕了。
还是先从南京开始。根据南京市房产局的“南京市网上房地产”的最新资料,传统上的南京六城区(包括重新规划后原来属郊区县的土地)商品住宅房价最高要7000出头,低的也在5500上下,我们取6000元为其中间值(实际上这个结果只能买到南京六城区边缘的房子),6000元×90平米=54万元。而南京城区居民的收入,根据统计局方面的最新资料,在比上年“激增”19.9%的“超常规增长”后(长三角速度第一),2005年南京市城市居民人均可支配收入为14997.47元,三口之家就是4.5万元。而按照南京本地媒体的另外一项报道,根据该市劳动保障部门的推算,去年南京职工人均收入正好突破2万元,如果家家都没下岗,平均每户的年收入为4万元。根据一套54万元的房价标准,按照前者的推算,54万元除以4.5万元,南京的房价收入比为12,正好比世界公认的房地产泡沫还多了一倍;而按照后者的推算,54万元除以4万元,南京的房价收入比又提高到了13.5,基本上相当于世界上公认为“国际最难承受地区”房价的一倍。这也就难怪国人就是再“吃苦耐劳”,也经不住这样的“超级高房价压迫”。
按照南京的计算模式和标准,部分根据《个人理财》杂志去年提供的2004年各大城市的人均收入资料,计算北京、上海、深圳、广州和杭州的市城区真实的房价收入比。由于《个人理财》杂志上的房价包括了郊区县这些非传统上市城区居民所居住住宅的价格,所以还是以各地网友提供的资料为依据。各城市的实际城区房价分别如下:
城市 人均可支配收入 三口之家一年 房价 90平米总价 房价收入比
北京 15638元 46914元 8000元/平米 72万元 15.35
上海 16683元 50049元 10000元/平米 90万元 17.98
深圳 27596元 82788元 7200元/平米 64.8万元 7.83
广州 16884元 50652元 8000元/平米 72万元 14.21
杭州 14565元 43695元 8000元/平米 72万元 16.48
这里应该特别说明的是:我们的房价收入比,是建立在“准小康”的低标准的基础上的。首先在面积上和欧美等发达国家相比,我们的标准只定在他们的三分之一到二分之一,也就是说,如果面积标准向欧美看齐,我们的房价收入比要现在提高3-2倍;如果再加上他们是精装修房,有的还配上高档电器,面积计算方法上也接近使用面积的话,我们的房价收入比还要抬高50%。
其实,不仅在相对负担上中国大中城市的房价创造了“世界之最”,而且国内上海、北京等大城市的绝对房价,也超过了一些世界上发达国家大中城市的水平。一年前记者就问过一位经常在上海工作的波音公司高级经理,同样水平、面积和档次的住房,是上海贵还是西雅图贵,没想到这位华人高级经理毫不犹豫的说,上海贵。上海有不少人在日本东京待过。这几年回来的人纷纷网上发出感叹,上海内环内的实际房价比东京还要贵。一位经常在中美两国之间往来的跨国公司亚太地区经理也公司记者,按照同样的标准,上海的房价比美国大多数城市的房价都要贵。
房价收入比,“穷国”真的该比“富国”高吗
据了解,国内的一些“既得利益集团”对“房价收入比”的国际比较极为“反感”,认为发达国家的房价收入比低是因为他们的经济水平所决定的,发展中国家本身由于经济实力的问题,房价收入比高是“很正常的”。情况真是这样吗?
稍有些经济常识的人都会知道,这种说法是极为荒唐和可笑的。首先,它违背了发展中国家由于吃饭问题占了居民收入的极高比例(恩格尔系数高),人们不可能有更多的财富支向住房,从而导致房价收入比低的基本的经济学常识;其次,它故意忽略了在新德里、墨西哥城和里约热内努这样的世界上人口最拥挤的发展中国家大都市,很多的中下收入者不要买地就私自盖房(如贫民窟等)或政府提供相应住房保障(如墨西哥城的低收入者住房保障)的基本事实。
联合国的权威研究应该最能说明问题。在1996年联合国召开的规模空前的世界人居大会上,联合国人居中心在《城市化的世界————全球人类住房报告》中不仅指出衡量住房可承受性的最常见的一个指标是房价收入比,而且还特别强调,根据人居中心九十年代对经济发展水平各不相同国家52个主要城市所作的统计资料分析:无论是低收入、中低收入、中等收入、中高收入国家,还是高收入国家,他们房价收入比的平均值的差别相当小。报告认为,当房价收入比的比值在2:1和3:1之间时,才意味着有相当大的一部分人能够购买住房。比值超过5以后,多数人购买住房的希望就变得比较“非常渺茫”。
其实,如果按照发达国家“含金量”高的标准来计算我们的房价收入比,那样的比值就不知道要高出“世界记录”多少倍。房价收入比突破50都是很正常的。在上面举例的平均年收入在5万元的大城市中,三口之家要花250万元买到“套内建筑面积”在200平米、单价5000元的精装修别墅,的,也只能在地点和品质上“降低要求”才能实现。
全民共享的国有土地,“最沉重的负担”压垮了谁?
尤其需要特别指出的是,虽然我国一些大中城市的房价收入比,按照“中国特色”的住房标准还是一般发达国家的数倍,但我们城市房地产市场的基础,还是社会主义制度下的土地国有,理论上应该是为全民“共享”。我们买房所用的土地,还是“租用”国有土地的使用权,属于租地买房,使用期限也只有70年。而那些房价收入比比我们低得多的发达国家和一般市场经济国家,他们的房地产市场体系是建立在土地私有制下的,属于买房买地,土地产权永远归买房人所有。
一个公民在社会主义制度下,用全民所有的国有土地“租地买房”,所承担的负担,竟然比在私有制下的发达资本主义国家“买地买房”还要高得多,这种不可理喻的“极端对比”所发出的强烈信号,已经非常明白无误的昭示我们,现在中国一些大中城市所进行的“住房制度改革”实际所导致的结果,不仅从根本上颠倒了社会主义公有制和资本主义私有制的制度优劣,破坏了社会主义制度的基础,并且从导致的后果来看,它已经异化成对社会主义制度优越性的最大的践踏和嘲讽,成为从人民大众手中巧取豪夺全民财富分“带血的工具”。
中国共产党的早期,曾有两份 “共产党宣言”。一份是建党时的《中国共产党宣言》。开门见山就亮出了共产主义者理想是主张将包括土地在内的生产工具“收归社会共有,社会共用”;另一份红军时期发布的《共产党宣言》,也坚决主张把革命的重心放在土地革命上。
从历史走回现实,执政党的理想和行政者作为的尖锐对比,难道给我们带来的仅仅还是“房价究竟应该是高还是低”之类的“经济技术分析层面”的思考吗?!
LDAP应用程序接口
rfc1823:LDAP应用程序接口
(RFC1823:The LDAP Application Program Interface)
目录
1. 摘要
2. LDAP模型的观点
3. LDAP API的使用
4. 调用LDAP操作
4.1. 打开一个联接
4.2. 绑定到目录
4.3. 关闭连接
4.4. 查询
4.5. 读取条目
4.6. 子条目列表
4.7. 修改条目
4.8. 修改条目的RDN
4.9. 增加条目
4.10. 删除条目
5. 取消操作
6. 结果处理
7. 出错处理
8. 对查询结果的处理
8.1. 条目结果的处理
8.2. 对属性结果的处理
8.3. 获得属性值
8.4. 目录项DN分析处理
9. 安全考虑
10. 鸣谢
11. 参考书目
12. 作者地址
13. 附: 一个简单的LDAP API代码
想购买佳文网络通讯协议图,请点击此处。
现在投稿将有机会获得佳文网络通讯协议图,详情请点击此处。
网络协议手册 Network Protocols Handbook
包含所以常用的网络协议解释
包括最新的TCP/IP、WAN、LAN、MAN、VOIP、Security/VPN、Storage、WLAN等协议
总揽了领导性的协议和技术
佳文公司致力将中国软件推向世界!
如果您有网络管理和安全方面(英文版)的软件,请与我们联系。
想得到佳文网络通讯协议图屏幕保护,请点击此处下载。
1. 摘要
该文档为轻量级目录访问协议(LDAP)定义了一个基于C语言的应用程序接口。该LDAP API 功能强大,且易于使用。为了适应应用的大量变化,它采用了兼容的LDAP同步和异步接口。该文档给出了LDAP模型的基本观点,以及一个应用程序怎样通过这些接口获得LDAP的信息。这些应用程序接口调用在本文里都有很详细的描述,并且,文档末尾附带有部分API使用的实例代码。
返回目录
2. LDAP模型的观点
LDAP——轻量级目录访问协议。其描述参见[2]和[7]。它能为X500目录[1],或独立的服务(stand-alone service)提供轻量级的访问。在任何模式下,LDAP都是基于客户-服务器模型的。在客户-服务器模型中,客户通过TCP联接与LDAP服务器相连,并且在此联接上发送请求和接收响应。
LDAP信息模型基于包含对象信息(例如,一个人就是一个对象)的条目。条目包含各种属性,属性由属性类型和属性值两部分组成。其中,一条属性类型可以包含多个属性值。每个属性都有一套语法,语法规定了属性允许值的类型(例如,一幅jpeg照片的类型为:
ASCII characters)以及在目录操作期间这些值是怎样运作的(例如,在进行值的比较时)。条目以树的结构进行组织,通常按行政、地域,以及组织分界限进行划分。每个条目拥有一个区别于其它兄弟条目的名字——相对区别名(RDN),相对区别名包含了一个或多个区别属性值,以此区别条目。条目至少有一个属性值属于RDN。例如,Babs Jensen可以被命名为"Barbara Jensen",这一值取自普通名属性值。条目全球唯一的名字被称为区别名或DN。DN由树状结构的根结点到条目的所有结点的RDN组成。例如,如果Babs在密切根大学(University of Michigan)工作,以他为条目的DN就可能是"cn=Barbara Jensen, o=University of Michigan,c=US"。这种用于LDAP的格式的定义参见[4]。
查询和恢复信息,修改信息,增加及删除条目这些操作都以身份鉴定为前提。下面将讨论:怎样使用API,以及LDAPL API调用的详细说明。
返回目录
3. LDAP API的使用
一个应用调用LDAP API一般有以下四步:
打开一个到LDAP 服务器的联接。 ldap_open()函数调用返回一个联接句柄,允许立即建立多联接。
认证LDAP服务器和/或X.500 DSA。函数ldap_bind()友好地支持各种认证方式。
执行其它LDAP操作并获得其值。ldap_search()及其同类函数返回的值能被ldap_result2error(), ldap_first_entry(), ldap_next_entry()等函数解析。
关闭联接。ldap_unbind()函数调用关闭此联接。
操作能同步或异步执行。同步调用的函数以_s结尾。例如,调用ldap_search_s()实现同步查询,调用ldap_search()实现异步查询。所有同步操作返回一个操作结果的指示(例如,操作成功返回LDAP_SUCCESS,操作失败返回错误代码)。异步操作返回操作初始化信息id。id能被用于随后的ldap_result()函数调用,以获得操作的结果。通过调用ldap_abandon()可以丢弃异步操作。
结果和错误被返回到一个称作LDAPMessage的不透明结构中。LDAP操作为这一结构提供语法分析,逐步分析返回的目录项或属性等。此外,LDAP操作还解释出现的错误。下面,将详细描述这些LDAP操作。
返回目录
4. 调用LDAP操作
这一部分详细描述了LDAP API的调用。所有调用依赖一个连接句柄,即指向一个包含所有连接信息的LDAP结构的指针。通常结果将返回到一个LDAPMessage结构中。部分结构将在下面说明。
4.1. 打开一个联接
ldap_open() 函数打开一个到LDAP服务器的联接。
typedef struct ldap {
/* ... 隐含参数 ... */
int ld_deref;
int ld_timelimit;
int ld_sizelimit;
int ld_errno;
char *ld_matched;
char *ld_error;
/* ...隐含参数... */
} LDAP;
LDAP *ldap_open( char *hostname, int portno );
参数:
host: 需要联接的LDAP服务器的一个分离空间的主机名列表或者是代表服务器IP地址的分离的字符串。有序列表中的主机都处于准备被联接状态,直到其中有一个被成功联接上为止。
port: 含用于联接的TCP端口号。缺省的LDAP端口能够从常量LDAP_PORT中获得。
ldap_open() 返回一个联结句柄, 即一个指向LDAP结构的指针。为随后的绑定到目录服务器提供参数值。如果打开操作失败,返回NULL。 在其它操作执行之前,必须完成ldap_bind操作(即绑定到目录服务器),关于ldap_bind的说明在后面会提到。
调用程序将不考虑LDAP结构域的顺序。可能结构中的某些域在国内图书馆会被采用。
以上的域在下面其它函数调用的描述中会有所提及。
4.2. 绑定到目录
ldap_bind()及同类函数用于绑定到目录。
int ldap_bind( LDAP *ld, char *dn, char *cred, int method );
int ldap_bind_s( LDAP *ld, char *dn, char *cred, int method );
int ldap_simple_bind( LDAP *ld, char *dn, char *passwd );
int ldap_simple_bind_s( LDAP *ld, char *dn, char *passwd );
int ldap_kerberos_bind( LDAP *ld, char *dn );
int ldap_kerberos_bind_s( LDAP *ld, char *dn );
参数:
ld 连接句柄;
dn 进行绑定操作的用户dn;
cred 认证的证件;
method LDAP_AUTH_SIMPLE,LDAP_AUTH_KRBV41,或LDAP_AUTH_ LDAPKRBV42中的一种,包含用于认证的方式。
passwd 为ldap_simple_bind()系列专有,用于与目录项中的userPassword属性值作比较。
这里有关于bind调用的三种类型,提供简单的认证,kerberos 认证,以及普通事务。由于第四版本的Kerberos认证使用了通用的ldap_bind() ,忽略了证书部分,因此系统假定存在有效的认证依据,并且能被用于恢复特定的服务依据。
与该事务一致的版本的名字以_s结尾。这些事务返回bind操作的结果。如果操作成功,返回LDAP_SUCCESS,否则返回错误代码。下一节将列出可能出现的错误句柄的更多信息并且对这些信息加以解释。
与以上事务一致的版本将返回初始化bind操作的信息id。随后调用ldap_result(),用于获取bind操作的结果。如果出错,返回-1,并在LDAP结构中设置ld_errno域。
注意,在bind操作成功之前,其它的所有操作都不能成功,其后的bind调用可用于同一联接之上的重复认证。
4.3. 关闭连接
ldap_unbind() 用于解除与目录的绑定并关闭连接。
int ldap_unbind( LDAP *ld );
参数:
ld 连接句柄。
ldap_unbind() 工作在同步模式,解除与目录的绑定,关掉连接,并在返回前释放ld结构。ldap_unbind()返回LDAP_SUCCESS (如果请求未被送往LDAP服务器,则返回一个LDAP错误代码)。在调用完ldap_unbind()之后,ld连接句柄失效。
4.4. 查询
ldap_search() 及同类函数用于查询LDAP目录,返回匹配的目录项的请求值。
这里有三种形式:
struct timeval {
long tv_sec;
long tv_usec;
};
int ldap_search(
LDAP *ld,
char *base,
int scope,
char *filter,
char *attrs[],
int attrsonly
);
int ldap_search_s(
LDAP *ld,
char *base,
int scope,
char *filter,
char *attrs[],
int attrsonly,
LDAPMessage **res
);
int ldap_search_st(
LDAP *ld,
char *base,
int scope,
char *filter,
char *attrs[],
int attrsonly,
struct timeval *timeout,
LDAPMessage **res
);
参数:
ld 连接句柄;
base 最基本的 dn 条件值;
scope 为LDAP_SCOPE_BASE, LDAP_SCOPE_ONELEVEL,或LDAP_SCOPE_SUBTREE,表示查询的范围。
filter 匹配的字符串,在RFC 1558 [3]中有详细的描述。
attrs 为NULL时表示返回所有匹配的条目。非空导致所有可用属性将被恢复。
attrsonly 布尔值。如果为0则返回属性类型以及属性值;非0则只返回属性类型。
timeout 当调用ldap_search_st()时,它将指出本地查询的时延值。
res 当同步调用时,该参数将包含函数调用结束时的一个返回值。
有三个域决定了查询操作的执行。它们分别是:
ld_sizelimit 限制查询结果的数目。为0时表示没有限制。
ld_timelimit 限制查询的时间。为0表示没有限制。
ld_deref 其值为以下任意一个LDAP_DEREF_NEVER, LDAP_DEREF_SEARCHING,LDAP_DEREF_FINDING, 或LDAP_DEREF_ALWAYS,指明查询时别名将怎样被绑定。 值为LDAP_DEREF_SEARCHING表示在查询时将不允许别名,但对base指定的条目将不受此限制;值为LDAP_DEREF_FINDING时情况刚好与LDAP_DEREF_SEARCHING相反。
ldap_search()进行的是异步查询。它返回初始化查询的id值。该值被随后调用的ldap_result()函数获得,对其进行语法分析,并进行相应的描述。如果操作失败,返回-1;并且,LDAP结构中的ld_errno域将被重设。
通过调用ldap_search_s()或ldap_search_st()进行同步查询。进程将保持一致,除了ldap_search_st()函数的额外参数将指定查询的时延。两个函数都将直接返回查询的结果:LDAP_SUCCESS或者错误代码。查询到的条目的返回值将保存在res参数里。该参数对于调用者来说是不透明的。条目、属性、值等将通过下面介绍的函数获得。保存在参数res中的结果当不再被使用时,将通过调用ldap_msgfree()函数进行释放,这在后面会有详细介绍。
4.5. 读取条目
LDAP 不支持直接的读操作。实际上,查询操作中的操作范围:LDAP_SCOPE_BASE,匹配条件:"(objectclass=*)"都利用到了读操作。参数attrs包含了返回的属性列表。
4.6. 子条目列表
LDAP 不直接支持列表操作。实际上,查询操作中的操作范围:LDAP_SCOPE_ ONELEVEL,匹配条件:"(objectclass=*)"也利用到了列表操作。参数attrs包含了返回的所有子条目的属性列表。
4.7. 修改条目
函数ldap_modify() and ldap_modify_s()用于修改存在的LDAP条目。
typedef struct ldapmod {
int mod_op;
char *mod_type;
union {
char **modv_strvals;
struct berval **modv_bvals;
} mod_vals;
} LDAPMod;
#define mod_values mod_vals.modv_strvals
#define mod_bvalues mod_vals.modv_bvals
int ldap_modify( LDAP *ld, char *dn, LDAPMod *mods[] );
int ldap_modify_s( LDAP *ld, char *dn, LDAPMod *mods[] );
参数:
ld 联接句柄;
dn 要修改的条目名;
mods 属性修改操作模式;
LDAPMod结构域包含以下信息:
mod_op 操作类型。其值可能为:LDAP_MOD_ADD, LDAP_MOD_DELETE,或 LDAP_MOD_REPLACE。该域同时指定了mod_vals单元值的类型。如果为LDAP_MOD_BVALUES 则选择了mod_bvalues模式。否则,mod_values定义的模式才有效。
mod_type 修改的属性类型;
mod_vals 值为:add, delete,或replace。mod_values与mod_bvalues不能共用。只有在mods_op中指定为LDAP_MOD_BVALUES时才能在这里设为mod_bvalues值。 mod_values为包含字符串的非空数组,字符串可为NULL。mod_bvalues为包含berval结构的非空数组,用于包含二进制类型的值,例如图片。
LDAP_MOD_ADD模式用于在条目中创建新的属性。LDAP_MOD_DELETE模式用于删除条目中不再需要的属性。如果是删除操作,那么mod_vals域的值将设为NULL。 LDAP_MOD_REPLACE模式中,属性值将设置为列表中定义的值。所有模式的运行顺序都遵循它们排列的先后顺序。
ldap_modify_s()返回修改操作的LDAP出错代码。该代码能被ldap_perror()及其友元函数解析。
ldap_modify() 返回初始化信息的id,出错时返回-1。操作结果通过调用ldap_result()获得。
4.8. 修改条目的RDN
ldap_modrdn()和ldap_modrdn_s()用于改变条目的名称。
int ldap_modrdn(
LDAP *ld,
char *dn,
char *newrdn,
int deleteoldrdn
);
int ldap_modrdn_s(
LDAP *ld,
char *dn,
char *newrdn,
int deleteoldrdn
);
参数:
ld 连接句柄;
dn RDN将改变的条目名称;
newrdn 给条目的新RDN;
deleteoldrdn 布尔值,非0表示旧的RDN 值将被删除,为0表示将继续保留旧的RDN值。
ldap_modrdn_s()是同步操作,返回操作结果的LDAP错误代码。
ldap_modrdn() 为异步函数,返回操作初始化的信息id。如果有误返回-1。通过调用dap_result()可以获得操作的结果。
4.9. 增加条目
ldap_add()和ldap_add_s()用于增加LDAP目录中的条目。
int ldap_add( LDAP *ld, char *dn, LDAPMod *attrs[] );
int ldap_add_s( LDAP *ld, char *dn, LDAPMod *attrs[] );
参数:
ld 连接句柄;
dn 新添加条目的名称;
attrs 条目的属性,采用ldap_modify()中定义的LDAPMod结构。要求输入mod_type与mod_vals域的值,mod_op域缺省。除非遇到常量LDAP_MOD_BVALUES时, 需要用到相应的mod_bvalues替代mod_vals。
注意,新添加条目的父条目必须已经存在。
ldap_add_s()是同步操作,返回操作结果的LDAP错误代码。
ldap_add()为异步操作,返回操作初始化的信息id。如果有误返回-1。通过调用dap_result()可以获得操作的结果。
4.10. 删除条目
ldap_delete()和ldap_delete_s()用于删除LDAP目录中的条目。
int ldap_delete( LDAP *ld, char *dn );
int ldap_delete_s( LDAP *ld, char *dn );
参数:
ld 连接句柄;
dn 需要删除的条目名称。
注意,被删除的条目必须是“叶”条目(即,不含有子条目)。LDAP不支持删除带有子条目的结点。
ldap_delete_s() 是同步操作,返回操作结果的LDAP错误代码。
ldap_delete() 是异步操作,返回操作初始化的信息id。如果有误返回-1。通过调用dap_result()可以获得操作的结果。
返回目录
5. 取消操作
ldap_abandon()用于取消进行的操作。
int ldap_abandon( LDAP *ld, int msgid );
ldap_abandon()通过信息的id号——msgid取消操作。如果操作成功,返回0;否则,返回-1。在成功调用ldap_abandon()之后,给定信息id的操作结果将不会有返回值。
返回目录
6. 结果处理
ldap_result()用于获得前面调用的异步初始化操作的结果。ldap_msgfree()释放由函数ldap_result()获得的结果或同步查询进程。
int ldap_result(
LDAP *ld,
int msgid,
int all,
struct timeval *timeout,
LDAPMessage **res
);
int ldap_msgfree( LDAPMessage *res );
参数:
ld 连接句柄;
msgid 操作结果的信息id。如果希望返回所有的结果,则设为常量LDAP_RES_ANY;
all 一个布尔型参数,仅对查询结果起作用。如果非0,表明在获取了所有查询结果后再一并返回;为0,表明查询到一个结果就返回一个结果。
timeout 等待结果返回的最短时间。为NULL表明没有时间限制。为0表示轮询操作。
res 在ldap_result()中,该参数将保存操作的结果;在ldap_msgfree()中,该参数用于释放从ldap_result()或ldap_search_s()或ldap_search_st()获得的结果。
若上面的操作成功,ldap_result()将通过res参数返回结果的类型。其类型为以下几种:
LDAP_RES_BIND
LDAP_RES_SEARCH_ENTRY
LDAP_RES_SEARCH_RESULT
LDAP_RES_MODIFY
LDAP_RES_ADD
LDAP_RES_DELETE
LDAP_RES_MODRDN
LDAP_RES_COMPARE
如果超出了时间限制,ldap_result()将返回0;如果出错,将返回-1,同时将出错信息记录到ld_errno 域。
ldap_msgfree()释放指向res结构的指针并返回释放信息的类型。
返回目录
7. 出错处理
下面的函数调用用于解释由其它LDAP API产生的出错信息。
int ldap_result2error(
LDAP *ld,
LDAPMessage *res,
int freeit
);
char *ldap_err2string( int err );
void ldap_perror( LDAP *ld, char *msg );
参数:
ld 连接句柄;
res 其它LDAP操作的返回值,即ldap_result()的返回值或其它同步API操作的结果;
freeit 布尔型参数,指明参数res是否被释放(非0,释放;0,不释放);
err LDAP出错代码,即ldap_result2error()的返回值或其它同步API操作的结果;
msg 在LDAP出错信息之前显示的信息。
ldap_result2error()用于将从ldap_result()中获得的LDAP结果,或同步API操作的结果转换为数字类型的LDAP错误代码。并且分析结果信息中的ld_matched and ld_error部分,将其放入连接句柄信息中。所有的同步操作进程在返回值之前调用ldap_result2error(),以确保各个域的正确设置。联接结构中相关的域有:
ld_matched 用于LDAP_NO_SUCH_OBJECT错误值返回时,该参数限制了匹配的DN的范围;
ld_error 该参数保存了LDAP服务器发送出的出错信息;
ld_errno LDAP出错代码指出了操作的结果。其值为以下的任意一个:
LDAP_SUCCESS
LDAP_OPERATIONS_ERROR
LDAP_PROTOCOL_ERROR
LDAP_TIMELIMIT_EXCEEDED
LDAP_SIZELIMIT_EXCEEDED
LDAP_COMPARE_FALSE
LDAP_COMPARE_TRUE
LDAP_STRONG_AUTH_NOT_SUPPORTED
LDAP_STRONG_AUTH_REQUIRED
LDAP_NO_SUCH_ATTRIBUTE
LDAP_UNDEFINED_TYPE
LDAP_INAPPROPRIATE_MATCHING
LDAP_CONSTRAINT_VIOLATION
LDAP_TYPE_OR_VALUE_EXISTS
LDAP_INVALID_SYNTAX
LDAP_NO_SUCH_OBJECT
LDAP_ALIAS_PROBLEM
LDAP_INVALID_DN_SYNTAX
LDAP_IS_LEAF
LDAP_ALIAS_DEREF_PROBLEM
LDAP_INAPPROPRIATE_AUTH
LDAP_INVALID_CREDENTIALS
LDAP_INSUFFICIENT_ACCESS
LDAP_BUSY
LDAP_UNAVAILABLE
LDAP_UNWILLING_TO_PERFORM
LDAP_LOOP_DETECT
LDAP_NAMING_VIOLATION
LDAP_OBJECT_CLASS_VIOLATION
LDAP_NOT_ALLOWED_ON_NONLEAF
LDAP_NOT_ALLOWED_ON_RDN
LDAP_ALREADY_EXISTS
LDAP_NO_OBJECT_CLASS_MODS
LDAP_RESULTS_TOO_LARGE
LDAP_OTHER
LDAP_SERVER_DOWN
LDAP_LOCAL_ERROR
LDAP_ENCODING_ERROR
LDAP_DECODING_ERROR
LDAP_TIMEOUT
LDAP_AUTH_UNKNOWN
LDAP_FILTER_ERROR
LDAP_USER_CANCELLED
LDAP_PARAM_ERROR
LDAP_NO_MEMORY
ldap_err2string()用于将数字型的LDAP出错代码(例如ldap_result2error()的返回值,或任意一同步API操作调用结果)转换为描述该出错信息的字符串。其返回一个指向静态数据的指针。
ldap_perror() 用于将ld_errno域中参数msg中包含的信息转换为标准的错误信息。
返回目录
8. 对查询结果的处理
下面的函数调用用于分析由ldap_search()及其友员函数返回的结果。这些返回值存放在一个不透明的结构中,只能通过调用下面这些函数来获得。这些函数可用于处理返回的条目、条目属性、获得条目名称,以及获得条目中给定属性的属性值。
8.1. 条目结果的处理
函数ldap_first_entry()和ldap_next_entry()用于处理查询到的条目结果。
ldap_count_entries() 用于计算返回的条目个数。
LDAPMesage *ldap_first_entry( LDAP *ld, LDAPMessage *res );
LDAPMesage *ldap_next_entry( LDAP *ld, LDAPMessage *entry );
int ldap_count_entries( LDAP *ld, LDAPMessage *res );
参数:
ld 连接句柄;
res 查询结果,由同步查询进程或函数ldap_result()获得;
entry 函数调用ldap_first_entry()或ldap_next_entry()的返回值;
当没有条目存在时ldap_first_entry()和ldap_next_entry()将返回NULL。当函数在运行过程中出错时也会返回NULL,但此时ld连接句柄中的ld_errno域将记录该错误信息。
ldap_count_entries() 返回在条目链中条目的个数。该函数也可被用于计算在函数调用ldap_first_entry()或ldap_next_entry()中符合条件的条目的个数。
8.2. 对属性结果的处理
函数ldap_first_attribute() 和ldap_next_attribute()用于对由某个条目返回的属性结果的处理。
char *ldap_first_attribute(
LDAP *ld,
LDAPMessage *entry,
void **ptr
);
char *ldap_next_attribute(
LDAP *ld,
LDAPMessage *entry,
void *ptr
);
参数:
ld 连接句柄;
entry 需处理的属性结果所在的条目,即ldap_first_entry()或ldap_next_entry()的返回值;
ptr 在函数ldap_first_attribute()中,用于保存当前条目所在位置的地址指针。函数ldap_next_attribute()所用到的指针为先前调用ldap_first_attribute()获得的返回值。
当达到最后一个属性时,函数ldap_first_attribute()和ldap_next_attribute()将返回NULL,由于这个原因,ld句柄中的ld_errno 域将被设置为error。两个进程都将返回一个指向包含当前属性名的联接缓冲的指针。这将被当作静态数据对待。ldap_first_attribute()将定位并返回一个指向BerElement类型的名为ptr的指针,以保存当前位置的路径。该指针将被后面的调用ldap_next_attribute() 所引用,以获得下一个条目的属性结果。
返回的属性名将被ldap_get_values()极其成员函数利用,以获得相关的属性值。
8.3. 获得属性值
ldap_get_values()和ldap_get_values_len()用于获得条目的属性值。ldap_count_values()和ldap_count_values_len()用于计算返回值的个数。ldap_value_free()和ldap_value_free_len()用于释放返回的属性值。
typedef struct berval {
unsigned long bv_len;
char *bv_val;
};
char **ldap_get_values(
LDAP *ld,
LDAPMessage *entry,
char *attr
);
struct berval **ldap_get_values_len(
LDAP *ld,
LDAPMessage *entry,
char *attr
);
int ldap_count_values( char **vals );
int ldap_count_values_len( struct berval **vals );
int ldap_value_free( char **vals );
int ldap_value_free_len( struct berval **vals );
参数:
ld 连接句柄;
entry 属性所属的条目,即ldap_first_entry()或ldap_next_entry()的返回值;
attr 返回值的属性,即ldap_first_attribute()或ldap_next_attribute()或一个字符串调用的返回值(例如,"mail");
vals 先前调用ldap_get_values()或ldap_get_values_len()的返回值。
两种不同形式的调用都是有条件的。第一重形式只适用于非二进制的字符串类型数据;第二种_len形式适用于任何形式的数据。
注意,返回的值当不再使用时,应调用函数ldap_value_free()或ldap_value_free_len()进行释放。
8.4. 目录项DN分析处理
ldap_get_dn() 用于获得条目的dn 。
ldap_explode_dn() 用于将 dn 中的各字段切开。
ldap_dn2ufn() 用于将dn 的名字转换成较易读取的名字。
char *ldap_get_dn( LDAP *ld, LDAPMessage *entry );
char **ldap_explode_dn( char *dn, int notypes );
char *ldap_dn2ufn( char *dn );
参数:
ld 连接句柄;
entry 为ldap_first_entry()或ldap_next_entry()返回的搜寻代号;
dn 可由ldap_get_dn()的返回值得到;
notypes 布尔型参数,如果非零表明dn的组成部分将只包含属性值,而不包含属性名。 (例如,"cn=Babs"将变为"Babs")。
ldap_get_dn() 当dn分析错误时返回NULL,为ld连接句柄设置ld_errno以指明错误。它返回一个指向预分配空间的指针,当此空间不再被使用时,调用free() 进行空间释放。这些DN的返回格式在[4]中有所描述。
ldap_explode_dn() 返回一个指针数组,该数组包含提供给DN的RDN部分。并通过notypes参数标识是否包含类型。当不再使用该数组返回值时,可调用ldap_value_free()进行资源释放。
ldap_dn2ufn() 将DN转换成较易读取的名字(User Friendly Name),描述参见[5]。UFN将返回值到预分配的空间,当此空间不再使用时,可调用free() 函数释放。
返回目录
9. 安全考虑
LDAP支持联结鉴别时的少量安全。
返回目录
10. 鸣谢
这篇材料基于国家科学中心授权的No. NCR-9416667的大力支持。
返回目录
11. 参考书目
The Directory: Selected Attribute Syntaxes. CCITT, Recommendation X.520.
Howes, T., Kille, S., Yeong, W., and C. Robbins, "The String Representation of Standard Attribute Syntaxes", University of Michigan, ISODE Consortium, Performance Systems International, NeXor Ltd., RFC 1778, March 1995.
Howes, T., "A String Representation of LDAP Search Filters", RFC 1558, University of Michigan, December 1993.
Kille, S., "A String Representation of Distinguished Names", RFC 1779, ISODE Consortium, March 1995.
Kille, S., "Using the OSI Directory to Achieve User Friendly Naming", RFC 1781, ISODE Consortium, March 1995.
S.P. Miller, B.C. Neuman, J.I. Schiller, J.H. Saltzer, "Kerberos Authentication and Authorization System", MIT Project Athena Documentation Section E.2.1, December 1987
Yeong, W., Howes, T., and S. Kille, "Lightweight Directory Access Protocol," RFC 1777, Performance Systems International, University of Michigan, ISODE Consortium, March 1995.
返回目录
13. 附: 一个简单的LDAP API代码
#include <ldap.h>
main()
{
LDAP *ld;
LDAPMessage *res, *e;
int i;
char *a, *dn;
void *ptr;
char **vals;
/* 打开一个连接 */
if ( (ld = ldap_open( "dotted.host.name", LDAP_PORT ))
== NULL )
exit( 1 );
/* 匿名登陆 */
if ( ldap_simple_bind_s( ld, NULL, NULL ) != LDAP_SUCCESS ) {
ldap_perror( ld, "ldap_simple_bind_s" );
exit( 1 );
}
/* 查询cn="Babs Jensen"的条目,并返回所有属性 */
if ( ldap_search_s( ld, "o=University of Michigan, c=US",
LDAP_SCOPE_SUBTREE, "(cn=Babs Jensen)", NULL, 0, &res )
!= LDAP_SUCCESS ) {
ldap_perror( ld, "ldap_search_s" );
exit( 1 );
}
/* 分析每个返回的条目 */
for ( e = ldap_first_entry( ld, res ); e != NULL;
e = ldap_next_entry( ld, e ) ) {
/* 打印对象的dn */
dn = ldap_get_dn( ld, e );
printf( "dn: %s0, dn );
free( dn );
/* 打印每个属性 */
for ( a = ldap_first_attribute( ld, e, &ptr );
a != NULL;
a = ldap_next_attribute( ld, e, ptr ) ) {
printf( "attribute: %s0, a );
/* 打印每个属性值 */
vals = ldap_get_values( ld, e, a );
for ( i = 0; vals[i] != NULL; i++ ) {
printf( "value: %s0, vals[i] );
}
ldap_value_free( vals );
}
}
/* 释放查询结果 */
ldap_msgfree( res );
/* 关闭资源连接 */
ldap_unbind( ld );
}
追MM与设计模式
创建型模式
1、FACTORY—追MM少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西,虽然口味有所不同,但不管你带MM去麦当劳或肯德基,只管向服务员说“来四个鸡翅”就行了。麦当劳和肯德基就是生产鸡翅的Factory
工厂模式:客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。缺点是当产品修改时,工厂类也要做相应的修改。如:如何创建及如何向客户端提供。
2、BUILDER—MM最爱听的就是“我爱你”这句话了,见到不同地方的MM,要能够用她们的方言跟她说这句话哦,我有一个多种语言翻译机,上面每种语言都有一个按键,见到MM我只要按对应的键,它就能够用相应的语言说出“我爱你”这句话了,国外的MM也可以轻松搞掂,这就是我的“我爱你”builder。(这一定比美军在伊拉克用的翻译机好卖)
建造模式:将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同的内部表象的产品对象。建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。
3、FACTORY METHOD—请MM去麦当劳吃汉堡,不同的MM有不同的口味,要每个都记住是一件烦人的事情,我一般采用Factory Method模式,带着MM到服务员那儿,说“要一个汉堡”,具体要什么样的汉堡呢,让MM直接跟服务员说就行了。
工厂方法模式:核心工厂类不再负责所有产品的创建,而是将具体创建的工作交给子类去做,成为一个抽象工厂角色,仅负责给出具体工厂类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。
4、PROTOTYPE—跟MM用QQ聊天,一定要说些深情的话语了,我搜集了好多肉麻的情话,需要时只要copy出来放到QQ里面就行了,这就是我的情话prototype了。(100块钱一份,你要不要)
原始模型模式:通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类,产品类不需要非得有任何事先确定的等级结构,原始模型模式适用于任何的等级结构。缺点是每一个类都必须配备一个克隆方法。
5、SINGLETON—俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公Sigleton,她们只要说道“老公”,都是指的同一个人,那就是我(刚才做了个梦啦,哪有这么好的事)
单例模式:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。单例模式只应在有真正的“单一实例”的需求时才可使用。
结构型模式
6、ADAPTER—在朋友聚会上碰到了一个美女Sarah,从香港来的,可我不会说粤语,她不会说普通话,只好求助于我的朋友kent了,他作为我和Sarah之间的Adapter,让我和Sarah可以相互交谈了(也不知道他会不会耍我)
适配器(变压器)模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。
7、BRIDGE—早上碰到MM,要说早上好,晚上碰到MM,要说晚上好;碰到MM穿了件新衣服,要说你的衣服好漂亮哦,碰到MM新做的发型,要说你的头发好漂亮哦。不要问我“早上碰到MM新做了个发型怎么说”这种问题,自己用BRIDGE组合一下不就行了
桥梁模式:将抽象化与实现化脱耦,使得二者可以独立的变化,也就是说将他们之间的强关联变成弱关联,也就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以独立的变化。
8、COMPOSITE—Mary今天过生日。“我过生日,你要送我一件礼物。”“嗯,好吧,去商店,你自己挑。”“这件T恤挺漂亮,买,这条裙子好看,买,这个包也不错,买。”“喂,买了三件了呀,我只答应送一件礼物的哦。”“什么呀,T恤加裙子加包包,正好配成一套呀,小姐,麻烦你包起来。”“……”,MM都会用Composite模式了,你会了没有?
合成模式:合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。合成模式就是一个处理对象的树结构的模式。合成模式把部分与整体的关系用树结构表示出来。合成模式使得客户端把一个个单独的成分对象和由他们复合而成的合成对象同等看待。
9、DECORATOR—Mary过完轮到Sarly过生日,还是不要叫她自己挑了,不然这个月伙食费肯定玩完,拿出我去年在华山顶上照的照片,在背面写上“最好的的礼物,就是爱你的Fita”,再到街上礼品店买了个像框(卖礼品的MM也很漂亮哦),再找隔壁搞美术设计的Mike设计了一个漂亮的盒子装起来……,我们都是Decorator,最终都在修饰我这个人呀,怎么样,看懂了吗?
装饰模式:装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,提供比继承更多的灵活性。动态给一个对象增加功能,这些功能可以再动态的撤消。增加由一些基本功能的排列组合而产生的非常大量的功能。
10、FACADE—我有一个专业的Nikon相机,我就喜欢自己手动调光圈、快门,这样照出来的照片才专业,但MM可不懂这些,教了半天也不会。幸好相机有Facade设计模式,把相机调整到自动档,只要对准目标按快门就行了,一切由相机自动调整,这样MM也可以用这个相机给我拍张照片了。
门面模式:外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。每一个子系统只有一个门面类,而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类。
11、FLYWEIGHT—每天跟MM发短信,手指都累死了,最近买了个新手机,可以把一些常用的句子存在手机里,要用的时候,直接拿出来,在前面加上MM的名字就可以发送了,再不用一个字一个字敲了。共享的句子就是Flyweight,MM的名字就是提取出来的外部特征,根据上下文情况使用。
享元模式:FLYWEIGHT在拳击比赛中指最轻量级。享元模式以共享的方式高效的支持大量的细粒度对象。享元模式能做到共享的关键是区分内蕴状态和外蕴状态。内蕴状态存储在享元内部,不会随环境的改变而有所不同。外蕴状态是随环境的改变而改变的。外蕴状态不能影响内蕴状态,它们是相互独立的。将可以共享的状态和不可以共享的状态从常规类中区分开来,将不可以共享的状态从类里剔除出去。客户端不可以直接创建被共享的对象,而应当使用一个工厂对象负责创建被共享的对象。享元模式大幅度的降低内存中对象的数量。
12、PROXY—跟MM在网上聊天,一开头总是“hi,你好”,“你从哪儿来呀?”“你多大了?”“身高多少呀?”这些话,真烦人,写个程序做为我的Proxy吧,凡是接收到这些话都设置好了自动的回答,接收到其他的话时再通知我回答,怎么样,酷吧。
代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。
行为模式
13、CHAIN OF RESPONSIBLEITY—晚上去上英语课,为了好开溜坐到了最后一排,哇,前面坐了好几个漂亮的MM哎,找张纸条,写上“Hi,可以做我的女朋友吗?如果不愿意请向前传”,纸条就一个接一个的传上去了,糟糕,传到第一排的MM把纸条传给老师了,听说是个老处女呀,快跑!
责任链模式:在责任链模式中,很多对象由每一个对象对其下家的引用而接 起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的情况下动态的重新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。
14、COMMAND—俺有一个MM家里管得特别严,没法见面,只好借助于她弟弟在我们俩之间传送信息,她对我有什么指示,就写一张纸条让她弟弟带给我。这不,她弟弟又传送过来一个COMMAND,为了感谢他,我请他吃了碗杂酱面,哪知道他说:“我同时给我姐姐三个男朋友送COMMAND,就数你最小气,才请我吃面。”,:-(
命令模式:命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否执行,何时被执行以及是怎么被执行的。系统支持命令的撤消。
15、INTERPRETER—俺有一个《泡MM真经》,上面有各种泡MM的攻略,比如说去吃西餐的步骤、去看电影的方法等等,跟MM约会时,只要做一个Interpreter,照着上面的脚本执行就可以了。
解释器模式:给定一个语言后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器。客户端可以使用这个解释器来解释这个语言中的句子。解释器模式将描述怎样在有了一个简单的文法后,使用模式设计解释这些语句。在解释器模式里面提到的语言是指任何解释器对象能够解释的任何组合。在解释器模式中需要定义一个代表文法的命令类的等级结构,也就是一系列的组合规则。每一个命令对象都有一个解释方法,代表对命令对象的解释。命令对象的等级结构中的对象的任何排列组合都是一个语言。
16、ITERATOR—我爱上了Mary,不顾一切的向她求婚。
Mary:“想要我跟你结婚,得答应我的条件”
我:“什么条件我都答应,你说吧”
Mary:“我看上了那个一克拉的钻石”
我:“我买,我买,还有吗?”
Mary:“我看上了湖边的那栋别墅”
我:“我买,我买,还有吗?”
Mary:“你的小弟弟必须要有50cm长”
我脑袋嗡的一声,坐在椅子上,一咬牙:“我剪,我剪,还有吗?”
……
迭代子模式:迭代子模式可以顺序访问一个聚集中的元素而不必暴露聚集的内部表象。多个对象聚在一起形成的总体称之为聚集,聚集对象是能够包容一组对象的容器对象。迭代子模式将迭代逻辑封装到一个独立的子对象中,从而与聚集本身隔开。迭代子模式简化了聚集的界面。每一个聚集对象都可以有一个或一个以上的迭代子对象,每一个迭代子的迭代状态可以是彼此独立的。迭代算法可以独立于聚集角色变化。
17、MEDIATOR—四个MM打麻将,相互之间谁应该给谁多少钱算不清楚了,幸亏当时我在旁边,按照各自的筹码数算钱,赚了钱的从我这里拿,赔了钱的也付给我,一切就OK啦,俺得到了四个MM的电话。
调停者模式:调停者模式包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用。从而使他们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用。保证这些作用可以彼此独立的变化。调停者模式将多对多的相互作用转化为一对多的相互作用。调停者模式将对象的行为和协作抽象化,把对象在小尺度的行为上与其他对象的相互作用分开处理。
18、MEMENTO—同时跟几个MM聊天时,一定要记清楚刚才跟MM说了些什么话,不然MM发现了会不高兴的哦,幸亏我有个备忘录,刚才与哪个MM说了什么话我都拷贝一份放到备忘录里面保存,这样可以随时察看以前的记录啦。
备忘录模式:备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。
19、OBSERVER—想知道咱们公司最新MM情报吗?加入公司的MM情报邮件组就行了,tom负责搜集情报,他发现的新情报不用一个一个通知我们,直接发布给邮件组,我们作为订阅者(观察者)就可以及时收到情报啦
观察者模式:观察者模式定义了一种一队多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使他们能够自动更新自己。
20、STATE—跟MM交往时,一定要注意她的状态哦,在不同的状态时她的行为会有不同,比如你约她今天晚上去看电影,对你没兴趣的MM就会说“有事情啦”,对你不讨厌但还没喜欢上的MM就会说“好啊,不过可以带上我同事么?”,已经喜欢上你的MM就会说“几点钟?看完电影再去泡吧怎么样?”,当然你看电影过程中表现良好的话,也可以把MM的状态从不讨厌不喜欢变成喜欢哦。
状态模式:状态模式允许一个对象在其内部状态改变的时候改变行为。这个对象看上去象是改变了它的类一样。状态模式把所研究的对象的行为包装在不同的状态对象里,每一个状态对象都属于一个抽象状态类的一个子类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式需要对每一个系统可能取得的状态创立一个状态类的子类。当系统的状态变化时,系统便改变所选的子类。
21、STRATEGY—跟不同类型的MM约会,要用不同的策略,有的请电影比较好,有的则去吃小吃效果不错,有的去海边浪漫最合适,单目的都是为了得到MM的芳心,我的追MM锦囊中有好多Strategy哦。
策略模式:策略模式针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。策略模式把行为和环境分开。环境类负责维持和查询行为类,各种算法在具体的策略类中提供。由于算法和环境独立开来,算法的增减,修改都不会影响到环境和客户端。
22、TEMPLATE METHOD——看过《如何说服女生上床》这部经典文章吗?女生从认识到上床的不变的步骤分为巧遇、打破僵局、展开追求、接吻、前戏、动手、爱抚、进去八大步骤(Template method),但每个步骤针对不同的情况,都有不一样的做法,这就要看你随机应变啦(具体实现);
模板方法模式:模板方法模式准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。
23、VISITOR—情人节到了,要给每个MM送一束鲜花和一张卡片,可是每个MM送的花都要针对她个人的特点,每张卡片也要根据个人的特点来挑,我一个人哪搞得清楚,还是找花店老板和礼品店老板做一下Visitor,让花店老板根据MM的特点选一束花,让礼品店老板也根据每个人特点选一张卡,这样就轻松多了;
访问者模式:访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。访问者模式使得增加新的操作变的很容易,就是增加一个新的访问者类。访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。当使用访问者模式时,要将尽可能多的对象浏览逻辑放在访问者类中,而不是放到它的子类中。访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。
IPC
Linux环境下的Socket编程
什么是Socket
Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解Socket接口。
Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Socket了。网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
Socket建立
为了建立Socket,程序可以调用Socket函数,该函数返回一个类似于文件描述符的句柄。socket函数原型为:
int socket(int domain, int type, int protocol);
domain指明所使用的协议族,通常为PF_INET,表示互联网协议族(TCP/IP协议族);type参数指定socket的类型:SOCK_STREAM 或SOCK_DGRAM,Socket接口还定义了原始Socket(SOCK_RAW),允许程序使用低层协议;protocol通常赋值"0"。Socket()调用返回一个整型socket描述符,你可以在后面的调用使用它。
Socket描述符是一个指向内部数据结构的指针,它指向描述符表入口。调用Socket函数时,socket执行体将建立一个Socket,实际上"建立一个Socket"意味着为一个Socket数据结构分配存储空间。Socket执行体为你管理描述符表。
两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。Socket数据结构中包含这五种信息。
Socket配置
通过socket调用返回一个socket描述符后,在使用socket进行网络传输以前,必须配置该socket。面向连接的socket客户端通过调用Connect函数在socket数据结构中保存本地和远端信息。无连接socket的客户端和服务端以及面向连接socket的服务端通过调用bind函数来配置本地信息。
Bind函数将socket与本机上的一个端口相关联,随后你就可以在该端口监听服务请求。Bind函数原型为:
int bind(int sockfd,struct sockaddr *my_addr, int addrlen);
Sockfd是调用socket函数返回的socket描述符,my_addr是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针;addrlen常被设置为sizeof(struct sockaddr)。
struct sockaddr结构类型是用来保存socket信息的:
struct sockaddr {
unsigned short sa_family; /* 地址族, AF_xxx */
char sa_data[14]; /* 14 字节的协议地址 */
};
sa_family一般为AF_INET,代表Internet(TCP/IP)地址族;sa_data则包含该socket的IP地址和端口号。
另外还有一种结构类型:
struct sockaddr_in {
short int sin_family; /* 地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[8]; /* 填充0 以保持与struct sockaddr同样大小 */
};
这个结构更方便使用。sin_zero用来将sockaddr_in结构填充到与struct sockaddr同样的长度,可以用bzero()或memset()函数将其置为零。指向sockaddr_in 的指针和指向sockaddr的指针可以相互转换,这意味着如果一个函数所需参数类型是sockaddr时,你可以在函数调用的时候将一个指向sockaddr_in的指针转换为指向sockaddr的指针;或者相反。
使用bind函数时,可以用下面的赋值实现自动获得本机IP地址和随机获取一个没有被占用的端口号:
my_addr.sin_port = 0; /* 系统随机选择一个未被使用的端口号 */
my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本机IP地址 */
通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。
注意在使用bind函数是需要将sin_port和sin_addr转换成为网络字节优先顺序;而sin_addr则不需要转换。
计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet上数据以高位字节优先顺序在网络上传输,所以对于在内部是以低位字节优先方式存储数据的机器,在Internet上传输数据时就需要进行转换,否则就会出现数据不一致。
下面是几个字节顺序转换函数:
·htonl():把32位值从主机字节序转换成网络字节序
·htons():把16位值从主机字节序转换成网络字节序
·ntohl():把32位值从网络字节序转换成主机字节序
·ntohs():把16位值从网络字节序转换成主机字节序
Bind()函数在成功被调用时返回0;出现错误时返回"-1"并将errno置为相应的错误号。需要注意的是,在调用bind函数时一般不要将端口号置为小于1024的值,因为1到1024是保留端口号,你可以选择大于1024中的任何一个没有被占用的端口号。
连接建立
面向连接的客户程序使用Connect函数来配置socket并与远端服务器建立一个TCP连接,其函数原型为:
int connect(int sockfd, struct sockaddr *serv_addr,int addrlen);
Sockfd是socket函数返回的socket描述符;serv_addr是包含远端主机IP地址和端口号的指针;addrlen是远端地质结构的长度。Connect函数在出现错误时返回-1,并且设置errno为相应的错误码。进行客户端程序设计无须调用bind(),因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候到打断口。
Connect函数启动和远端主机的直接连接。只有面向连接的客户程序使用socket时才需要将此socket与远端主机相连。无连接协议从不建立直接连接。面向连接的服务器也从不启动一个连接,它只是被动的在协议端口监听客户的请求。
Listen函数使socket处于被动的监听模式,并为该socket建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们。
int listen(int sockfd, int backlog);
Sockfd是Socket系统调用返回的socket 描述符;backlog指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待accept()它们(参考下文)。Backlog对队列中等待服务的请求的数目进行了限制,大多数系统缺省值为20。如果一个服务请求到来时,输入队列已满,该socket将拒绝连接请求,客户将收到一个出错信息。
当出现错误时listen函数返回-1,并置相应的errno错误码。
accept()函数让服务器接收客户的连接请求。在建立好输入队列后,服务器就调用accept函数,然后睡眠并等待客户的连接请求。
int accept(int sockfd, void *addr, int *addrlen);
sockfd是被监听的socket描述符,addr通常是一个指向sockaddr_in变量的指针,该变量用来存放提出连接请求服务的主机的信息(某台主机从某个端口发出该请求);addrten通常为一个指向值为sizeof(struct sockaddr_in)的整型指针变量。出现错误时accept函数返回-1并置相应的errno值。
首先,当accept函数监视的socket收到连接请求时,socket执行体将建立一个新的socket,执行体将这个新socket和请求连接进程的地址联系起来,收到服务请求的初始socket仍可以继续在以前的 socket上监听,同时可以在新的socket描述符上进行数据传输操作。
数据传输
Send()和recv()这两个函数用于面向连接的socket上进行数据传输。
Send()函数原型为:
int send(int sockfd, const void *msg, int len, int flags);
Sockfd是你想用来传输数据的socket描述符;msg是一个指向要发送数据的指针;Len是以字节为单位的数据的长度;flags一般情况下置为0(关于该参数的用法可参照man手册)。
Send()函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序中应该将send()的返回值与欲发送的字节数进行比较。当send()返回值与len不匹配时,应该对这种情况进行处理。
char *msg = "Hello!";
int len, bytes_sent;
……
len = strlen(msg);
bytes_sent = send(sockfd, msg,len,0);
……
recv()函数原型为:
int recv(int sockfd,void *buf,int len,unsigned int flags);
Sockfd是接受数据的socket描述符;buf 是存放接收数据的缓冲区;len是缓冲的长度。Flags也被置为0。Recv()返回实际上接收的字节数,当出现错误时,返回-1并置相应的errno值。
Sendto()和recvfrom()用于在无连接的数据报socket方式下进行数据传输。由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址。
sendto()函数原型为:
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof (struct sockaddr)。Sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。
Recvfrom()函数原型为:
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
from是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。fromlen常置为sizeof (struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。Recvfrom()函数返回接收到的字节数或当出现错误时返回-1,并置相应的errno。
如果你对数据报socket调用了connect()函数时,你也可以利用send()和recv()进行数据传输,但该socket仍然是数据报socket,并且利用传输层的UDP服务。但在发送或接收数据报时,内核会自动为之加上目地和源地址信息。
结束传输
当所有的数据操作结束以后,你可以调用close()函数来释放该socket,从而停止在该socket上的任何数据操作:
close(sockfd);
你也可以调用shutdown()函数来关闭该socket。该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行。如你可以关闭某socket的写操作而允许继续在该socket上接受数据,直至读入所有数据。
int shutdown(int sockfd,int how);
Sockfd是需要关闭的socket的描述符。参数 how允许为shutdown操作选择以下几种方式:
·0-------不允许继续接收数据
·1-------不允许继续发送数据
·2-------不允许继续发送和接收数据,
·均为允许则调用close ()
shutdown在操作成功时返回0,在出现错误时返回-1并置相应errno。
面向连接的Socket实例
代码实例中的服务器通过socket连接向客户端发送字符串"Hello, you are connected!"。只要在服务器上运行该服务器软件,在客户端运行客户软件,客户端就会收到该字符串。
该服务器软件代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define SERVPORT 3333 /*服务器监听端口号 */
#define BACKLOG 10 /* 最大同时连接请求数 */
main()
{
int sockfd,client_fd; /*sock_fd:监听socket;client_fd:数据传输socket */
struct sockaddr_in my_addr; /* 本机地址信息 */
struct sockaddr_in remote_addr; /* 客户端地址信息 */
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket创建出错!"); exit(1);
}
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(SERVPORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(my_addr.sin_zero),8);
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) \
== -1) {
perror("bind出错!");
exit(1);
}
if (listen(sockfd, BACKLOG) == -1) {
perror("listen出错!");
exit(1);
}
while(1) {
sin_size = sizeof(struct sockaddr_in);
if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, \
&sin_size)) == -1) {
perror("accept出错");
continue;
}
printf("received a connection from %s\n", inet_ntoa(remote_addr.sin_addr));
if (!fork()) { /* 子进程代码段 */
if (send(client_fd, "Hello, you are connected!\n", 26, 0) == -1)
perror("send出错!");
close(client_fd);
exit(0);
}
close(client_fd);
}
}
}
服务器的工作流程是这样的:首先调用socket函数创建一个Socket,然后调用bind函数将其与本机地址以及一个本地端口号绑定,然后调用listen在相应的socket上监听,当accpet接收到一个连接服务请求时,将生成一个新的socket。服务器显示该客户机的IP地址,并通过新的socket向客户端发送字符串"Hello,you are connected!"。最后关闭该socket。
代码实例中的fork()函数生成一个子进程来处理数据传输部分,fork()语句对于子进程返回的值为0。所以包含fork函数的if语句是子进程代码部分,它与if语句后面的父进程代码部分是并发执行的。
客户端程序代码如下:
#include<stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define SERVPORT 3333
#define MAXDATASIZE 100 /*每次最大数据传输量 */
main(int argc, char *argv[]){
int sockfd, recvbytes;
char buf[MAXDATASIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
if (argc < 2) {
fprintf(stderr,"Please enter the server's hostname!\n");
exit(1);
}
if((host=gethostbyname(argv[1]))==NULL) {
herror("gethostbyname出错!");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket创建出错!");
exit(1);
}
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if (connect(sockfd, (struct sockaddr *)&serv_addr, \
sizeof(struct sockaddr)) == -1) {
perror("connect出错!");
exit(1);
}
if ((recvbytes=recv(sockfd, buf, MAXDATASIZE, 0)) ==-1) {
perror("recv出错!");
exit(1);
}
buf[recvbytes] = '\0';
printf("Received: %s",buf);
close(sockfd);
}
客户端程序首先通过服务器域名获得服务器的IP地址,然后创建一个socket,调用connect函数与服务器建立连接,连接成功之后接收从服务器发送过来的数据,最后关闭socket。
函数gethostbyname()是完成域名转换的。由于IP地址难以记忆和读写,所以为了方便,人们常常用域名来表示主机,这就需要进行域名和IP地址的转换。函数原型为:
struct hostent *gethostbyname(const char *name);
函数返回为hosten的结构类型,它的定义如下:
struct hostent {
char *h_name; /* 主机的官方域名 */
char **h_aliases; /* 一个以NULL结尾的主机别名数组 */
int h_addrtype; /* 返回的地址类型,在Internet环境下为AF-INET */
int h_length; /* 地址的字节长度 */
char **h_addr_list; /* 一个以0结尾的数组,包含该主机的所有地址*/
};
#define h_addr h_addr_list[0] /*在h-addr-list中的第一个地址*/
当 gethostname()调用成功时,返回指向struct hosten的指针,当调用失败时返回-1。当调用gethostbyname时,你不能使用perror()函数来输出错误信息,而应该使用herror()函数来输出。
无连接的客户/服务器程序的在原理上和连接的客户/服务器是一样的,两者的区别在于无连接的客户/服务器中的客户一般不需要建立连接,而且在发送接收数据时,需要指定远端机的地址。
阻塞和非阻塞
阻塞函数在完成其指定的任务以前不允许程序调用另一个函数。例如,程序执行一个读数据的函数调用时,在此函数完成读操作以前将不会执行下一程序语句。当服务器运行到accept语句时,而没有客户连接服务请求到来,服务器就会停止在accept语句上等待连接服务请求的到来。这种情况称为阻塞(blocking)。而非阻塞操作则可以立即完成。比如,如果你希望服务器仅仅注意检查是否有客户在等待连接,有就接受连接,否则就继续做其他事情,则可以通过将Socket设置为非阻塞方式来实现。非阻塞socket在没有客户在等待时就使accept调用立即返回。
#include <unistd.h>
#include <fcntl.h>
……
sockfd = socket(AF_INET,SOCK_STREAM,0);
fcntl(sockfd,F_SETFL,O_NONBLOCK);
……
通过设置socket为非阻塞方式,可以实现"轮询"若干Socket。当企图从一个没有数据等待处理的非阻塞Socket读入数据时,函数将立即返回,返回值为-1,并置errno值为EWOULDBLOCK。但是这种"轮询"会使CPU处于忙等待方式,从而降低性能,浪费系统资源。而调用select()会有效地解决这个问题,它允许你把进程本身挂起来,而同时使系统内核监听所要求的一组文件描述符的任何活动,只要确认在任何被监控的文件描述符上出现活动,select()调用将返回指示该文件描述符已准备好的信息,从而实现了为进程选出随机的变化,而不必由进程本身对输入进行测试而浪费CPU开销。Select函数原型为:
int select(int numfds,fd_set *readfds,fd_set *writefds,
fd_set *exceptfds,struct timeval *timeout);
其中readfds、writefds、exceptfds分别是被select()监视的读、写和异常处理的文件描述符集合。如果你希望确定是否可以从标准输入和某个socket描述符读取数据,你只需要将标准输入的文件描述符0和相应的sockdtfd加入到readfds集合中;numfds的值是需要检查的号码最高的文件描述符加1,这个例子中numfds的值应为sockfd+1;当select返回时,readfds将被修改,指示某个文件描述符已经准备被读取,你可以通过FD_ISSSET()来测试。为了实现fd_set中对应的文件描述符的设置、复位和测试,它提供了一组宏:
FD_ZERO(fd_set *set)----清除一个文件描述符集;
FD_SET(int fd,fd_set *set)----将一个文件描述符加入文件描述符集中;
FD_CLR(int fd,fd_set *set)----将一个文件描述符从文件描述符集中清除;
FD_ISSET(int fd,fd_set *set)----试判断是否文件描述符被置位。
Timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout长时间后没有文件描述符准备好即返回。struct timeval数据结构为:
struct timeval {
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
};
POP3客户端实例
下面的代码实例基于POP3的客户协议,与邮件服务器连接并取回指定用户帐号的邮件。与邮件服务器交互的命令存储在字符串数组POPMessage中,程序通过一个do-while循环依次发送这些命令。
#include<stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define POP3SERVPORT 110
#define MAXDATASIZE 4096
main(int argc, char *argv[]){
int sockfd;
struct hostent *host;
struct sockaddr_in serv_addr;
char *POPMessage[]={
"USER userid\r\n",
"PASS password\r\n",
"STAT\r\n",
"LIST\r\n",
"RETR 1\r\n",
"DELE 1\r\n",
"QUIT\r\n",
NULL
};
int iLength;
int iMsg=0;
int iEnd=0;
char buf[MAXDATASIZE];
if((host=gethostbyname("your.server"))==NULL) {
perror("gethostbyname error");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket error");
exit(1);
}
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(POP3SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if (connect(sockfd, (struct sockaddr *)&serv_addr,sizeof(struct sockaddr))==-1){
perror("connect error");
exit(1);
}
do {
send(sockfd,POPMessage[iMsg],strlen(POPMessage[iMsg]),0);
printf("have sent: %s",POPMessage[iMsg]);
iLength=recv(sockfd,buf+iEnd,sizeof(buf)-iEnd,0);
iEnd+=iLength;
buf[iEnd]='\0';
printf("received: %s,%d\n",buf,iMsg);
iMsg++;
} while (POPMessage[iMsg]);
close(sockfd);
}
Struts,MVC 的一种开放源码实现
Struts原理与实践(一)
一、 什么是Struts
框架(Framework)是可重用的,半完成的应用程序,可以用来产生专门的定制程序。
您只要细心地研究真实的应用程序,就会发现程序大致上由两类性质不同的组件组成,一类与程序要处理的具体事务密切相关,我们不妨把它们叫做业务组件;另一类是应用服务。比如说:一个税务征管系统和一个图书管理系统会在处理它们的业务方面存在很大的差异,这些直接处理业务的组件由于业务性质的不同不大可能在不同的系统中重用,而另一些组件如决定程序流向的控制、输入的校验、错误处理及标签库等这些只与程序相关的组件在不同的系统中可以很好地得到重用。人们自然会想要是把这些在不同应用程序中有共性的一些东西抽取出来,做成一个半成品程序,这样的半成品就是所谓的程序框架,再做一个新的东西时就不必白手起家,而是可以在这个基础上开始搭建。实际上,有些大型软件企业选择自己搭建这样的框架。但大多数中小型软件企业或者其他组织,没有条件自己建立框架。
Struts作为一个开放原代码的应用框架,在最近几年得到了飞速的发展,在JSP Web应用开发中应用得非常广泛,有的文献上说它已经成为JSP Web应用框架的事实上的标准。那么,究竟什么是Struts呢?
要回答这个问题还得从JSP Web应用的两种基本的结构模式:Model 1和Model 2说起,为了给读者一些实实在在的帮助,并力图让学习曲线变得平坦一些,我想采用实例驱动的方法来逐步深入地回答有关问题,因为,学一门技术的最好方法莫过于在实践中学习、在实践中体会,逐步加深对其精神实质的理解和把握,而不是一上来就引入一大堆新概念让大家觉得无所适从,或者死记硬背一大堆概念而面对一个真正的实际需求束手无策。正如,一个人即使在书本上学成了游泳博士,只要他不下水,我想他也是不大可能真正会游泳的。
Model 1结构如图1所示:
图1
mode1 1是一个以JSP文件为中心的模式,在这种模式中JSP页面不仅负责表现逻辑,也负责控制逻辑。专业书籍上称之为逻辑耦合在页面中,这种处理方式,对一些规模很小的项目如:一个简单的留言簿,也没什么太大的坏处,实际上,人们开始接触一些对自己来说是新的东西的时候,比如,用JSP访问数据库时,往往喜欢别人能提供一个包含这一切的单个JSP页面,因为这样在一个页面上他就可以把握全局,便于理解。但是,用Model 1模式开发大型时,程序流向由一些互相能够感知的页面决定,当页面很多时要清楚地把握其流向将是很复杂的事情,当您修改一页时可能会影响相关的很多页面,大有牵一发而动全身的感觉,使得程序的修改与维护变得异常困难;还有一个问题就是程序逻辑开发与页面设计纠缠在一起,既不便于分工合作也不利于代码的重用,这样的程序其健壮性和可伸缩性都不好。
Grady Booch等人在UML用户指南一书中,强调建模的重要性时,打了一个制作狗窝、私人住宅、和大厦的形象比喻来说明人们处理不同规模的事物时应该采用的合理方法一样,人们对不同规模的应用程序也应该采用不同的模式。
为了克服Model 1的缺陷,人们引入了Model 2,如图2所示:
图2
它引入了"控制器"这个概念,控制器一般由servlet来担任,客户端的请求不再直接送给一个处理业务逻辑的JSP页面,而是送给这个控制器,再由控制器根据具体的请求调用不同的事务逻辑,并将处理结果返回到合适的页面。因此,这个servlet控制器为应用程序提供了一个进行前-后端处理的中枢。一方面为输入数据的验证、身份认证、日志及实现国际化编程提供了一个合适的切入点;另一方面也提供了将业务逻辑从JSP文件剥离的可能。业务逻辑从JSP页面分离后,JSP文件蜕变成一个单纯完成显示任务的东西,这就是常说的View。而独立出来的事务逻辑变成人们常说的Model,再加上控制器Control本身,就构成了MVC模式。实践证明,MVC模式为大型程序的开发及维护提供了巨大的便利。
其实,MVC开始并不是为Web应用程序提出的模式,传统的MVC要求M将其状态变化通报给V,但由于Web浏览器工作在典型的拉模式而非推模式,很难做到这一点。因此有些人又将用于Web应用的MVC称之为MVC2。正如上面所提到的MVC是一种模式,当然可以有各种不同的具体实现,包括您自己就可以实现一个体现MVC思想的程序框架,Struts就是一种具体实现MVC2的程序框架。它的大致结构如图三所示:
图三
图三基本勾勒出了一个基于Struts的应用程序的结构,从左到右,分别是其表示层(view)、控制层(controller)、和模型层(Model)。其表示层使用Struts标签库构建。来自客户的所有需要通过框架的请求统一由叫ActionServlet的servlet接收(ActionServlet Struts已经为我们写好了,只要您应用没有什么特别的要求,它基本上都能满足您的要求),根据接收的请求参数和Struts配置(struts-config.xml)中ActionMapping,将请求送给合适的Action去处理,解决由谁做的问题,它们共同构成Struts的控制器。Action则是Struts应用中真正干活的组件,开发人员一般都要在这里耗费大量的时间,它解决的是做什么的问题,它通过调用需要的业务组件(模型)来完成应用的业务,业务组件解决的是如何做的问题,并将执行的结果返回一个代表所需的描绘响应的JSP(或Action)的ActionForward对象给ActionServlet以将响应呈现给客户。
过程如图四所示:
图四
这里要特别说明一下的是:就是Action这个类,上面已经说到了它是Struts中真正干活的地方,也是值得我们高度关注的地方。可是,关于它到底是属于控制层还是属于模型层,存在两种不同的意见,一种认为它属于模型层,如:《JSP Web编程指南》;另一些则认为它属于控制层如:《Programming Jakarta Struts》、《Mastering Jakarta Struts》和《Struts Kick Start》等认为它是控制器的一部分,还有其他一些书如《Struts in Action》也建议要避免将业务逻辑放在Action类中,也就是说,图3中Action后的括号中的内容应该从中移出,但实际中确有一些系统将比较简单的且不打算重用的业务逻辑放在Action中,所以在图中还是这样表示。显然,将业务对象从Action分离出来后有利于它的重用,同时也增强了应用程序的健壮性和设计的灵活性。因此,它实际上可以看作是Controller与Model的适配器,如果硬要把它归于那一部分,笔者更倾向于后一种看法,即它是Controller的一部分,换句话说,它不应该包含过多的业务逻辑,而应该只是简单地收集业务方法所需要的数据并传递给业务对象。实际上,它的主要职责是:
|
|
|
|
下面,我们就从一个最简单的登录例子入手,以对Struts的主要部分有一些直观而清晰的认识。这个例子功能非常简单,假设有一个名为lhb的用户,其密码是awave,程序要完成的任务是,呈现一个登录界面给用户,如果用户输入的名称和密码都正确返回一个欢迎页面给用户,否则,就返回登录页面要求用户重新登录并显示相应的出错信息。这个例子在我们讲述Struts的基础部分时会反复用到。之所以选用这个简单的程序作为例子是因为不想让过于复杂的业务逻辑来冲淡我们的主题。
因为Struts是建立在MVC设计模式上的框架,你可以遵从标准的开发步骤来开发你的Struts Web应用程序,这些步骤大致可以描述如下:
1定义并生成所有代表应用程序的用户接口的Views,同时生成这些Views所用到的所有ActionForms并将它们添加到struts-config.xml文件中。
2在ApplicationResource.properties文件中添加必要的MessageResources项目
3生成应用程序的控制器。
4在struts-config.xml文件中定义Views与 Controller的关系。
5生成应用程序所需要的model组件
6编译、运行你的应用程序.
(第2部分)
(第三部分)
一、JDBC的工作原理
Struts在本质上是java程序,要在Struts应用程序中访问数据库,首先,必须搞清楚Java Database Connectivity API(JDBC)的工作原理。正如其名字揭示的,JDBC库提供了一个底层API,用来支持独立于任何特定SQL实现的基本SQL功能。提供数据库访问的基本功能。它是将各种数据库访问的公共概念抽取出来组成的类和接口。JDBC API包括两个包:java.sql(称之为JDBC内核API)和javax.sql(称之为JDBC标准扩展)。它们合在一起,包含了用Java开发数据库应用程序所需的类。这些类或接口主要有:
Java.sql.DriverManager
Java.sql.Driver
Java.sql.Connection
Java.sql.Statement
Java.sql.PreparedStatement
Java.sql.ResultSet等
这使得从Java程序发送SQL语句到数据库变得比较容易,并且适合所有SQL方言。也就是说为一种数据库如Oracle写好了java应用程序后,没有必要再为MS SQL Server再重新写一遍。而是可以针对各种数据库系统都使用同一个java应用程序。这样表述大家可能有些难以接受,我们这里可以打一个比方:联合国开会时,联合国的成员国的与会者(相当我们这里的具体的数据库管理系统)往往都有自己的语言(方言)。大会发言人(相当于我们这里的java应用程序)不可能用各种语言来发言。你只需要使用一种语言(相当于我们这里的JDBC)来发言就行了。那么怎么保证各成员国的与会者都听懂发言呢,这就要依靠同声翻译(相当于我们这里的JDBC驱动程序)。实际上是驱动程序将java程序中的SQL语句翻译成具体的数据库能执行的语句,再交由相应的数据库管理系统去执行。因此,使用JDBC API访问数据库时,我们要针对不同的数据库采用不同的驱动程序,驱动程序实际上是适合特定的数据库JDBC接口的具体实现,它们一般具有如下三种功能:
UTF-8 编码字符理论上可以最多到 6 个字节长, 然而 16 位 BMP 字符最多只用到 3 字节长。
字节 0xFE 和 0xFF 在 UTF-8 编码中从未用到。
通过,UTF-8这种形式,Unicode终于可以广泛的在各种情况下使用了。在讨论struts的国际化编程之前,我们先来看看我们以前在jsp编程中是怎样处理中文问题以及我们经常遇到的:
二、中文字符乱码的原因及解决办法
java的内核是Unicode的,也就是说,在程序处理字符时是用Unicode来表示字符的,但是文件和流的保存方式是使用字节流的。在java的基本数据类型中,char是Unicode的,而byte是字节,因此,在不同的环节java要对字节流和char进行转换。这种转换发生时如果字符集的编码选择不当,就会出现乱码问题。
我们常见的乱码大致有如下几种情形:
1、汉字变成了问号"?"
2、有的汉字显示正确,有的则显示错误
3、显示乱码(有些是汉字但并不是你预期的)
4、读写数据库出现乱码
下面我们逐一对它们出现的原因做一些解释:
首先,我们讨论汉字变成问号的问题。
Java中byte与char相互转换的方法在sun.io包中。其中,byte到char的常用转换方法是:
public static ByteToCharConverter getConverter(String encoding);
为了便于大家理解,我们先来做一个小实验:比如,汉字"你"的GBK编码为0xc4e3,其Unicode编码是\u4f60。我们的实验是这样的,先有一个页面比如名为a_gbk.jsp输入汉字"你",提交给页面b_gbk.jsp。在b_gbk.jsp文件中以某种编码方式得到"你"的字节数组,再将该数组以某种编码方式转换成char,如果得到的char值是0x4f60则转换是正确的。
a_gbk.jsp的代码如下:
第5部分
一个支持i18n的应用程序应该有如下一些特征:
1增加支持的语言时要求不更改程序代码
2字符元素、消息、和图象保存在原代码之外
3依赖于不同文化的数据如:日期时间、小数、及现金符号等数据对用户的语言和地理位置应该有正确的格式
4应用程序能迅速地适应新语言和/或新地区
Struts主要采用两个i18n组件来实现国际化编程:
第一个组件是一个被应用程序控制器管理的消息类,它引用包含地区相关信息串的资源包。第二个组件是一个JSP定制标签,
用Struts实现国际化编程的标准做法是:生成一个java属性文件集。每个文件包含您的应用程序要显示的所有消息的键/值对。
这些文件的命名要遵守如下规则,代表英文消息的文件可作为缺省的文件,它的名称是ApplicationResources.properties;其他语种的文件在文件名中都要带上相应的地区和语言编码串,如代表中文的文件名应为ApplicationResources_zh_CN.properties。并且其他语种的文件与ApplicationResources.properties文件要放在同一目录中。
ApplicationResources.properties文件的键/值都是英文的,而其他语种文件的键是英文的,值则是对应的语言。如在我们前面的登录例子中的键/值对:logon.jsp.prompt.username=Username:在中文文件中就是:logon.jsp.prompt.username=用户名:当然,在实际应用时要把中文转换为AscII码。
有了上一篇文章和以上介绍的一些基础知识后。我们就可以将我们的登录程序进行国际化编程了。
首先,我们所有jsp页面文件的字符集都设置为UTF-8。即在页面文件的开始写如下指令行:
,在我们的登录例子中已经这样做了,这里不需要再改动。
其次,将所有的request的字符集也设置为UTF-8。虽然,我们可以在每个文件中加入这样的句子:request.setCharacterEncoding("UTF-8");来解决,但这样显得很麻烦。一种更简单的解决方法是使用filter。具体步骤如下:
在mystruts\WEB-INF\classes目录下再新建一个名为filters的目录,新建一个名为:SetCharacterEncodingFilter的类,并保存在该目录下。其实,这个类并不要您亲自来写,可以借用tomcat中的例子。现将该例子的程序节选如下:
|
① 节的代码是引用一个服务器边的验证器,其对应的代码清单如下:
|
② 节是验证失败后的出错信息,要将对应这些键值的信息写入到ApplicationResources.properity文件中,常见的错误信息如下:
|
③ 节的代码用于客户边的JavaScript验证
其次,在validation.xml文件中配置要验证的form极其相应的字段,下面是该文件中的代码:
|
这里要注意的是:该文中的
再次,为了使服务器边的验证能够进行,将用到的formBean从ActionForm的子类改为ValidatorForm的子类,即:
将public class UserInfoForm extends ActionForm改为:public class UserInfoForm extends ValidatorForm
到此,进行服务器边的验证工作已经一切准备得差不多了,此时,只要完成最后步骤就可以实验服务器边的验证了。但大多数情况下,人们总希望把这些基本的简单验证放在客户边进行。
为了能进行客户边的验证,我们还要对logon.jsp文件做适当的修改。
将
|
改为
|
在标签后加上:
|
最后,对struts的配置文件struts-config.xml作适当的修改:
1、将
|
改为
|
其作用是要求进行校验
2、将下列代码放在struts-config.xml文件中的标签前。其作用是将用于校验的各个组件结合在一起。
|
到此为止,我们的一切工作准备就绪,您可以享受自己的劳动成果了,试着输入各种组合的用户名和口令,看看它们的验证效果。仔细体会你会发现,服务器边的验证要更全面一些,比如对password的字符长度的验证。
参考文献:
《Struts in Action》Ted Husted Cedric Dumoulin George Franciscus David Winterfeldt著
《Programming Jakarta Struts》Chuck Cavaness著
第7部分
上一篇文章中介绍校验时提到客户边的校验用到了JavaScript,实际上用Struts配合JavaScript还可以实现许多有用的功能,比如,级联下拉菜单的实现就是一个典型的例子:
本例假设要实现的是一个文章发布系统,我们要发布的文章分为新闻类和技术类,其中新闻类又分为时事新闻和行业动态;技术类又分为操作系统、数据库、和编程语言等,为了便于添加新的条目,所有这些都保存在数据库表中。
为此,我们建立一个名为articleClass的表和一个名为articleSubClass的表。
|
|
|
|
|
|
|
|
|
|
|
|
其中,request.setCharacterEncoding(encoding);是一个关键句子。
为了让该类工作,我们还要在web.xml文件中对它进行配置,配置代码如下:
|
最后,就是准备资源包文件,我们以创建一个中文文件为例:
将ApplicationResources.properties文件打开,另存为ApplicationResources_zh.properties,这只是一个过渡性质的文件。将文件中键/值对的值都用中文表示。更改完后的代码如下:
|
使用native2ascii工具将上面文件中的中文字符转换为ascii码,并生成一个最终使用的资源文件ApplicationResources_zh_CN.properties。
具体做法是打开一个dos窗口,到mystruts\WEB-INF\classes目录下,运行如下语句:
native2ascii -encoding GBK ApplicationResources_zh.properties ApplicationResources_zh_CN.properties
生成的文件ApplicationResources_zh_CN.properties的内容如下:
|
从这里可以看出,所有的中文字都转换成了对应的Unicode码。
现在,再运行登录例子程序,您会发现它已经是显示的中文了。在浏览器的"工具"--"Internet选项"的"语言首选项"对话框中,去掉"中文(中国)"加上英文,再试登录程序,此时,又会显示英文。这就是说不同国家(地区)的客户都可以看到自己语言的内容,这就实现了国际化编程的基本要求。如果还要显示其他语言,可采用类似处理中文的方法进行,这里就不细讲了。
本文中的例子程序所采用的数据库仍然是MS SQLServer2000,数据库字符集为gbk。实验表明,对简、繁体中文,英文及日文字符都能支持。
参考文献:
《Programming Jakarta Struts》Chuck Cavaness著
《Mastering Jakarta Struts》James Goodwill著
第6部分
本文我们来讨论一下Struts中的输入校验问题。我们知道,信息系统有垃圾进垃圾出的特点,为了避免垃圾数据的输入,对输入进行校验是任何信息系统都要面对的问题。在传统的编程实践中,我们往往在需要进行校验的地方分别对它们进行校验,而实际上需要校验的东西大多都很类似,如必需的字段、日期、范围等等。因此,应用程序中往往到处充斥着这样一些显得冗余的代码。而与此形成鲜明对照的是Struts采用Validator框架(Validator框架现在是Jakarta Commons项目的一部分)来解决校验问题,它将校验规则代码集中到外部的且对具体的应用程序中立的.xml文件中,这样,就将那些到处出现的校验逻辑从应用程序中分离出来,任何一个Struts应用都可以使用这个文件,同时还为校验规则的扩展提供了便利。更难能可贵的是由于Validator框架将校验中要用到的一些消息等信息与资源绑定有机结合在一起,使得校验部分的国际化编程变得十分的便捷和自然。
Validator框架大致有如下几个主要组件:
Validators:是Validator框架调用的一个Java类,它处理那些基本的通用的校验,包括required、mask(匹配正则表达式)、最小长度、最大长度、范围、日期等
.xml配置文件:主要包括两个配置文件,一个是validator-rules.xml,另一个是validation.xml。前者的内容主要包含一些校验规则,后者则包含需要校验的一些form及其组件的集合。
资源绑定:提供(本地化)标签和消息,缺省地共享struts的资源绑定。即校验所用到的一些标签与消息都写在ApplicationResources.properity文件中。
Jsp tag:为给定的form或者action path生成JavaScript validations。
ValidatorForm:它是ActionForm的一个子类。
为了对Validator框架有一个比较直观的认识,我们还是以前面的登陆例子的输入来示范一下Validator框架的使用过程:
首先,找一个validator-rules.xml文件放在mystruts\WEB-INF目录下,下面是该文件中涉及到的required验证部分代码的清单:
|
b_gbk.jsp的代码如下:
|
在浏览器中打开a_gbk.jsp并输入一个"你"字,点击OK按钮提交表单,则会出现如图1所示的结果:
图1
从图1可以看出,在b_gbk.jsp中这样将byte转换为char是正确的,即得到的char是\u4f60。这里要注意的是:byte b[]=a.getBytes("ISO8859-1");中的编码是ISO8859-1,这就是我们前面提到的有些web容器在您没有指定request的字符集时它就采用缺省的ISO8859-1。
从图1中我们还看到表达式中的a并没有正确地显示"你"而是变成"??"这是什么原因呢?这里的a是作为一个String被显示的,我们来看看我们常用的String构造函数:
String(byte[] bytes,String encoding);
在国标平台上,该函数会认为bytes是按GBK编码的,如果后一个参数省略,它也会认为是encoding是GBK。
对前一个参数就相当于将b_gbk.jsp文件的这句byte b[]=a.getBytes("ISO8859-1");中的ISO8859-1改为GBK,这样显然在GBK字符集中找不到相应的目的编码,它给出的结果是0x3f、0x3f。因此,就会显示为"??",这也就是造成乱码的第一种现象的原因。我们的例子是演示的从byte到char的转换过程,相反的过程也会造成同样的问题,限于篇幅,就不在此讨论了,大家自己可以做类似的实验来验证。
解决该问题的方法就是象例子中a1那样,在获取byte数组时,指定编码为ISO8859-1。
接下来,我们讨论有些汉字能正常显示,有些不能正常显示的问题。
如果我们将String a1=new String(a.getBytes("ISO8859-1"),"GBK");中的GBK改为GB2312则象朱镕基的"镕"字就不能正常显示,这是因为该字是GBK中的字符而在GB2312中不存在。
解决上述两种问题的方法就是象a1那样构造String,也就是人们常说的同时也是常用的转码的方法。采用这种方法会在程序中到处出现这种语句,特别是在Struts中,Struts有一个回写表单的功能,在回写时也要做这种转换,这样的语句差不多要多一倍。因此,这是个比较笨拙的方法,有没有简捷一些的方法呢?其实是有的,只要在取得request的字符串前加上request.setCharacterEncoding("GBK");这句,指定request的字符集。则中的a就能正常显示,a1反而不能正常显示。此时要将byte b[]=a.getBytes("ISO8859-1");中的ISO8859-1变成GBK,从byte到char的转换才是正确的,这就是此时a能正常显示而a1反而不能正常显示的原因。如果此时要a1正常显示则必须将String a1=new String(a.getBytes("ISO8859-1"),"GBK");中的ISO8859-1改为GBK。
很显然,使用request.setCharacterEncoding("GBK");只能解决GBK字符问题,要解决i18n问题则要使用UTF-8来取代GBK。我们接着做上述实验,将a_gbk.jsp和b_gbk.jsp分别另存为a.jsp和b.jsp将文件中的GBK改为UTF-8,更改后的代码分别如下:
a.jsp代码:
|
再在a.jsp中输入"你"字,你会发现显示结果中,一个汉字是用三个byte表示的,它们的值分别是0xe4、0xbd、0xa0,也就是说用UTF-8来表示汉字,每个汉字要比GBK多占用一个byte,这也是使用UTF-8要多付出的一点代价吧。
现在,我们讨论一下第三个问题,即显示乱码,有些莫名其妙的汉字并不是你预期的结果。
在上例中将String a1=new String(a.getBytes("UTF-8"),"UTF-8");改为String a1=new String(a.getBytes("UTF-8"),"GBK");再输入"你"字,则a1会显示成"浣?",您只要看一看"浣"的UTF-8码和GBK码就会知道其中的奥秘了。
下面,我们讨论一下最后一个问题,就是读写数据库时出现乱码。
现在一些常用的数据库都支持数据库encoding,也就是说在创建数据库时可以指定它自己的字符集设置,数据库数据以指定的编码形式存储。当应用程序访问数据库时,在入口和出口处都会有encoding转换。如果,在应用程序中字符本来已变成了乱码,当然也就无法正确地转换为数据库的字符集了。数据库的encoding可根据需要来设置,比如要支持简、繁体中文、日、韩、英语选GBK,如果还要支持其他语言最好选UTF-8。
本篇文章对字符集及中文乱码问题做了一下探讨,为实现国际化编程的实践打下一个基础。下一篇文章,我们将介绍struts中实现国际化编程的具体步骤,并将我们前面介绍的登录例子进行国际化。
下面,我们就一步步按照上面所说的步骤来完成我们的应用程序:
第一步,我们的应用程序的Views部分包含两个.jsp页面:一个是登录页面logon.jsp,另一个是用户登录成功后的用户功能页main.jsp,暂时这个页面只是个简单的欢迎页面。
其中,logon.jsp的代码清单如下:
|
main.jsp的代码清单如下:
|
首先,我们看一下logon.jsp文件,会发现它有这么两个鲜明的特点:一是文件头部有诸如:
这样的指令代码,他们的作用就是指示页面要用到struts的自定义标签,标签库uri是一个逻辑引用,标签库的描述符(tld)的位置在web.xml文件中给出,见上篇文章的配置部分。struts的标签库主要由四组标签组成,它们分别是:
参考文献:
UTF-8 and Unicode FAQ
《JSP动态网站技术入门与提高》太阳工作室 孙晓龙 赵莉编著
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
用WMI实现Windows系统自动管理(多图)
与以前的操作系统相比,Windows 2K/XP的优点之一是具有更好的可管理性。例如它支持Windows 2000服务器终端服务下的远程管理模式,支持Microsoft管理控制台(MMC),再有一个就是支持WMI。WMI是Windows Management Instrumentation的缩写,即Windows管理规范。WMI有许多用途,其中之一就是通过脚本编程实现管理自动化。
一、为什么要用脚本?
回顾九十年代,Windows NT之所以获得成功,原因之一就在于相对而言这个操作系统比较容易使用,任何能够使用Windows 3.1的用户差不多就可以管理一个简单的NT网络(这在今天听起来有点不可思议,但事实是,NT 3.1比Windows 2K/XP功能少得多,因而简单得多)。只要看看NT的控制面板,这个操作系统可以做些什么就大致有个印象了。
随着操作系统的发展,原来友好的界面变得更加友好。在Windows 2K/XP中,几乎每一个操作过程都有向导,每一个操作系统级的对象都有图形化的属性页;不同的操作选择引导你到达最终完成任务的对话框,MMC允许你把常用的工具(甚至是第三方的工具)插入到定制的工具集。
然而,高级用户感到友好的GUI实在太繁琐了。另外,尽管在命令行上也可以执行某些任务,但命令行没有图形工具那样完备的功能。为了让Windows 2K/XP下的操作任务自动化,一种较好的途径是通过脚本程序直接访问图形化工具访问的管理接口。WMI允许用户通过一个统一的接口,用脚本语言访问操作系统的几乎任意一个部分。当然,用脚本对WMI编程也有一些限制,例如不能直接访问Win32 API。
二、WMI是什么?
WMI是Windows 2K/XP管理系统的核心;对于其他的Win32操作系统,WMI是一个有用的插件。WMI以CIMOM为基础,CIMOM即公共信息模型对象管理器(Common Information Model Object Manager),是一个描述操作系统构成单元的对象数据库,为MMC和脚本程序提供了一个访问操作系统构成单元的公共接口。有了WMI,工具软件和脚本程序访问操作系统的不同部分时不需要使用不同的API;相反,操作系统的不同部分都可以插入WMI,如图一所示(该图来自MSDN),工具软件和WMI可以方便地读写WMI。

图一
Windows 2K/XP和Windows 98都支持WMI;如果为NT 4.0和Windows 95加上了Service Pack 4或更高版本,NT 4.0和Win95也支持WMI。因此,用WMI进行远程管理时,并非一定要用Windows 2K/XP(当然,如果WMI脚本在一台没有性能监视器的Win9x机器上运行,就不能在远程Win9x系统上查询Windows 2K/XP的性能监视器。
如前所述,WMI允许通过一个公共的接口访问多种操作系统构成单元,因此不必分别对待各种底层接口或所谓的“提供者”。利用WMI可以高效地管理远程和本地的计算机;与此相对,并非所有的Windows 2K/XP命令行工具都支持远程运行。
WMI是WBEM模型的一种实现。WBEM即Web-Based Enterprise Management,或基于Web的企业管理,WBEM由DMTF(Distributed Management Task Force,分布式管理任务组)在许多厂商的帮助下创立,包括Compaq、Sun、Microsoft等。WBEM的目标是,为管理企业环境开发一个标准的接口集。WBEM模型最关键的部分是它的数据模型(或描述和定义对象的方式)、编码规范(Encoding Specification),以及在客户端和服务器端之间传输数据的模式。
WBEM的数据模型是CIM(Common Information Model,公共信息模型)。CIM是一个用来命名计算机的物理和逻辑单元的标准的命名系统(或称为命名模式),例如硬盘的逻辑分区、正在运行的应用的一个实例,或者一条电缆。
CIM是一个面向对象的模型,使用一组面向对象的术语进行描述。CIM包含类(Class),类是被管理单元的模板。类的实例称为对象(Object),对象代表着底层系统的一个具体单元。名称空间(Namespace)是一个类的集合,每个名称空间面向一个特定的管理领域。类包含属性(Property)和方法(Method)。
CIM分三层。第一层是核心模型(Core Model),这一层包含的类定义对于所有管理领域来说都是共同的。第二层是公共模型(Common Model),这一层包含的类定义对于特定的管理领域来说是公共的,但与具体的操作系统和系统设计无关。第三层是扩展模型(Extension model),这一层包含的类定义与特定的操作系统或技术有关。
WMI是Microsoft扩展CIM 2.0得到的面向Win32系统的扩展模型。引用WMI类和属性的形式是“扩展前缀_类名称.属性名称”,例如Win32_ComputerSystem.Name,其中Win32是CIM模式cimv2名称空间内WMI扩展类的前缀,ComputerSystem是类,Name是属性。
编写WMI脚本的很大一部分工作涉及到读取和设置属性值。当前,WMI提供的方法还很有限,但随着时间的推移,相信WMI和CIM提供的方法都会越来越丰富。
三、WMI软件开发包
利用WMI软件开发包(SDK)可以方便地查看可用的CIM和Win32类。WMI SDK可以从http://msdn.microsoft.com/downloads/default.asp?URL=/code/sample.asp?url=/msdn-files/027/001/566/msdncompositedoc.xml下载,有8M多,可谓不小。
WMI SDK要求操作系统必须是Windows 2K/XP或者NT 4.0 SP4或更高版本;尽管Win9x系统上可以安装WMI支持软件,但SDK不能在Win9x上运行。另外,为支持SDK的ActiveX控件,SDK还要求有IE 5.0或更高版本。SDK对机器性能的最低要求是:Pentium处理器,32 Mb的RAM,40 Mb的磁盘空间,以及至少要有800 x 600、256色的显示设备。对于运行Windows 2K/XP的机器来说,这些要求应该不会成为问题。CIMOM默认以服务的形式运行,但如果机器没有网卡,CIMOM不能作为服务运行,不过此时可以作为一个应用运行,只需执行winmgmt.exe即可。winmgmt.exe在%systemroot%\system32\wbem的WMI主目录下。
SDK必须由管理员组的成员安装。安装过程很简单,执行WMISdk.exe启动向导,指定安装的目标目录(默认是\program files\wmi)。选择要安装的可选组件(默认安装除了SNMP支持以外的所有组件),最后点击Finish。安装SDK不需要重新启动。安装完成后,“开始/程序”菜单上会增加一个WMI SDK组。
点击WMI SDK程序组的WMI CIM Studio。CIM Studio提示连接名称空间,并显示默认连接的名称空间是root\cimv2,确认即可。如果你用Administrator身分登录Windows,再次点击确定以当前身份登录;如果你用其他的身份登录Windows,请改用Administrator登录。
现在,假设我们要在当前的机器上查找一个对象:C:驱动器。我们不知道C:驱动器在CIM或WMI中的具体名称,浏览CIM Studio列出的数百个类又太麻烦,怎么办呢?可以使用Find按钮(左边上方的望远镜,参见图三)。图二显示了点击Find按钮后显示的Search for Class对话框,在这里输入我们猜想C:驱动器的类名称中应当包含的单词,然后点击Go!按钮。由于我们正在寻找一个命名的磁盘分区,而且我们知道Windows把这种分区叫做logical disk或logical drive,因此这里的搜索关键词可以是logical。当然,搜索关键词也可以是disk,但这时会有大量的搜索结果出现。

图二
图二显示了搜索关键词logical得到的结果。选择Win32_LogicalDisk并点击OK,图三的窗口出现(为什么不选择CIM_LogicalDisk?前面已经提到,WMI管理的所有对象都带有Win32前缀。如果选择CIM_LogicalDisk然后要求显示出它的实例,不可能看到可用逻辑驱动器的任何具体信息,只能看到对应每一个可用逻辑驱动器的Win32_LogicalDisk条目)。现在,窗口的右边显示出Win32_logicalDisk类的属性。可以看到,属性的值都为空,这是因为我们正在查看的是一个类,而不是类的具体实例。要显示出Win32_LogicalDisk类的实例,点击右边上面的Instances按钮(右数第四)。

图三
点击Instances按钮之后,窗口显示出当前机器上所有逻辑驱动器的实例,包括网络逻辑驱动器。点击设备ID为“C:”的实例,显示出图四的结果。右边窗格包含了当前实例的属性和方法,当前逻辑驱动器的名称显示在右边窗格的上方。

图四
利用脚本可以修改这些属性或调用这些方法。如果对某个属性的含义不太清楚,只需选择Win32_LogialDisk类或Win32_LogicalDisk.DeviceID="C:"实例,再点击Help按钮。大多数对象的属性和方法都有详细的说明。
四、脚本编程初步
前面我们通过SDK查看了Win32_LogicalDisk类和它的属性,下面来看看如何在脚本中访问这些信息。如果你的系统上安装了Microsoft Windows 2000 Resource Kit,\program files\resource kit文件夹下默认会有一个listfreespace.vbs脚本。这个脚本查询Win32_LogicalDisk类的各个对象,分别提取一组属性值:DeviceID,即驱动器标识符;FreeSpace,驱动器空闲空间的字节数。假设一台机器的A:是软盘驱动器,D:是CD-ROM驱动器,listfreespace.vbs的输出类如:

下面显示了getfree.vbs脚本程序的代码,它用更少的代码获取类似的信息。启动getfree.vbs时要指定驱动器标识符,getfree.vbs将显示出驱动器空闲空间和文件系统类型。

在Windows命令行窗口中,执行“Wscript getfree.vbs c:”将显示出类如图五的结果。如果执行“Cscript getfree.vbs c:”,则提示信息以字符方式显示。

图五
又如,下面的VBScript脚本提示输入远程机器的名字,然后关闭指定的远程机器:

当然,如果只有本文的知识,你还不能算是一个WMI脚本编程的高手。但现在你已经了解了如何用SDK查询信息,如何通过脚本访问对象的属性和方法。继续努力吧!
pq magic的出错代码
Window 消息大全使用详解
| Window 消息大全使用详解(转载) | |
| 作者:unknown 更新时间: 2005-05-06 | |
|