2013年12月 ’ 的文章存档

绝对不容错过的全球开发人员搞怪代码注释集锦

bbb

 

相信每一个编程极客都知道什么是注释,也都知道如何在代码中添加注释,今天这篇文章中,我们将不会讨论如何添加注释,或者如何添加一个完美的注释,在今天的文章里,我们将给大家奉献一场来自全球开发人员的注释盛宴,看看大家是怎么在代码中添加自己富有想象力的注释吧,绝对会让你乐此不彼!

当然,如果你也有很多超有趣的注释,请留言和我们分享!我们的口号是:

“快乐编程,娱乐注释” !!!

注重语法的注释

return 1; # returns 1

 

来自绝对菜鸟的注释

// I am not sure if we need this, but too scared to delete.
...
...

 中文:个人不确认是不是需要,但是实在不敢删除

来自正直程序员的注释

// I am not responsible of this code.
// They made me write it, against my will.

 中文:个人不负责这块的质量,因为他们逼迫我违心的写了这段代码

来自电影的注释

options.BatchSize = 300; //Madness? THIS IS SPARTA!

 中文:疯了吧?这是斯巴达!

绝对尽责的注释

i++; // increase i by 1

 中文:给变量i增加一个记数

绝对会被忽略的注释

Catch (Exception e) {
//who cares?
}

  中文:谁在意?

绝对不能信任注释

/**
* Always returns true.
*/
public boolean isAvailable() {
return false;
}

中文:返回为true (编辑:永远不能相信注释)

典型的遗留系统代码里的注释

/*
* You may think you know what the following code does.
* But you dont. Trust me.
* Fiddle with it, and youll spend many a sleepless
* night cursing the moment you thought youd be clever
* enough to "optimize" the code below.
* Now close this file and go play with something else.
*/

 中文:你可能相信你能看懂以下代码,但是其实绝对不可能,相信我。一旦你调试了,你绝对会后悔装聪明去尝试优化这段代码。最好的方式是关闭文件,去玩点儿你喜欢的东西吧

Java程序里经常能看到的“典型”注释

try {

} finally { // should never happen

}

 中文:绝对不会运行到这里

超级有自知之明的代码注释

//This code sucks, you know it and I know it.
//Move on and call me an idiot later.

 中文:这段代码的确很挫,我知道你也知道,先不要骂我2B了,请先继续往下看

绝对有年头的注释

long long ago; /* in a galaxy far far away */

 中文:在很远很远的银河系外 (编辑:这段代码能运行,绝对是个奇迹)

“情色”代码让我如何注释为好

double penetration; // ouch

 中文:我擦!(编辑:如果你不熟悉英文,double penetration 绝对无法理解,但是如果你熟悉AV,了解情色,应该知道什么体位是“双管齐下”吧! 嘿嘿,有点儿淘气了,请大家不要见怪)

绝对无法挑剔的注释

/////////////////////////////////////// this is a well commented line

 中文:这注释绝对完全没有问题

保证正确体位的注释

// I don't know why I need this, but it stops the people being upside-down
x = -x;

 中文:我也不知道为什么需要这个,但是这个能保持大家不会倒立

来自Java1.2 SwingUtilities的注释

doRun.run(); // ... "a doo run run".

 

最好的帮助你了解递归的注释

# To understand recursion, see the bottom of this file

At the bottom of the file:

# To understand recursion, see the top of this file

 

中文:

#如果想了解递归,请看最下面的注释

...

#如果想了解递归,请看最上面的注释

绝对督促你工作的注释

/* Please work */

 

绝对菜鸟注释

//I am not sure why this works but it fixes the problem.

  中文:不知道为什么,但是的确解决了这个问题

希望这些有趣的注释能够博君一笑,如果大家有兴趣继续欢乐的话,请径直前往StackOverflow阅读:你所遇到的最棒的注释,相信你会欢乐一天滴!

译文来自:GBin1.com

最牛B的编码套路(译)

译者:happydeer          来源:@豆巴陆其明

最近,我大量阅读了Steve Yegge的文章。其中有一篇叫“Practicing Programming”(练习编程),写成于2005年,读后令我惊讶不已:

与你所相信的恰恰相反,单纯地每天埋头于工作并不能算是真正意义上的锻炼——参加会议并不能锻炼你的人际交往能力;回复邮件并不能提高你的打字水平。你必须定期留出时间,集中锻炼,这样才能把事情做得更好。

我认识很多杰出的程序员——这是在亚马逊工作最好的额外“福利”之一。如果仔细观察他们,你会发现他们时时都在锻炼。他们已经很优秀了,但他们仍然不忘锻炼。他们锻炼的方法林林总总,而我在这篇文章中只会介绍其中的几种。

据我了解,这些杰出程序员之所以如此成功,就是因为他们一直在锻炼。完美的身材要靠定期的锻炼才能获得,而且必须坚持锻炼才能保持,否则身材就会走形。对于编程和软件工程来说,道理是一样的。

这是一个重要的区别——我每天都开车去上班,但我的驾驶水平远远不如专业车手;类似的情况,天天编程可能并不足以使你成为一名专业的程序员。那么,什么才能把一个普通人变成一名专业车手或者专业程序员呢?你需要锻炼什么呢?

答案就在《科学美国人》的一篇名为“The Expert Mind”(专家思维)的文章里:

爱立信提出,重要的并不是经验本身,而是“努力的学习”,也就是要不断地挑战自身能力之外的东西。一些狂热的爱好者花费了大量的时间去下棋、打高尔夫球或者玩乐器,但他们可能始终停留在业余水平上,而一个训练有素的学生却可以在相对较短的时间里超越他们,原因就在这里。值得注意的是,在提高水平方面,花费在下棋上的大量时间(即使参加各种比赛)似乎还是比不过专门的训练来得更为有效。训练的主要价值在于发现弱点,并有针对性地进行提高。

“努力的学习”意味着,要常常去处理那些刚好在你能力极限上的问题,也就是那些对你来说有很大可能失败的事情。如果不经历一些失败的话,你可能就不会成长。你必须不断地挑战自我,超越自己的极限。

那样的挑战有时会在工作中碰到,但也未必。将锻炼从职业工作中分离出来,这在编程领域常被人称为“编码套路”(Code Kata)。

Code Kata的概念是由David Thomas提出的,他是《程序员修炼之道:从小工到专家》的作者之一。这个概念主要指的是,针对某一种特定技术或技能进行重复性的练习,从而将其熟练掌握。——译者注

1

所谓套路,就是一系列的招式。这个概念借鉴于武术。

如果你想要看一些编码套路的例子(也就是努力学习和磨练编程技能的方法),SteveYegge的文章里倒是提出了一些不错的建议。他把它们称作为“实践演练”:

1.     写一份自己的简历。把自己所有的相关技能都罗列出来,然后把那些在100年后还用得到的标出来。给每个技能打分,满分为10分。

2.     罗列出你所景仰的程序员。尽量包括那些与你一起工作的人,因为你会在工作中从他们身上获取一些技能。记录下他们身上的1 ~ 2个闪光点,也就是你希望自己有所提高的方面。

3.     去查看维基百科(Wikipedia.Org)上“计算机科学”栏目,找到“计算机科学的卓越先驱”这部分,从这个列表中挑选一个人,阅读他的事迹,并且在阅读时打开任何你感兴趣的链接。

4.     花20分钟通读别人的代码。读出色的代码和读糟糕的代码都是有益的,两者都要读,轮流切换。如果你无法感觉出它们之间的区别,可以求助于一位你尊敬的程序员,让他给你展示一下什么是出色的代码、什么是糟糕的代码。把你读过的代码给别人也看看,问问他们的看法。

5.     罗列出你最喜欢的10个编程工具——那些你觉得你用得最多、非有不行的工具。随机挑选其中的一个工具,花一个小时去阅读它的文档。在这一个小时里,努力去学习这个工具的某个你不曾意识到的新功能,或者发现某种新的使用方法。

6.     想一想,除了编程之外你最擅长什么事情?再想一想,你是通过怎样的锻炼才变得如此熟练和专业的?这对于你的编程工作又有什么启发呢?(怎么把这些经验应用到编程方面?)

7.     拿出一叠简历,并和一组面试官在同一个房间里待上一个小时。确保每份简历都至少被3个面试官看过,并且要给出1 ~ 3分的评分。针对那些不同面试官评判大相径庭的简历展开讨论。

8.     参与一个电话面试。事后写下你的反馈,抛出你的观点,然后与主持电话面试的人聊一聊,看看你们是否达成了一致的结论。

9.     进行一次技术面试,并且被面试的人应该是某个你不太了解的领域里的专家。让他假定听众在该领域里一无所知,因此请他从最基础的讲起。努力去理解他所说的,必要时问一些问题。

10.   有机会参与别人的技术面试。期间,你只是认真地听、认真地学。在应聘者努力解决技术问题的同时,你也要在自己脑子里尝试解决这些问题。

11.   找到一个能和你交换实际问题的人,每隔一周,相互交流编程问题。花10 ~ 15分钟来尝试解决这些问题,再用10 ~ 15分钟进行讨论(无论能否解决)。

12.   当你听到任何你一时之间也无法解决的面试问题时,赶紧回到你的座位上,把这个问题用电子邮件发给自己,以留作日后的提醒。在那一周里找出点时间,用自己最喜欢的编程语言来解决它。

我之所以喜欢Steve开出的这个清单,是因为它看上去很全面。有些程序员一想到“锻炼”,总认为就是一些编码上的难题。但在我看来,编程更在于人,而不是代码。因此,通过解决世上所有的、并且晦涩的编程面试题目,在提高你的个人能力方面,这种方法是有局限的。

关于“努力的学习”,我也很喜欢Peter Norvig在“Teach Yourself Programming in TenYears”(花10年时间自学编程)一文中提出的诸多建议:

1. 与别的程序员交流。读别人的代码。这比任何书籍或培训课程都更重要。

2. 动手写程序!最好的学习方法就是边做边学。

3. 在本科或研究生的课程中学习编程课程。

4. 找一些项目来做,并且需要与其他程序员形成团队来合作。在项目的进行过程中,学会辨别最出色的程序员以及最糟糕的程序员。

5. 在项目中跟随别的程序员一起工作,了解如何维护那些不是你写的代码,并且学习如何写出利于他人维护的代码。

6. 学习多种不同的编程语言,特别是那些与你现在所熟悉的语言有着不同的世界观和编程模型的。

7. 了解硬件对软件的影响。知道你的电脑执行一条指令需要多少时间,从内存中取出一个字(在有缓存或没缓存的情况下)需要多少时间,在以太网(或者因特网)上传输数据需要多少时间,从磁盘中读取连续的数据或者在磁盘上跳转到另一个位置需要多少时间,等等。

你还可以从Dave Thomas的21种实用的编码套路中获取灵感(CodeKata.com),或者你更愿意加入一个你家当地的“编程武馆”(CodingDojo.org)。

对于“努力的学习”,我无法像Steve,Peter或者Dave那样提供一个长长的建议列表。我远不如他们有耐心。实际上,在我看来,“编程套路”只需两个招式:

1.      写博客。我在2004年初创办了CodingHorror.com博客,作为我自己努力学习的一种形式。它在一开始很不起眼,到后来成为我职业生涯中做过的最重要的一件事。所以,你也应该写博客。最后“闻达于天下”的人,往往就是那些能够有效书写和沟通的人。他们的声音最响亮,是他们在制定游戏规则,并且引领世界的潮流。

