<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
  <channel>
    <title>sasion</title>
    <description></description>
    <link>http://sasion.javaeye.com</link>
    <language>UTF-8</language>
    <copyright>Copyright 2003-2008, JavaEye.com</copyright>
    <docs>http://blogs.law.harvard.edu/tech/rss</docs>
    <generator>JavaEye - 做最棒的软件开发交流社区</generator>
      <item>
        <title>RUP 持续过程2</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/177942" style="color:red;">http://sasion.javaeye.com/blog/177942</a>&nbsp;
          发表时间: 2008年03月31日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          <p align="center"><strong>捕获用例(UseCase)</strong></p><p>通过分析每个执行者的相关问题，来识别用例：</p><ul><li>执行者要求系统提供哪些功能？</li><li>执行者需要读，产生，删除，修改或者存储系统中的哪些信息？</li><li>必须提醒执行者的系统事件有哪些？</li></ul><p>另外：还可以针对整个系统提出下列问题，以帮助获取用例：</p><ul><li>系统需要哪些输入，输出？输入从何处来，输出到何处去。</li><li>当前系统运行的主要问题是什么？</li></ul><p>如果建立了业务模型，可以从业务活动图中，识别出支持业务自动的哪些用例。</p>
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/177942#comments" style="color:red;">已有 <strong>0</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Mon, 31 Mar 2008 11:11:06 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/177942</link>
        <guid>http://sasion.javaeye.com/blog/177942</guid>
      </item>
      <item>
        <title>RUP 持续过程1</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/177935" style="color:red;">http://sasion.javaeye.com/blog/177935</a>&nbsp;
          发表时间: 2008年03月31日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          <p align="center"><strong>识别执行者（Actor)</strong> </p><p>通过让用户回答以下的问题，来确定执行者：</p><ul><li>首要执行者：</li></ul><ol><li>谁使用系统的主要功能？</li><li>谁需要系统支持他们的日常工作？</li></ol><ul><li>辅助执行者：</li></ul><ol><li><div>谁来维护和管理系统，使期能正常工作？系统需要控制哪些硬件？</div></li><li><div>系统需要与其它哪些系统交互？</div></li></ol><ul><li>背后执行者：</li></ul><ol><li><div>哪些人或者事物对系统产生的结果感兴趣；</div></li></ol>
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/177935#comments" style="color:red;">已有 <strong>0</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Mon, 31 Mar 2008 10:51:38 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/177935</link>
        <guid>http://sasion.javaeye.com/blog/177935</guid>
      </item>
      <item>
        <title>软件高手是这样练成的</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/177188" style="color:red;">http://sasion.javaeye.com/blog/177188</a>&nbsp;
          发表时间: 2008年03月28日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          中国人大都喜欢用武侠小说来比较软件开发，但是在实战武功中，只有葵花宝典才是最厉害的，也只有掌握了葵花宝典，才能称为“不败”。 <br />       但什么才是软件开发的葵花宝典？ <br />       让我们先从一些现象出发。我们的前提是，软件开发是一项智力密集型劳动。对于智力密集型劳动，我们观察到的现象是，个体的表现差异很大，团队的表现差异很大，组织的表现差异很大，国家的表现差异很大。这不象体力占主要的劳动，象百米王跑百米的速度也仅比我快50%。但在棋类运动中，一个高手可以车轮战数位低手，而且毫无例外地将他们一一击败！ <br />      这些智力运动员表现出的特点是，计算精确而且速度快。其行为很象东方不败。虽然关于葵花宝典的传说很多，但最准确的描述只有一个字“快”。东方不败已经快到了吓人的地步。就象卡斯帕罗夫已快到了深蓝的地步。 <br /><br />　　有一则关于物理学家玻尔的轶事，有一次玻尔在普林斯顿大学听两个年青教授演讲他们的工作成果。期间玻尔突然发言说，如果照你们的研究算下去，会得到一个很有意思的推论。结果两个年青教授回去计算了两天，果然得出了同样的结论。玻尔是如何做到这样快的？ <br /><br />　　在软件开发中，我们同样注意到这样一种高手，他们可以每天写出一千行左右的高品质代码。他们可以运用已有的一些软件包，迅速完成一个新的产品。他们可以在很短的时间内，学会一项新的程序语言或是新技术。他们表现出一种神奇的速度。 <br /><br />　　在武侠小说中，所有的高手都有一些凡人不能企及的表现。象张无忌学太极，用龙爪手击败龙爪手名家；乔峰用太祖长拳击败天下英雄；姑苏慕容以其人之道还治其人之身，令狐冲一剑剌瞎十几双眼睛等等。我认为，之所以他们能做到这样，关键是在于他们快。 <br /><br />　　快并不意味着不准或品质差。快与品质并不矛盾。 <br /><br />　　高手的快，其实包含着很高的品质在其中。如果你因为高手的快，就质疑其品质，那就相当于在问：东方不败出手那么快，会不会刺不准？东方不败并不满足于刺死对手，他会在对手身上刺朵花。他把杀人变成了艺术。准确来说，他真正的兴趣不在杀人，而在于艺术。 <br /><br />　　退一步说，就算东方不败第一击有点偏差，他稍作修正后，马上跟上的第二第三击，也会击中他想击中的地方。在武功差的对手剑还没拨出来的时候，他已杀死对方并刺上了一朵花。 <br /><br />　　所以真正的软件高手，他并不满足于他的代码能有效地工作了，他认为编程是艺术，并醉心于其中。在低手能写出一个版本的时间里，他已经写出了第十版。其品质当然不可同日而语。就象一个九段棋手，在给定的时间里，他能计算十种可能，并将每种可能计算到100手之后，从中选择一种最有利的下法。低手岂有苟全的机会？ <br /><br />　　高手写软件总是不停地在重构(refactoring）。高手喜欢迭代式开发。高手说，增量就是打补丁，迭代就是推倒重来。对于软件这种东西，写一遍它可能ok（做到这一点也不容易），写十遍就是一个伟大的产品，再多写一遍它就更伟大些。 <br /><br />　　高手快的诀窍在于他很熟悉各种东西。高手看书很快，因为每一本新书里，值得他好好看的新技术只有一两章的内容。他能迅速看完，并准确领会这本书的中心思想和价值。而对于一个新手，每句话都是新的，他都需要去理解，每一段例子，他都需要去试。 <br /><br />　　很少看到一种100%全新的技术或理论。就象java language specification里说的，java没有使用任何新技术，用的都是业界久经考验的技术。对于高手来说，那些技术都是他所熟悉的。自然，很快他就从一个c++高手变成了java高手。如果一个编程新手学java，学两年也不如一个高手学两个月的。高手学新东西快。 <br /><br />　　高手写代码速度快。统计结果说，人均每人月的有效代码速度大概是300至400行。但那是业界平均生产效率。对于高手来说，这个数字太低了。每天写300至400行是完全有可能的。因为在写代码时，所有知识都已具备，已经没有任何需要他多花时间的事情了。他甚至很少需要debug。 <br /><br />　　高手重用代码的能力很强，熟悉新的api的速度很快。这也是因为，他曾经使用过很多的api，重用过很多的代码。他知道哪些是可用的，哪些有缺陷。他既过用qt，也用过gtk+，也用过windows api & mfc，也用过awt & swing。新的api对他来说，也是老熟人。 <br /><br /><br /><br />高手喜欢用轻量级的工具，象vi，notepad，最多到ultraedit这样复杂的。高手用这种工具写出很多的东西。这些工具就象东方不败的针。那根针已具有神奇的魔力，有时候它可以当激光枪来用。 <br /><br />　　对于一些重量级的工具，高手虽不常用，但一经使出也威力大于常人。如果让东方不败用剑，最厉害的剑术名家也会败得很难看。高手其实用过很多的重量级工具，而且深知其优缺点。所以使出来，就会把威力发挥到最大，而把缺陷减少到最小。而低手则不然，总是把缺陷加以大大的发扬而浑不知其精髓何在。就象很多人学用uml、rup、xp、design pattern那样。 <br /><br />　　高手所学博杂且融会贯通。高手做什么都快，当低手还在一愁莫展的时候，高手已经圆满解决问题，去干别的事去了。 <br /><br />　　相信你有一点点想成为高手了。但是有一个问题亟等解决，那就是“欲练神功，必先自宫”的问题。这一点其实是有比喻意义的。就是说，你必需抛弃一些世俗的人们很看重的东西。有诗为证： <br /><br />　　世人都晓高手好，只是寂寞受不了 <br /><br />　　世人都晓高手好, 只有名利忘不了 <br /><br />　　世人都晓高手好, 只有金钱一定要 <br /><br />　　世人都晓高手好, 天下美女都要抱 <br /><br />世人都晓高手好, 不写代码最最好 <br /><br />　　高手的武功不是一朝一夕练成的。还记得玻尔那件轶事吗，玻尔回答说，他年青时也计算过很多的问题。在很多计算的基础上，高手能培养起一种感觉。高手不写代码就能做设计是因为他以前写了很多的代码。而且他们会保持写代码，以保证自已的水平不下降。想一想九段高手是如何练成的。最难做到的是能忍受十年磨一剑的寂寞。别人在父母那里撒娇时，他们在一旁用功。十年磨一剑，剑就成了东方不败的针。 <br /><br />　　在你下定决心要做高手之后，也就是下定决心抛弃那些世俗的追求之后，也就是你下决心忍受那些来自于庸俗的人的白眼、攻击和谩骂之后，你就具备了练成神功的必要条件。 <br /><br />　　事实上其实你不必一开始就练神功，一开始大家可能是为了钱，房子，汽车，美女才编程序的，然而后来艺术就从中产生了。那时高手就不再关注那些东西了。卓别林曾说过，他开始进入那个圈子也是为了钱，后来艺术就从中产生了。当然，也有人一开始是为了艺术，后来变成为了钱。 <br /><br />　　所谓三十而立，就是说到了三十，你找到了你的真爱，值得用一生去追求的那种。比如说有的人到了三十认为这一辈子应该赚尽可能多的钱，这也没什么不好，也可以把赚钱本身变成一种艺术，所谓资本运作是也。所以在三十以前，有些私心杂念没什么。三十以后还这样是可耻的。而我，想做一个程序员。 <br /><br />　　每个人做自己最喜欢的事。这个世界需要程序员，也需要资本运作。所有真正的程序员，他最喜欢的事是编程和他自已。如果他后来去做ceo去了，不再编程，只说明他本来不是一个真正的程序员。 <br /><br />　　在成为高手的路上，要有热情，要循序渐进，要持之以恒。 <br /><br />　　要靠自己，书要快快地看。要试图迅速理解其主旨。其实你快快看所接受的信息量，与慢慢看接受的差不多。能明白多少很大程度上取决于你的功底。以后用到再回过头来看。一本对你来说新东西太多的书，不要指望看一次就全理解吸收。就象很多功力不够的人看design patterns那本书一样。慢慢看还不如找到多种 信息来源，都快快看一遍。对于一个完全陌生的领域，只看一本书很远远不够的。 <br /><br />要靠自已，事要快快做。有一个朋友，几年前我介绍他去玩玩linux，他也表示想玩，但他现在还没碰过。他失去了很多机会。<br /><br />平时要有意识提高自己写代码的速度，其实你一天写15行有效代码，与你写50行有效代码，其品质是差不多的。你应该把那些业界平均水平抛诸脑后，把超越自己做为唯一目标。等到你写了很多各式各样的代码，你的水平就不一般了。一个老师曾向我介绍他的学英语的决窍，他说你去啃原版小说，啃到50本，就和一般人有很大距离了。就是这个理。如果你写得太慢，怎么能写得多？水平怎么能提高？ <br /><br />　　要靠自己，学很多别人怕学的东西。低手总会说：这么多东西怎么学得过来啊。于是就少学或不学。这样就成不了高手了。高手有非常广的知识面，有很丰富的经验。知道很多低手不知道的事。玩过很多低手听都没听过的东西。 <br /><br />　　要靠自己，努力满足客户的各种需求。个人技能是在满足客户的各种需求的过程中提高的。比如你喜欢用delphi，客户说一定要用vb，那你就答应他，然后把自己培养成为vb的高手。用户的需求看似**，但对你是一个机会。 <br /><br />　　怎样才能做到看书快，写代码快，学新东西快，一个显而易见的途径就是将工作并行化。你在一台机器上make时，同时可以在看别的文档和聊天。对于计算机是这样，对人也是这样。如果你只能串行地处理问题，你的速度将提高有限。你的大脑有很大潜力可挖，它应该是一个多任务分时系统。努力减少它idle的时间。搞经济的samuelson被人称为human brain main frame，可见他的大脑有多快。 <br /><br />　　让你的思维快起来，你就会区别于那些反应迟钝的人。如果你不能让人生的道路变长，就让它变宽。这世界变化快，需要你变得比它快才行。 <br /><br />　　这样加快并不会让你短命，相反，你有更多的时间来享受生活和锻炼身体。你的生活将更有品质，更丰富，更有意义。面对变化，你将立于不败之地。我们都是和自己赛跑的人，需要跑得比昨天的自己更快。
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/177188#comments" style="color:red;">已有 <strong>1</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Fri, 28 Mar 2008 11:51:02 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/177188</link>
        <guid>http://sasion.javaeye.com/blog/177188</guid>
      </item>
      <item>
        <title>森林里的办公台</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/176211" style="color:red;">http://sasion.javaeye.com/blog/176211</a>&nbsp;
          发表时间: 2008年03月26日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          想像中的理想Coding环境应该是如此的：温暖舒适的阳光照在疏落有致的落木丛中.一张平坦光滑的简单办公桌放置在落木中间,阳光依稀可照办公桌上,落木丛上的小鸟轻快地唱歌嬉戏而不会飞来办公桌上.一张坐上去刚好齐胸的办公椅距办公桌一米平静地放置着,办公桌上放上轻便而功能齐全的笔记本电脑,电脑里放出自己喜欢的美妙歌声.此时打开熟悉的IDE进行Coding,在这种环境下Coding灵感可谓是汹涌澎湃。。
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/176211#comments" style="color:red;">已有 <strong>0</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Wed, 26 Mar 2008 11:45:54 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/176211</link>
        <guid>http://sasion.javaeye.com/blog/176211</guid>
      </item>
      <item>
        <title>JAVA 通讯协议基础框架</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/176099" style="color:red;">http://sasion.javaeye.com/blog/176099</a>&nbsp;
          发表时间: 2008年03月26日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          前两天做的一个基于MINA的基本协议框架。实际当中只要实现自己的编解码器，处理类。并启动服务线程就行了。<br />   需要提供的包：<br />    1.mina-core-1.1.5.jar<br />   2.log4j-1.2.14.jar<br />   3.slf4j-api-1.4.3.jar<br />   4.slf4j-log4j12-1.4.3.jar
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/176099#comments" style="color:red;">已有 <strong>4</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Wed, 26 Mar 2008 08:45:16 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/176099</link>
        <guid>http://sasion.javaeye.com/blog/176099</guid>
      </item>
      <item>
        <title>高性能的HTTP引擎—— Grizzly(三)    Grizzly的特点</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/174036" style="color:red;">http://sasion.javaeye.com/blog/174036</a>&nbsp;
          发表时间: 2008年03月20日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          对企业级的服务器软件,高性能和可扩展性是基本的要求。除此之外,还应该有应对各种不同环境的能力。例如,一个好的服务器软件不应该假设所有的客户端都有很快的处理能力和很好的网络环境。如果一个客户端的运行速度很慢,或者网络速度很慢,这就意味着整个请求的时间变长。而对于服务器来说,这就意味着这个客户端的请求将占用更长的时间。这个时间的延迟不是由服务器造成的,因此CPU的占用不会增加什么,但是网络连接的时间会增加,处理线程的占用时间也会增加。这就造成了当前处理线程和其他资源得不到很快的释放,无法被其他客户端的请求来重用。例如Tomcat,当存在大量慢速连接的客户端时,线程资源被这些慢速的连接消耗掉,使得服务器不能响应其他的请求了。<br /><br />前面介绍过,NIO的异步非阻塞的形式,使得很少的线程就能服务于大量的请求。通过Selector的注册功能,可以有选择性地返回已经准备好的频道,这样就不需要为每一个请求分配单独的线程来服务。<br /><br />在一些流行的NIO的框架中,都能看到对OP_ACCEPT和OP_READ的处理。很少有对OP_WRITE的处理。我们经常看到的代码就是在请求处理完成后,直接通过下面的代码将结果返回给客户端:<br /><br />【例17.7】不对OP_WRITE进行处理的样例:<br /><br />while (bb.hasRemaining()) {<br /><br />    int len = socketChannel.write(bb);<br /><br />    if (len &lt; 0) {<br /><br />        throw new EOFException();<br /><br />    } <br /><br />}<br /><br />这样写在大多数的情况下都没有什么问题。但是在客户端的网络环境很糟糕的情况下,服务器会遭到很沉重的打击。<br /><br />因为如果客户端的网络或者是中间交换机的问题,使得网络传输的效率很低,这时候会出现服务器已经准备好的返回结果无法通过TCP/IP层传输到客户端。这时候在执行上面这段程序的时候就会出现以下情况。<br /><br />(1) bb.hasRemaining()一直为“true”,因为服务器的返回结果已经准备好了。<br /><br />(2) socketChannel.write(bb)的结果一直为0,因为由于网络原因数据一直传不过去。<br /><br />(3)   因为是异步非阻塞的方式,socketChannel.write(bb)不会被阻塞,立刻被返回。<br /><br />(4)   在一段时间内,这段代码会被无休止地快速执行着,消耗着大量的CPU的资源。事实上什么具体的任务也没有做,一直到网络允许当前的数据传送出去为止。<br /><br />这样的结果显然不是我们想要的。因此,我们对OP_WRITE也应该加以处理。在NIO中最常用的方法如下。<br /><br />【例17.8】一般NIO框架中对OP_WRITE的处理:<br /><br />while (bb.hasRemaining()) {<br /><br />    int len = socketChannel.write(bb);<br /><br />    if (len &lt; 0){ <br /><br />        throw new EOFException();<br /><br />    } <br /><br />    if (len == 0) {<br /><br />        selectionKey.interestOps(<br /><br />                        selectionKey.interestOps() | SelectionKey.OP_WRITE);<br /><br />        mainSelector.wakeup(); <br /><br />        break;<br /><br />    }<br /><br />}<br /><br />上面的程序在网络不好的时候,将此频道的OP_WRITE操作注册到Selector上,这样,当网络恢复,频道可以继续将结果数据返回客户端的时候,Selector会通过SelectionKey来通知应用程序,再去执行写的操作。这样就能节约大量的CPU资源,使得服务器能适应各种恶劣的网络环境。<br /><br />可是,Grizzly中对OP_WRITE的处理并不是这样的。我们先看看Grizzly的源码吧。在Grizzly中,对请求结果的返回是在ProcessTask中处理的,经过SocketChannelOutputBuffer的类,最终通过OutputWriter类来完成返回结果的动作。在OutputWriter中处理OP_WRITE的代码如下:<br /><br />【例17.9】Grizzly中对OP_WRITE的处理:<br /><br />public static long flushChannel(SocketChannel socketChannel, <br /><br />        ByteBuffer bb, long writeTimeout) throws IOException<br /><br />{<br /><br />    SelectionKey key = null;<br /><br />    Selector writeSelector = null;<br /><br />    int attempts = 0;<br /><br />    int bytesProduced = 0;<br /><br />    try {<br /><br />        while (bb.hasRemaining()) {<br /><br />            int len = socketChannel.write(bb);<br /><br />            attempts++;<br /><br />            if (len &lt; 0){<br /><br />                throw new EOFException();<br /><br />            } <br /><br />            bytesProduced += len;<br /><br />            if (len == 0) {<br /><br />                if (writeSelector == null){<br /><br />                    writeSelector = SelectorFactory.getSelector();<br /><br />                    if (writeSelector == null){<br /><br />                        // Continue using the main one<br /><br />                        continue;<br /><br />                    } <br /><br />                }<br /><br />                key = socketChannel.register(writeSelector, key.OP_WRITE);<br /><br />                if (writeSelector.select(writeTimeout) == 0) {<br /><br />                    if (attempts > 2)<br /><br />                        throw new IOException("Client disconnected");<br /><br />                } else {<br /><br />                    attempts--;<br /><br />                }<br /><br />            } else {<br /><br />                attempts = 0;<br /><br />            }<br /><br />        }<br /><br />    } finally {<br /><br />        if (key != null) {<br /><br />            key.cancel();<br /><br />            key = null;<br /><br />        }<br /><br />        if (writeSelector != null) {<br /><br />            // Cancel the key.<br /><br />            writeSelector.selectNow();<br /><br />            SelectorFactory.returnSelector(writeSelector);<br /><br />        }<br /><br />    }<br /><br />    return bytesProduced;<br /><br />}  <br /><br />上面的程序例17.9与例17.8的区别之处在于:当发现由于网络情况而导致的发送数据受阻(len==0)时,例17.8的处理是将当前的频道注册到当前的Selector中;而在例17.9中,程序从SelectorFactory中获得了一个临时的Selector。在获得这个临时的Selector之后,程序做了一个阻塞的操作:writeSelector.select(writeTimeout)。这个阻塞操作会在一定时间内(writeTimeout)等待这个频道的发送状态。如果等待时间过长,便认为当前的客户端的连接异常中断了。<br /><br />这种实现方式颇受争议。有很多开发者置疑Grizzly的作者为什么不使用例17.8的模式。另外在实际处理中,Grizzly的处理方式事实上放弃了NIO中的非阻塞的优势,使用writeSelector.select(writeTimeout)做了个阻塞操作。虽然CPU的资源没有浪费,可是线程资源在阻塞的时间内,被这个请求所占有,不能释放给其他请求来使用。<br /><br />Grizzly的作者对此的回应如下。<br /><br />(1)   使用临时的Selector的目的是减少线程间的切换。当前的Selector一般用来处理OP_ACCEPT,和OP_READ的操作。使用临时的Selector可减轻主Selector的负担;而在注册的时候则需要进行线程切换,会引起不必要的系统调用。这种方式避免了线程之间的频繁切换,有利于系统的性能提高。<br /><br />(2)   虽然writeSelector.select(writeTimeout)做了阻塞操作,但是这种情况只是少数极端的环境下才会发生。大多数的客户端是不会频繁出现这种现象的,因此在同一时刻被阻塞的线程不会很多。<br /><br />(3)   利用这个阻塞操作来判断异常中断的客户连接。<br /><br />(4)   经过压力实验证明这种实现的性能是非常好的。<br /><br />17.3.2  如何避免内存泄漏<br /><br />在NIO的框架模型中,值得注意的是有一个API由于NIO非阻塞的特点,其使用比较频繁,那就是java.nio.channel.SelectionKey.attach()。<br /><br />这是因为在非阻塞的频道中,在socketChannel.read(byteBuffer)的调用中,往往不能返回所有的请求数据,其他的部分数据可能要在下一次(或几次)的读取中才能完全返回。因此在读取一些数据之后,需要将当前的频道重新注册到Selector上:<br /><br />selectionKey.interestOps(<br /><br />selectionKey.interestOps() | SelectionKey.OP_READ);<br /><br />这样还不够,因为前几次读取的部分数据也需要保留,将所有读取的数据综合起来才是完整的数据,因此需要调用下面的函数将部分数据保存起来供以后使用:<br /><br />selectionKey.attach(...)<br /><br />这个函数设计的目的也在于此,主要用于异步非阻塞的情况保存恢复与频道相关的数据。但是,这个函数非常容易造成内存泄漏。这是因为在非阻塞的情况下,你无法保证这个带有附件的SelectionKey什么时候再次返回到准备好的状态。在一些特殊的情况下(例如,客户端的突然断电或网络问题)导致代表这些连接的SelectionKey永远也不会返回到准备好状态了,而一直存放在Selector中,它们所带的附件也就不会被Java自动回收内存的机制释放掉。内存泄漏对长时间运行的服务器端软件是不能容忍的重大隐患。那么我们看看在Grizzly中是如何处理这种问题的。<br /><br />事实上,在Grizzly的实现中很少看到selectionKey.attach(...)的代码。在入口程序SelectThread中的enableSelectionKeys()方法中有这个方法的调用。<br /><br />【例17.10】SelectThread中的enableSelectionKeys()方法:<br /><br />public void enableSelectionKeys(){<br /><br />    SelectionKey selectionKey;<br /><br />    int size = keysToEnable.size();<br /><br />    long currentTime = (Long)System.currentTimeMillis();<br /><br />    for (int i=0; i &lt; size; i++) {<br /><br />        selectionKey = keysToEnable.poll();<br /><br />        selectionKey.interestOps(<br /><br />                selectionKey.interestOps() | SelectionKey.OP_READ);<br /><br />        if (selectionKey.attachment() == null)<br /><br />            selectionKey.attach(currentTime);<br /><br />            keepAlivePipeline.trap(selectionKey);   <br /><br />        } <br /><br />}<br /><br />}<br /><br />显而易见,这个函数的目的在这里是要给每个selectionKey加上一个时间戳。这个时间戳是为KeepAlive系统而加的。怎样防止这个long类型对象的内存泄漏呢?在SelectThread的doSelect()方法中有一个expireIdleKeys()的调用。<br /><br />【例17.11】SelectThread的expireIdleKeys()方法:<br /><br />protected void expireIdleKeys(){<br /><br />    if (keepAliveTimeoutInSeconds &lt;= 0 || !selector.isOpen()) return;<br /><br />    long current = System.currentTimeMillis();<br /><br />    if (current &lt; nextKeysExpiration) {<br /><br />        return;<br /><br />    }<br /><br />    nextKeysExpiration = current + kaTimeout;<br /><br />        <br /><br />    Set&lt;SelectionKey> readyKeys = selector.keys();<br /><br />    if (readyKeys.isEmpty()){<br /><br />        return;<br /><br />    }<br /><br />    Iterator&lt;SelectionKey> iterator = readyKeys.iterator();<br /><br />    SelectionKey key;<br /><br />    while (iterator.hasNext()) {<br /><br />        key = iterator.next();<br /><br />        if (!key.isValid()) {<br /><br />            keepAlivePipeline.untrap(key); <br /><br />            continue;<br /><br />        }  <br /><br />                        <br /><br />        // Keep-alive expired<br /><br />        if (key.attachment() != null) {<br /><br />                <br /><br />            if (!defaultAlgorithmInstalled <br /><br />                        && !(key.attachment() instanceof Long)) { <br /><br />                continue; <br /><br />            }<br /><br />                    <br /><br />            try{<br /><br />                long expire = (Long)key.attachment();<br /><br />                if (current - expire >= kaTimeout) {<br /><br />                    if (enableNioLogging){<br /><br />                        logger.log(Level.INFO,<br /><br />                                  "Keep-Alive expired for SocketChannel " + <br /><br />                                  key.channel());<br /><br />                    }                    <br /><br />                    cancelKey(key);<br /><br />                } else if (expire + kaTimeout &lt; nextKeysExpiration){<br /><br />                    nextKeysExpiration = expire + kaTimeout;<br /><br />                }<br /><br />            } catch (ClassCastException ex){                            <br /><br />                if (logger.isLoggable(Level.FINEST)){<br /><br />                    logger.log(Level.FINEST,<br /><br />                               "Invalid SelectionKey attachment",ex);<br /><br />                }<br /><br />            }<br /><br />        }<br /><br />    }                    <br /><br />}  <br /><br />上面代码的作用显而易见:在每次doSelect()的调用中,expireIdleKeys()都会被执行一次,来查看selector中的每个SelectionKey,将它们的时间戳与当前的时间相比,判断是否当前的SelectionKey很长时间没有响应了,然后根据配置的timeout时间,强行将其释放和回收。<br /><br />那么系统用来存放每一次请求读取的数据放在哪里了呢?一般来说这个存放频道数据的对象应该是ByteBuffer。在DefaultReadTask类中,可以看到ByteBuffer的使用情况。<br /><br />【例17.12】DefaultReadTask中的doTask()方法:<br /><br />public void doTask() throws IOException {   <br /><br />    if (byteBuffer == null) {<br /><br />        WorkerThread workerThread = (WorkerThread)Thread.currentThread();<br /><br />        byteBuffer = workerThread.getByteBuffer();<br /><br />        if (workerThread.getByteBuffer() == null){<br /><br />            byteBuffer = algorithm.allocate(useDirectByteBuffer,<br /><br />                      useByteBufferView,selectorThread.getBufferSize());<br /><br />            workerThread.setByteBuffer(byteBuffer);<br /><br />        }<br /><br />    }<br /><br />    doTask(byteBuffer);<br /><br />}<br /><br />上面的方法透露出两个重要的信息:<br /><br />l   对ByteBuffer的分配,并不是每个SelectionKey(或者说每个网络连接)都有自己的ByteBuffer,而是每个工作线程拥有一个ByteBuffer。<br /><br />l   ByteBuffer的分配也不是新创建的ByteBuffer对象,而是通过ByteBufferView来对原有的ByteBuffer对象进行重新分割。原因是新建一个ByteBuffer对象的系统消耗比较大,因此Grizzly在启动的时候初始创建了一个大的ByteBuffer对象。以后每个线程再需要ByteBuffer对象的时候,就通过ByteBufferView来在原有ByteBuffer之上创建一个视图,这样的性能要好得多。<br /><br />如果说每个线程只使用一个ByteBuffer对象(确切地说是ByteBufferView对象),而在NIO中,每个线程是要服务于多个连接请求的,那么线程是怎样维护每个连接请求的数据的独立性呢?从DefaultReadTask中的doTask(ByteBuffer byteBuffer)方法中,我们可以看到最初始的读取调用以及对读取数据的处理过程。<br /><br />【例17.13】DefaultReadTask中的doTask(ByteBuffer byteBuffer)方法:<br /><br />protected void doTask(ByteBuffer byteBuffer){<br /><br />    int count = 0;<br /><br />    Socket socket = null;<br /><br />    SocketChannel socketChannel = null;<br /><br />    boolean keepAlive = false;<br /><br />    Exception exception = null;<br /><br />    key.attach(null);<br /><br />     <br /><br />    try {<br /><br />        socketChannel = (SocketChannel)key.channel();<br /><br />        socket = socketChannel.socket();<br /><br />        algorithm.setSocketChannel(socketChannel);            <br /><br />           <br /><br />        int loop = 0;<br /><br />        int bufferSize = 0;<br /><br />        while (socketChannel.isOpen() && (bytesAvailable || <br /><br />                ((count = socketChannel.read(byteBuffer))> -1))){  // [1]<br /><br />            ...<br /><br />            byteBuffer = algorithm.preParse(byteBuffer);<br /><br />            inputStream.setByteBuffer(byteBuffer);   // [2]<br /><br />            inputStream.setSelectionKey(key);<br /><br />            <br /><br />            // try to predict which HTTP method we are processing<br /><br />            if (algorithm.parse(byteBuffer) ){   // [3]<br /><br />                keepAlive = executeProcessorTask();  //  [4]<br /><br />                if (!keepAlive) {<br /><br />                    break;<br /><br />                }<br /><br />            }<br /><br />            ...<br /><br />        }<br /><br />}<br /><br />...<br /><br />}<br /><br />在例17.13的方法中,可以清楚地看到,在此方法中程序作了初始的读取动作[1]socketChannel.read(byteBuffer)。初始读取完后,其实并不知道是否所有的请求数据都已经读进来了。于是程序交给HTTP的一个解析算法类(algorithm)来决定是否所有的请求数据都已经读取进来了。接着这个请求就交给[4]executeProcessorTask()去执行了。在executeProcessorTask()中使用了一个ByteBufferInputStream类,这个类是对ByteBuffer的一个封装,并在[2]中进行了设置和初始化。事实上,在默认的解析算法中,客户端的请求在第一次读取动作中如果没有全部完成,那剩余部分的数据其实就交给ByteBufferInputStream来完成了。<br /><br />【例17.14】ByteBufferInputStream中的doRead()方法:<br /><br />/**<br /><br /> * Read bytes using the ReadSelector<br /><br /> */<br /><br />protected int doRead() throws IOException{        <br /><br />    if (key == null) return -1;<br /><br />        <br /><br />    byteBuffer.clear();<br /><br />    int count = 1;<br /><br />    int byteRead = 0;<br /><br />    Selector readSelector = null;<br /><br />    SelectionKey tmpKey = null;<br /><br />    try{<br /><br />        SocketChannel socketChannel = (SocketChannel)key.channel();<br /><br />        while (count > 0){<br /><br />            count = socketChannel.read(byteBuffer);  //[1]<br /><br />            if (count > -1)<br /><br />                byteRead += count;<br /><br />            else<br /><br />                byteRead = count;<br /><br />        }            <br /><br />            <br /><br />        if (byteRead == 0){<br /><br />            readSelector = SelectorFactory.getSelector(); //[2]<br /><br />            if (readSelector == null){<br /><br />                return 0;<br /><br />            }<br /><br />            count = 1;<br /><br />            tmpKey = socketChannel<br /><br />                    .register(readSelector,SelectionKey.OP_READ);               <br /><br />            tmpKey.interestOps(<br /><br />tmpKey.interestOps() | SelectionKey.OP_READ);<br /><br />            int code = readSelector.select(readTimeout); // [3]<br /><br />            tmpKey.interestOps(<br /><br />                    tmpKey.interestOps() & (~SelectionKey.OP_READ));<br /><br />            if (code == 0){<br /><br />                return 0; // Return on the main Selector and try again.<br /><br />            }<br /><br />            while (count > 0){<br /><br />                count = socketChannel.read(byteBuffer); // [4]<br /><br />                if (count > -1)<br /><br />                    byteRead += count;<br /><br />                else<br /><br />                    byteRead = count;                    <br /><br />            }<br /><br />        }<br /><br />    } finally {<br /><br />        if (tmpKey != null)<br /><br />            tmpKey.cancel();<br /><br />        if (readSelector != null){<br /><br />            // Bug 6403933<br /><br />            try{<br /><br />                readSelector.selectNow();<br /><br />            } catch (IOException ex){<br /><br />                ;<br /><br />            }<br /><br />            SelectorFactory.returnSelector(readSelector);<br /><br />        }<br /><br />    }<br /><br />    byteBuffer.flip();<br /><br />    return byteRead;<br /><br />}<br /><br />查看过这个方法之后,觉得很有意思:对每个连接保存的数据的ByteBuffer对象,在Grizzly中根本不会有什么内存泄漏的问题。因为在Grizzly中根本没有使用NIO模式中设计方法(将ByteBuffer附加到SelectionKey中,再将SelectionKey重新注册到Selector中等待下次激活)。在Grizzly中对请求数据的读取完全使用了传统的阻塞方式,根本不需要attach和将SelectionKey重新注册到Selector。<br /><br />当读取数据的任务交给ByteBufferInputStream的时候,ByteBufferInputStream会再做一次最大的努力来读取可能有的数据[1]。如果还是没有读取到什么数据的话,Grizzly并没有将SelectionKey重新注册到主线程的Selector,而是从Selector池中获得一个临时的Selector[2],将SelectionKey重新注册到这个临时的Selector中。接着这个临时的Selector做了一个阻塞的操作readSelector.select(readTimeout)[3],这个动作一直会阻塞到当前频道有数据进来,或者阻塞时间超过Timeout的时间。<br /><br />这种算法也颇受争议。有的人认为使用阻塞的模式性能不会比NIO中非阻塞的模式好,特别是在有很多网络速度很慢的客户端的情况下,这样会大量造成线程的占用而变得不具有很好的可扩展性。<br /><br />Grizzly的作者也承认,如果在大量慢速的客户端的情况下,使用非阻塞模式肯定要好些。但是他为自己的实现算法也给了下面一些理由。<br /><br />(1)   假设大多数客户端的速度良好是合理的。因此大多数的请求数据在一到两次都能全部读取。<br /><br />(2)   对连接异常的客户端可以在最早时间范围内进行判断和做出放弃的决定,保护系统的资源不被浪费。<br /><br />(3)   这样实现没有内存泄漏的问题,而且内存消耗也要小些,能够获得更好的性能。<br /><br />(4)   因为每个连接所有的读取过程都在一个线程中完成,不用在主线程(Selector所在的线程)之间切换,可以减少操作系统的线程调度负担,并且减少主线程的消耗。<br /><br />17.3.3  使用多个Selector<br /><br />经常会有人问:什么是企业级应用?也经常看到一些产品的说明书标称该软件产品为企业级产品。究竟什么样的产品才能有资格被称作企业级产品?这个问题很难被回答,每个人的标准是不一样的。最近在进行一些企业级别应用测试的时候,发现扩展性是比较重要的一个指标。<br /><br />当时测试的硬件是比较高档的服务器,具有32个或64个以上的CPU。由于稳定性和安全性的原因,这些多CPU的UNIX服务器是当前大型企业应用关键业务系统所愿意使用的运行环境。但是在测试的时候发现,在少量CPU的情况下(2到6个),绝大多数软件系统都能比较充分地利用机器提供的资源,获得不错的性能指标。当测试压力不断增加,需要更多的CPU的时候,不同的应用系统所表现出来的扩展性就大不相同了。很多开源的软件,包括一些开源的数据库软件和应用服务器在CPU超过8个的时候性能不升反降,无法充分利用硬件系统提供CPU资源。而那些商业的数据库软件(包括Oracle、DB2、Informix、Sybase)和应用服务器(BEA Weblogic、Sun JES)都能够在多达64个CPU的系统上扩展得很好。并不是说开源软件不好,这是个定位问题。如果是企业级软件系统,那么就应该在最初的设计和最后的测试环节都应该考虑到扩展性的问题:当这个系统给予了更多的硬件资源的时候,是不是能够运行得更快,或者能支持更多的服务请求。<br /><br />从源码中可以看到,GlassFish到处都考虑到扩展性的问题,真正将自己定位于企业级的应用了,先不提GlassFish中对负载均衡和集群的支持,在Grizzly中Selector的设计和实现就充分考虑了扩展性的要求。<br /><br />一般NIO的框架结构应该是这样的:首先创建ServerSocketChannel的实例,并且获得一个Selector的实例,将它绑定到相应的端口上,再将ServerSocketChannel配置成非阻塞模式,接着将OP_ACCEPT注册到这个Selector上。<br /><br />【例17.15】传统NIO中的主线程:<br /><br />serverSocketChannel = ServerSocketChannel.open();<br /><br />selector = Selector.open();<br /><br />serverSocket = serverSocketChannel.socket();<br /><br />serverSocket.setReuseAddress(true);<br /><br />serverSocket.bind(new InetSocketAddress(port),ssBackLog);<br /><br />serverSocketChannel.configureBlocking(false);<br /><br />serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);<br /><br />当OP_ACCEPT事件发生的时候,需要将新产生的SocketChannel的OP_READ注册到这个Selector中去。<br /><br />【例17.16】传统对OP_ACCEPT的处理:<br /><br />protected void handleAccept(SelectionKey key) throws IOException{<br /><br />    ServerSocketChannel server = (ServerSocketChannel)key.channel();<br /><br />    SocketChannel channel = server.accept();<br /><br />    if (channel != null) {<br /><br />        channel.configureBlocking(false);<br /><br />        SelectionKey readKey =<br /><br />                channel.register(selector, SelectionKey.OP_READ);<br /><br />        setSocketOptions(((SocketChannel)readKey.channel()).socket());<br /><br />    }<br /><br />    ...<br /><br />}<br /><br />这种处理方式在大并发客户数量的情况下,很容易使得这个主线程变得很繁忙:它既要负责OP_ACCEPT和OP_READ的注册和监控,还需要负责OP_ACCEPT的处理(OP_READ的处理一般在另外的线程中);除此以外,主线程还有可能要负责监控客户端的连接是否异常,来保证没有内存泄漏的情况。因为单个线程只能在单个CPU中执行,在用户并发数量很多的情况下,主线程可能被延迟。一旦主线程被延迟,系统其他部分的运行都会受到很大的影响。<br /><br />在Grizzly中可以配置使用多个Selector(和多个Selector线程)。在Grizzly中存在与多个Selector配置相关的参数。<br /><br />【例17.17】SelectorThread中对多个Selector配置相关参数的定义:<br /><br />/**<br /><br /> * The number of SelectorReadThread<br /><br /> */<br /><br />protected int multiSelectorsCount = 0;<br /><br />/**<br /><br /> * The Selector used to register OP_READ<br /><br /> */    <br /><br />protected MultiSelectorThread[] readThreads; <br /><br />【例17.18】SelectorThread中对多个Selector配置相关参数的使用:<br /><br />protected void handleAccept(SelectionKey key) throws IOException{<br /><br />    ServerSocketChannel server = (ServerSocketChannel)key.channel();<br /><br />    SocketChannel channel = server.accept();<br /><br />...<br /><br />    if (channel != null) {<br /><br />        if (multiSelectorsCount > 1) {<br /><br />            MultiSelectorThread srt = getSelectorReadThread();<br /><br />            srt.addChannel(channel);<br /><br />        } else {<br /><br />            channel.configureBlocking(false);<br /><br />            SelectionKey readKey = <br /><br />                   channel.register(selector, SelectionKey.OP_READ);<br /><br />setSocketOptions(((SocketChannel)readKey<br /><br />.channel()).socket());<br /><br />        }<br /><br />    }<br /><br />}<br /><br />【例17.19】MultiSelectorThread接口的实现类SelectorReadThread:<br /><br />public class SelectorReadThread extends SelectorThread <br /><br />        implements MultiSelectorThread{<br /><br />    /**<br /><br />     * List of Channel to process<br /><br />     */<br /><br />    ArrayList&lt;SocketChannel> channels = new ArrayList&lt;SocketChannel>();<br /><br />    /**<br /><br />     * Int used to differenciate this instance<br /><br />     */<br /><br />    public static int countName;<br /><br />    /**<br /><br />     * Add a Channel to be processed by this Selector<br /><br />     */<br /><br />    public synchronized void addChannel(SocketChannel channel) <br /><br />            throws IOException, ClosedChannelException {<br /><br />        channels.add(channel);<br /><br />        getSelector().wakeup();<br /><br />}<br /><br />    /**<br /><br />     * Register all Channel with an OP_READ opeation<br /><br />     */<br /><br />    private synchronized void registerNewChannels() throws IOException{<br /><br />        int size = channels.size();<br /><br />        for (int i = 0; i &lt; size; i++) {<br /><br />            SocketChannel sc = channels.get(i);<br /><br />            sc.configureBlocking(false);<br /><br />            try {<br /><br />                SelectionKey readKey = <br /><br />                        sc.register(getSelector(), SelectionKey.OP_READ);<br /><br />setSocketOptions(((SocketChannel)readKey<br /><br />.channel()).socket());<br /><br />            } catch (ClosedChannelException cce) {}<br /><br />        }<br /><br />        channels.clear();<br /><br />    }<br /><br />...<br /><br />}<br /><br />从例17.18和例17.19的代码中可以看出,当配置有多个Selector的时候,在处理OP_ACCEPT时,新建立的连接可以交给MultiSelectorThread的类来监控和管理这些连接的OP_READ事件,分担了主线程的负担。在多CPU大并发用户的情况下,使得系统具有较好的扩展性。<br /><br />17.3.4  Grizzly其他的特点<br /><br />1. 异步请求处理<br />在应用服务器中,我们通常使用的请求都是同步的请求。当客户端的请求进来以后被服务器所解析,随后Servlet或JSP被调用,运行的结果被返回到客户端。但是在一些情况下,这种同步的处理过程不能很好地满足要求。例如,被执行的业务逻辑需要调用外部的一个服务,而这个服务响应得很慢;或者客户的请求介入到一个工作流程当中,被外部的因素所中断(需要老板批准等)。在这些情况下,虽然使用同步机制也能实现,但是轮询或阻塞的方法对系统资源的消耗比较大,系统结构也因此变得复杂。<br /><br />在Grizzly中有一个com.sun.enterprise.web.connector.grizzly.async包,用来实现异步的请求处理。<br /><br />2. 服务器推送技术<br />服务器推送技术(Comet)现在非常的流行,结合AJAX和服务器推送技术,可以实现非常灵活和高性能的应用程序。在com.sun.enterprise.web.connector.grizzly.comet的包中,Grizzly将异步处理请求和服务器推送技术完美地结合在一起,使GlassFish成为支持服务器推送技术的开源产品之一。<br /><br />3. 资源分配和管理<br />Application Resource Allocation(RAR,应用资源分配)本应该是操作系统或硬件层面上的话题。现代的计算机系统提供了各种各样的资源虚拟技术,有的在操作系统中,有的是在服务器硬件中,还有的是通过跨平台的框架(例如网格技术)将企业内部的各种资源进行划分和合理应用。这种需求非常多,因为在企业内部存在的各种应用的重要程度和优先级别都不同,级别高的重要应用不应该受到其他应用的影响,应当享有资源分配的优先权。<br /><br />但是不同操作系统、不同的网格技术对应用资源分配的方式各不相同。在Grizzly中存在着三个包:com.sun.enterprise.web.ara、com.sun.enterprise.web.ara.algorithms、com.sun.enterprise. web.ara.rules。通过这三个包,在Grizzly中就可以实现对部署在它上面的应用进行资源分配和管理。管理的规则主要包括以下两类:<br /><br />l   当前应用所占Java Heap的百分比。<br /><br />l   当前应用所占线程数量的百分比。<br /><br />4. 统一端口<br />在安装应用服务器的过程中,很重要的一件事情就是分配端口号。一般来说,一个应用服务器分配的端口不只一个,可能有三、四个,还可能更多。这些端口包括各种不同的服务或协议监听所在的Socket,有HTTP端口,有HTTPS端口,有IIOP端口,还有其他通信端口。如果是在一台共享的服务器上安装应用服务器,情况要更加糟糕,因为有的端口已经被别的应用所占用,有时不得不在启动服务器的时候手动修改端口号。<br /><br />在Grizzly中有两个包:com.sun.enterprise.web.portunif和com.sun.enterprise.web.portunif. util,这两个包的功能是统一端口号。通过这两个包,GlassFish可以只启动一个端口,仍然可以服务于多个不同的协议。例如4848端口既是HTTP的端口,又是HTTPS的端口,还是IIOP的端口。这样就大大简化了管理员的工作。<br /><br />Grizzly通过可插拔的形式来定义不同的协议和协议的处理程序。主要的接口如下。<br /><br />l   ProtocolFinder:当请求进来以后,通过ProtocolFinder来确定当前的请求是什么协议。Grizzly默认实现了HttpProtocolFinder和HttpsProtocolFinder。<br /><br />l   ProtocolHandler:当确定了使用什么协议以后,相应的协议处理单元就会被调用。在协议处理单元中可以做任何想做的事情,比如进行EJB的调用,进行负载均衡或请求转发等。<br /><br />17.3.5  Grizzly的性能<br /><br />Grizzly在整个设计和开发过程中,性能和高扩展性是它的核心。从源码的各个细节都可以看出Grizzly对高性能的追求。例如,对ByteBufferView的使用、对多个Selector的支持、对不同线程模型的配置、对多个HTTP解析算法的选择,以及对OP_READ和OP_WRITE的特殊处理都反映了Grizzly对高性能一丝不苟的严格要求。<br /><br />通过内部的性能测试,并且和传统的Web服务器的比较,事实数据证明了Grizzly是性能和可扩展性都非常高的HTTP引擎,请参看图17-2。在大并发压力的测试下,它的性能甚至超过了两款使用C语言编写的Web服务器。与传统的Java阻塞式的HTTP引擎相比,Grizzly的性能和扩展性远远地超过它们<br /><img src="http://book.csdn.net/BookFiles/551/img/image034.jpg" />
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/174036#comments" style="color:red;">已有 <strong>1</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Thu, 20 Mar 2008 11:47:50 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/174036</link>
        <guid>http://sasion.javaeye.com/blog/174036</guid>
      </item>
      <item>
        <title>高性能的HTTP引擎—— Grizzly(二)    Grizzly简介</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/174032" style="color:red;">http://sasion.javaeye.com/blog/174032</a>&nbsp;
          发表时间: 2008年03月20日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          Grizzly简介<br />正如前文所说,用Java技术来编写一个扩展性能很高的服务器软件是件很困难的事情。Java虚拟机的线程管理机制使得纯Java写的HTTP引擎很难响应成千上万的并发用户。正如Tomcat一样,在并发用户数不是很高的情况下能够获得很高的吞吐量,但是在高并发的情况下性能下降很快,变得不太稳定。<br /><br />在JDK 1.4推出NIO之后,有很多基于NIO的框架出现,利用NIO的新特性,来编写高性能的HTTP引擎。其中以Jean-Francois Arcand的Grizzly最为引人瞩目。Grizzly最早被用于Sun Java System Application Server, Platform Edition 8.1。随后成为开源软件GlassFish的一部分。在今后,Sun Java System Application Server 9.x的Platform Edition和Enterprise Edition都会使用Grizzly作为HTTP引擎。<br /><br />17.2.1  Grizzly的基本架构<br /><br />图17-1描述了Grizzly的基本架构。<br /><br /><img src="http://book.csdn.net/BookFiles/551/img/image033.jpg" /><br /><br />图17-1  Grizzly的基本架构<br /><br />Grizzly的基本架构主要包含以下几个方面:Pipeline、SelectorThread和Task。下面分别加以介绍。<br /><br />1. Pipeline<br />在com.sun.enterprise.web.connector.grizzly包下,有许多与Pipeline相关的类,例如Pipeline、KeepAlivePipeline、ThreadPoolExecutorPipeline、LinkedListPipeline等。Pipeline是个不太好理解的词汇,其实把这些类叫做ThreadPoolWrapper可能更加合适和容易理解。只要熟悉服务器端的软件,对Thread Pool(线程池)一定不会陌生。线程比起进程来说,消耗的资源要少,共享数据更加简单。因此,现在大多数服务器软件(特别是HTTP服务器)都会采用多线程模式。但是线程的创建和关闭仍然是比较慢的系统服务,聪明的服务器软件设计者会在系统启动的时候,预先创建一些线程,并且将这些线程管理起来,在系统正常运行的时候服务于客户的请求。通过这样的手段,线程不需要在使用的时候临时创建,大大提高了软件的运行速度和效率。对这种线程的管理方法叫做线程池。线程池中的线程需要互相协作,有序地执行客户的请求。一般用于同步线程的结构叫任务队列。客户的请求根据先后顺序被放到了任务队列中,线程池中空闲的线程会从任务队列中获得任务并执行。<br /><br />Grizzly中的Pipeline实际上封装了一个Thread Pool(线程池)和一个任务队列。Pipeline的主要目的是封装了一个统一的接口,可以让Grizzly根据配置文件任意选择不同算法的线程池,来获得不同的特点和性能。在Grizzly中已经实现了好几种线程池。其中有ThreadPoolExecutorPipeline(基于java.util.concurrent.ThreadPoolExecutor来实现的线程池),还有LinkedListPipeline(使用简单的linklist数据结构管理的线程池)。在早期的Grizzly中还会看到一些其他的实现。经过测试以后,淘汰了一些性能不好的算法,目前只剩下这两种Pipeline了。事实上在大并发用户的测试中,LinkedListPipeline的性能是最好的,因此被设置为默认的选择。在以后的版本中,ThreadPoolExecutorPipeline也可能会消失,只保留性能最好的算法是明智的选择。但是现在还存在两种算法,其主要原因是java.util.concurrent.ThreadPoolExecutor的名声太响,所有的文章和测试都曾经证明过它的高性能。就连Grizzly的作者本身都不相信LinkedListPipeline的性能要比ThreadPoolExecutorPipeline好,只不过当前的测试结果事实如此。因此该作者自己也说,一旦有证据证明ThreadPoolExecutorPipeline的性能又重新超过LinkedListPipeline,他会立即将默认的设置指向ThreadPoolExecutorPipeline。<br /><br />KeepAlivePipeline是一个特例,它并不是用来执行特定任务的,而是用来维护HTTP协议中的持久连接的状态,例如维护最大的持久连接数,持久连接的timeout时间等。另外,异步的socketChannel中缺少一个类似socket.setSoTimeout的函数,这个函数在保证服务器软件的可靠性和安全性(抗DOS攻击)上,具有重要的作用。Grizzly是用KeepAlivePipeline类来模拟socket.setSoTimeout的作用。<br /><br />2. SelectorThread<br />这是Grizzly的主要入口类,位于com.sun.enterprise.web.connector.grizzly的包下。在SelectorThread中,SocketChannel和Selector被创建并被初始化。当网络有请求进来的时候,Selector会根据不同的请求类型和NIO的不同事件进行不同的处理。<br /><br />当NIO的事件为OP_READ的时候,表明是原有的连接中有新的请求数据传过来了。这类请求属于ReadTask,应该交给负责处理ReadTask的处理器来处理。ReadTask有自己的Pipeline(也就是线程池)来处理,这样就不会占用主线程来处理Read的请求。<br /><br />当NIO的事件为OP_ACCEPT的时候,表明是有新的请求进来了,这类请求属于AcceptTask,应该交给负责处理AcceptTask的处理器来处理。在老版本的GlassFish中,AcceptTask也有自己的Pipeline来处理,这样就让AcceptTask在主线程以外的线程中执行。但是经过多次性能测试和比较,发现当AcceptTask在主线程(SelectorThread)中执行的时候,性能最好。因此,在读最新的Grizzly源代码的时候,会发现图17-1中的AcceptPipeline根本不存在,因为AcceptTask已经由SelectThread类中HandleAccept函数来执行了。<br /><br />当ReadTask执行完以后,表明整个请求的数据已经完全接收到,就可以进行请求处理了,请求处理属于ProcessTask,交给负责处理ProcessTask的处理器来处理。ProcessTask有自己的Pipeline(也就是线程池)来处理,这样就不会占用主线程来处理请求。<br /><br />3. Task<br />在Grizzly的框架中包含下面几种任务。<br /><br />(1)   AcceptTask:用于响应新的连接请求。前面已经说过,这个任务的类事实上已经不存在,没有单独抽象出来。因为处理Accept已经成为SelectThread内部的一部分了。<br /><br />(2)   ProcessTask:用于处理并且响应请求。这个任务通常是对请求的数据进行解析,解析完后再将请求传递给其他服务的容器(如Servlet容器)进行真正的业务处理。<br /><br />(3)   ReadTask:用于SocketChannel最初的读取操作。由于NIO是非阻塞的操作,最初的读取往往不能获得全部的请求数据,这时候,ReakTask会将任务委托给StreamAlgorithm,根据不同实现,用不同的方法将剩下的请求数据获取。<br /><br />在com.sun.enterprise.web.connector.grizzly.algorithms的包下,Grizzly默认实现了4个算法:<br /><br />l   ContentLengthAlgorithm<br /><br />l   SeekHeaderAlgorithm<br /><br />l   StateMachineAlgorithm<br /><br />l   NoParsingAlgorithm<br /><br />前3个算法主要是围绕HTTP请求中的Content-length字段来进行解析。只要能读到这个字段的值,那么我们就可以预先判断整个请求的长度,从而确定什么时候完成请求读取,接着进行请求处理了。第4个算法是对请求数据根本不进行预处理,假设所有的数据都读进来了。如果最后发现请求数据读得不完全,再交给请求处理任务(ProcessTask)来负责将剩下的内容读取过来。<br /><br />17.2.2  源码阅读指南<br /><br />根据图17-1的结构,结合Grizzly的源代码,可以看到Grizzly的大致脉络。<br /><br />SelectorThread是个入口,根据Grizzly所在的不同环境,启动的方法有所不同。如果Grizzly作为单独可运行的应用(Grizzly可以从GlassFish中独立出来),在com.sun.enterprise. web.connector.grizzly.standalone包下的Main类是这样使用SelectorThread的:<br /><br />【例17.4】单独运行的Grizzly对SelectorThread的调用:<br /><br />private static void start(String args[]) throws Exception {<br /><br />...<br /><br />    SelectorThread selectorThread = null;<br /><br />    String selectorThreadClassname = System.getProperty(SELECTOR_THREAD);<br /><br />    if (selectorThreadClassname != null){<br /><br />        selectorThread = loadInstance(selectorThreadClassname);<br /><br />    } else {<br /><br />        selectorThread = new SelectorThread();<br /><br />    }<br /><br />    selectorThread.setPort(port);<br /><br />    StaticResourcesAdapter adapter = new StaticResourcesAdapter();<br /><br />    adapter.setRootFolder(folder);       <br /><br />    selectorThread.setAdapter(adapter);<br /><br />    selectorThread.setDisplayConfiguration(true);<br /><br />    selectorThread.initEndpoint();<br /><br />    selectorThread.startEndpoint();<br /><br />}<br /><br />如果Grizzly是在GlassFish中,它作为服务线程,run()方法是整个线程启动的钥匙。从源码中很容易看出在run()方法中调用了startEndpoint()方法,startEndpoint()在做好一些准备工作之后,调用了startListener()。startListener()便进入了主线程的循环之中。在循环中只有一个方法,那就是doSelect()方法。<br /><br />在doSelect()中,可以很清楚地看到NIO的框架结构。<br /><br />【例17.5】SelectorThread中的doSelect():<br /><br />selectorState = selector.select(selectorTimeout);<br /><br />...<br /><br />readyKeys = selector.selectedKeys();<br /><br />iterator = readyKeys.iterator();<br /><br />while (iterator.hasNext()) {<br /><br />    key = iterator.next();<br /><br />    iterator.remove();<br /><br />    if (key.isValid()) {<br /><br />        handleConnection(key);<br /><br />    } else {<br /><br />        cancelKey(key);<br /><br />    }<br /><br />}<br /><br />与大多数NIO的架构一样,先是调用selector.select(selectorTimeout),看看当前的频道有没有数据准备好了。如果有的话,通过selector.selectedKeys()将准备好的这些频道的SelectionKey取到。对这些频道的处理就交给handleConnection(key)函数了。<br /><br />【例17.6】SelectorThread中的handleConnection:<br /><br />protected void handleConnection(SelectionKey key) throws <br /><br />IOException,InterruptedException<br /><br />{<br /><br />    Task task = null;<br /><br />    if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT){<br /><br />        handleAccept(key);<br /><br />        return;<br /><br />    } else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ){<br /><br />        task = handleRead(key);<br /><br />    } <br /><br />if (((SocketChannel)key.channel()).isOpen()) {<br /><br />        task.execute();<br /><br />} else {<br /><br />        cancelKey(key);<br /><br />} <br /><br />}<br /><br />handleConnection函数很短,但是有一些重要的特点需要指出来。handleConnection的主要功能是区分那些已经准备好的频道,看看它们是属于新的连接(OP_ACCEPT)还是有新的请求数据(OP_READ)。<br /><br />如果是OP_ACCEPT,那么就调用函数handleAccept(key)。这个函数会在当前的线程内执行,主要的功能就是根据新来的连接创建新的频道,再将这个频道注册到Selector中。如果是OP_READ,那么就调用函数handleRead(key)。这个函数返回了一个Task。通过task.execute()将这个任务的实际运行交给Pipeline中的线程池来执行。换句话说,对新的请求数据的处理是在另外的线程中来处理的,而不是当前的线程。<br /><br />事实上,在早期的Grizzly的版本中,对OP_ACCEPT的处理与OP_READ一样,也是有单独的任务(AcceptTask)和单独的线程来执行。但是经过性能测试,证明当对OP_ACCEPT的处理在主线程的时候性能最好。因此就取消了AcceptTask在单独线程中的处理,演化为当前的模型。<br /><br />再随后的工作主要就交给ReadTask和ProcessTask去做了。这里不作详细的介绍。
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/174032#comments" style="color:red;">已有 <strong>0</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Thu, 20 Mar 2008 11:44:37 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/174032</link>
        <guid>http://sasion.javaeye.com/blog/174032</guid>
      </item>
      <item>
        <title>高性能的HTTP引擎—— Grizzly(一)   NIO简介</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/174030" style="color:red;">http://sasion.javaeye.com/blog/174030</a>&nbsp;
          发表时间: 2008年03月20日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          NIO简介<br />作为Java EE Web层面的最前端,HTTP引擎是负责接收客户请求的最开始的部分,这部分的性能在很大程度上决定了整个Java EE产品的性能和可扩展性。回顾现有的J2EE产品,大部分的HTTP引擎都不是用纯Java编写的。例如,Sun的JES应用服务器内置了一个用本地语言(C/C++)开发Web服务器,JBoss的Web Server也不是纯Java的,它使用了大量与平台相关的运行库,只不过通过Apache的APR项目(http://apr.apache.org)来维护跨平台的特性。而那些纯Java的J2EE服务器,在部署的时候也推荐前置一个其他的Web服务器,例如(Apache、IIS等)。<br /><br />使用纯Java来构建具有扩展性很好的服务器软件,一直是一个比较困难的事情,特别是在单个的Java虚拟机上(非集群的环境)。这是由Java的线程模型和网络IO的特性所决定的。在JDK 1.4以前,Java的网络IO的接口都是阻塞式的,这意味着网络的阻塞会引起处理线程的停止,因此每个用户请求的处理从开始到最后完成,需要单独的处理线程。而Java的线程资源的分配和线程的调度都是有很大开销的,这使得在大量请求(数千个甚至上万个)同时到达的情况下,单个Java虚拟机很难满足大并发性的需要。为了解决可扩展性的问题,一些解决方案使用了多个Java虚拟机或者多个机器节点进行集群来满足大并发的请求。<br /><br />JDK 1.4版本(包括之后的版本)最显著的新特性就是增加了NIO(New IO),能够以非阻塞的方式处理网络的请求,这就使得在Java中只需要少量的线程就能处理大量的并发请求了。但是使用NIO不是一件简单的技术,它的一些特点使得编程的模型比原来阻塞的方式更为复杂。<br /><br />Grizzly作为GlassFish中非常重要的一个项目,就是用NIO的技术来实现应用服务器中的高性能纯Java的HTTP引擎。Grizzly还是一个独立于GlassFish的框架结构,可以单独用来扩展和构建自己的服务器软件。<br /><br />本章重点:<br />l   NIO的基本特点和编程方式<br /><br />l   Grizzly的基本结构<br /><br />l   Grizzly对NIO技术的运用手段<br /><br />l   Grizzly对性能上的考虑和优化<br /><br />17.1  NIO简介<br />理解NIO是学习本章的重要前提,因为Grizzly本身就是基于NIO的框架结构,所有的技术问题都是在NIO的技术上进行讨论的。如果读者对NIO不了解的话,建议首先了解NIO的基本概念。对NIO的介绍和学习指南很多,本章不会对NIO做详细的讲解。下面仅对NIO做一个简单的介绍,并列出与本章内容相关的一些NIO特性。<br /><br />17.1.1  NIO的基本概念<br /><br />在JDK 1.4的新特性中,NIO无疑是最显著和鼓舞人心的。NIO的出现事实上意味着Java虚拟机的性能比以前的版本有了较大的飞跃。在以前的JVM的版本中,代码的执行效率不高(在最原始的版本中Java是解释执行的语言),用Java编写的应用程序通常所消耗的主要资源就是CPU,也就是说应用系统的瓶颈是CPU的计算和运行能力。在不断更新的Java虚拟机版本中,通过动态编译技术使得Java代码执行的效率得到大幅度提高,几乎和操作系统的本地语言(例如C/C++)的程序不相上下。在这种情况下,应用系统的性能瓶颈就从CPU转移到IO操作了。尤其是服务器端的应用,大量的网络IO和磁盘IO的操作,使得IO数据等待的延迟成为影响性能的主要因素。NIO的出现使得Java应用程序能够更加紧密地结合操作系统,更加充分地利用操作系统的高级特性,获得高性能的IO操作。<br /><br />NIO在磁盘IO处理和文件处理上有很多新的特性来提高性能,本文不作详细的解释,而仅仅介绍NIO在处理网络IO方面的新特点,这些特点是理解Grizzly的最基本的概念。<br /><br />1. 数据缓冲(Buffer)处理<br />数据缓冲(Buffer)是IO操作的基本元素。其实从本质上来说,无论是磁盘IO还是网络IO,应用程序所作的所有事情就是把数据放到相应的数据缓冲当中去(写操作),或者从相应的数据缓冲中提取数据(读操作)。至于数据缓冲中的数据和IO设备之间的交互,则是操作系统和硬件驱动程序所关心的事情了。因此,数据缓冲在IO操作中具有重要的作用,是操作系统与应用之间的IO桥梁。在NIO的包中,Buffer类是所有类的基础。Buffer类当中定义数据缓冲的基本操作,包括put、get、reset、clear、flip、rewind等,这些基本操作是进行数据输入输出的手段。每一个基本的Java类型(boolean除外)都有相应的Buffer类,例如CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer和ByteBuffer。我们所关心的是ByteBuffer,因为操作系统与应用程序之间的数据通信最原始的类型就是Byte。<br /><br />“Direct ByteBuffer”是一个值得关注的Buffer类型。在创建ByteBuffer的时候可以使用ByteBuffer.allocateDirect()来创建一块直接(Direct)的ByteBuffer。这一块数据缓冲和一般的缓冲不一样。第一,它是一块连续的空间。第二,它的实现不是纯Java的代码,而是本地代码,它内存的分配不在Java的堆栈中,不受Java内存回收的影响。这种直接的ByteBuffer是NIO用来保证性能的重要手段。刚才提到,数据缓冲是操作系统和应用程序之间的IO接口。应用程序将需要“写出去”的数据放到数据缓冲中,操作系统从这块缓冲中获得数据执行写的操作。当IO设备数据传进来的时候,操作系统就会将数据放到相应的数据缓冲中,应用程序从缓冲中“读进”数据进行处理。一般的Java对象很难胜任这个直接的数据缓冲的工作。因为Java对象所占用的内存空间不一定是连续的,而且经常由于内存回收而改变地址。而操作系统需要的是一片连续的不变动的地址空间,才能完成IO操作。在原来的Java版本中需要Java虚拟机的介入,将数据进行转换、拷贝才能被操作系统所使用。而通过“Direct ByteBuffer”,应用程序能够直接与操作系统进行交流,大大减少了系统调用的次数,提高了执行的效率。<br /><br />数据缓冲的另外一个重要的特点是可以在一个数据缓冲上再建立一个或多个视图(View)缓冲。这个概念有些类似于数据库视图的概念:在数据库的物理表(Table)结构之上可以建立多个视图。同样,在一个数据缓冲之上也可以建立多个逻辑的视图缓冲。视图缓冲的用处很多,例如可以将Byte类型的缓冲当作Int类型的视图,来进行类型转换。视图缓冲也可以将一个大的缓冲看成是很多小的缓冲视图。这对提高性能很有帮助,因为创建物理的数据缓冲(特别是直接的数据缓冲)是非常耗时的操作,而创建视图却非常快。在Grizzly中就有这方面的考虑。<br /><br />2. 异步通道(Channel)<br />Channel(后文又称频道,译法仅暗示存在多通道可选)是NIO的另外一个比较重要的新特点。Channel并不是对原有Java类的扩充和完善,而是完全崭新的实现。通过Channel,Java应用程序能够更好地与操作系统的IO服务结合起来,充分地利用上文提到的ByteBuffer,完成高性能的IO操作。Channel的实现也不是纯Java的,而是和操作系统结合紧密的本地代码。<br /><br />Channel的一个重要的特点是在网络套接字频道(SocketChannel)中,可以将其设置为异步非阻塞的方式。<br /><br />【例17.1】非阻塞方式的频道使用:<br /><br />SocketChannel sc = SocketChannel.open();<br /><br />sc.configureBlocking(false); // nonblocking<br /><br />...<br /><br />if (!sc.isBlocking()) {<br /><br />doSomething(cs);<br /><br />}<br /><br />通过SocketChannel.configureBlocking(false)就可以将网络套接字频道设置为异步非阻塞模式。一旦设置成非阻塞的方式,从Socket中读和写就再也不会阻塞。虽然非阻塞只是一个设置问题,但是对应用程序的结构和性能却产生了天翻地覆的变化。<br /><br />3. 有条件的选择(Readiness Selection)<br />熟悉UNIX的程序员对POSIX的select()或poll()函数应该比较熟悉。在现在大多数流行的操作系统中,都支持有条件地选择已经准备好的IO通道,这就使得只需要一个线程就能同时有效地管理多个IO通道。在JDK 1.4以前,Java语言是不具备这个功能的。<br /><br />NIO通过几个关键的类来实现这种有条件的选择的功能:<br /><br />(1)   Selector<br /><br />Selector类维护了多个注册的Channel以及它们的状态。Channel需要向Selector注册,Selector负责维护和更新Channel的状态,以表明哪些Channel是准备好的。<br /><br />(2)   SelectableChannel<br /><br />SelectableChannel是可以被Selector所管理的Channel。FileChannel不属于Selectable- Channel,而SocketChannel是属于这类的Channel。因此在NIO中,只有网络的IO操作才有可能被有条件地选择。<br /><br />(3)   SelectionKey<br /><br />SelectionKey用于维护Selector和SelectableChannel之间的映射关系。当一个Channel向Selector注册之后,就会返回一个SelectionKey作为注册的凭证。SelectionKey中保存了两类状态值,一是这个Channel中哪些操作是被注册了的,二是有哪些操作是已经准备好的。<br /><br />17.1.2  NIO之前的Server程序的架构<br /><br />在NIO出现以前(甚至在NIO出现了很长时间的现在),在用Java编写服务器端的程序时,服务请求的接收模块大多数都会采用以下的框架(例如在Tomcat中的连接接入点:org.apache.tomcat.util.net.PoolTcpEndpoint就有相类似的结构)。<br /><br />【例17.2】阻塞方式的server编程框架:<br /><br />class Server implements Runnable {<br /><br />public void run() {<br /><br />try {<br /><br />ServerSocket ss = new ServerSocket(PORT);<br /><br />while (!Thread.interrupted())<br /><br />new Thread(new Handler(ss.accept())).start();<br /><br />} catch (IOException ex) { /* ... */ }<br /><br />}<br /><br />static class Handler implements Runnable {<br /><br />final Socket socket;<br /><br />Handler(Socket s) { socket = s; }<br /><br />public void run() {<br /><br />try {<br /><br />byte[] input = new byte[MAX_INPUT];<br /><br />socket.getInputStream().read(input);<br /><br />byte[] output = process(input);<br /><br />socket.getOutputStream().write(output);<br /><br />} catch (IOException ex) { /* ... */ }<br /><br />}<br /><br />private byte[] process(byte[] cmd) { /* ... */ }<br /><br />}<br /><br />}<br /><br />上面的结构比较简单:在主线程的run()方法中,会有ServerSocket的accept()方法,它被循环地调用着,直到服务停止。accept()方法会被阻塞,直到新的连接请求的到来。当新的连接请求进来以后,系统会使用另外的线程来处理这个请求。处理线程在socket端口进行read()调用,读取所有的请求数据。read()也是一个阻塞的方法,一直到读取完所有的数据才会返回。数据经过处理以后,在同一个处理线程中将请求结果返回给客户端。在实际情况中,会比这个结构复杂得多,例如,处理线程是从一个线程池中获取,而不是每次都产生一个新的线程。<br /><br />这种结构在大多数情况下都可以获得很好的性能。例如Tomcat在性能指标的测试中获得了很高的吞吐量测量值。但是在并发性很大的情况下,这种结构不具有很好的可扩展性。例如有2000个客户请求同时到来,如果想要这2000个请求被同时处理,则需要2000个处理线程。这些线程在大多数的情况下可能都不在运行,而是阻塞在read()或write()的方法上了。在一台机器或者一个Java虚拟机上运行上千个线程是个挑战,线程经常会阻塞,因此CPU会在这些线程之间来回调度和切换,这会引起大量的系统调用和资源竞争,使得整个系统的扩展性能不高。<br /><br />17.1.3  使用NIO来提高系统扩展性<br /><br />NIO使用非阻塞的API,通过实现少量的线程就能服务于大量的并发用户的请求。并且通过操作系统都支持的POSIX标准的select方式,来获得系统准备就绪的资源。使用这些手段,NIO就能够充分利用每个活动的线程来服务于大量的请求,减少系统资源的浪费。通常来说,一个NIO的服务架构会采用以下的结构。<br /><br />【例17.3】使用NIO的server编程框架:<br /><br />public class Server {<br /><br />    public static void main(String[] argv) throws Exception {<br /><br />        ServerSocketChannel serverCh = ServerSocketChannel.open();<br /><br />        Selector selector = Selector.open();<br /><br />        ServerSocket serverSocket = serverCh.socket();<br /><br />        serverSocket.bind(new InetSocketAddress(80));<br /><br />        serverCh.configureBlocking(false);<br /><br />        serverCh.register(selector,SelectionKey.OP_ACCEPT);<br /><br />        while(true){<br /><br />            selector.select();<br /><br />            Iterator it = selector.selectedKeys().iterator();<br /><br />            while (it.hasNext()) {<br /><br />                SelectionKey key = (SelectionKey)it.next();<br /><br />                if (key.isAcceptable()) {<br /><br />ServerSocketChannel server = <br /><br />(ServerSocketChannel)key.channel();<br /><br />                    SocketChannel channel = server.accept();<br /><br />                     channel.configureBlocking(false);<br /><br />                    channel.register(selector, SelectionKey.OP_READ);<br /><br />                }<br /><br />                if (key.isReadable()) {<br /><br />                    readDataFromSocket(key);<br /><br />                }<br /><br />                it.remove();<br /><br />            }<br /><br />        }<br /><br />}<br /><br />}<br /><br />上面的结构比起阻塞式的框架都复杂一些。具体说明如下:<br /><br />l   通过ServerSocketChannel.open()获得一个Server的Channel对象。<br /><br />l   通过Selector.open()来获得一个Selector对象。<br /><br />l   从Server的Channel对象上可以获得一个Server的Socket,并让它在80端口监听。<br /><br />l   通过ServerSocketChannel.configureBlocking(false)可以将当前的Channel配置成异步非阻塞的方式。如果没有这一步,那么Channel默认的方式跟传统的一样,是阻塞式的。<br /><br />l   将当前的Channel注册到Selector对象中去,并告诉Selector当前的Channel关心的操作是OP_ACCEPT,也就是当有新的请求的时候,Selector负责更新此Channel的状态。<br /><br />l   在循环当中调用selector.select(),如果当前没有任何新的请求过来,并且原来的连接也没有新的请求数据到达,这个方法会阻塞住,一直等到新的请求数据过来为止。<br /><br />l   如果当前都请求的数据到达,那么selector.select()就会立刻退出,这时候可以从selector.selectedKeys()获得所有在当前selector注册过的并且有数据到达的这些Channel的信息(SelectionKey)。<br /><br />l   遍历所有的这些SelectionKey来获得相关的信息。如果某个SelectionKey的操作是OP_ACCEPT,也就是isAcceptable,那么可以判定这是那个Server Channel,并且是有新的连接请求到达了。<br /><br />l   当有新的请求来的时候,通过accept()方法可以获得新的channel服务于这个新来的请求。然后通过configureBlocking(false)可以将当前的Channel配置成异步非阻塞的方式。<br /><br />l   接着将这个新的channel也注册到selector中,并告诉Selector当前的Channel关心的操作是OP_READ,也就是当前Channel有新的数据到达的时候,Selector负责更新此Channel的状态。<br /><br />l   如果在循环当中发现某个SelectionKey的操作是OP_READ,也就是isReadable,那么可以判定这不是那个Server Channel,而是在循环内部注册的连接Channel,表明当前SelectionKey对应的这个Channel有数据到达了。<br /><br />l   有数据到达之后的处理方式是下面要详细讨论的问题,在这里,我们简单地用一个方法readDataFromSocket(key)来表示,功能就是从这个Channel中读取数据。<br /><br />从这个框架结构中可以看到,在一个线程中可以同时服务于多个连接,包括Server的监听服务。在同一个时刻,并不是所有的连接都会有数据到达,因此为每一个连接分配单独的线程没有必要。使用异步非阻塞方式,可以使用很少的线程,通过Select的方式来服务于多个连接请求,效率大大提高。<br /><br />17.1.4  使用NIO来制作HTTP引擎的最大挑战<br /><br />程序实例17.3使用了configureBlocking(false)方法来将一个Channel设置成非阻塞式的。如何使用这个非阻塞的特性,请参看下面的方法调用:<br /><br />count = socketChannel.read(byteBuffer));  //非阻塞的方式<br /><br />阻塞式的方法调用如下:<br /><br />count = socket.getInputStream().read(input);  //阻塞的方式<br /><br />阻塞的方式下的read,会一直等到byte[]类型的input被充满,或者InputStream遇到EOF(socket连接被关闭)的时候,这个函数调用才会被返回。而非阻塞的方式,立刻就返回了,当前连接中有多少数据就读多少。正因为有了这种非阻塞的模式,当前的线程在读了某个通道的数据之后,可以接着再读另外一个通道的数据,线程的利用率大大提高。<br /><br />虽然线程的利用率提高了,却带来了一些其他的挑战。最大的挑战就在于:当一个请求过来的时候,很难判断什么时候所有请求的数据全部读进来了。因为每次非阻塞方式的read都可能只读了一部分数据,甚至什么也没有读到。例如,一个HTTP请求:<br /><br />HTTP/1.1 206 Partial content<br /><br />GET http://www.w3.org/pub/WWW/TheProject.html<br /><br />所有的请求数据都是以文本方式传输。在非阻塞的方式下,每一次对Channel进行读取的数据量大小不可预测,也许第一次读了“HTTP/1.1 206 Partial content”,第二次读取了“GET http://www.w3.org/pub/WWW”,第三次什么也没有读到。到底什么时候能把请求全部读完很难预测,在极端的情况下,也许最后几个字符永远也读不到。在请求没有完全读到以前,一般不进行请求处理,因为请求还不完整。在阻塞的情况下,读取的函数会一直等到请求的数据全部到来并且连接关闭以后才会返回,处理起来比较简单。但是非阻塞的方式就很复杂了。因为工作线程从一个连接读取完准备好的数据之后,又要为另一个连接服务。下次再转到先前连接的时候,以前读取的数据还需要恢复。还需要判断到底所有的请求数据是否都读完,是否可以开始对该请求的处理了。<br /><br />在本章的后面各节中,我们会看到Grizzly采用了一个有限状态机来解析HTTP请求的header信息,读取其中的content-length数值,以便预先判断什么时候到达请求的末尾。
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/174030#comments" style="color:red;">已有 <strong>0</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Thu, 20 Mar 2008 11:41:04 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/174030</link>
        <guid>http://sasion.javaeye.com/blog/174030</guid>
      </item>
      <item>
        <title>springwith--2</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/163306" style="color:red;">http://sasion.javaeye.com/blog/163306</a>&nbsp;
          发表时间: 2008年02月19日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          新年新开始.......<br />  新的一年回来，用了几天的时间看了一下JBPM工作流引擎，也把它搬进了SPRINGWITH里，并在其里面抽象了一个自己的JbpmDaoSupport,并以此抛砖引玉了一个请假流程。<br />以此作为学习的结果的总结吧：）<br />   项目文件跟之前一样，所引用的包放到了邮箱里。
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/163306#comments" style="color:red;">已有 <strong>0</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Tue, 19 Feb 2008 12:10:59 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/163306</link>
        <guid>http://sasion.javaeye.com/blog/163306</guid>
      </item>
      <item>
        <title>springwith--1</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/160785" style="color:red;">http://sasion.javaeye.com/blog/160785</a>&nbsp;
          发表时间: 2008年01月31日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          这几天试着写了一下幻想中的springwith。在spring里加入了一个对hibernateDaoSupport扩充的几个类来方便业务上的分页以及结合了mina.不知道这样算不算是真正的结合，mina里也有相关的跟spring结合的例子，但这里我采用的自己的结合方式，主要的内容是一些接口可让其跑起来，自己的协议要自己去扩展实现，在里面也提供了一个加法的例子，不过完全可在里面扩展开来.<br />   知道这根本算不了什么，但主要是作为本人的学习积累与练习提高.希望高手们不要嘲笑.<br />   顺便在这里上传一下近期的源码作为成果的保留备分...,在这里上传的源码，所依赖的包由于这里上传的文件大小有限制，故我将其放到一个126邮箱里共享。用户名/密码:springwith/springwith需要的可进去里面下载或者可到相应的官方网下载,现在所依赖的包主要有struts,spring,hibernate,mina这几个，以后会陆续引入其它更多的东西.
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/160785#comments" style="color:red;">已有 <strong>0</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Thu, 31 Jan 2008 18:29:59 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/160785</link>
        <guid>http://sasion.javaeye.com/blog/160785</guid>
      </item>
      <item>
        <title>为springwith 准备干粮</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/158249" style="color:red;">http://sasion.javaeye.com/blog/158249</a>&nbsp;
          发表时间: 2008年01月22日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          <p>&nbsp;</p><p><span style="font-size: x-small; color: #000000">优美的Spring总是让人有一些冲动，尤其是在知道她的对外提供的可扩展的接口后，就会令人很想用一用她这些优美的接口来扩充她.看过Springside里的core就知道她也是受不了这一股冲动而实现了这些如此的接口把通常化的CRUD进行了一下包装，并加进了开源世界里较好的实现了J2EE里面的各种规范，如activemq,jbpm,jbossrule,compass,xfire等,我想在不久的将来里也会引入MINA，MULE等内容.</span></p><p>&nbsp;</p><p>springwith 是Spring引起我的冲动一个念头.人总是幻想中前进着:)</p><blockquote><p>1,幻想着在Spring强大的背后引进axis2，以提供接口与外界对互;</p><p>2,幻想着在Spring强大的背后引进MINA，以提供可自定义协议与外界对互.</p><p>3,幻想着在Spring强大的背后引进JIPLET,以提供SIP协议的交互.</p><p>&nbsp;</p></blockquote><p>以上纯粹个人半夜乍醒的冲动想法,以练习我看书的成效.鸡蛋来了-------我看到有大师们在扔我扔鸡蛋了<img title="confused" src="../../images/smiles/icon_confused.gif" border="0" alt="confused" /></p>
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/158249#comments" style="color:red;">已有 <strong>1</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Tue, 22 Jan 2008 10:42:56 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/158249</link>
        <guid>http://sasion.javaeye.com/blog/158249</guid>
      </item>
      <item>
        <title>activeMQ+Mule 练习</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/156976" style="color:red;">http://sasion.javaeye.com/blog/156976</a>&nbsp;
          发表时间: 2008年01月17日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          最近在看ACTIVE,MULE等东西。经过一段时间的埋案跟练习，初有一些成果。<br />本工程作为我即将打算要做的一个东西的初形，特发上来作备案.高手们也用不着看的，只作为本人的一个学习总结罢了,相应的包请自己添加...
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/156976#comments" style="color:red;">已有 <strong>1</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Thu, 17 Jan 2008 18:36:44 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/156976</link>
        <guid>http://sasion.javaeye.com/blog/156976</guid>
      </item>
      <item>
        <title>java sca 启航</title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/156971" style="color:red;">http://sasion.javaeye.com/blog/156971</a>&nbsp;
          发表时间: 2008年01月17日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          学走路,以下引自Apache上的一篇文章,共大家共勉:)~~~<br /><br />The first thing you do is to create a folder on you disk into which you will download the TUSCANY distribution.<br /><br /><br /><br />Next you download the latest release distribution. Launch your browser and enter one of the<br />following URL's.<br />Latest Release - http://cwiki.apache.org/TUSCANY/sca-java-releases.html<br />Download both the bin zip as well as the src zip to the folder that you created on your disk. Once you completed the download you should see the following on your disk.<br /><br /><br /> <br />Next you unzip the bin zip in place, you should see the following folder file structure on your disk after unzip is complete.<br /><br /><br />Setup Eclipse for Tuscany<br />Start Eclipse and create a User Library to contain the TUSCANY runtime jar's as well as their<br />depending jar's.<br /><br />From the menu bar select Window and then Preferences... . The Preferences dialog will appear,<br />in its left navigation tree select Java, followed by Build Path, and followed by User Libraries.<br />Select the New... pushbutton on the right of the New Libraries dialog to create a new user library.<br /><br /><br /><br />The user library created is empty, select the Add JARs... pushbutton on the right to add all the<br />jar's from your Tuscany installation lib folder. When completed all the jar's will appear under the<br />TUSCANY user library.<br /><br /><br /><br />Since some of you maybe interested in debugging also the Tuscany runtime code we will attach<br />the Tuscany source to the Tuscany runtime jar in the following step. In the User Libraies dialog<br />scroll down until you see the Tuscany runtime jar and select its Source attachment.<br /><br /><br /><br />Select the Edit... pushbutton on the right and in the Edit dialog use the External File... pushbutton<br />to the select the Tuscany src zip that we downloaded earlier.<br /><br /><br /><br />Select OK to complete this and the Preferences dialog, and you are done with the Tuscany setup<br />for Eclipse.<br /><br />Create your 1st Composite Service Application<br />The following shows the composition diagram for the composite service application you are about<br />to create.<br /><br /><br /><br />The composite service application you will create is a composition of four services. The composed<br />service provided is that of an on-line store.<br />There is a Catalog service which you can ask for catalog items, and depending on its currency<br />code property configuration it will provide the item prices in USD or EUR. The Catalog service is not<br />doing the currency conversion itself it references a CurrencyConverter service to do that task. Then<br />there is the ShoppingCart service into which items chosen from the catalog can be added, it is<br />implemented as a REST service. The Catalog is bound using the JSONRPC binding, and the<br />ShoppingCart service is bound using the ATOM binding. Finally there is the Store user facing<br />service that provides the browser based user interface of the store. The Store service makes use of<br />the Catalog and ShoppingCart service using the JSONRPC, and ATOM binding respectively.<br /><br />Create a Java Project<br />In this step you create a Java Project in Eclipse to hold the composite service application.<br />Click on the New Java Project button   in the toolbar to launch the project creation dialog.<br />Next you enter "store" as the Project name, and for Project Layout select Create separate<br />folders for sources and class files.<br /><br /> <br /><br /> <br />Hit the Next button, and on the following page go to the Libraries tab. Use the Add Library...<br />button on the right to add the TUSCANY user library to the project.<br /> <br /><br /> <br /> <br />Hit the Finish button to complete the New Java Project dialog to create the "store" java project. <br /> <br /><br /> <br /> <br /><br />Construct Services<br />First you create two package folders into which later in this step you place service implementations.<br />Select the "store" project and click on the New Java Package button  in the toolbar to launch<br />the package creation dialog.<br /><br />Next you enter "services" as the package Name, and press the Finish button to complete the<br />dialog.<br /><br /> <br />Repeat the previous step to create another package named "ufservices". The store project now<br />should look as follows.<br /> <br /><br /> <br />In the following you will place in the "services" package the regular services, and in the "ufservices"<br />package the user facing services of the composite service application you create.<br /><br />Catalog<br />In this step you create the Catalog service interface and implementation.<br />Select the "services" package. Next you click on the dropdown arrow next to the New Java Class<br />button    and select the New Java Interface    option from the dropdown list. In the dialog<br />enter "Catalog" as the Name of the interface and select the Finish button to complete the dialog.<br />The Java editor will open on the new created Java interface. Replace the content of the editor by<br />copy-paste of the following Java interface code snippet.<br /><br />package services;<br />import org.osoa.sca.annotations.Remotable;<br />@Remotable<br />public interface Catalog {<br />    String[] get();<br />}Select the "services" package again. Select the New Java Class button  . In the dialog enter<br />"CatalogImpl" as the Name of the class, add "Catalog" as the interface this class implements, and<br />then select Finish to complete the dialog.<br /><br />The Java editor will open on the new created Java class. Replace the content of the editor by<br />copy-paste of the following Java class code snippet.<br /><br />package services;<br />import java.util.ArrayList;<br />import java.util.List;<br />import org.osoa.sca.annotations.Init;<br />import org.osoa.sca.annotations.Property;<br />import org.osoa.sca.annotations.Reference;<br />public class CatalogImpl implements Catalog {<br /> @Property<br /> public String currencyCode = "USD";<br /> @Reference<br /> public CurrencyConverter currencyConverter;<br /> private List&lt;String> catalog = new ArrayList&lt;String>();<br /> @Init<br /> public void init() {<br />  String currencySymbol = currencyConverter.getCurrencySymbol(currencyCode);<br />  catalog.add("Apple - " + currencySymbol +<br />  currencyConverter.getConversion("USD", currencyCode, 2.99f));<br />  catalog.add("Orange - " + currencySymbol +<br />  currencyConverter.getConversion("USD", currencyCode, 3.55f));<br />  catalog.add("Pear - " + currencySymbol +<br />  currencyConverter.getConversion("USD", currencyCode, 1.55f));<br /> }<br /> public String[] get() {<br />  String[] catalogArray = new String[catalog.size()];<br />  catalog.toArray(catalogArray);<br />  return catalogArray;<br /> }<br />}After completing these steps the content of the "store" project will look as follows.<br /><br /> <br />Note: CatalogImpl is red x'ed because it makes use of the CurrencyConverter interface that we<br />have not implemented yet.<br /> <br /><br />CurrencyConverter<br />In this step you create the CurrencyConverter service interface and implementation.<br />You follow the same steps that you learned previously to create the interface and implementation.<br />First create a Java interface in the "services" package named "CurrencyConverter" and copy-paste<br />the following Java interface code snippet into it.<br />  <br /><br />package services;<br />import org.osoa.sca.annotations.Remotable;<br />@Remotable<br />public interface CurrencyConverter {<br /> public float getConversion(String fromCurrenycCode,<br /> String toCurrencyCode, float amount);<br /> public String getCurrencySymbol(String currencyCode);<br />}Next create a Java class in the "services" package named "CurrencyConverterImpl" and copy-paste<br />the following Java class code snippet into it.<br /><br />package services;<br />public class CurrencyConverterImpl implements CurrencyConverter {<br /> public float getConversion(String fromCurrencyCode,<br />  String toCurrencyCode, float amount) {<br />  if (toCurrencyCode.equals("USD"))<br />   return amount;<br />  else    if (toCurrencyCode.equals("EUR"))<br />    return amount*0.7256f;<br />  return 0;<br /> }<br /> public String getCurrencySymbol(String currencyCode) {<br />  if (currencyCode.equals("USD"))<br />   return "$";<br />  else<br />   if (currencyCode.equals("EUR"))<br />    return "?";<br />  return "?";<br /> }<br />}After completing these steps the content of the "store" project will look as follows.<br /><br /><br /><br />ShoppingCart<br />In this step you create the ShoppingCart service implementation.<br /><br />You follow the same steps that you learned previously to create the implementation.<br /><br />Create a Java class in the "services" package named "ShoppingCartImpl" and copy-paste the<br />following Java class code snippet into it.<br /><br />package services;<br />import java.util.Date;<br />import java.util.HashMap;<br />import java.util.Map;<br />import java.util.UUID;<br />import org.apache.tuscany.sca.binding.feed.collection.Collection;<br />import org.apache.tuscany.sca.binding.feed.collection.NotFoundException;<br />import com.sun.syndication.feed.atom.Content;<br />import com.sun.syndication.feed.atom.Entry;<br />import com.sun.syndication.feed.atom.Feed;<br />import com.sun.syndication.feed.atom.Link;<br />public class ShoppingCartImpl implements Collection {<br /> // needs to change to instance var once conversation scope works<br /> private static Map&lt;String, Entry> cart = new HashMap&lt;String, Entry>();<br /> public Feed getFeed() {<br />  Feed feed = new Feed();<br />  feed.setTitle("shopping cart");<br />  Content subtitle = new Content();<br />  subtitle.setValue("Total : " + getTotal());<br />  feed.setSubtitle(subtitle);<br />  feed.getEntries().addAll(cart.values());<br />  return feed;<br /> }<br /> public Entry get(String id) throws NotFoundException {<br />  return cart.get(id);<br /> }<br /> public Entry post(Entry entry) {<br />  String id = "cart-" + UUID.randomUUID().toString();<br />  entry.setId(id);<br />  Link link = new Link();<br />  link.setRel("edit");<br />  link.setHref("" + id);<br />  entry.getOtherLinks().add(link);<br />  link = new Link();<br />  link.setRel("alternate");<br />  link.setHref("" + id);<br />  entry.getAlternateLinks().add(link);<br />  entry.setCreated(new Date());<br />  cart.put(id, entry);<br />  return entry;<br /> }<br />  public Entry put(String id, Entry entry) throws NotFoundException {<br />  entry.setUpdated(new Date());<br />  cart.put(id, entry);<br />  return entry;<br /> }<br /> public void delete(String id) throws NotFoundException {<br />  if (id.equals(""))<br />   cart.clear();<br />  else<br />   cart.remove(id);<br /> }  private String getTotal() {<br />  float total = 0;<br />  String symbol = "";<br />  if (!cart.isEmpty()) {<br />   Entry entry = cart.values().iterator().next();<br />   String item = ((Content)entry.getContents().get(0)).getValue();<br />   symbol = item.substring(item.indexOf("-")+2, item.indexOf("-")+3);<br />  }<br />  for (Entry entry : cart.values()) {<br />   String item = ((Content)entry.getContents().get(0)).getValue();<br />   total += Float.valueOf(item.substring(item.indexOf("-")+3));<br />  }<br />  return symbol + String.valueOf(total);<br /> }<br />}Note: Since the Tuscany conversational support is not ready yet the cart is realized through a hack.<br />The cart field is defined as static.<br /><br />After completing these steps the content of the "store" project will look as follows.<br /><br /><br /> <br /><br />Store<br />In this step you create the user facing Store service that will run in a Web browser and provide the<br />user interface to the other services you created.<br /><br />Select the "ufservices" package. Right click to get the context menu, select New, and then File. In<br />the New File dialog enter "store.html" for the File name, and then select Finish to complete the<br />dialog.<br /><br />The Text editor will open on the new created html file. Replace the content of the editor by copy-paste<br />of the following html snippet.<br /><br />&lt;html><br /> &lt;head><br /> &lt;title>Store&lt;/title><br /> &lt;script type="text/javascript" src="binding-atom.js">&lt;/script><br /> &lt;script type="text/javascript" src="binding-jsonrpc.js">&lt;/script><br /> &lt;script language="JavaScript"><br /> //Reference<br /> catalog = (new JSONRpcClient("../Catalog")).Catalog;<br /> //Reference<br /> shoppingCart = new AtomClient("../ShoppingCart");<br /> function catalog_getResponse(items) {<br />  var catalog = "";<br />  for (var i=0; i&lt;items.length; i++)<br />   catalog += '&lt;input name="items" type="checkbox" value="' +<br />    items[i] + '">' + items[i]+ ' &lt;br>';<br />  document.getElementById('catalog').innerHTML=catalog;<br /> }<br /> function shoppingCart_getResponse(feed) {<br />  if (feed != null) {<br />   var entries = feed.getElementsByTagName("entry");<br />   var list = "";<br />   for (var i=0; i&lt;entries.length; i++) {<br />    var item =<br />     entries[i].getElementsByTagName("content")[0].firstChild.nodeValue;<br />    list += item + ' &lt;br>';<br />   }<br />   document.getElementById("shoppingCart").innerHTML = list;<br />   if (list != "")<br />    document.getElementById('total').innerHTML =<br />      feed.getElementsByTagName("subtitle")[0].firstChild.nodeValue;<br />  }<br /> }<br /> function shoppingCart_postResponse(entry) {<br />  shoppingCart.get("", shoppingCart_getResponse);<br /> }<br /> function addToCart() {<br />  var items = document.catalogForm.items;<br />  var j = 0;<br />  for (var i=0; i&lt;items.length; i++)<br />   if (items[i].checked) {<br />    var entry = '&lt;entry xmlns="http://www.w3.org/2005/Atom">' +<br />       '&lt;title>cart-item&lt;/title>' +<br />       '&lt;content type="text">'+items[i].value+'&lt;/content>' +<br />       '&lt;/entry>';<br />    shoppingCart.post(entry, shoppingCart_postResponse);<br />    items[i].checked = false;<br />   }<br /> }<br /> function checkoutCart() {<br />  document.getElementById('store').innerHTML='&lt;h2>' +<br />   'Thanks for Shopping With Us!&lt;/h2>'+<br />   '&lt;h2>Your Order&lt;/h2>'+<br />   '&lt;form name="orderForm" action="/ufs/store.html">'+<br />   document.getElementById('shoppingCart').innerHTML+<br />   '&lt;br>'+<br />   document.getElementById('total').innerHTML+<br />   '&lt;br>'+<br />   '&lt;br>'+<br />   '&lt;input type="submit" value="Continue Shopping">'+<br />   '&lt;/form>';<br />  shoppingCart.delete("", null);<br /> }<br /> function deleteCart() {<br />  shoppingCart.delete("", null);<br />  document.getElementById('shoppingCart').innerHTML = "";<br />  document.getElementById('total').innerHTML = "";<br /> }<br /> window.onload = function() {<br />   catalog.get(catalog_getResponse);<br />   shoppingCart.get("", shoppingCart_getResponse);<br />  }<br /> &lt;/script><br /> &lt;link rel="stylesheet" type="text/css" href="style.css" /><br /> &lt;/head><br /> &lt;body><br /> &lt;h1>Store&lt;/h1><br /> &lt;div id="store"><br /> &lt;h2>Catalog&lt;/h2><br /> &lt;form name="catalogForm"><br />  &lt;div id="catalog" >&lt;/div><br />  &lt;br><br />  &lt;input type="button" onClick="addToCart()" value="Add to Cart"><br /> &lt;/form><br /> &lt;br><br /> &lt;h2>Your Shopping Cart&lt;/h2><br /> &lt;form name="shoppingCartForm"><br />  &lt;div id="shoppingCart">&lt;/div><br />  &lt;br><br />  &lt;div id="total">&lt;/div><br />  &lt;br><br />  &lt;input type="button" onClick="checkoutCart()" value="Checkout"><br />  &lt;input type="button" onClick="deleteCart()" value="Empty"><br />  &lt;a href="../ShoppingCart/"><br />  &lt;img src="http://projects.apache.org/images/blue-icon.png" border="0"><br />  &lt;/a><br /> &lt;/form><br /> &lt;/div><br /> &lt;/body><br />&lt;/html>Next select the "ufservices" package again. Right click to get the context menu, select New, and<br />then File. In the New File dialog enter "binding-jsonrpc.js" for the File name, and then select<br />Finish to complete the dialog.<br /><br />The Text editor will open on the new created javascript file. Replace the content of the editor by<br />copy-paste of the javascript snippet you find here:<br /><br />http://svn.apache.org/repos/asf/incubator/tuscany/java/sca/modules/binding-jsonrpc/src/main/resources/org/apache/tuscany/sca/binding/jsonrpc/jsonrpc.js<br /><br />Next select the "ufservices" package again. Right click to get the context menu, select New, and<br />then File. In the New File dialog enter "binding-atom.js" for the File name, and then select Finish<br />to complete the dialog.<br /><br />The Text editor will open on the new created javascript file. Replace the content of the editor by<br />copy-paste of the following javascript snippet.<br /><br />function AtomClient(uri) {<br /> this.uri=uri;<br /> this.get = function(id, responseFunction) {<br />  var xhr = this.createXMLHttpRequest();<br />  xhr.onreadystatechange = function() {<br />   if (xhr.readyState == 4) {<br />    if (xhr.status == 200) {<br />     if (responseFunction != null)<br />      responseFunction(xhr.responseXML);<br />    } else {<br />    alert("get - Error getting data from the server");<br />    }<br />   }<br />  }<br />  xhr.open("GET", uri + id, true);<br />  xhr.send(null);<br /> }<br /> this.post = function (entry, responseFunction) {<br /> var xhr = this.createXMLHttpRequest();<br /> xhr.onreadystatechange = function() {<br />  if (xhr.readyState == 4) {<br />   if (xhr.status == 201) {<br />    if (responseFunction != null)<br />     responseFunction(xhr.responseXML);<br />    } else {<br />     alert("post - Error getting data from the server");<br />    }<br />   }<br />  }<br />  xhr.open("POST", uri, true);<br />  xhr.setRequestHeader("Content-Type", "application/atom+xml");<br />  xhr.send(entry);<br /> }<br /> this.put = function (id, entry, responseFunction) {<br />  var xhr = this.createXMLHttpRequest();<br />  xhr.onreadystatechange = function() {<br />   if (xhr.readyState == 4) {<br />    if (xhr.status == 200) {<br />     if (responseFunction != null)<br />      responseFunction(xhr.responseXML);<br />    } else {<br />     alert("put - Error getting data from the server");<br />    }<br />   }<br />  }<br />  xhr.open("PUT", uri + id, true);<br />  xhr.setRequestHeader("Content-Type", "application/atom+xml");<br />  xhr.send(entry);<br /> }<br /> this.delete = function (id, responseFunction) {<br />  var xhr = this.createXMLHttpRequest();<br />  xhr.onreadystatechange = function() {<br />   if (xhr.readyState == 4) {<br />    if (xhr.status == 200) {<br />     if (responseFunction != null) responseFunction();<br />    } else {<br />     alert("delete - Error getting data from the server");<br />    }<br />   }<br />  }<br />  xhr.open("DELETE", uri + id, true);<br />  xhr.send(null);<br /> }<br /> this.createXMLHttpRequest = function () {<br />  try {return new XMLHttpRequest();} catch(e) {}<br />  try {return new ActiveXObject("Msxml2.XMLHTTP");} catch(e) {}<br />  try {return new ActiveXObject("Microsoft.XMLHTTP");} catch(e) {}<br />  alert("XML http request not supported");<br />  return null;<br /> }<br />}Note: That we have to have the bindig-jsonrpc.js, and binding-atom.js local in our project is only<br />temporary, so this step will be removed in the future.<br /><br />After completing these steps the content of the "store" project will look as follows.<br /><br /> <br /><br />Compose Services<br />Now that you have all the required service implementations you compose them together to provide<br />the store composite service. The composition is stored in a .composite file.<br /><br />Select the "src" folder of the "store" project. Right click to get the context menu, select New, and<br />then File. In the New File dialog enter "store.composite" for the File name, and then select Finish<br />to complete the dialog.<br /><br />The Text editor will open on the new created composite file. Replace the content of the editor by<br />copy-paste of the following composite snippet.<br /><br />&lt;?xml version="1.0" encoding="UTF-8"?><br />&lt;composite xmlns="http://www.osoa.org/xmlns/sca/1.0"<br /> xmlns:t="http://tuscany.apache.org/xmlns/sca/1.0"<br /> xmlns:s="http://store"<br /> name="store"><br /> &lt;component name="ufs"><br />  &lt;t:implementation.resource location="ufservices"/><br />  &lt;service name="Resource"><br />   &lt;t:binding.http/><br />  &lt;/service><br /> &lt;/component><br /> &lt;component name="Catalog"><br />  &lt;implementation.java class="services.CatalogImpl"/><br />  &lt;property name="currencyCode">USD&lt;/property><br />  &lt;service name="Catalog"><br />   &lt;t:binding.jsonrpc/><br />  &lt;/service><br />  &lt;reference name="currencyConverter" target="CurrencyConverter"/><br /> &lt;/component><br /> &lt;component name="ShoppingCart"><br />  &lt;implementation.java class="services.ShoppingCartImpl"/><br />  &lt;service name="Collection"><br />   &lt;t:binding.atom/><br />  &lt;/service><br /> &lt;/component><br /> &lt;component name="CurrencyConverter"><br />  &lt;implementation.java class="services.CurrencyConverterImpl"/><br /> &lt;/component><br />&lt;/composite>After completing these steps the content of the "store" project will look as follows.<br /><br /><br /> <br /><br />Launch Services<br />In this step you create the code to launch the Tuscany runtime with the new store composite<br />service you created.<br /><br />Select the "store" project and click on the New Java Package button   in the toolbar to start the<br /><br />package creation dialog. Use the dialog to create a new package named "launch". <br />Select the "launch" package. Select the New Java Class button   . In the dialog enter "Launch"<br />as the Name of the class, check the checkbox for creating a main method stub, and then select<br />Finish to complete the dialog.<br /><br />The Java editor will open on the new created Java class. Replace the content of the editor by<br />copy-paste of the following Java class code snippet.<br /><br />package launch;<br />import org.apache.tuscany.sca.host.embedded.SCADomain;<br />public class Launch {<br /> public static void main(String[] args) throws Exception {<br />  System.out.println("Starting ...");<br />  SCADomain scaDomain = SCADomain.newInstance("store.composite");<br />  System.out.println("store.composite ready for big business !!!");<br />  System.out.println();<br />  System.in.read();<br />  scaDomain.close();<br /> }<br />} After completing these steps the content of the "store" project will look as follows.<br /><br /><br />Congratulations you completed your 1st composite service applications, now its time to take it into<br />action.<br /><br />Use Services<br />In this step you launch and use the store composite service application you created.<br /><br />First select the "Launch" class in the "launch" package of your "store" project. Right click to get the<br />context menu, select Run As, and then Java application. The Tuscany runtime will start up adding<br />the store composition to its domain.<br /><br />The Eclipse console will show the following messages.<br /><br /> <br /><br /> Next Launch your Web browser and enter the following address:<br /><br />http://localhost:8080/ufs/store.html <br /><br /> You get to the Store user facing service of the composite service application.<br /><br /><br /><br />You can select items from the Catalog and add them to your Shopping Cart.<br /><br />Note: When adding items for the first time you will be asked for userid and password by the<br />browser. Enter "admin" for both.<br /><br /><br /><br />Since the ShoppingCart service is bound using the ATOM binding, you can also look at the<br />shopping card content in ATOM feed form by clicking on the feed icon . You get the browsers default rendering for ATOM feeds.<br /><br /> <br /><br /> Use the browser back button to get back to the Store page.<br /><br /><br /><br />And then you can Checkout to complete your order.<br /><br /><br /><br />Explore the Samples from the Tuscany Distribution<br />The sample folder of the Tuscany distribution provides a rich set of samples ready for you to explore.<br /><br /><br /> <br />In Eclipse create a New Java Project, specify the project name, select Create project from<br />existing source, and specify the folder that contains the sample source.<br /><br /><br /> <br />Use Next to get to the next page in the New Java Project dialog. There go to the Libraries tab, use<br />the Add Library... pushbutton to add the JUnit library and the user library TUSCANY.<br /><br /><br /> <br />Finish the New Java Project dialog. You now have the sample project available in the Eclipse workbench.<br /><br /><br /> <br />For the calculator sample that we've chosen go to its CalculatorClient class and select Run As -><br />Java Application. You will see the following output in the console.
          <br/><br/>
          <span style="color:red;">
            <a href="http://sasion.javaeye.com/blog/156971#comments" style="color:red;">已有 <strong>1</strong> 人发表留言，猛击-&gt;&gt;<strong>这里</strong>&lt;&lt;-参与讨论</a>
          </span>
          <br/><br/><br/>
          <span style="color:#E28822;">JavaEye推荐</span>
          <br/>
          <ul class='adverts'><li><a href='/adverts/42' target='_blank'><span style="color:red;font-weight:bold;">搜狐网站诚聘Java、PHP和C++工程师</span></a></li><li><a href='/adverts/41' target='_blank'><span style="color:red;font-weight:bold;">北京: 千橡集团暨校内网诚聘软件研发工程师</span></a></li></ul>
          <br/><br/><br/>
          ]]>
        </description>
        <pubDate>Thu, 17 Jan 2008 18:25:09 +0800</pubDate>
        <link>http://sasion.javaeye.com/blog/156971</link>
        <guid>http://sasion.javaeye.com/blog/156971</guid>
      </item>
      <item>
        <title>(转)SCA1.0应用开发过程 </title>
        <author>sasion</author>
        <description>
          <![CDATA[
          <br/>
          作者: <a href="http://sasion.javaeye.com">sasion</a>&nbsp;
          链接：<a href="http://sasion.javaeye.com/blog/156970" style="color:red;">http://sasion.javaeye.com/blog/156970</a>&nbsp;
          发表时间: 2008年01月17日
          <br/><br/>
          声明：本文系JavaEye网站发布的原创博客文章，未经作者书面许可，严禁任何网站转载本文，否则必将追究法律责任！
          <br/><br/>
          引言<br />介绍<br />Tuscnay近日将推出 1.0版本，它在实现了 SCA1.0的基础上，还开发了一些扩展内容，为开发者提供了将更多主流技术一直到Tuscany环境下的手段。<br /><br />目标<br />本文是在认真研究 Tuscany SCA环境的前提下，总结以前我们在 SOA方面所作的研究，提出了 一个利用 Tuscany1.0的技术