2.      积极参与著名的开源项目。所有的高谈阔论听起来都很好,但是,你是一个大话王还是一名实干家呢?别光说不练,这个非常重要,因为人们会用你的行动来衡量你,而不是你的言论。努力在公众面前留下些实实在在有用的东西吧,到时候你就可以说,“我在那个项目中出过力。”

当你能编写精彩的代码、并且能用精彩的言辞向世人解释那些代码时,到那时候,我会觉得你已经掌握了最牛的编码套路!

苦逼的程序员:被诅咒的七宗罪

点评:作为一名程序员,除了要应付颐指气使的老板,还要搞定时不时出现的各种难题。一不小心就落入魔鬼的陷阱:容易受到诱惑、贪多、固步自封、懒惰、脾气暴躁……

七宗罪(Seven deadly sins),13世纪道明会神父圣多玛斯•阿奎纳列举出各种恶行的表现。这些恶行最初是由希腊神学修道士庞义伐草撰出8种损害个人灵性的恶行,分别是贪食、色欲、贪婪、暴怒、懒惰、伤悲、自负及傲慢。

3

 

程序员生来不平等。有的伟大。有的渴望伟大。

下面是一些程序员经常会走入的歧途。听起来很恐怖,但享用吧。上帝就在你身边,警惕这些危险的信号,跟随主救赎的指引。

1. 色欲(Lust)

凡犯色欲者:在硫磺和火焰中熏闷

a1

作为程序员,这种罪恶的表现是不断的受绚丽的新事物的诱惑。下一代编程语言,最新的框架,最新的平台。

我们程序员天生好奇。我们受惑于追求高效,坚信所有的东西都要经过优化。只有用了那种最新的语言,我们才能工作。

虽然不断的追求改进是非常值得赞赏,但采用新事物也是有代价的。有避免不了的学习曲线。有适应问题。有未知的依赖问题。有未知的未知问题。

清除这些杂念。专心解决你手头上的问题。充分利用你知道的,停止贪恋那些光鲜新事物。

2. 贪食(Gluttony)

凡犯贪食者:强迫进食老鼠,蟾蜍和蛇

a2

这是过度之罪。过度的企图多做,过度的扩展深度和广度。

不必要的功能特征溜进了产品里。大量无用的代码被生产出来。宝贵的编程时间被消耗,被浪费。

这些行为增加了不必要的复杂度,带来的高昂的维护代价。通常导致的结果是,预期不能完工,bug层出不穷。

警惕那些不该有的功能、警惕那些对不必要的复杂架构的伪辩护、警惕过早优化的迹象。让产品简洁。

3. 贪婪(Greed)

凡犯贪婪者:在油中煎熬

a3

过度专业化和功能化会导致形成个人的领地。固步自封。我的代码。我的模块。我相干的区域。没有分享。没有合作。

一种不健康的对这些人的依赖会逐渐形成。所谓的“编程教父”、“编程巨星”和“编程领袖”就代表了这些趋势。

相反,应该建立一个崇尚代码集体所有和充分合作(比如结对编程或相互代码审查)的文化。

4. 怠惰(Sloth)

凡犯懒惰者:丢入蛇坑

a4

根据Perl语言的创造者Larry Wall的话,懒惰是程序员的三个伟大美德之一。

但懒惰不能和冷漠混为一谈。长时间不理出现的问题。允许代码腐烂异味。不重构拷贝/粘贴过来的重复代码。

对软件开发中这些需要修改的东西要有一种紧迫感。事无巨细。这是保持软件健康的必要态度。

5. 暴怒(Wrath)

凡犯暴怒者:活体肢解

a5

在有些地方,有些程序员是每个人都尊敬,也是每个人都害怕。你也许遇到过这样的火星极客。他们恃才放旷,为所欲为,其他人在他身边都惦着脚走。避免和他冲突。

他们喜怒无常,他们的怒气经常撒错方向。他们贬低他人,破坏团队和谐。

警惕这种不受约束的对峙气氛的滋生。拒绝忍受这样的撒野。立即辞掉他们。

6. 妒忌(Envy)

凡犯妒忌者:投入冰水之中

a6

不满足于现有的工具和系统,有些程序员眼睛总是盯着别人的。

我曾经遇到过这样的经历,一个wiki系统正在使用中,另外一个却同时被引进,因为它的标记语法感觉更好一些。两个问题跟踪系统,多种聊天系统,不兼容的博客平台,等等。

如果你不喜欢某个工具,相信有比它更好的,那好,去找到它,使用它。但是,请完全放弃你现有的。吃着碗里又想占这锅里,只会得不偿失,给自己制造麻烦。

7. 傲慢(Pride)

凡犯傲慢者:轮裂

a7

有些程序员喜欢孤芳自赏。对自己的能力过度自信。从不寻求帮助。

更糟糕的,他认为所有的事情都应该由自己来完成。虽然他有能力完成任何的任务,但他却没能完成,因为他承担的太多了,无法集中精力。他分不清什么是核心什么是次要的。在可以使用云服务时他建造自己的服务器,在能使用成熟的部署系统时他重新发明自己的,他开发出跟现有框架功能相同的框架,等等。

诚然,做研究是有趣的。这些研究经常被辩称为“基础”或“革新”,但却因没有更快捷的创造商业价值而使产品丧失市场先机。

小心“非我发明(Not Invented Here)”综合征。准确的定义你的核心目标和你的首要工作。其它的都是次要的,可以借用别人的。这没有什么好羞愧的。

(责任编辑/李亚超)

来源:外刊IT评论网

Python高效编程技巧

我已经使用Python编程有多年了,即使今天我仍然惊奇于这种语言所能让代码表现出的整洁和对DRY编程原则的适用。这些年来的经历让我学到了很多的小技巧和知识,大多数是通过阅读很流行的开源软件,如Django, Flask, Requests中获得的。 下面我挑选出的这几个技巧常常会被人们忽略,但它们在日常编程中能真正的给我们带来不少帮助。

下面我挑选出的这几个技巧常常会被人们忽略,但它们在日常编程中能真正的给我们带来不少帮助。

字典推导(Dictionary comprehensions)和集合推导(Set comprehensions)

大多数的Python程序员都知道且使用过列表推导(list comprehensions)。如果你对list comprehensions概念不是很熟悉——一个list comprehension就是一个更简短、简洁的创建一个list的方法。

>>> some_list = [1, 2, 3, 4, 5]

>>> another_list = [ x + 1 for x in some_list ]

>>> another_list
[2, 3, 4, 5, 6]

自从python 3.1 (甚至是Python 2.7)起,我们可以用同样的语法来创建集合和字典表:

>>> # Set Comprehensions

>>> some_list = [1, 2, 3, 4, 5, 2, 5, 1, 4, 8]

>>> even_set = { x for x in some_list if x % 2 == 0 }

>>> even_set
set([8, 2, 4])

>>> # Dict Comprehensions

>>> d = { x: x % 2 == 0 for x in range(1, 11) }

>>> d
{1: False, 2: True, 3: False, 4: True, 5: False, 6: True, 7: False, 8: True, 9: False, 10: True}

在第一个例子里,我们以some_list为基础,创建了一个具有不重复元素的集合,而且集合里只包含偶数。而在字典表的例子里,我们创建了一个key是不重复的1到10之间的整数,value是布尔型,用来指示key是否是偶数。

这里另外一个值得注意的事情是集合的字面量表示法。我们可以简单的用这种方法创建一个集合:

>>> my_set = {1, 2, 1, 2, 3, 4}

>>> my_set
set([1, 2, 3, 4])

而不需要使用内置函数set()。

计数时使用Counter计数对象

这听起来显而易见,但经常被人忘记。对于大多数程序员来说,数一个东西是一项很常见的任务,而且在大多数情况下并不是很有挑战性的事情——这里有几种方法能更简单的完成这种任务。

Python的collections类库里有个内置的dict类的子类,是专门来干这种事情的:

>>> from collections import Counter

>>> c = Counter('hello world')

>>> c
Counter({'l': 3, 'o': 2, ' ': 1, 'e': 1, 'd': 1, 'h': 1, 'r': 1, 'w': 1})

>>> c.most_common(2)
[('l', 3), ('o', 2)]

 

漂亮的打印出JSON

JSON是一种非常好的数据序列化的形式,被如今的各种API和web service大量的使用。使用python内置的json处理,可以使JSON串具有一定的可读性,但当遇到大型数据时,它表现成一个很长的、连续的一行时,人的肉眼就很难观看了。

为了能让JSON数据表现的更友好,我们可以使用indent参数来输出漂亮的JSON。当在控制台交互式编程或做日志时,这尤其有用:

>>> import json

>>> print(json.dumps(data))  # No indention
{"status": "OK", "count": 2, "results": [{"age": 27, "name": "Oz", "lactose_intolerant": true}, {"age": 29, "name": "Joe", "lactose_intolerant": false}]}

>>> print(json.dumps(data, indent=2))  # With indention

{
  "status": "OK",
  "count": 2,
  "results": [

    {
      "age": 27,
      "name": "Oz",

      "lactose_intolerant": true
    },
    {
      "age": 29,

      "name": "Joe",
      "lactose_intolerant": false
    }
  ]

}

同样,使用内置的pprint模块,也可以让其它任何东西打印输出的更漂亮。

创建一次性的、快速的小型web服务

有时候,我们需要在两台机器或服务之间做一些简便的、很基础的RPC之类的交互。

我们希望用一种简单的方式使用B程序调用A程序里的一个方法——有时是在另一台机器上。仅内部使用。 我并不鼓励将这里介绍的方法用在非内部的、一次性的编程中。我们可以使用一种叫做XML-RPC的协议 (相对应的是这个Python库),来做这种事情。

下面是一个使用SimpleXMLRPCServer模块建立一个快速的小的文件读取服务器的例子:

from SimpleXMLRPCServer import SimpleXMLRPCServer

def file_reader(file_name):

    with open(file_name, 'r') as f:
        return f.read()

server = SimpleXMLRPCServer(('localhost', 8000))
server.register_introspection_functions()

server.register_function(file_reader)

server.serve_forever()

客户端:

import xmlrpclib

proxy = xmlrpclib.ServerProxy('http://localhost:8000/')

proxy.file_reader('/tmp/secret.txt')

我们这样就得到了一个远程文件读取工具,没有外部的依赖,只有几句代码(当然,没有任何安全措施,所以只可以在家里这样做)。

Python神奇的开源社区

这里我提到的几个东西都是Python标准库里的,如果你安装了Python,你就已经可以这样使用了。而对于很多其它类型的任务,这里有大量的社区维护的第三方库可供你使用。

下面这个清单是我认为的好用且健壮的开源库的必备条件:

好的开源库必须… 包含一个很清楚的许可声明,能适用于你的使用场景。

开发和维护工作很活跃(或,你能参与开发维护它。)

能够简单的使用pip安装或反复部署。

有测试套件,具有足够的测试覆盖率。

如果你发现一个好的程序库,符合你的要求,不要不好意思————大部分的开源项目都欢迎捐赠代码和欢迎提供帮助——即使你不是一个Python高手。

 

本文译自 Improving Your Python Productivity

文章来源:外刊IT评论网

Comet:基于 HTTP 长连接的“服务器推”技术

简介: 很多应用譬如监控、即时通信、即时报价系统都需要将后台发生的变化实时传送到客户端而无须客户端不停地刷新、发送请求。本文首先介绍、比较了常用的“服务器推”方案,着重介绍了 Comet - 使用 HTTP 长连接、无须浏览器安装插件的两种“服务器推”方案:基于 AJAX 的长轮询方式;基于 iframe 及 htmlfile 的流方式。最后分析了开发 Comet 应用需要注意的一些问题,以及如何借助开源的 Comet 框架-pushlet 构建自己的“服务器推”应用。

———————————————————

“服务器推”技术的应用

传统模式的 Web 系统以客户端发出请求、服务器端响应的方式工作。这种方式并不能满足很多现实应用的需求,譬如:

  • 监控系统:后台硬件热插拔、LED、温度、电压发生变化;
  • 即时通信系统:其它用户登录、发送信息;
  • 即时报价系统:后台数据库内容发生变化;

这些应用都需要服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求。“服务器推”技术在现实应用中有一些解决方案,本文将这些解决方案分为两类:一类需要在浏览器端安装插件,基于套接口传送信息,或是使用 RMI、CORBA 进行远程调用;而另一类则无须浏览器安装任何插件、基于 HTTP 长连接。

将“服务器推”应用在 Web 程序中,首先考虑的是如何在功能有限的浏览器端接收、处理信息:

  1. 客户端如何接收、处理信息,是否需要使用套接口或是使用远程调用。客户端呈现给用户的是 HTML 页面还是 Java applet 或 Flash 窗口。如果使用套接口和远程调用,怎么和 JavaScript 结合修改 HTML 的显示。
  2. 客户与服务器端通信的信息格式,采取怎样的出错处理机制。
  3. 客户端是否需要支持不同类型的浏览器如 IE、Firefox,是否需要同时支持 Windows 和 Linux 平台。

基于客户端套接口的“服务器推”技术

Flash XMLSocket

如果 Web 应用的用户接受应用只有在安装了 Flash 播放器才能正常运行, 那么使用 Flash 的 XMLSocket 也是一个可行的方案。

这种方案实现的基础是:

  1. Flash 提供了 XMLSocket 类。
  2. JavaScript 和 Flash 的紧密结合:在 JavaScript 可以直接调用 Flash 程序提供的接口。

具体实现方法:在 HTML 页面中内嵌入一个使用了 XMLSocket 类的 Flash 程序。JavaScript 通过调用此 Flash 程序提供的套接口接口与服务器端的套接口进行通信。JavaScript 在收到服务器端以 XML 格式传送的信息后可以很容易地控制 HTML 页面的内容显示。

关于如何去构建充当了 JavaScript 与 Flash XMLSocket 桥梁的 Flash 程序,以及如何在 JavaScript 里调用 Flash 提供的接口,我们可以参考 AFLAX(Asynchronous Flash and XML)项目提供的 Socket Demo 以及 SocketJS(请参见 参考资源)。

Javascript 与 Flash 的紧密结合,极大增强了客户端的处理能力。从 Flash 播放器 V7.0.19 开始,已经取消了 XMLSocket 的端口必须大于 1023 的限制。Linux 平台也支持 Flash XMLSocket 方案。但此方案的缺点在于:

  1. 客户端必须安装 Flash 播放器;
  2. 因为 XMLSocket 没有 HTTP 隧道功能,XMLSocket 类不能自动穿过防火墙;
  3. 因为是使用套接口,需要设置一个通信端口,防火墙、代理服务器也可能对非 HTTP 通道端口进行限制;

不过这种方案在一些网络聊天室,网络互动游戏中已得到广泛使用。

Java Applet 套接口

在客户端使用 Java Applet,通过 java.net.Socket 或 java.net.DatagramSocket 或 java.net.MulticastSocket 建立与服务器端的套接口连接,从而实现“服务器推”。

这种方案最大的不足在于 Java applet 在收到服务器端返回的信息后,无法通过 JavaScript 去更新 HTML 页面的内容。

基于 HTTP 长连接的“服务器推”技术

Comet 简介

浏览器作为 Web 应用的前台,自身的处理功能比较有限。浏览器的发展需要客户端升级软件,同时由于客户端浏览器软件的多样性,在某种意义上,也影响了浏览器新技术的推广。在 Web 应用中,浏览器的主要工作是发送请求、解析服务器返回的信息以不同的风格显示。AJAX 是浏览器技术发展的成果,通过在浏览器端发送异步请求,提高了单用户操作的响应性。但 Web 本质上是一个多用户的系统,对任何用户来说,可以认为服务器是另外一个用户。现有 AJAX 技术的发展并不能解决在一个多用户的 Web 应用中,将更新的信息实时传送给客户端,从而用户可能在“过时”的信息下进行操作。而 AJAX 的应用又使后台数据更新更加频繁成为可能。

图 1. 传统的 Web 应用模型与基于 AJAX 的模型之比较

fig001

 

“服务器推”是一种很早就存在的技术,以前在实现上主要是通过客户端的套接口,或是服务器端的远程调用。因为浏览器技术的发展比较缓慢,没有为“服务器推”的实现提供很好的支持,在纯浏览器的应用中很难有一个完善的方案去实现“服务器推”并用于商业程序。最近几年,因为 AJAX 技术的普及,以及把 IFrame 嵌在“htmlfile“的 ActiveX 组件中可以解决 IE 的加载显示问题,一些受欢迎的应用如 meebo,gmail+gtalk 在实现中使用了这些新技术;同时“服务器推”在现实应用中确实存在很多需求。因为这些原因,基于纯浏览器的“服务器推”技术开始受到较多关注,Alex Russell(Dojo Toolkit 的项目 Lead)称这种基于 HTTP 长连接、无须在浏览器端安装插件的“服务器推”技术为“Comet”。目前已经出现了一些成熟的 Comet 应用以及各种开源框架;一些 Web 服务器如 Jetty 也在为支持大量并发的长连接进行了很多改进。关于 Comet 技术最新的发展状况请参考关于 Comet 的 wiki。

下面将介绍两种 Comet 应用的实现模型。

基于 AJAX 的长轮询(long-polling)方式

如 图 1 所示,AJAX 的出现使得 JavaScript 可以调用 XMLHttpRequest 对象发出 HTTP 请求,JavaScript 响应处理函数根据服务器返回的信息对 HTML 页面的显示进行更新。使用 AJAX 实现“服务器推”与传统的 AJAX 应用不同之处在于:

  1. 服务器端会阻塞请求直到有数据传递或超时才返回。
  2. 客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。
  3. 当客户端处理接收的数据、重新建立连接时,服务器端可能有新的数据到达;这些信息会被服务器端保存直到客户端重新建立连接,客户端会一次把当前服务器端所有的信息取回。

图 2. 基于长轮询的服务器推模型

fig002

 

一些应用及示例如 “Meebo”, “Pushlet Chat” 都采用了这种长轮询的方式。相对于“轮询”(poll),这种长轮询方式也可以称为“拉”(pull)。因为这种方案基于 AJAX,具有以下一些优点:请求异步发出;无须安装插件;IE、Mozilla FireFox 都支持 AJAX。

在这种长轮询方式下,客户端是在 XMLHttpRequest 的 readystate 为 4(即数据传输结束)时调用回调函数,进行信息处理。当 readystate 为 4 时,数据传输结束,连接已经关闭。Mozilla Firefox 提供了对 Streaming AJAX 的支持, 即 readystate 为 3 时(数据仍在传输中),客户端可以读取数据,从而无须关闭连接,就能读取处理服务器端返回的信息。IE 在 readystate 为 3 时,不能读取服务器返回的数据,目前 IE 不支持基于 Streaming AJAX。

基于 Iframe 及 htmlfile 的流(streaming)方式

iframe 是很早就存在的一种 HTML 标记, 通过在 HTML 页面里嵌入一个隐蔵帧,然后将这个隐蔵帧的 SRC 属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。

图 3. 基于流方式的服务器推模型

fig003

 

上节提到的 AJAX 方案是在 JavaScript 里处理 XMLHttpRequest 从服务器取回的数据,然后 Javascript 可以很方便的去控制 HTML 页面的显示。同样的思路用在 iframe 方案的客户端,iframe 服务器端并不返回直接显示在页面的数据,而是返回对客户端 Javascript 函数的调用,如“<script type="text/javascript">js_func(“data from server ”)</script>”。服务器端将返回的数据作为客户端 JavaScript 函数的参数传递;客户端浏览器的 Javascript 引擎在收到服务器返回的 JavaScript 调用时就会去执行代码。

从 图 3 可以看到,每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接, 服务器端可以设置一个超时时间, 超时后通知客户端重新建立连接,并关闭原来的连接)。

使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。Google 的天才们使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法用到了 gmail+gtalk 产品中。Alex Russell 在 “What else is burried down in the depth’s of Google’s amazing JavaScript?”文章中介绍了这种方法。Zeitoun 网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe 和 htmlfile 的 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器,可以作为参考。(请参见 参考资源

使用 Comet 模型开发自己的应用

上面介绍了两种基于 HTTP 长连接的“服务器推”架构,更多描述了客户端处理长连接的技术。对于一个实际的应用而言,系统的稳定性和性能是非常重要的。将 HTTP 长连接用于实际应用,很多细节需要考虑。

不要在同一客户端同时使用超过两个的 HTTP 长连接

我们使用 IE 下载文件时会有这样的体验,从同一个 Web 服务器下载文件,最多只能有两个文件同时被下载。第三个文件的下载会被阻塞,直到前面下载的文件下载完毕。这是因为 HTTP 1.1 规范中规定,客户端不应该与服务器端建立超过两个的 HTTP 连接, 新的连接会被阻塞。而 IE 在实现中严格遵守了这种规定。

HTTP 1.1 对两个长连接的限制,会对使用了长连接的 Web 应用带来如下现象:在客户端如果打开超过两个的 IE 窗口去访问同一个使用了长连接的 Web 服务器,第三个 IE 窗口的 HTTP 请求被前两个窗口的长连接阻塞。

所以在开发长连接的应用时, 必须注意在使用了多个 frame 的页面中,不要为每个 frame 的页面都建立一个 HTTP 长连接,这样会阻塞其它的 HTTP 请求,在设计上考虑让多个 frame 的更新共用一个长连接。

服务器端的性能和可扩展性

一般 Web 服务器会为每个连接创建一个线程,如果在大型的商业应用中使用 Comet,服务器端需要维护大量并发的长连接。在这种应用背景下,服务器端需要考虑负载均衡和集群技术;或是在服务器端为长连接作一些改进。

应用和技术的发展总是带来新的需求,从而推动新技术的发展。HTTP 1.1 与 1.0 规范有一个很大的不同:1.0 规范下服务器在处理完每个 Get/Post 请求后会关闭套接口连接; 而 1.1 规范下服务器会保持这个连接,在处理两个请求的间隔时间里,这个连接处于空闲状态。 Java 1.4 引入了支持异步 IO 的 java.nio 包。当连接处于空闲时,为这个连接分配的线程资源会返还到线程池,可以供新的连接使用;当原来处于空闲的连接的客户发出新的请求,会从线程池里分配一个线程资源处理这个请求。 这种技术在连接处于空闲的机率较高、并发连接数目很多的场景下对于降低服务器的资源负载非常有效。

但是 AJAX 的应用使请求的出现变得频繁,而 Comet 则会长时间占用一个连接,上述的服务器模型在新的应用背景下会变得非常低效,线程池里有限的线程数甚至可能会阻塞新的连接。Jetty 6 Web 服务器针对 AJAX、Comet 应用的特点进行了很多创新的改进,请参考文章“AJAX,Comet and Jetty”(请参见 参考资源)。

控制信息与数据信息使用不同的 HTTP 连接

使用长连接时,存在一个很常见的场景:客户端网页需要关闭,而服务器端还处在读取数据的堵塞状态,客户端需要及时通知服务器端关闭数据连接。服务器在收到关闭请求后首先要从读取数据的阻塞状态唤醒,然后释放为这个客户端分配的资源,再关闭连接。

所以在设计上,我们需要使客户端的控制请求和数据请求使用不同的 HTTP 连接,才能使控制请求不会被阻塞。

在实现上,如果是基于 iframe 流方式的长连接,客户端页面需要使用两个 iframe,一个是控制帧,用于往服务器端发送控制请求,控制请求能很快收到响应,不会被堵塞;一个是显示帧,用于往服务器端发送长连接请求。如果是基于 AJAX 的长轮询方式,客户端可以异步地发出一个 XMLHttpRequest 请求,通知服务器端关闭数据连接。

在客户和服务器之间保持“心跳”信息

在浏览器与服务器之间维持一个长连接会为通信带来一些不确定性:因为数据传输是随机的,客户端不知道何时服务器才有数据传送。服务器端需要确保当客户端不再工作时,释放为这个客户端分配的资源,防止内存泄漏。因此需要一种机制使双方知道大家都在正常运行。在实现上:

  1. 服务器端在阻塞读时会设置一个时限,超时后阻塞读调用会返回,同时发给客户端没有新数据到达的心跳信息。此时如果客户端已经关闭,服务器往通道写数据会出现异常,服务器端就会及时释放为这个客户端分配的资源。
  2. 如果客户端使用的是基于 AJAX 的长轮询方式;服务器端返回数据、关闭连接后,经过某个时限没有收到客户端的再次请求,会认为客户端不能正常工作,会释放为这个客户端分配、维护的资源。
  3. 当服务器处理信息出现异常情况,需要发送错误信息通知客户端,同时释放资源、关闭连接。

Pushlet – 开源 Comet 框架

Pushlet 是一个开源的 Comet 框架,在设计上有很多值得借鉴的地方,对于开发轻量级的 Comet 应用很有参考价值。

观察者模型

Pushlet 使用了观察者模型:客户端发送请求,订阅感兴趣的事件;服务器端为每个客户端分配一个会话 ID 作为标记,事件源会把新产生的事件以多播的方式发送到订阅者的事件队列里。

客户端 JavaScript 库

pushlet 提供了基于 AJAX 的 JavaScript 库文件用于实现长轮询方式的“服务器推”;还提供了基于 iframe 的 JavaScript 库文件用于实现流方式的“服务器推”。

JavaScript 库做了很多封装工作:

  1. 定义客户端的通信状态:STATE_ERRORSTATE_ABORTSTATE_NULLSTATE_READYSTATE_JOINEDSTATE_LISTENING
  2. 保存服务器分配的会话 ID,在建立连接之后的每次请求中会附上会话 ID 表明身份;
  3. 提供了 join()leave()subscribe()、 unsubsribe()listen() 等 API 供页面调用;
  4. 提供了处理响应的 JavaScript 函数接口 onData()onEvent()

网页可以很方便地使用这两个 JavaScript 库文件封装的 API 与服务器进行通信。

客户端与服务器端通信信息格式

pushlet 定义了一套客户与服务器通信的信息格式,使用 XML 格式。定义了客户端发送请求的类型:joinleavesubscribeunsubscribelistenrefresh;以及响应的事件类型:datajoin_acklisten_ackrefreshheartbeaterrorabortsubscribe_ackunsubscribe_ack

服务器端事件队列管理

pushlet 在服务器端使用 Java Servlet 实现,其数据结构的设计框架仍可适用于 PHP、C 编写的后台客户端。

Pushlet 支持客户端自己选择使用流、拉(长轮询)、轮询方式。服务器端根据客户选择的方式在读取事件队列(fetchEvents)时进行不同的处理。“轮询”模式下 fetchEvents() 会马上返回。”流“和”拉“模式使用阻塞的方式读事件,如果超时,会发给客户端发送一个没有新信息收到的“heartbeat“事件,如果是“拉”模式,会把“heartbeat”与“refresh”事件一起传给客户端,通知客户端重新发出请求、建立连接。

客户服务器之间的会话管理

服务端在客户端发送 join 请求时,会为客户端分配一个会话 ID, 并传给客户端,然后客户端就通过此会话 ID 标明身份发出subscribe 和 listen 请求。服务器端会为每个会话维护一个订阅的主题集合、事件队列。

服务器端的事件源会把新产生的事件以多播的方式发送到每个会话(即订阅者)的事件队列里。

小结

本文介绍了如何在现有的技术基础上选择合适的方案开发一个“服务器推”的应用,最优的方案还是取决于应用需求的本身。相对于传统的 Web 应用, 目前开发 Comet 应用还是具有一定的挑战性。

“服务器推”存在广泛的应用需求,为了使 Comet 模型适用于大规模的商业应用,以及方便用户构建 Comet 应用,最近几年,无论是服务器还是浏览器都出现了很多新技术,同时也出现了很多开源的 Comet 框架、协议。需求推动技术的发展,相信 Comet 的应用会变得和 AJAX 一样普及。

 

作者:周婷,软件工程师,目前在 IBM 中国软件开发技术实验室从事刀片服务器管理固件的开发工作。

来源:developerworks

Linux环境进程间通信: 共享内存

第一部分

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

——————————————-

采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,本文将主要介绍mmap()系统调用及系统V共享内存API的原理及应用。

一、内核怎样保证各个进程寻址到同一个共享内存区域的内存页面

1、page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。

2、文件与address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

3、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。

4、对于共享内存映射情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。
注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。

5、所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
注:一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

上面涉及到了一些数据结构,围绕数据结构理解问题会容易一些。

 

二、mmap()及其相关系统调用

mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

1、mmap()系统调用形式如下:

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。这里不再详细介绍mmap()的参数,读者可参考mmap()手册页获得进一步的信息。

2、系统调用mmap()用于共享内存的两种方式:

(1)使用普通文件提供的内存映射:适用于任何进程之间; 此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:

fd=open(name, flag, mode);
if(fd<0)
	...

 

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。

(2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间; 由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可,参见范例2。

3、系统调用munmap()

int munmap( void * addr, size_t len )
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

4、系统调用msync()

int msync ( void * addr , size_t len, int flags)
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

三、mmap()范例

下面将给出使用mmap()的两个范例:范例1给出两个进程通过映射普通文件实现共享内存通信;范例2给出父子进程通过匿名映射实现共享内存。系统调用mmap()有许多有趣的地方,下面是通过mmap()映射普通文件实现进程间的通信的范例,我们通过该范例来说明mmap()实现共享内存的特点及注意事项。

范例1:两个进程通过映射普通文件实现共享内存通信

范例1包含两个子程序:map_normalfile1.c及map_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1及map_normalfile2。两个程序通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。map_normalfile1把命令行参数指定的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。

下面是两个程序代码:

/*-------------map_normalfile1.c-----------*/
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
  char name[4];
  int  age;
}people;
main(int argc, char** argv) // map a normal file as shared mem:
{
  int fd,i;
  people *p_map;
  char temp;

  fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
  lseek(fd,sizeof(people)*5-1,SEEK_SET);
  write(fd,"",1);

  p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
        MAP_SHARED,fd,0 );
  close( fd );
  temp = 'a';
  for(i=0; i<10; i++)
  {
    temp += 1;
    memcpy( ( *(p_map+i) ).name, &temp,2 );
    ( *(p_map+i) ).age = 20+i;
  }
  printf(" initialize over \n ");
  sleep(10);
  munmap( p_map, sizeof(people)*10 );
  printf( "umap ok \n" );
}
/*-------------map_normalfile2.c-----------*/
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
  char name[4];
  int  age;
}people;
main(int argc, char** argv)  // map a normal file as shared mem:
{
  int fd,i;
  people *p_map;
  fd=open( argv[1],O_CREAT|O_RDWR,00777 );
  p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
       MAP_SHARED,fd,0);
  for(i = 0;i<10;i++)
  {
  printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );
  }
  munmap( p_map,sizeof(people)*10 );
}

map_normalfile1.c首先定义了一个people数据结构,(在这里采用数据结构的方式是因为,共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,采用结构的方式有普遍代表性)。map_normfile1首先打开或创建一个文件,并把文件的长度设置为5个people结构大小。然后从mmap()的返回地址开始,设置了10个people结构。然后,进程睡眠10秒钟,等待其他进程映射同一个文件,最后解除映射。

map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,然后解除映射。

分别把两个程序编译成可执行文件map_normalfile1和map_normalfile2后,在一个终端上先运行./map_normalfile2 /tmp/test_shm,程序输出结果如下:

 

initialize over
umap ok

在map_normalfile1输出initialize over 之后,输出umap ok之前,在另一个终端上运行map_normalfile2 /tmp/test_shm,将会产生如下输出(为了节省空间,输出结果为稍作整理后的结果):

name: b	age 20;	name: c	age 21;	name: d	age 22;	name: e	age 23;	name: f	age 24;
name: g	age 25;	name: h	age 26;	name: I	age 27;	name: j	age 28;	name: k	age 29;

在map_normalfile1 输出umap ok后,运行map_normalfile2则输出如下结果:

name: b	age 20;	name: c	age 21;	name: d	age 22;	name: e	age 23;	name: f	age 24;
name:	age 0;	name:	age 0;	name:	age 0;	name:	age 0;	name:	age 0;

 

从程序的运行结果中可以得出的结论

1、 最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小;

2、 可以用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。打开文件被截短为5个people结构大小,而在map_normalfile1中初始化了10个people数据结构,在恰当时候(map_normalfile1输出initialize over 之后,输出umap ok之前)调用map_normalfile2会发现map_normalfile2将输出全部10个people结构的值,后面将给出详细讨论。
注:在linux中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程可以对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;但是,如果对一个页面以外的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有效地址空间大小不会超过文件大小及一个页面大小的和。

3、 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。

范例2:父子进程通过匿名映射实现共享内存

#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
  char name[4];
  int  age;
}people;
main(int argc, char** argv)
{
  int i;
  people *p_map;
  char temp;
  p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
       MAP_SHARED|MAP_ANONYMOUS,-1,0);
  if(fork() == 0)
  {
    sleep(2);
    for(i = 0;i<5;i++)
      printf("child read: the %d people's age is %d\n",i+1,(*(p_map+i)).age);
    (*p_map).age = 100;
    munmap(p_map,sizeof(people)*10); //实际上,进程终止时,会自动解除映射。
    exit();
  }
  temp = 'a';
  for(i = 0;i<5;i++)
  {
    temp += 1;
    memcpy((*(p_map+i)).name, &temp,2);
    (*(p_map+i)).age=20+i;
  }
  sleep(5);
  printf( "parent read: the first people,s age is %d\n",(*p_map).age );
  printf("umap\n");
  munmap( p_map,sizeof(people)*10 );
  printf( "umap ok\n" );
}

考察程序的输出结果,体会父子进程匿名共享内存:

 

child read: the 1 people's age is 20
child read: the 2 people's age is 21
child read: the 3 people's age is 22
child read: the 4 people's age is 23
child read: the 5 people's age is 24
parent read: the first people,s age is 100
umap
umap ok

 

四、对mmap()返回地址的访问

前面对范例运行结构的讨论中已经提到,linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明:

image001

 

注意:文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文件的偏移部分,一定要注意为页面大小的整数倍。下面是对进程映射地址空间的访问范例:

#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
	char name[4];
	int  age;
}people;
main(int argc, char** argv)
{
	int fd,i;
	int pagesize,offset;
	people *p_map;

	pagesize = sysconf(_SC_PAGESIZE);
	printf("pagesize is %d\n",pagesize);
	fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
	lseek(fd,pagesize*2-100,SEEK_SET);
	write(fd,"",1);
	offset = 0;	//此处offset = 0编译成版本1;offset = pagesize编译成版本2
	p_map = (people*)mmap(NULL,pagesize*3,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset);
	close(fd);

	for(i = 1; i<10; i++)
	{
		(*(p_map+pagesize/sizeof(people)*i-2)).age = 100;
		printf("access page %d over\n",i);
		(*(p_map+pagesize/sizeof(people)*i-1)).age = 100;
		printf("access page %d edge over, now begin to access page %d\n",i, i+1);
		(*(p_map+pagesize/sizeof(people)*i)).age = 100;
		printf("access page %d over\n",i+1);
	}
	munmap(p_map,sizeof(people)*10);
}

如程序中所注释的那样,把程序编译成两个版本,两个版本主要体现在文件被映射部分的大小不同。文件的大小介于一个页面与两个页面之间(大小为:pagesize*2-99),版本1的被映射部分是整个文件,版本2的文件被映射部分是文件大小减去一个页面后的剩余部分,不到一个页面大小(大小为:pagesize-99)。程序中试图访问每一个页面边界,两个版本都试图在进程空间中映射pagesize*3的字节数。

版本1的输出结果如下:

pagesize is 4096
access page 1 over
access page 1 edge over, now begin to access page 2
access page 2 over
access page 2 over
access page 2 edge over, now begin to access page 3
Bus error		//被映射文件在进程空间中覆盖了两个页面,此时,进程试图访问第三个页面

版本2的输出结果如下:

pagesize is 4096
access page 1 over
access page 1 edge over, now begin to access page 2
Bus error		//被映射文件在进程空间中覆盖了一个页面,此时,进程试图访问第二个页面

结论:采用系统调用mmap()实现进程间通信是很方便的,在应用层上接口非常简洁。内部实现机制区涉及到了linux存储管理以及文件系统等方面的内容,可以参考一下相关重要数据结构来加深理解。在本专题的后面部分,将介绍系统v共享内存的实现。

 

第二部分

系统调用mmap()通过映射一个普通文件实现共享内存。系统V则是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信。也就是说,每个共享内存区域对应特殊文件系统shm中的一个文件(这是通过shmid_kernel结构联系起来的),后面还将阐述。

1、系统V共享内存原理

进程间需要共享的数据被放在一个叫做IPC共享内存区域的地方,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。系统V共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。内核在保证shmget获得或创建一个共享内存区,初始化该共享内存区相应的shmid_kernel结构注同时,还将在特殊文件系统shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentry及inode结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。所有这一切都是系统调用shmget完成的。

注:每一个共享内存区都有一个控制结构struct shmid_kernel,shmid_kernel是共享内存区域中非常重要的一个数据结构,它是存储管理和文件系统结合起来的桥梁,定义如下:

struct shmid_kernel /* private to the kernel */
{	
	struct kern_ipc_perm	shm_perm;
	struct file *		shm_file;
	int			id;
	unsigned long		shm_nattch;
	unsigned long		shm_segsz;
	time_t			shm_atim;
	time_t			shm_dtim;
	time_t			shm_ctim;
	pid_t			shm_cprid;
	pid_t			shm_lprid;
};

 

该结构中最重要的一个域应该是shm_file,它存储了将被映射文件的地址。每个共享内存区对象都对应特殊文件系统shm中的一个文件,一般情况下,特殊文件系统shm中的文件是不能用read()、write()等方法访问的,当采取共享内存的方式把其中的文件映射到进程地址空间后,可直接采用访问内存的方式对其访问。

这里我们采用[1]中的图表给出与系统V共享内存相关数据结构:

image001

正如消息队列和信号灯一样,内核通过数据结构struct ipc_ids shm_ids维护系统中的所有共享内存区域。上图中的shm_ids.entries变量指向一个ipc_id结构数组,而每个ipc_id结构数组中有个指向kern_ipc_perm结构的指针。到这里读者应该很熟悉了,对于系统V共享内存区来说,kern_ipc_perm的宿主是shmid_kernel结构,shmid_kernel是用来描述一个共享内存区域的,这样内核就能够控制系统中所有的共享区域。同时,在shmid_kernel结构的file类型指针shm_file指向文件系统shm中相应的文件,这样,共享内存区域就与shm文件系统中的文件对应起来。

在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用shmat()完成此项功能。由于在调用shmget()时,已经创建了文件系统shm中的一个同名文件与共享内存区域相对应,因此,调用shmat()的过程相当于映射文件系统shm中的同名文件过程,原理与mmap()大同小异。

2、系统V共享内存API

对于系统V共享内存,主要有以下几个API:shmget()、shmat()、shmdt()及shmctl()。

#include <sys/ipc.h>
#include <sys/shm.h>

shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就可以方便地对共享区域进行访问操作。shmdt()调用用来解除进程对共享内存区域的映射。shmctl实现对共享内存区域的控制操作。这里我们不对这些系统调用作具体的介绍,读者可参考相应的手册页面,后面的范例中将给出它们的调用方法。

注:shmget的内部实现包含了许多重要的系统V共享内存机制;shmat在把共享内存区域映射到进程空间时,并不真正改变进程的页表。当进程第一次访问内存映射区域访问时,会因为没有物理页表的分配而导致一个缺页异常,然后内核再根据相应的存储管理机制为共享内存映射区域分配相应的页表。

3、系统V共享内存限制

在/proc/sys/kernel/目录下,记录着系统V共享内存的一下限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等,可以手工对其调整,但不推荐这样做。

在[2]中,给出了这些限制的测试方法,不再赘述。

4、系统V共享内存范例

本部分将给出系统V共享内存API的使用方法,并对比分析系统V共享内存机制与mmap()映射普通文件实现共享内存之间的差异,首先给出两个进程通过系统V共享内存通信的范例:

/***** testwrite.c *******/
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
	char name[4];
	int age;
} people;
main(int argc, char** argv)
{
	int shm_id,i;
	key_t key;
	char temp;
	people *p_map;
	char* name = "/dev/shm/myshm2";
	key = ftok(name,0);
	if(key==-1)
		perror("ftok error");
	shm_id=shmget(key,4096,IPC_CREAT);	
	if(shm_id==-1)
	{
		perror("shmget error");
		return;
	}
	p_map=(people*)shmat(shm_id,NULL,0);
	temp='a';
	for(i = 0;i<10;i++)
	{
		temp+=1;
		memcpy((*(p_map+i)).name,&temp,1);
		(*(p_map+i)).age=20+i;
	}
	if(shmdt(p_map)==-1)
		perror(" detach error ");
}
/********** testread.c ************/
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
	char name[4];
	int age;
} people;
main(int argc, char** argv)
{
	int shm_id,i;
	key_t key;
	people *p_map;
	char* name = "/dev/shm/myshm2";
	key = ftok(name,0);
	if(key == -1)
		perror("ftok error");
	shm_id = shmget(key,4096,IPC_CREAT);	
	if(shm_id == -1)
	{
		perror("shmget error");
		return;
	}
	p_map = (people*)shmat(shm_id,NULL,0);
	for(i = 0;i<10;i++)
	{
	printf( "name:%s\n",(*(p_map+i)).name );
	printf( "age %d\n",(*(p_map+i)).age );
	}
	if(shmdt(p_map) == -1)
		perror(" detach error ");
}

testwrite.c创建一个系统V共享内存区,并在其中写入格式化数据;testread.c访问同一个系统V共享内存区,读出其中的格式化数据。分别把两个程序编译为testwrite及testread,先后执行./testwrite及./testread 则./testread输出结果如下:

name: b	age 20;	name: c	age 21;	name: d	age 22;	name: e	age 23;	name: f	age 24;
name: g	age 25;	name: h	age 26;	name: I	age 27;	name: j	age 28;	name: k	age 29;

通过对试验结果分析,对比系统V与mmap()映射普通文件实现共享内存通信,可以得出如下结论:

1、 系统V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。 注:前面讲到,系统V共享内存机制实际是通过映射特殊文件系统shm中的文件实现的,文件系统shm的安装点在交换分区上,系统重新引导后,所有的内容都丢失。

2、 系统V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。

3、 通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响。而通过系统V共享内存实现通信的进程则不然。 注:这里没有给出shmctl的使用范例,原理与消息队列大同小异。

结论:

共享内存允许两个或多个进程共享一给定的存储区,因为数据不需要来回复制,所以是最快的一种进程间通信机制。共享内存可以通过mmap()映射普通文件(特殊情况下还可以采用匿名映射)机制实现,也可以通过系统V共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通信,往往还与信号灯等同步机制共同使用。

共享内存涉及到了存储管理以及文件系统等方面的知识,深入理解其内部机制有一定的难度,关键还要紧紧抓住内核使用的重要数据结构。系统V共享内存是以文件的形式组织在特殊文件系统shm中的。通过shmget可以创建或获得共享内存的标识符。取得共享内存标识符后,要通过shmat将这个内存区映射到本进程的虚拟地址空间。

 

文章作者:郑彦兴,国防科大攻读博士学位。

来源:developerworks

MySQL数据库Filesort过程

导读:本文作者通过图文并茂的方式对MySQL数据库Filesort过程作了详尽的介绍,相信一定会对DBA的工作有所启发。

看mysql源码的收获

  • 为优化提供理论依据
  • 为优化提供方向
  • 学习解决问题的算法和思路

filesort algorithm

  • 读取所有需要排序的数据
  • 每行数据
  • 算法1(original):存储排序key和行指针
  • 算法2(modified):存储排序key和select中的字段
  • 每次排序sort_buffer_size能容纳的行数,排序结果写入IO_CACHE对象(不妨称为IO1),本次排序结果的位置信息写入另一个IO_CACHE对象(不妨称为IO2);
  • IO_CACHE超过64k时写入临时文件
  • 当order by有limit n时,只需要把前n条排序结果写入IO_CACHE;
  • 排序KEY长度<=20且排序KEY数量在一千和十万之间时使用radixsort,否则使用quicksort
  • Merge buffer
  • 读取排序结果(算法2直接从临时文件读取结果;算法1从临时文件读取行指针,再从表中读取数据)

c1

 

c2

filesort algorithm选择

select bgid from bigt order by bgname;

Create Table: CREATE TABLE `bigt` (

`bgid` int(10) unsigned NOT NULL AUTO_INCREMENT,

`bgname` varchar(100) DEFAULT NULL,

`status` tinyint(4) DEFAULT ’0′,

PRIMARY KEY (`bgid`)

) ENGINE=InnoDB DEFAULT CHARSET=latin1

bgid(4字节)、bgname(102字节)、(null_fields+7)/8=1

其中null_fields是1,bgname是可以为空的字段

length=4+102+1=107

sort_length=101(bgname长度)

  • 满足下两个条件之一时选择original算法
  • 有text或者blob字段
  • length+sortlength > max_length_for_sort_data
  • 否则选择modified算法
  • 本例选择了modified算法
  • 没有text和blob字段
  • length+sortlength=208
  • max_length_for_sort_data=1024

Sort buffer内存使用

  • keys= sort_buff_size/(rec_length+sizeof(char*))
  • rec_length=length+sortlength
  • 本例中
  • rec_length=208
  • sizeof(char*)=4
  • sort_buff_size=2097116
  • keys=9892
  • 即能在内存中一次排序的key为9892个

倒序的实现

  • 不是在比较KEY值大小时实现
  • 发现正序、倒序,在比较KEY值大小的函数中没有区别对待
  • 差点以为把整个排序过程看错了
  • 是在向排序区写入KEY值时实现
  • 在跟踪字符类型倒序倒序时
  • make_sortkey中对每个字节取反
  • 这样后续的正序排序就相当于倒序排序

正序排序Merge buffer示例

  • 实际mysql源码中是每7个buffer进行合并
  • 本例做了简化,只对5个buffer进行合并
  • 所谓buffer是一次排序结果,保存在临时文件(IO_CACHE)中
  • 5个buffer就是临时文件中的五个段,每段保存一次排序的结果
  • Merge buffer的算法是heapsort实现的mergesort
  • 首先每个段取第一个排序key,加入heap
  • 加入时保证heap的排序

c3

 

 

c4

 

c5

 

c6

 

c7

 

c8

 

c9

 

c10

 

Merge buffer总结

  • MySQL源码中,周而复始进行合并
  • 每次合并7个buffer,直到全部合并
  • 合并时仍然使用sort buffer内存
  • 最后一次合并时不再向排序结果中写入排序KEY,只写需要的字段值
  • 各buffer自己的最小值,在一起再取最小值,就是所有buffer数据的最小值
  • 除去当前取得的最小值,再算当前buffer最小值的最小值,以此类推,得到排序的所有buffer数据
  • 用heapsort实现的mergesort

为什么用heapsort?

  • 每次合并若干buffer时,不能拿到所有buffer的全部数据
  • 对能取到sort buffer内的所有数据完全排序是没意义的
  • 以顺序排序举例,这些数据中,只有当前各buffer的最小值中的最小值能够保证是所有buffer中最小的值,依次得到这个最小值,则得到完全排序的所有数据
  • heapsort也恰好是不完全排序,只保证root是最小的

运维上的思考

  • 计算一个SQL是否能在内存中完成排序
  • 计算一个SQL使用哪种filesort算法
  • Merge buffer的代价?
  • filesort旧算法与新算法资源消耗的评估?

 

来源:http://www.mysqlops.com/2012/08/20/filesort%E8%BF%87%E7%A8%8B.html

前端开发学习 AngularJS EmberJS 需要有哪些思维上的转变?

 

问题: 对于前端开发, 如果之前有过jQuery的经验,转型学习 AngularJS /EmberJS ,需要有哪些思维上的转变

回答:

1 不要直接设计页面上的操作DOM功能

使用jQuery的时候, 经常是从设计一个页面开始,然后增加一些动态功能. 这是因为jQuery主要为了是从一个小处操作入手,然后逐渐增加更多操作.

但是在AngularJS中, 必须一开始就从功能的结构入手. 同时要时刻提醒不要按照jQuery的设计思维”这里有一段DOM要修改,完成一个X功能”,  而是直接先构想功能的结构,然后设计应用,最后在设计视图.

2 不要在AngularJS,过度使用jQuery

同样, 不要一开始就有一种”用jQuery做X,Y,Z功能” , 然后在结合AngularJS的 Model和Controller一起使用. 很多人从jQuery到AngularJS过渡时总觉得这样很棒, 但我却推荐如果一个刚接触AngularJS的新手不要使用jQuery,直到熟悉AngularJS并总有一种”AngularJS Way 思维”后再引入jQuery.

我见过很多的开发者在邮件组中费劲心思的讨论一个令人糊涂的复杂问题, 150或200行用jQuery的实现的插件 混合在一堆集合数据的回调的AngularJS代码中, 然后再用 $apply 让AngularJS渲染 , 这样是可以工作. 但问题是大多数场景下,这些jQuery 插件可以用AngularJS思维方式用很精练的代码重写, 同时瞬间一切都变为更直接更容易理解的代码.

所以, 做事情前首先要有”AngularJS的思维”, 如果你自己不太明白,最好的方法就是向社区求助, 如果还是没有找到方法,那么再使用jQuery的思维,但是不要把jQuery作为可依靠的拐杖, 否则很难精通AngularJS

3 时刻要从结构上思考问题

首先要理解SPA(single-page applications),单页面应用不是传统的一堆页面. 所以需要像后端开发者那样来思考前端开发. 同时需要考虑我们的应用如何达到模块化,并可扩展,还有提供可测试的组件.

那么如何做到”AngularJS 思维”, 对比jQuery, 有以下一些原则:

The view is the “official record”  视图是一种”官方的记录”(或理解为”表面的视角”)

用jQuery时,我们经常通过编程来改变视图. 例如用ul标签做一个下拉菜单,

 

<ul class="main-menu">
<li class="active">
<a href="#/home">Home</a>
</li>
<li>
<a href="#/menu1">Menu 1</a>
<ul>
<li><a href="#/sm1">Submenu 1</a></li>
<li><a href="#/sm2">Submenu 2</a></li>
<li><a href="#/sm3">Submenu 3</a></li>
</ul>
</li>
<li>
<a href="#/home">Menu 2</a>
</li>
</ul>

然后js代码中,通过以下代码激活事件

$('.main-menu').dropdownMenu();

来看一下视图的Html,无法看到该视图里面的是否有下拉框功能. 对于小的功能,这么做没问题. 但如果是复杂的应用,代码很容易混乱和不容易维护.

在Angular中, 视图的HTML上就可以表明视图的功能. UI标签上会写上如下的代码

<ul class="main-menu" dropdown-menu>
...
</ul>

两种方法都可以做到同样的事情, 但是angular的模板任何人都可以知道功能是什么. 无论是一个新加入团队前端美女都可以通过 “dropdown-menu” 的指令来认识到是如何功能,并不需要去猜或看js代码. 视图就能告诉我们这个功能应该是什么样的,非常清晰.

刚学习AngularJS的开发者经常问一个问题: 如何找到一个指定功能,然后在其上面加入directive指令标签. 令人吃惊的回答是”不需要” 因为你不需要一半类似jQuery,一半类似AngularJS. 问题在于开发者总是想在angularJS的环境中做”do jQuery” 的事情. 这样永远都不是一个好方法. 视图应该是官方的记录表明功能是什么. 在指令标签外面, 你永远永远不应该改变DOM.(操作DOM的事情应该在指令中完成, 而控制器中不应该有DOM操作).

记住,不要直接设计页面中的操作和HTML标签,  你必须先架构你的应用然后再设计.

 

数据绑定 Data binding

这是一个非常赞的功能, 节省了非常多的操作DOM的代码. AngularJS会自动更新视图,不需要用jQuery去更新DOM节点.

例如jQuery中,

$.ajax({
url: '/myEndpoint.json',
success: function ( data, status ) {
$('ul#log').append('<li>Data Received!</li>');
}
});

视图html代码例如

<ul class="messages" id="log">
</ul>

除了之前提到的代码意图不清晰的问题, 更重要的是我们需要手动去更新DOM节点. 如果要删除,同样需要手动删除DOM节点.而且如何在测试中分离出DOM的部分,如果视图表现改变了怎么办?

的确这有点糟糕. 在AngularJS中, 我们可以这么做

$http( '/myEndpoint.json' ).then( function ( response ) {
$scope.log.push( { msg: 'Data Received!' } );
});

视图代码:

<ul class="messages">
<li ng-repeat="entry in log">{{ entry.msg }}</li>
</ul>

或者

<div class="messages">
<div class="alert" ng-repeat="entry in log">
{{ entry.msg }}
</div>
</div>

这次没有使用ul 这种无序标签, 我们使用了Bootstrap alert 组件.  同时我们不再需要改变控制器里面的代码.更重要的是无论log数据如何变化, 视图会自动更新,真优雅!

尽管这里没有示例展示,但数据绑定是双向的. 所以在视图中log信息可以这样输入。例如:<input ng-model=”entry.msg” />. 真TM带劲!!

Distinct model layer   独立的数据层
在jQuery中, DOM本身就像一个数据层. 但在AngularJS中, 我们需要把数据层分开,这样方便管理数据并独立于视图外面.也有益于视图的数据绑定,管理维护,测试.

Separation of concerns  分离的思想(解耦)

上面所有的例子都指向一个主题: 时刻保持结构化分离的思想. 你的视图起到能表达功能意图的”官方的视角”.  你的数据层用来展现数据, 同时服务层用来完成一些重复的任务. 视图中操作DOM和修改的放到指令中directive, 然后把这一切的放到控制器中完成. 同时要可测试的.

Dependency injection 依赖注入

依赖注入功能 同样可以帮助我们使程序保持结构分离的思想. 如果你有后端开发的经验, 例如Java或PHP, 可能对DI已经很熟悉了. 但如果你是前端用jQuery开发的人员, 会感觉DI是很傻的或多余的. 但事实DI并不是那样.

通常来说, DI能帮助你很容易的在任何组件中调用一个组件, 你不需要关心加载顺序,文件路径,或其他烦心事. 也许DI的作用不一定非常强大,但用于测试,DI非常好用.

例如在应用中, 我们服务器端通过REST API, 同时依赖于程序状态和本地储存. 当测试的控制器时候, 控制器需要与后端数据通信, 我们只要通过DI替换与原数据层名字一样的服务层的组件, 这样控制器就自动得到假的数据, 控制器不需要改变任何代码.

4 Test-driven development – 总是采用TDD 测试驱动开发模式

TDD是非常重要的, 所以把TDD单独列出来

纵观网上非常多的jQuery 插件, 有多少有测试用例? 并不多,因为jQuery没有提供测试的土壤, 但是AngularJS提供了.

jQuery中, 经常只有一种方法去测试,创建一个带有DEMO或Sample的独立的组件,然后在测试用例中测试DOM操作. 我们需要单独开发组件,然后集成到我们的应用中测试. 的确不太方便. 用jQuery开发的很多时间里面,我们都在反复的人工测试而不是TDD.

但根据分离思想, 我们可以在Angular中很容易实现TDD. 例如我们想做一个非常简单指令标签 来实现当前选中的菜单,我们可以这样

<a href="/hello" when-active>Hello</a>

那么首先我们写一段测试

it( 'should add "active" when the route changes', inject(function() {
var elm = $compile( '<a href="/hello" when-active>Hello</a>' )( $scope );

$location.path('/not-matching');
expect( elm.hasClass('active') ).toBeFalsey();

$location.path( '/hello' );
expect( elm.hasClass('active') ).toBeTruthy();
}));

我们运行测试,结果失败. 然后我们实现我们的逻辑.

.directive( 'whenActive', function ( $location ) {
return {
scope: true,
link: function ( scope, element, attrs ) {
scope.$on( '$routeChangeSuccess', function () {
if ( $location.path() == element.attr( 'href' ) ) {
element.addClass( 'active' );
}
else {
element.removeClass( 'active' );
}
});
}
};
});

在运行测试, 通过了. 我们的开发过程既是迭代模式又是 测试驱动模式. 很赞

5 理论上,指令directives 并不是仅仅把jQuery打包.

你经常会听到 “只在指令中完成DOM操作” 这是必须的, 也是对的.

但如果深入一下

有些指令直接就操作视图中的属性,例如ng-Class. 因此有时DOM操作很直接就完成了.但如果指令是类似 “widget”组件这种有模板的. 同样要考虑分离的思想. 模板要与link 函数分离.

AngularJS有很多指令使操作DOM的工作更简单, 用ng-Class可以动态改变样式, ng-Bind可以双向绑定. ngShow和ngHide可以隐藏显示DOM元素.  就是说有很多方法避免DOM操作或少用DOM操作. 越少的DOM操作,就越容易测试,也越容易理解, 也容易维护, 也更方便重复使用.

我看到很多学习AngularJS的新人, 总是把一大坨jQuery的代码放到指令中. 因为新人想”不能在控制器中操作DOM,就把代码放到指令中”. 虽然这样已经好了点, 但仍然有问题.

在第三点中提到. 即使使用指令,同样要有”Angular 思想”  尽量要少操作DOM. 尽管有很多情况下需要操作DOM完成,但绝对比你想象的情况少. 任何时候想直接操作DOM的情况下, 先问一下自己”是否真的需要” 也许这是一个好方法避免直接操作DOM

这里有个简单的例子, 我们要做一个切换的按钮 (这个例子有点做作和有点长, 主要是为了表示一下很复杂的情况也是这样解决的.)

.directive( 'myDirective', function () {
return {
template: '<a class="btn">Toggle me!</a>',
link: function ( scope, element, attrs ) {
var on = false;

$(element).click( function () {
if ( on ) {
$(element).removeClass( 'active' );
}
else {
$(element).addClass( 'active' );
}

on = !on;
});
}
};
});

这个例子中有些错误, 第一,jQuery是不需要的. 第二即使其他地方引入了jQuery,我们还是可以用 angular.element 来替换. 第三, 即使要使用jQuery, jqLite (angular.element) 也会在引入jQuery时优先使用jQuery. 所以不要用$,而是angular.element.  第四, jqLite 不需要包裹$, 在link函数中,element 已经是一个jQuery元素被传了进去. 第五,之前说过,模板中与逻辑混在一起

改指令改进后

.directive( 'myDirective', function () {
return {
scope: true,
template: '<a class="btn" ng-class="{active: on}" ng-click="toggle()">Toggle me!</a>',
link: function ( scope, element, attrs ) {
scope.on = false;

scope.toggle = function () {
scope.on = !$scope.on;
};
}
};
});

总结

尽量不要用jQuery, 尽量不要引入jQuery (jQuery 着谁惹谁了?) 这样就会能把你引入正确的方向.
遇到问题时,尽管你知道如何用jQuery解决, 在使用$美元前,要想一下如何使用AngularJS的思维解决.  如果不会,就向社区问. 19次不行20次. 好的方法就是尽量不用jQuery解决.

 

译者:jinwyp  来源:angulargirl.com

原帖 :How do I “think in AngularJS/EmberJS (or other client MVC frameworks)” if I have a jQuery background?

 

关于Hadoop你需要知道的几件事情

在当今的技术领域,大数据是个热门的IT流行词语。为了减轻处理大量数据时的复杂度,Apache开发了Hadoop——一个可靠的、可扩展的分布式计算框架。Hadoop特别适合大数据处理任务,并且它可以利用其分布式的文件系统,可靠并且低成本的将数据块复制到集群中的节点上去,从而使数据能在本地机器上进行处理。Anoop Kumar从十个方面讲解了利用Hadoop处理大数据所需要的技巧。对于从HDFS中导入/导出数据方面,Anoop指出,在Hadoop的世界中,数据可以从多种不同的来源中被导入到Hadoop分布式文件系统中(HDFS)。在向HDFS中导入数据后,将通过用MapReduce或者其他语言比如Hive、Pig等来对数据进行某一层次的处理。

Hadoop系统不仅提供了处理大量数据的灵活性,并且同时也可以对数据进行过滤和聚合等处理,并且被处理转换过的数据可以导出到外部数据库或者其他使用Sqoop的数据库中。从MySQL、SQL Server或者MongoDB等其他数据库中导出数据也是一个强大的功能。这样的益处是可以更好的控制数据。

第二个方面是HDFS中的数据压缩,Hadoop中的数据存储在HDFS上,并且支持数据的压缩与解压缩。数据压缩可以通过一些压缩算法来实现,例如bzip2gzip、LZO等。不同的算法可以根据其功能在不同的情况下使用,比如压缩/解压缩的速度或者文件分割的能力等。

Hadoop的转换方面,Hadoop是一个用于提取和转换大量数据的理想环境。同时,Hadoop提供了一个可扩展、可靠的并且分布式的处理环境。通过使用MapReduce、Hive和Pig等,可以用很多种方式来提取并转换数据。

一旦输入数据被导入或放置到HDFS中,之后Hadoop集群可以被用于并行转换大型数据集。正如刚才提到的,数据转换可以通过可用工具来实现。例如,如果你想把数据转换为一个被制表符分开的文件,那么MapReduce则是最好的工具之一。同理,Hive和Python可以被用于清理和转换地理事件的数据资料。

对于如何实现通用的任务,Anoop介绍说,有很多通用的任务需要在数据的日常处理中被完成,并且其使用频率是很高的。一些如Hive、Pig和MapReduce等可用的语言可以协助你完成这些任务,并使你的生活更加轻松。

有时候一个任务可以通过多种方式来实现。在这种情况下开发人员或者架构师得做出正确的决定,从而实施最正确的方案。例如,Hive和Pig提供了数据流和查询之间的一个抽象层,并且提供了它们编译产生的MapReduc工作流。MapReduce的功能可以用于扩展查询。Hive可以用HiveQL(像SQL一样的说明性语言)来建立并且分析数据。并且,可以通过在Pig Latin中写入操作来利用Pig语言。

在Hadoop组合大量数据,一般情况下,为了得到最终的结果,数据需要加入多个数据集一起被处理和联合。Hadoop中有很多方法可以加入多个数据集。MapReduce提供了Map端和Reduce端的数据连接。这些连接是非平凡的连接,并且可能会是非常昂贵的操作。Pig和Hive也具有同等的能力来申请连接到多个数据集。Pig提供了复制连接,合并连接和倾斜连接(skewed join),并且Hive提供了map端的连接和完整外部连接来分析数据。一个重要的事实是,通过使用各种工具,比如MapReduce、Pig和Hive等,数据可以基于它们的内置功能和实际需求来使用它们。

如何在Hadoop分析大量数据,Anoop指出,通常,在大数据/Hadoop的世界,一些问题可能并不复杂,并且解决方案也是直截了当的,但面临的挑战是数据量。在这种情况下需要不同的解决办法来解决问题。一些分析任务是从日志文件中统计明确的ID的数目、在特定的日期范围内改造存储的数据、以及网友排名等。所有这些任务都可以通过Hadoop中的多种工具和技术如MapReduce、Hive、Pig、GiraphMahout等来解决。这些工具在自定义例程的帮助下可以灵活地扩展它们的能力。

例如,图和机器学习的问题可以通过使用一个Giraph框架被解决,而不是通过MapReduce任务解决,这样可以避免写复杂的算法。Giraph框架在解决图和机器学习问题时比MapReduce任务更加有用,因为一些问题可能需要运用迭代的步骤来解决。

Hadoop世界中的调试,调试在任何一个开发过程中都永远是个重要的过程。Hadoop环境中对于调试的需求和对Hadoop本身的需求一样重要。有一种说法是格式错误和意外的输入是很常见的,这将造成一切事务在一个较高的规模上中断。这也是处理大规模非结构化数据中的一个不幸的缺点。

虽然,单个任务被隔离并且给予了不同组的输入,但当跟踪各种事件时,它需要理解每个任务的状态。这可以通过多种可用的工具和技术来支持调试Hadoop任务的过程,从而实现目标。例如,为了避免任何工作失败,有一种方法可以跳过坏记录,并且可以使用MapReduce中的计数器来跟踪不良记录等。

易于控制的Hadoop系统,产品开发是一项重要的活动,系统维护也是同样重要的,它有助于决定产品的未来。在Hadoop中,环境设置、维护和环境监测、以及处理和调整MapReduce任务都非常需要从Hadoop系统中受益。为此Hadoop提供了很大的灵活性来控制整个系统,Hadoop的可在三种不同的模式中进行配置:即独立模式、伪分布式模式和完全分布式模式。

Ganglia框架的帮助下,整个系统可以被监测并且能对节点的健康状态进行跟踪。另外,参数配置功能提供了对MapReduce的任务控制。Hadoop系统有很好的灵活性可以轻松搞定整个系统的级别控制。

可扩展的持久性。有很多选择可以处理海量的结构化和非结构化的数据,但是储存海量数据的可扩展性仍然是数据世界中的主要问题之一。Hadoop系统打算用Accumulo来缓解这个问题。Accumulo是被谷歌的BigTable的设计所启发的,并且建立在Hadoop、Zookeeper 和Thrift的基础之上,同时它给Hadoop提供可扩展的、分布式的、且基于单元持久性的数据备份。Acumulo带来了一些BigTable设计之上的改进,以一种基于单元的访问控制和服务器端的编程机制来帮助在数据管理过程中修改不同点的键/值对。

Hadoop中的数据读取和写入发生在HDFS上。HDFS即Hadoop的分布式文件系统,并且是具有容错性的分布式文件系统。它在对进行文件流读取的大型文件进行了优化,而且和I/O吞吐量相比,更倾向于低延迟。有很多可以高效的从HDFS中读取和写入文件的方法,比如说API文件系统、MapReduce以及高级串行化库等。

 

作者 崔康   来源:infoq

InnoDB Memcached Plugin源码实现调研

背景

MySQL 5.6版本,新增了一个NoSQL的接口,通过将memcached嵌入到MySQL系统之中,用户可以直接使用memcached接口直接操作MySQL中的InnoDB表,绕过MySQL Server层面的SQL解析,优化,甚至绕过InnoDB Handler层,直接操作InnoDB内部的方法,从而达到更优的响应时间与效率。关于此功能的官方介绍,请见:InnoDB Integration with memcached 。嵌入memcached之后,整个MySQL的架构如下图所示:

b1

本文接下来的部分,将从源码的角度,详细分析InnoDB Integration with memcached的实现细节问题。

导读

InnoDB引擎为了支持Memcached API,在Handler层面进行的改动,请见(一);

Memcached Plugin的初始化流程,请见(二);

InnoDB提供的Callback方法在InnoDB Handlerton中的data中以及Memcached Plugin的data中,是如何传递的呢?请见(三);

InnoDB Memcached Engine提供的方法集合,请见(四);

InnoDB Memcached Engine提供的方法,如何调用到InnoDB Engine提供的方法,请见(五);

InnoDB Memcached Engine中,存在两个Engine实例:InnoDB Engine vs Default Engine,二者的功能异同,请见(六);

InnoDB Memcached Engine中,需要配置Container Table,Container表详解,请见(七);

InnoDB Memcached Engine的初始化与使用流程,可参考engine_testapp.c测试文件;

(一) InnoDB Handler层面改动

InnoDB为了支持Memcached接口,在其Handler层面提供了新的Callback方法,这些方法的定义如下:

   /** Set up InnoDB API callback function array */
            ib_cb_t innodb_api_cb[] = {
                (ib_cb_t) ib_cursor_open_table,
                (ib_cb_t) ib_cursor_read_row,
                (ib_cb_t) ib_cursor_insert_row,
                (ib_cb_t) ib_cursor_delete_row,
                (ib_cb_t) ib_cursor_update_row,
                (ib_cb_t) ib_cursor_next,
                (ib_cb_t) ib_cursor_last,
                (ib_cb_t) ib_tuple_get_n_cols,
                (ib_cb_t) ib_col_set_value,
                (ib_cb_t) ib_col_get_value,
                (ib_cb_t) ib_col_get_meta,
                (ib_cb_t) ib_trx_begin,
                (ib_cb_t) ib_trx_commit,
                (ib_cb_t) ib_trx_rollback,
                (ib_cb_t) ib_trx_start,
                …
            };

同时,innodb_api_cb[]数组,被存储在InnoDB Handlerton的data字段中,可以向上传递给InnoDB Memcached Engine。InnoDB Memcached Engine可以调用这些Callback方法,达到直接操作InnoDB Engine的目的。

ha_innodb.cc::innobase_init();
            …
            innobase_hton->data = &innodb_api_cb;

(二) Memcached Plugin的初始化流程

Memcached Plugin,同样注册为MySQL的一个插件(memcached_mysql.cc),此插件的定义如下:

 mysql_declare_plugin(daemon_memcached)
        {
            MYSQL_DAEMON_PLUGIN,
            &daemon_memcached_plugin,
            ”daemon_memcached”,
            ”Oracle Corporation”,
            ”Memcached Daemon”,
            PLUGIN_LICENSE_GPL,
            daemon_memcached_plugin_init,        /* Plugin Init */
            daemon_memcached_plugin_deinit,    /* Plugin Deinit */
            0×0100                                                            /* 1.0 */,
            NULL,                                                               /* status variables */
            daemon_memcached_sys_var,              /* system variables */
            NULL                                                                /* config options */
        }
        mysql_declare_plugin_end;

Memcached Plugin插件,初始化为daemon_memcached_plugin_init函数,该函数同样需要一个Handlerton输入,初始化Memcached Plugin插件,尤其是初始化InnoDB Memcached Engine。详细的初始化流程如下所示:

struct mysql_memcached_context * con;

    // plugin为InnoDB的Handlerton,plugin->data指向InnoDB Handlerton中的data,
    // 在InnoDB的init函数中被初始化为innodb_api_cb数组。innodb_api_cb数组中,
    // 注册了各种InnoDB提供的方法。
    …
    con->memcached_conf.m_innodb_api_cb = plugin->data;
    …

    // 创建Memcached Plugin后台线程,线程的主函数为daemon_memcached_main(),
    // 线程传入参数,即为从InnoDB Handlerton处获取的InnoDB提供的Callback方法
    pthread_create(&con->memcached_thread, &attr,
                daemon_memcached_main, (void *)&con->memcached_conf);
        …
        // 创建、加载InnoDB Memcached Engine
        Engine_loader.c::load_engine();
            // Create InnoDB Memcached Engine
            // 注册InnoDB Memcached Engine的各种方法,例如:
            // 初始化方法:innodb_eng->engine.initialize = innodb_initialize;
            innodb_engine.c::create_instance();
                …
        // 初始化InnoDB Memcached Engine
        Engine_loader.c::init_engine(engine_handle,engine_config…);
            Innodb_engine.c::innodb_initialize(engine, config_str);
                // 注册InnoDB Engine提供的内部方法至InnoDB Memcached Engine,
                // 包括:读取、插入、删除、更新、事务创建/提交/回滚等操作。
                // 至此,后续的InnoDB Memcached Engine的所有操作,均可被映射
                // 为InnoDB Storage Engine提供的内部方法进行实际操作。
                Innodb_api.c::register_innodb_cb();
                // Fetch InnoDB specific settings
                innodb_eng->meta_info = innodb_config(NULL, 0, &innodb_eng->meta_hash);
                    // Find the table and column info that used for memcached data
                    // Cantainers表:InnoDB内部存储元数据的系统表,
                    // MCI_CFG_DB_NAME;MCI_CFG_CONTAINER_TABLE;…
                    innodb_config.c::innodb_config_container();
                …
                // 开启InnoDB Memcached Engine的后台自动提交线程
                innodb_engine.c::innodb_bk_thread();
                    …
        …

    // 在InnoDB Handlerton中存储更多Memcached Plugin所需要的信息
    plugin->data = (void *)con;

(三) Memcached Plugin如何获取使用InnoDB Handlerton中的innodb_api_cb?

// 在此函数中完成innodb_api_cb的传递
sql_plugin.cc::plugin_initialize();
    // 此函数指针,直接调用到Plugin注册的init方法
    (*plugin_type_initialize[plugin->plugin->type])(plugin);
    …
    // 判断出当前是InnoDB引擎完成了初始化,则将InnoDB Handlerton中
    // 的data拷贝到临时变量中
    if (strcmp(plugin->name.str, “InnoDB”) == 0)
        innodb_callback_data = ((Handlerton *)plugin->data)->data;
    // 判断出当前Plugin为Memcached Plugin,则将临时变量赋值到memcached plugin
    // 的data中,完成InnoDB Handlerton提供的Callback方法的传递
    else if (plugin->plugin->init)
        if (strcmp(plugin->name.str, “daemon_memcached”) == 0)
            plugin->data = (void *)innodb_callback_data;
        // 初始化Memcached Plugin
        plugin->plugin->init();

(四) InnoDB Memcached Plugin提供的方法集合

 innodb_engine.c::create_instance();
            innodb_eng->engine.interface.interface = 1;
            innodb_eng->engine.get_info = innodb_get_info;
            innodb_eng->engine.initialize = innodb_initialize;
            innodb_eng->engine.destroy = innodb_destroy;
            innodb_eng->engine.allocate = innodb_allocate;
            innodb_eng->engine.remove = innodb_remove;
            innodb_eng->engine.release = innodb_release;
            innodb_eng->engine.clean_engine= innodb_clean_engine;
            innodb_eng->engine.get = innodb_get;
            innodb_eng->engine.get_stats = innodb_get_stats;
            innodb_eng->engine.reset_stats = innodb_reset_stats;
            innodb_eng->engine.store = innodb_store;
            innodb_eng->engine.arithmetic = innodb_arithmetic;
            innodb_eng->engine.flush = innodb_flush;
            innodb_eng->engine.unknown_command = innodb_unknown_command;
            innodb_eng->engine.item_set_cas = item_set_cas;
            innodb_eng->engine.get_item_info = innodb_get_item_info;
            innodb_eng->engine.get_stats_struct = NULL;
            innodb_eng->engine.errinfo = NULL;
            innodb_eng->engine.bind = innodb_bind;

所有InnoDB Memcached Engine提供的方法,都是类Memcached方法。例如:get/remove/store方法等。

(五) 一个InnoDB Memcached Plugin的操作的流程

通过Memcached Plugin进行一个删除操作的函数处理流程,如下:

注1:删除操作,对应的InnoDB Handlerton提供的方法为: remove()
注2:删除操作,对应的InnoDB Memcached Engine的方法为: ib_cursor_delete_row()

// 通过Memcached接口删除操作的处理流程
memcached.c::process_delete_command();
    settings.engine.v1->remove(settings.engine.v0, c, key, nkey, 0, 0);
    // InnoDB Memcached Engine层面提供的删除方法
    innodb_engine.c::innodb_remove(ENGINE_HANDLE*, cookie, key, nkey, …);
        …
        // 初始化InnoDB的连接,传入参数分析:
        // CONN_MODE_WRITE:当前为写操作
        // IB_LOCK_X:        当前操作需要对记录行加X锁
        conn_data = innodb_conn_init(innodb_eng, cookie, CONN_MODE_WRITE, …);
            if (conn_option == CONN_MODE_WRITE)
                // 开始一个事务
                innodb_api.c::innodb_cb_trx_begin();
                    ib_cb_trx_begin() -> api0api.cc::ib_trx_begin();
                // 打开当前表,并设置游标
                innodb_api.c::innodb_api_begin();
                    …
        // 进行真正的删除操作
        innodb_api.c::innodb_api_delete(innodb_eng, conn_data, key, nkey);
            // 根据传入的key,将游标定位到正确的位置
            innodb_api.c::innodb_api_search();
                …
            // 如果开启了binlog,则需要将定位到的记录拷贝出来
            if (engine->enable_binlog)
                …
            // 删除记录
                ib_cb_delete_row() -> api0api.cc::ib_cursor_delete_row();

            // 记录binlog
                handler_api.cc::handler_binlog_row();

            // 删除构造出来的Tuple对象
            ib_cb_tuple_delete() -> api0api.cc::ib_tuple_delete();

(六) InnoDB Engine vs Default Engine

在InnoDB Engine结构的内部(innodb_engine.h::struct innodb_engine),有两个实例化的Engine Handle,分别为:

ENGINE_HANDLE_V1    engine;
        ENGINE_HANDLE*        default_engine;

两个engine handle有何区别:

ENGINE_HANDLE_V1为InnoDB Engine Handle,封装了所有InnoDB引擎提供的方法;

ENGINE_HANDLE(default_engine)为Memcached自带的默认Engine,封装了所有标准Memcached所提供的方案,包括:slab分配,数据的存取、失效等等。

InnoDB Engine与Default Engine是否启用?是否同时启用?通过参数控制:

 /** Tells if we will used Memcached default engine or InnoDB Memcached engine to handle the request */
    typedef enum meta_cache_opt {
        META_CACHE_OPT_INNODB = 1,        /*!< Use InnoDB Memcached Engine only */
        META_CACHE_OPT_DEFAULT,             /*!< Use Default Memcached Engine only */
        META_CACHE_OPT_MIX,                        /*!< Use both, first use default memcached engine */
        META_CACHE_OPT_DISABLE,               /*!< This operation is disabled */
        META_CACHE_NUM_OPT                        /*!< Number of options */
    } meta_cache_opt_t;

默认的参数值为META_CACHE_OPT_INNODB,仅仅启用InnoDB Engine,数据通过InnoDB引擎提供的Callback方法操作;若同时启用了Memcached Default Engine(META_CACHE_OPT_MIX),那么数据读取时,首先从Default Engine中读取;记录删除时,也需要先删除Default Engine中的记录;

(七) Container表详解

InnoDB Memcached Engine,包含一个Container Table,用于存储Memcached与InnoDB Table之间的映射关系。
Container Table,必须包含9列,分别是:

/** Columns in the “containers” system table, this maps the Memcached
    operation to a consistent InnoDB table */
    typedef enum container {
        CONTAINER_NAME,         /*!< name for this mapping */
        CONTAINER_DB,                /*!< database name */
        CONTAINER_TABLE,        /*!< table name */
        CONTAINER_KEY,             /*!< column name for column maps to    memcached “key” */
        CONTAINER_VALUE,       /*!< column name for column maps to    memcached “value” */
        CONTAINER_FLAG,          /*!< column name for column maps to    memcached “flag” value */
        CONTAINER_CAS,             /*!< column name for column maps to    memcached “cas” value */
        CONTAINER_EXP,             /*!< column name for column maps to “expiration” value */
        CONTAINER_NUM_COLS    /*!< number of columns */
    } container_t;

其中:
CONTAINER_TABLE为InnoDB表名,CONTAINER_DB为对应的Database名;

CONTAINER_KEY为Memcached的Key对应的InnoDB表中的列名(只能一列);

CONTAINER_VALUE为Memcached的Value对应的InnoDB表中的列名(可以有多列,以” ;,|\n”作为分隔符,详见innodb_config.c::innodb_config_parse_value_col()函数);

CONTAINER Table的最后一列,为CONTAINER_KEY列对应的InnoDB表的唯一索引名(必须存在);

CONTAINER Table,在innodb_engine.cc::innodb_initialize函数被读取,将其中的每一项都解析并存储到InnoDB Engine的meta_hash结构之中:

innodb_config.c::innodb_config(NULL, 0, &innodb_eng->meta_hash);
        innodb_config_meta_hash_init();
            …
            // 打开CONTAINER Table
            innodb_api.c::innodb_api_begin(…, MCI_CFG_CONTAINER_TABLE, …);
            innodb_api.c::innodb_cb_read_row();
            innodb_config.c::innodb_config_add_item();
                …
                // 针对Value列,需要解析此列对应于InnoDB Table的哪些列
                // InnoDB中的不同列,以” ;,|\n”作为分隔符
                if (i == CONTAINER_VALUE)
                    innodb_config_parse_value_col();
                …

在innodb_engine.cc::innodb_initialize函数,完成Container Table的读取以及Meta Hash的填充之后,后续的Memcached方法,才可以根据规则操作,完成记录在Memcached与InnoDB引擎间的传递。

作者:何_登成 来源:深入MySQL内核