hk2000c技术专栏

技术源于哲学,哲学来源于生活 关心生活,关注健康,关心他人

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  111 随笔 :: 1 文章 :: 28 评论 :: 0 Trackbacks

#

乒乓球基本技术动作口诀

  一、头诀
  口诀不能罗万象,仅把要点来提供, 速度旋转多变化,有赖触类能旁通。
  二、准备姿势
  立足肩宽微提踵,屈膝弯腰莫挺胸, 拍置腹前眼注视,准备移动体放松。
  三、发球
  伸掌抛球向上空,球落击法有多种, 上下侧旋擦球面,长短轻急力不同。
  四、接发球
  “准备姿势”接发球,来球旋转反向送, 上旋推挡下旋搓,长抽短吊争抢攻。
  五、正手攻球
  切忌抬肘握拍松,前臂向前向上动, 左脚稍前体右转,倾拍一般击球中。
  六、反手攻球
  前臂搁腹臂贴胸,肘为轴心臂腕动, 左脚移后腰左转,倾拍斜击球上中。
  七、推挡球
   推挡多用反手方,动作犹似反手攻, 前臂发力向前下,倾拍推挡球上中。
  八、搓球
  拍先稍仰后平送,向前摩擦球下中, 手腕配合小臂动,球转不转靠腕动。
  九、削球
  判断来球先移动,屈腿转体引拍送, 先仰后平擦球底,向前下作弧形动。
  十、弧圈球
  挥拍力向前上冲,薄擦球面位上中, 越薄越转成弧圈,擦面要宽力加重。
  十一、回击弧圈球
  倾拍盖住球上中、及时调拍心放松, 削击移拍上而下,短促截球位偏中。
  十二、放高球
  离台抖切球下中,高抛物线前上送, 远吊对方左右角,越高、远、转越成功。
  十三、杀高球
  球弹高起莫着急,移位待落额上空, 手臂环转向前压,亦可斜砍球侧中。  
  十四、滑板球
  拍向右前似击球,手腕突转向左送, 斜擦球左侧力抽,声东击西奏奇功。
  十五、短球
  对方离台宜短吊,貌似长拍宜轻送, 拍平减力轻递球,竖板轻挡亦可用。
  十六、步法
  练球切忌丢步法,击球应先步移动, 先动后打是关键,步法混乱手法空

posted @ 2008-02-28 22:37 hk2000c 阅读(365) | 评论 (0)编辑 收藏

(一)正手发奔球
  1、 特点 球速急、落点长、冲力大,发至对方右大角或中左位置,对对方威胁较大。
  2、 要点①抛球不宜太高;②提高击球瞬间的挥拍速度;③第一落点要靠近本方台面的端线;④击球点与网同高或稍低于网。

  (二)反手发急球与发急下旋球
  1、 特点 球速快、弧线低,前冲大,迫使对方后退接球,有利于抢攻,常与发急下旋球配合使用。
  2、 要点①击球点应在身体的左前侧与网同高或比网稍低;②注意手腕的抖动发力;③第一落点在本方台区的端线附近。

  (三)发短球
  1、 特点 击球动作小,出手快,球落到对方台面后的第二跳下不出台,使对方不易发力抢拉、冲或抢攻。
  2、 要点 ①抛球不宜太高;②击球时,手腕的力量大于前臂的力量;③发球的第一落点在球台中区,不要离网太近;④发球动作尽可能与发长球相似,使对方不易判断。

  (四)正手发转与不转球
  1、 特点 球速较慢,前冲力小,主要用相似的发球动作,制造旋转变化去迷惑对方,造成对方接发球失误或为自己抢攻创造机会。
  2、 要点①抛球不宜太高;②发转球时,拍面稍后抑,切球的中下部;越是加转球,越应注意手臂的前送动作;③发不转球时,击球瞬间减小拍面后仰角度,增加前推的力量。

  (五)正手发左侧上(下)旋球
  1、 特点 左侧上(下)旋转力较强,对方挡球时向其右侧上(下)方反弹,一般站在中线偏左或侧身发球。
  2、 要点:①发球时要收腹,击球点不可远离身体;②尽量加大由右向左挥动的幅度和弧线,以增强侧旋强度。③发左侧上旋时,击球瞬间手腕快速内收,球拍从球的正中向左上方摩擦。④发左侧下旋时,拍面稍后仰,球拍从球的中下部向左下方摩擦。

  (六) 反手发右侧上(下)旋球
  1. 特点 右侧上(下)旋球力强,对方挡住后,向其左侧上(下)反弹。发球落点以左方斜线长球配合中右近网短球为佳。
  2. 要点 ①注意收腹和转腰动作;②充分利用手腕转动配合前臂发力;③发右侧上旋球时,击球瞬间球拍从球的中部向右上方摩擦,手腕有一个上勾动作;④发右侧下旋球时,拍面稍后仰,击球瞬间球拍从球的中下部向右侧下摩擦。

  (七)下蹲发球
  1.特点 下蹲发球属于上手类发球,我国运动员早在50年代就开始使用。横拍选手发下蹲球比直拍选手方便些,直拍选手发球时需变化握拍方法,即将食指移放到球拍的背面。下蹲发球可以发出左侧旋和右侧旋,在对方不适应的情况下,威胁很大,关键时候发出高质量的球,往往能直接得分。
  2. 要点①注意抛球和挥拍击球动作的配合,掌握好击球时间。②发球要有质量,发球动作要利落,以防在还未完全站起时已被对方抢攻③发下蹲右侧上、下旋球时,左脚稍前,身体略向右偏转,挥拍路线为从左后方向右前方。拍触球中部向右侧上摩擦为右侧上旋;从球中下部向右侧下摩擦为右侧下旋。④发下蹲左侧上、下旋球时,站位稍平,身体基本正对球台,挥拍路线为从右后方向左前方。拍触球右中部向左上方摩擦为左侧上旋;从球中部向左下部摩擦为左侧下旋。⑤发左(右)侧上、下旋球时,要特别注意快速做半圆形摩擦球的动作。

  (八)正手高抛发球
  1、 特点 最显著的特点是抛球高,增大了球下降时对拍的正压力,发出的球速度快,冲力大,旋转变化多,着台后拐弯飞行。但高抛发球动作复杂,有一定的难度。
  2、 要点:①抛球勿离台及身体太远。②击球点与网同高或比网稍低,在近腰的中右处(15厘米)为好③尽量加大向内摆动的幅度和弧线。④发左侧上、下旋球与低抛发球同。⑤触球后,附加一个向右前方的回收动作,可增加对方的判断(结合发右侧旋球,更有威力)。

posted @ 2008-02-05 13:13 hk2000c 阅读(282) | 评论 (0)编辑 收藏

 接连看了几场中欧女子乒乓球对抗赛,对欧洲女队鲍罗斯的反手拉球在这次系列比赛中的运用效果印象颇深。尽管原来也见过她的拉球,感觉很有一些男子运动员的味道,但每当与中国人对抗,由于对中国运动员的节奏适应不好,其力量和速度都难以发挥,能用上的时候也很少。

  仔细观察鲍罗斯的反手拉球,大致上主要用于以下三种情况:一是反手位的第一板上手,二是正手变反手位时,三是反手位的机会比较好时。而这三种球也恰恰是中国运动员,特别是水平不高的运动员常感到很别扭的球。

  中国队的打法历来讲究积极主动,体现在打法上的最明显特征就是突出正手单面进攻的使用率和杀伤力。曾有一段时间,欧洲人在比较清楚地把握了中国人的惯性套路后,以接发球“晃撇”反手底线长球和“调右压左”,从正手突破的战术,一度使中国队陷入非常被动的境地,常常令中国队“拨”不出手来,难以形成有效的正手单面攻,造成失利。所以,无论是横板快攻还是弧圈打法,在训练过程中,掌握一板比较高质量的反手拉是非常有必要的,可以为发挥正手的杀伤力创造更多的机会。

  谈横板反手拉球,不能不说一说它的基本要领。常有业余爱好者问,正手拉球时,一脚在前,一脚在后,那么反手拉球时,是否需要进行重心交换呢?可以说,其基本的道理是差不多的。但反手拉球由于受到身体解剖结构的限制,不能像正手拉球时过于侧向的站位,一般要接近于两脚的平行,只要能够完成基本的重心交换即可。在拉球引拍时,也不能像正手拉一样有很大的幅度,适当地把肘关节抬起来一点,以增加手臂的发力距离,这是与正手拉本质的区别;在接触球的一瞬间,重心往上拨,尽可能向前发力,以使球体现足够的向前质量。

  在练习和运用反手拉球过程中,要注意,作为具有中国人特点打法的运动员,其进攻和上手的指导思想都应建立在正手的基础上,再运用反手,千万不能形成左来左打,右来右打的打法风格,有失积极意义,毕竟正手和反手的杀伤力是不同的,这也是中国队在长期的实践中摸索的打法风格的成功经验之一。但作为辅助技术手段,鲍罗斯反手拉球的运用和所产生的效果是值得我们借鉴和学习的。
posted @ 2008-02-05 13:10 hk2000c 阅读(1096) | 评论 (0)编辑 收藏

这样可以做到对所有对象的安全性控制。
可以扩展到一切的业务对象。
包括基础方法,操作。
甚至安全对象本身
有点神学的味道。

可能是因为采用其他被创造者不知道的机制,所以被创造者无法理解和知晓。



posted @ 2008-01-24 00:04 hk2000c 阅读(246) | 评论 (0)编辑 收藏

您认为把 NIO 和 Servlet API 组合在一起是不可能的?请再好好想一下。在本文中,Java 开发人员 Taylor Cowan 向您展示了如何把生产者/消费者模型应用到消费者非阻塞 I/O,从而轻松地让 Servlet API 全新地兼容 NIO。在这个过程中,您将会看到采用了什么来创建实际的基于 Servlet 并实现了 NIO 的 Web 服务器;您也将发现在企业环境中,那个服务器是如何以标准的 Java I/O 服务器(Tomcat 5.0)为基础而创建的。

NIO 是带有 JDK 1.4 的 Java 平台的最有名(如果不是最出色的)的添加部分之一。下面的许多文章阐述了 NIO 的基本知识及如何利用非阻塞通道的好处。但它们所遗漏的一件事正是,没有充分地展示 NIO 如何可以提高 J2EE Web 层的可伸缩性。对于企业开发人员来说,这些信息特别密切相关,因为实现 NIO 不像把少数几个 import 语句改变成一个新的 I/O 包那样简单。首先,Servlet API 采用阻塞 I/O 语义,因此默认情况下,它不能利用非阻塞 I/O。其次,不像 JDK 1.0 中那样,线程不再是“资源独占”(resource hog),因此使用较少的线程不一定表明服务器可以处理更多的客户机。

在本文中,为了创建基于 Servlet 并实现了 NIO 的 Web 服务器,您将学习如何解决 Servlet API 与非阻塞 I/O 的不配合问题。我们将会看到在多元的 Web 服务器环境中,这个服务器是如何针对标准 I/O 服务器(Tomcat 5.0)进行伸缩的。为符合企业中生存期的事实,我们将重点放在当保持 socket 连接的客户机数量以指数级增长时,NIO 与标准 I/O 相比较的情况如何。

注意,本文针对某些 Java 开发人员,他们已经熟悉了 Java 平台上 I/O 编程的基础知识。有关非阻塞 I/O 的介绍,请参阅 参考资料 部分。

线程不再昂贵

大家都知道,线程是比较昂贵的。在 Java 平台的早期(JDK 1.0),线程的开销是一个很大负担,因此强制开发人员自定义生成解决方案。一个常见的解决方案是使用 VM 启动时创建的线程池,而不是按需创建每个新线程。尽管最近在 VM 层上提高了线程的性能,但标准 I/O 仍然要求分配惟一的线程来处理每个新打开的 socket。就短期而言,这工作得相当不错,但当线程的数量增加超过了 1K,标准 I/O 的不足就表现出来了。由于要在线程间进行上下文切换,因此 CPU 简直变成了超载。

由于 JDK 1.4 中引入了 NIO,企业开发人员最终有了“单线程”模型的一个内置解决方案:多元 I/O 使得固定数量的线程可以服务不断增长的用户数量。

多路复用(Multiplexing)指的是通过一个载波来同时发送多个信号或流。当使用手机时,日常的多路复用例子就发生了。无线频率是稀有的资源,因此无线频率提供商使用多路复用技术通过一个频率发送多个呼叫。在一个例子中,把呼叫分成一些段,然后给这些段很短的持续时间,并在接收端重新装配。这就叫做 时分多路复用(time-division multiplexing),即 TDM。

在 NIO 中,接收端相当于“选择器”(参阅 java.nio.channels.Selector )。不是处理呼叫,选择器是处理多个打开的 socket。就像在 TDM 中那样,选择器重新装配从多个客户机写入的数据段。这使得服务器可以用单个线程管理多个客户机。





回页首


Servlet API 和 NIO

对于 NIO,非阻塞读写是必要的,但它们并不是完全没有麻烦。除了不会阻塞之外,非阻塞读不能给呼叫方任何保证。客户机或服务器应用程序可能读取完整信息、部分消息或者根本读取不到消息。另外,非阻塞读可能读取到太多的消息,从而强制为下一个呼叫准备一个额外的缓冲区。最后,不像流那样,读取了零字节并不表明已经完全接收了消息。

这些因素使得没有轮询就不可能实现甚至是简单的 readline 方法。所有的 servlet 容器必须在它们的输入流上提供 readline 方法。因此,许多开发人员放弃了创建基于 Servlet 并实现了 NIO 的 Web 应用程序服务器。不过这里有一个解决方案,它组合了 Servlet API 和 NIO 的多元 I/O 的能力。

在下面的几节中,您将学习如何使用 java.io.PipedInputPipedOutputStream 类来把生产者/消费者模型应用到消费者非阻塞 I/O。当读取非阻塞通道时,把它写到正由第二个线程消费的管道。注意,这种分解映射线程不同于大多数基于 Java 的客户机/服务器应用程序。这里,我们让一个线程单独负责处理非阻塞通道(生产者),让另一个线程单独负责把数据作为流消费(消费者)。管道也为应用程序服务器解决了非阻塞 I/O 问题,因为 servlet 在消费 I/O 时将采用阻塞语义。





回页首


示例服务器

示例服务器展示了 Servlet API 和 NIO 不兼容的生产者/消费者解决方案。该服务器与 Servlet API 非常相似,可以为成熟的基于 NIO 应用程序服务器提供 POC (proof of concept),是专门编写来衡量 NIO 相对于标准 Java I/O 的性能的。它处理简单的 HTTP get 请求,并支持来自客户机的 Keep-Alive 连接。这是重要的,因为多路复用 I/O 只证明在要求服务器处理大量打开的 scoket 连接时是有意的。

该服务器被分成两个包: org.sse.serverorg.sse.http 包中有提供主要 服务器 功能的类,比如如下的一些功能:接收新客户机连接、阅读消息和生成工作线程以处理请求。 http 包支持 HTTP 协议的一个子集。详细阐述 HTTP 超出了本文的范围。有关实现细节,请从 参考资料 部分下载代码示例。

现在让我们来看一下 org.sse.server 包中一些最重要的类。





回页首


Server 类

Server 类拥有多路复用循环 —— 任何基于 NIO 服务器的核心。在清单 1 中,在服务器接收新客户机或检测到正把可用的字节写到打开的 socket 前, select() 的调用阻塞了。这与标准 Java I/O 的主要区别是,所有的数据都是在这个循环中读取的。通常会把从特定 socket 中读取字节的任务分配给一个新线程。使用 NIO 选择器事件驱动方法,实际上可以用单个线程处理成千上万的客户机,不过,我们还会在后面看到线程仍有一个角色要扮演。

每个 select() 调用返回一组事件,指出新客户机可用;新数据准备就绪,可以读取;或者客户机准备就绪,可以接收响应。server 的 handleKey() 方法只对新客户机( key.isAcceptable() )和传入数据 ( key.isReadable() ) 感兴趣。到这里,工作就结束了,转入 ServerEventHandler 类。


清单 1. Server.java 选择器循环
public void listen() {
            SelectionKey key = null;
            try {
            while (true) {
            selector.select();
            Iterator it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
            key = (SelectionKey) it.next();
            handleKey(key);
            it.remove();
            }
            }
            } catch (IOException e) {
            key.cancel();
            } catch (NullPointerException e) {
            // NullPointer at sun.nio.ch.WindowsSelectorImpl, Bug: 4729342
            e.printStackTrace();
            }
            }
            





回页首


ServerEventHandler 类

ServerEventHandler 类响应服务器事件。当新客户机变为可用时,它就实例化一个新的 Client 对象,该对象代表了那个客户机的状态。数据是以非阻塞方式从通道中读取的,并被写到 Client 对象中。 ServerEventHandler 对象也维护请求队列。为了处理(消费)队列中的请求,生成了不定数量的工作线程。在传统的生产者/消费者方式下,为了在队列变为空时线程会阻塞,并在新请求可用时线程会得到通知,需要写 Queue

为了支持等待的线程,在清单 2 中已经重写了 remove() 方法。如果列表为空,就会增加等待线程的数量,并阻塞当前线程。它实质上提供了非常简单的线程池。


清单 2. Queue.java
public class Queue extends LinkedList
            {
            private int waitingThreads = 0;
            public synchronized void insert(Object obj)
            {
            addLast(obj);
            notify();
            }
            public synchronized Object remove()
            {
            if ( isEmpty() ) {
            try	{ waitingThreads++; wait();}
            catch (InterruptedException e)  {Thread.interrupted();}
            waitingThreads--;
            }
            return removeFirst();
            }
            public boolean isEmpty() {
            return 	(size() - waitingThreads <= 0);
            }
            }
            

工作线程的数量与 Web 客户机的数量无关。不是为每个打开的 socket 分配一个线程,相反,我们把所有请求放到一个由一组 RequestHandlerThread 实例所服务的通用队列中。理想情况下,线程的数量应该根据处理器的数量和请求的长度或持续时间进行调整。如果请求通过资源或处理需求花了很长时间,那么通过添加更多的线程,可以提高感知到的服务质量。

注意,这不一定提高整体的吞吐量,但确实改善了用户体验。即使在超载的情况下,也会给每个线程一个处理时间片。这一原则同样适用于基于标准 Java I/O 的服务器;不过这些服务器是受到限制的,因为会 要求 它们为每个打开的 socket 连接分配一个线程。NIO 服务器完全不用担心这一点,因此它们可以扩展到大量用户。最后的结果是 NIO 服务器仍然需要线程,只是不需要那么多。





回页首


请求处理

Client 类有两个用途。首先,通过把传入的非阻塞 I/O 转换成可由 Servlet API 消费的阻塞 InputStream ,它解决了阻塞/非阻塞问题。其次,它管理特定客户机的请求状态。因为当全部读取消息时,非阻塞通道没有给出任何提示,所以强制我们在协议层处理这一情况。 Client 类在任意指定的时刻都指出了它是否正在参与进行中的请求。如果它准备处理新请求, write() 方法就会为请求处理而将该客户机排到队列中。如果它已经参与了请求,它就只是使用 PipedInputStreamPipedOutputStream 类把传入的字节转换成一个 InputStream

图 1 展示了两个线程围绕管道进行交互。主线程把从通道读取的数据写到管道中。管道把相同的数据作为 InputStream 提供给消费者。管道的另一个重要特性是:它是进行缓冲处理的。如果没有进行缓冲处理,主线程在尝试写到管道时就会阻塞。因为主线程单独负责所有客户机间的多路复用,因此我们不能让它阻塞。


图 1. PipedInput/OutputStream
关系的图形表示

Client 自己排队后,工作线程就可以消费它了。 RequestHandlerThread 类承担了这个角色。至此,我们已经看到主线程是如何连续地循环的,它要么接受新客户机,要么读取新的 I/O。工作线程循环等待新请求。当客户机在请求队列上变为可用时,它就马上被 remove() 方法中阻塞的第一个等待线程所消费。


清单 3. RequestHandlerThread.java
public void run() {
            while (true) {
            Client client = (Client) myQueue.remove();
            try {
            for (; ; ) {
            HttpRequest req = new HttpRequest(client.clientInputStream,
            myServletContext);
            HttpResponse res = new HttpResponse(client.key);
            defaultServlet.service(req, res);
            if (client.notifyRequestDone())
            break;
            }
            } catch (Exception e) {
            client.key.cancel();
            client.key.selector().wakeup();
            }
            }
            }
            

然后该线程创建新的 HttpRequestHttpResponse 实例,并调用 defaultServlet 的 service 方法。注意, HttpRequest 是用 Client 对象的 clientInputStream 属性构造的。 PipedInputStream 就是负责把非阻塞 I/O 转换成阻塞流。

从现在开始,请求处理就与您在 J2EE Servlet API 中期望的相似。当对 servlet 的调用返回时,工作线程在返回到池中之前,会检查是否有来自相同客户机的另一个请求可用。注意,这里用到了单词 池 (pool)。事实上,线程会对队列尝试另一个 remove() 调用,并变成阻塞,直到下一个请求可用。





回页首


运行示例

示例服务器实现了 HTTP 1.1 协议的一个子集。它处理普通的 HTTP get 请求。它带有两个命令行参数。第一个指定端口号,第二个指定 HTML 文件所驻留的目录。在解压文件后, 切换到项目目录,然后执行下面的命令,注意要把下面的 webroot 目录替换为您自己的目录:

java -cp bin org.sse.server.Start 8080
            "C:\mywebroot"
            

还请注意,服务器并没有实现目录清单,因此必须指定有效的 URL 来指向您的 webroot 目录下的文件。





回页首


性能结果

示例 NIO 服务器是在重负载下与 Tomcat 5.0 进行比较的。选择 Tomcat 是因为它是基于标准 Java I/O 的纯 Java 解决方案。为了提高可伸缩性,一些高级的应用程序服务器是用 JNI 本机代码优化的,因此它们没有提供标准 I/O 和 NIO 之间的很好比较。目标是要确定 NIO 是否给出了大量的性能优势,以及是在什么条件下给出的。

如下是一些说明:

  • Tomcat 是用最大的线程数量 2000 来配置的,而示例服务器只允许用 4 个工作线程运行。
  • 每个服务器是针对相同的一组简单 HTTP get 测试的,这些 HTTP get 基本上由文本内容组成。

  • 把加载工具(Microsoft Web Application Stress Tool)设置为使用“Keep-Alive”会话,导致了大约要为每个用户分配一个 socket。然后它导致了在 Tomcat 上为每个用户分配一个线程,而 NIO 服务器用固定数量的线程来处理相同的负载。

图 2 展示了在不断增加负载下的“请求/秒”率。在 200 个用户时,性能是相似的。但当用户数量超过 600 时,Tomcat 的性能开始急剧下降。这最有可能是由于在这么多的线程间切换上下文的开销而导致的。相反,基于 NIO 的服务器的性能则以线性方式下降。记住,Tomcat 必须为每个用户分配一个线程,而 NIO 服务器只配置有 4 个工作线程。


图 2. 请求/秒
关系的图形表示

图 3 进一步显示了 NIO 的性能。它展示了操作的 Socket 连接错误数/分钟。同样,在大约 600 个用户时,Tomcat 的性能急剧下降,而基于 NIO 的服务器的错误率保持相对较低。


图 3. Socket 连接错误数/分钟
关系的图形表式




回页首



结束语

在本文中您已经学习了,实际上可以使用 NIO 编写基于 Servlet 的 Web 服务器,甚至可以启用它的非阻塞特性。对于企业开发人员来说,这是好消息,因为在企业环境中,NIO 比标准 Java I/O 更能够进行伸缩。不像标准的 Java I/O,NIO 可以用固定数量的线程处理许多客户机。当基于 Servlet 的 NIO Web 服务器用来处理保持和拥有 socket 连接的客户机时,会获得更好的性能。



参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.

  • 下载本文中使用的 源代码



  • 看看“ Merlin 给 Java 平台带来了非阻塞 I/O”( developerWorks,2002 年 3 月),获得 NIO 语义的进一步知识。



  • 综合的 developerWorks 教程“ NIO 入门”( developerWorks,2003 年 11 月)从高级的概念到底层的编程细节,详细论及了 NIO 库。



  • Merlin Hughes 的由两部分组成的文章“ Turning streams inside out”( developerWorks,2002 年 9 月)为 Java I/O(标准版和 NIO 版)的一些普遍的挑战提供了制作精巧的设计解决方案。



  • 为获取有关 Java I/O 问题的一些背景知识,请参阅 Allen Holub 的“ 关于解决 Java 编程语言线程问题的建议 ”( developerWorks,2000 年 10 月)。



  • 访问 NIO 主页,从资源中学习非阻塞 I/O。



  • JavaNIO.info 是查找有关 NIO 的资源的理想地方。



  • 为从书本系统学习 NIO,请参阅该领域的经典著作:Ron Hitchens 的 Java NIO(O'Reilly & Associates,2002 年)。



  • 在 developerWorks Java 技术专区可以找到有关 Java 编程各个方面的文章。



  • 访问 Developer Bookstore,获取技术书籍的完整列表,其中包括数百本 Java 相关的图书



  • 也请参阅 Java 技术专区教程页,从 developerWorks 获取免费的 Java 专门教程的完整列表。




关于作者

 

Taylor Cowan 是一位软件工程师,也是一位专攻 J2EE 的自由撰稿人。他从 North Texas 大学的计算机科学专业获得了硕士学位,另外,他还从 Jazz Arranging 获得了音乐学士学位。

posted @ 2008-01-03 22:58 hk2000c 阅读(402) | 评论 (0)编辑 收藏


    一种是继承自Thread类.Thread 类是一个具体的类,即不是抽象类,该类封装了线程的行为。要创建一个线程,程序员必须创建一个从 Thread 类导出的新类。程序员通过覆盖 Thread 的 run() 函数来完成有用的工作用户并不直接调用此函数;而是通过调用 Thread 的 start() 函数,该函数再调用 run()。
   
    例如:

 

    public class Test extends Thread{
      public Test(){
      }
      public static void main(String args[]){
        Test t1 = new Test();
        Test t2 = new Test();
        t1.start();
        t2.start();
      }
      public void run(){
        //do thread's things
      }
    }

 



    
    另一种是实现Runnable接口,此接口只有一个函数,run(),此函数必须由实现了此接口的类实现。
   
    例如:

 

    public class Test implements Runnable{
      Thread thread1;
      Thread thread2;
      public Test(){
        thread1 = new Thread(this,"1");
        thread2 = new Thread(this,"2");
      }
      public static void main(String args[]){
        Test t = new Test();
        t.startThreads();
      }
      public void run(){
        //do thread's things
      }
      public void startThreads(){
        thread1.start();
        thread2.start();
      }
    }

    两种创建方式看起来差别不大,但是弄不清楚的话,也许会将你的程序弄得一团糟。两者区别有以下几点:

1.当你想继承某一其它类时,你只能用后一种方式.

2.第一种因为继承自Thread,只创建了自身对象,但是在数量上,需要几个线程,就得创建几个自身对象;第二种只创建一个自身对象,却创建几个Thread对象.而两种方法重大的区别就在于此,请你考虑:如果你在第一种里创建数个自身对象并且start()后,你会发现好像synchronized不起作用了,已经加锁的代码块或者方法居然同时可以有几个线程进去,而且同样一个变量,居然可以有好几个线程同时可以去更改它。(例如下面的代码)这是因为,在这个程序中,虽然你起了数个线程,可是你也创建了数个对象,而且,每个线程对应了每个对象也就是说,每个线程更改和占有的对象都不一样,所以就出现了同时有几个线程进入一个方法的现象,其实,那也不是一个方法,而是不同对象的相同的方法。所以,这时候你要加锁的话,只能将方法或者变量声明为静态,将static加上后,你就会发现,线程又能管住方法了,同时不可能有两个线程进入同样一个方法,那是因为,现在不是每个对象都拥有一个方法了,而是所有的对象共同拥有一个方法,这个方法就是静态方法。

    而你如果用第二种方法使用线程的话,就不会有上述的情况,因为此时,你只创建了一个自身对象,所以,自身对象的属性和方法对于线程来说是共有的。

    因此,我建议,最好用后一种方法来使用线程。

public class mainThread extends Thread{
  int i=0;
  public static void main(String args[]){
    mainThread m1 = new mainThread();
    mainThread m2 = new mainThread();
    mainThread m3 = new mainThread();
    mainThread m4 = new mainThread();
    mainThread m5 = new mainThread();
    mainThread m6 = new mainThread();
    m1.start();
    m2.start();
    m3.start();
    m4.start();
    m5.start();
    m6.start();
  }
  public synchronized void t1(){
    i=++i;
    try{
      Thread.sleep(500);
    }
    catch(Exception e){}
    //每个线程都进入各自的t1()方法,分别打印各自的i
    System.out.println(Thread.currentThread().getName()+" "+i);
  }
  public void run(){
    synchronized(this){
      while (true) {
        t1();
      }
    }
  }
}

 

 


 

 

    下面我们来讲synchronized的4种用法吧:

    1.方法声明时使用,放在范围操作符(public等)之后,返回类型声明(void等)之前.即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入.
 
      例如:

      public synchronized void synMethod() {
        //方法体
      }

    2.对某一代码块使用,synchronized后跟括号,括号里是变量,这样,一次只有一个线程进入该代码块.例如:

      public int synMethod(int a1){
        synchronized(a1) {
          //一次只能有一个线程进入
        }
      }
    3.synchronized后面括号里是一对象,此时,线程获得的是对象锁.例如:

public class MyThread implements Runnable {
  public static void main(String args[]) {
    MyThread mt = new MyThread();
    Thread t1 = new Thread(mt, "t1");
    Thread t2 = new Thread(mt, "t2");
    Thread t3 = new Thread(mt, "t3");
    Thread t4 = new Thread(mt, "t4");
    Thread t5 = new Thread(mt, "t5");
    Thread t6 = new Thread(mt, "t6");
    t1.start();
    t2.start();
    t3.start();
    t4.start();
    t5.start();
    t6.start();
  }

  public void run() {
    synchronized (this) {
      System.out.println(Thread.currentThread().getName());
    }
  }
}


 
    对于3,如果线程进入,则得到对象锁,那么别的线程在该类所有对象上的任何操作都不能进行.在对象级使用锁通常是一种比较粗糙的方法。为什么要将整个对象都上锁,而不允许其他线程短暂地使用对象中其他同步方法来访问共享资源?如果一个对象拥有多个资源,就不需要只为了让一个线程使用其中一部分资源,就将所有线程都锁在外面。由于每个对象都有锁,可以如下所示使用虚拟对象来上锁:

class FineGrainLock {

   MyMemberClass x, y;
   Object xlock = new Object(), ylock = new Object();

   public void foo() {
      synchronized(xlock) {
         //access x here
      }

      //do something here - but don't use shared resources

      synchronized(ylock) {
         //access y here
      }
   }

   public void bar() {
      synchronized(this) {
         //access both x and y here
      }
      //do something here - but don't use shared resources
   }
}

 

    4.synchronized后面括号里是类.例如:

class ArrayWithLockOrder{
  private static long num_locks = 0;
  private long lock_order;
  private int[] arr;

  public ArrayWithLockOrder(int[] a)
  {
    arr = a;
    synchronized(ArrayWithLockOrder.class) {//-----------------------------------------这里
      num_locks++;             // 锁数加 1。
      lock_order = num_locks;  // 为此对象实例设置唯一的 lock_order。
    }
  }
  public long lockOrder()
  {
    return lock_order;
  }
  public int[] array()
  {
    return arr;
  }
}

class SomeClass implements Runnable
{
  public int sumArrays(ArrayWithLockOrder a1,
                       ArrayWithLockOrder a2)
  {
    int value = 0;
    ArrayWithLockOrder first = a1;       // 保留数组引用的一个
    ArrayWithLockOrder last = a2;        // 本地副本。
    int size = a1.array().length;
    if (size == a2.array().length)
    {
      if (a1.lockOrder() > a2.lockOrder())  // 确定并设置对象的锁定
      {                                     // 顺序。
        first = a2;
        last = a1;
      }
      synchronized(first) {              // 按正确的顺序锁定对象。
        synchronized(last) {
          int[] arr1 = a1.array();
          int[] arr2 = a2.array();
          for (int i=0; i<size; i++)
            value += arr1[i] + arr2[i];
        }
      }
    }
    return value;
  }
  public void run() {
    //...
  }
}

 

    对于4,如果线程进入,则线程在该类中所有操作不能进行,包括静态变量和静态方法,实际上,对于含有静态方法和静态变量的代码块的同步,我们通常用4来加锁.

以上4种之间的关系:

    锁是和对象相关联的,每个对象有一把锁,为了执行synchronized语句,线程必须能够获得synchronized语句中表达式指定的对象的锁,一个对象只有一把锁,被一个线程获得之后它就不再拥有这把锁,线程在执行完synchronized语句后,将获得锁交还给对象。
    在方法前面加上synchronized修饰符即可以将一个方法声明为同步化方法。同步化方法在执行之前获得一个锁。如果这是一个类方法,那么获得的锁是和声明方法的类相关的Class类对象的锁。如果这是一个实例方法,那么此锁是this对象的锁。

 


 

  下面谈一谈一些常用的方法:

  wait(),wait(long),notify(),notifyAll()等方法是当前类的实例方法,
   
        wait()是使持有对象锁的线程释放锁;
        wait(long)是使持有对象锁的线程释放锁时间为long(毫秒)后,再次获得锁,wait()和wait(0)等价;
        notify()是唤醒一个正在等待该对象锁的线程,如果等待的线程不止一个,那么被唤醒的线程由jvm确定;
        notifyAll是唤醒所有正在等待该对象锁的线程.
        在这里我也重申一下,我们应该优先使用notifyAll()方法,因为唤醒所有线程比唤醒一个线程更容易让jvm找到最适合被唤醒的线程.

    对于上述方法,只有在当前线程中才能使用,否则报运行时错误java.lang.IllegalMonitorStateException: current thread not owner.

 


 

    下面,我谈一下synchronized和wait()、notify()等的关系:

1.有synchronized的地方不一定有wait,notify

2.有wait,notify的地方必有synchronized.这是因为wait和notify不是属于线程类,而是每一个对象都具有的方法,而且,这两个方法都和对象锁有关,有锁的地方,必有synchronized。

另外,请注意一点:如果要把notify和wait方法放在一起用的话,必须先调用notify后调用wait,因为如果调用完wait,该线程就已经不是current thread了。如下例:

/**
 * Title:        Jdeveloper's Java Projdect
 * Description:  n/a
 * Copyright:    Copyright (c) 2001
 * Company:      soho  http://www.ChinaJavaWorld.com
 * @author jdeveloper@21cn.com
 * @version 1.0
 */
import java.lang.Runnable;
import java.lang.Thread;

public class DemoThread
    implements Runnable {

  public DemoThread() {
    TestThread testthread1 = new TestThread(this, "1");
    TestThread testthread2 = new TestThread(this, "2");

    testthread2.start();
    testthread1.start();

  }

  public static void main(String[] args) {
    DemoThread demoThread1 = new DemoThread();

  }

  public void run() {

    TestThread t = (TestThread) Thread.currentThread();
    try {
      if (!t.getName().equalsIgnoreCase("1")) {
        synchronized (this) {
          wait();
        }
      }
      while (true) {

        System.out.println("@time in thread" + t.getName() + "=" +
                           t.increaseTime());

        if (t.getTime() % 10 == 0) {
          synchronized (this) {
            System.out.println("****************************************");
            notify();
            if (t.getTime() == 100)
              break;
            wait();
          }
        }
      }
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }

}

class TestThread
    extends Thread {
  private int time = 0;
  public TestThread(Runnable r, String name) {
    super(r, name);
  }

  public int getTime() {
    return time;
  }

  public int increaseTime() {
    return++time;
  }

}

    下面我们用生产者/消费者这个例子来说明他们之间的关系:

    public class test {
  public static void main(String args[]) {
    Semaphore s = new Semaphore(1);
    Thread t1 = new Thread(s, "producer1");
    Thread t2 = new Thread(s, "producer2");
    Thread t3 = new Thread(s, "producer3");
    Thread t4 = new Thread(s, "consumer1");
    Thread t5 = new Thread(s, "consumer2");
    Thread t6 = new Thread(s, "consumer3");
    t1.start();
    t2.start();
    t3.start();
    t4.start();
    t5.start();
    t6.start();
  }
}

class Semaphore
    implements Runnable {
  private int count;
  public Semaphore(int n) {
    this.count = n;
  }

  public synchronized void acquire() {
    while (count == 0) {
      try {
        wait();
      }
      catch (InterruptedException e) {
        //keep trying
      }
    }
    count--;
  }

  public synchronized void release() {
    while (count == 10) {
      try {
        wait();
      }
      catch (InterruptedException e) {
        //keep trying
      }
    }
    count++;
    notifyAll(); //alert a thread that's blocking on this semaphore
  }

  public void run() {
    while (true) {
      if (Thread.currentThread().getName().substring(0,8).equalsIgnoreCase("consumer")) {
        acquire();
      }
      else if (Thread.currentThread().getName().substring(0,8).equalsIgnoreCase("producer")) {
        release();
      }
      System.out.println(Thread.currentThread().getName() + " " + count);
    }
  }
}

       生产者生产,消费者消费,一般没有冲突,但当库存为0时,消费者要消费是不行的,但当库存为上限(这里是10)时,生产者也不能生产.请好好研读上面的程序,你一定会比以前进步很多.

      上面的代码说明了synchronized和wait,notify没有绝对的关系,在synchronized声明的方法、代码块中,你完全可以不用wait,notify等方法,但是,如果当线程对某一资源存在某种争用的情况下,你必须适时得将线程放入等待或者唤醒.

 
posted @ 2008-01-02 18:14 hk2000c 阅读(349) | 评论 (0)编辑 收藏

2003 年 11 月 24 日

尽管 SSL 阻塞操作――当读写数据的时候套接字的访问被阻塞――与对应的非阻塞方式相比提供了更好的 I/O 错误通知,但是非阻塞操作允许调用的线程继续运行。本文中,作者同时就客户端和服务器端描述了如何使用Java Secure Socket Extensions (JSSE) 和 Java NIO (新 I/O)库创建非阻塞的安全连接,并且介绍了创建非阻塞套接字的传统方法,以及使用JSSE 和 NIO 的一种可选的(必需的)方法。

阻塞,还是非阻塞?这就是问题所在。无论在程序员的头脑中多么高贵……当然这不是莎士比亚,本文提出了任何程序员在编写 Internet 客户程序时都应该考虑的一个重要问题。通信操作应该是阻塞的还是非阻塞的?

许多程序员在使用 Java 语言编写 Internet 客户程序时并没有考虑这个问题,主要是因为在以前只有一种选择――阻塞通信。但是现在 Java 程序员有了新的选择,因此我们编写的每个客户程序也许都应该考虑一下。

非阻塞通信在 Java 2 SDK 的 1.4 版被引入 Java 语言。如果您曾经使用该版本编过程序,可能会对新的 I/O 库(NIO)留下了印象。在引入它之前,非阻塞通信只有在实现第三方库的时候才能使用,而第三方库常常会给应用程序引入缺陷。

NIO 库包含了文件、管道以及客户机和服务器套接字的非阻塞功能。库中缺少的一个特性是安全的非阻塞套接字连接。在 NIO 或者 JSSE 库中没有建立安全的非阻塞通道类,但这并不意味着不能使用安全的非阻塞通信。只不过稍微麻烦一点。

要完全领会本文,您需要熟悉:

  • Java 套接字通信的概念。您也应该实际编写过应用程序。而且不只是打开连接、读取一行然后退出的简单应用程序,应该是实现 POP3 或 HTTP 之类协议的客户机或通信库这样的程序。
  • SSL 基本概念和加密之类的概念。基本上就是知道如何设置一个安全连接(但不必担心 JSSE ――这就是关于它的一个“紧急教程”)。
  • NIO 库。
  • 在您选择的平台上安装 Java 2 SDK 1.4 或以后的版本。(我是在 Windows 98 上使用 1.4.1_01 版。)

如果需要关于这些技术的介绍,请参阅 参考资料部分。

那么到底什么是阻塞和非阻塞通信呢?

阻塞和非阻塞通信

阻塞通信意味着通信方法在尝试访问套接字或者读写数据时阻塞了对套接字的访问。在 JDK 1.4 之前,绕过阻塞限制的方法是无限制地使用线程,但这样常常会造成大量的线程开销,对系统的性能和可伸缩性产生影响。java.nio 包改变了这种状况,允许服务器有效地使用 I/O 流,在合理的时间内处理所服务的客户请求。

没有非阻塞通信,这个过程就像我所喜欢说的“为所欲为”那样。基本上,这个过程就是发送和读取任何能够发送/读取的东西。如果没有可以读取的东西,它就中止读操作,做其他的事情直到能够读取为止。当发送数据时,该过程将试图发送所有的数据,但返回实际发送出的内容。可能是全部数据、部分数据或者根本没有发送数据。

阻塞与非阻塞相比确实有一些优点,特别是遇到错误控制问题的时候。在阻塞套接字通信中,如果出现错误,该访问会自动返回标志错误的代码。错误可能是由于网络超时、套接字关闭或者任何类型的 I/O 错误造成的。在非阻塞套接字通信中,该方法能够处理的唯一错误是网络超时。为了检测使用非阻塞通信的网络超时,需要编写稍微多一点的代码,以确定自从上一次收到数据以来已经多长时间了。

哪种方式更好取决于应用程序。如果使用的是同步通信,如果数据不必在读取任何数据之前处理的话,阻塞通信更好一些,而非阻塞通信则提供了处理任何已经读取的数据的机会。而异步通信,如 IRC 和聊天客户机则要求非阻塞通信以避免冻结套接字。





回页首


创建传统的非阻塞客户机套接字

Java NIO 库使用通道而非流。通道可同时用于阻塞和非阻塞通信,但创建时默认为非阻塞版本。但是所有的非阻塞通信都要通过一个名字中包含 Channel 的类完成。在套接字通信中使用的类是 SocketChannel, 而创建该类的对象的过程不同于典型的套接字所用的过程,如清单 1 所示。


清单 1. 创建并连接 SocketChannel 对象
SocketChannel sc = SocketChannel.open();
            sc.connect("www.ibm.com",80);
            sc.finishConnect();
            

必须声明一个 SocketChannel 类型的指针,但是不能使用 new 操作符创建对象。相反,必须调用 SocketChannel 类的一个静态方法打开通道。打开通道后,可以通过调用 connect() 方法与它连接。但是当该方法返回时,套接字不一定是连接的。为了确保套接字已经连接,必须接着调用 finishConnect()

当套接字连接之后,非阻塞通信就可以开始使用 SocketChannel 类的 read()write() 方法了。也可以把该对象强制转换成单独的 ReadableByteChannelWritableByteChannel 对象。无论哪种方式,都要对数据使用 Buffer 对象。因为 NIO 库的使用超出了本文的范围,我们不再对此进一步讨论。

当不再需要套接字时,可以使用 close() 方法将其关闭:

sc.close();
            

这样就会同时关闭套接字连接和底层的通信通道。





回页首


创建替代的非阻塞的客户机套接字

上述方法比传统的创建套接字连接的例程稍微麻烦一点。不过,传统的例程也能用于创建非阻塞套接字,不过需要增加几个步骤以支持非阻塞通信。

SocketChannel 对象中的底层通信包括两个 Channel 类: ReadableByteChannelWritableByteChannel。 这两个类可以分别从现有的 InputStreamOutputStream 阻塞流中使用 Channels 类的 newChannel() 方法创建,如清单 2 所示:


清单 2. 从流中派生通道
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream());
            WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());
            

Channels 类也用于把通道转换成流或者 reader 和 writer。这似乎是把通信切换到阻塞模式,但并非如此。如果试图读取从通道派生的流,读方法将抛出 IllegalBlockingModeException 异常。

相反方向的转换也是如此。不能使用 Channels 类把流转换成通道而指望进行非阻塞通信。如果试图读从流派生的通道,读仍然是阻塞的。但是像编程中的许多事情一样,这一规则也有例外。

这种例外适合于实现 SelectableChannel 抽象类的类。 SelectableChannel 和它的派生类能够选择使用阻塞或者非阻塞模式。 SocketChannel 就是这样的一个派生类。

但是,为了能够在两者之间来回切换,接口必须作为 SelectableChannel 实现。对于套接字而言,为了实现这种能力必须使用 SocketChannel 而不是 Socket

回顾一下,要创建套接字,首先必须像通常使用 Socket 类那样创建一个套接字。套接字连接之后,使用 清单 2中的两行代码把流转换成通道。


清单 3. 创建套接字的另一种方法
Socket s = new Socket("www.ibm.com", 80);
            ReadableByteChannel rbc = Channels.newChannel(s.getInputStream());
            WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());
            

如前所述,这样并不能实现非阻塞套接字通信――所有的通信仍然在阻塞模式下。在这种情况下,非阻塞通信必须模拟实现。模拟层不需要多少代码。让我们来看一看。

从模拟层读数据

模拟层在尝试读操作之前首先检查数据的可用性。如果数据可读则开始读。如果没有数据可用,可能是因为套接字被关闭,则返回表示这种情况的代码。在清单 4 中要注意仍然使用了 ReadableByteChannel 读,尽管 InputStream 完全可以执行这个动作。为什么这样做呢?为了造成是 NIO 而不是模拟层执行通信的假象。此外,还可以使模拟层与其他通道更容易结合,比如向文件通道内写入数据。


清单 4. 模拟非阻塞的读操作
/* The checkConnection method returns the character read when
            determining if a connection is open.
            */
            y = checkConnection();
            if(y <= 0) return y;
            buffer.putChar((char ) y);
            return rbc.read(buffer);
            

向模拟层写入数据

对于非阻塞通信,写操作只写入能够写的数据。发送缓冲区的大小和一次可以写入的数据多少有很大关系。缓冲区的大小可以通过调用 Socket 对象的 getSendBufferSize() 方法确定。在尝试非阻塞写操作时必须考虑到这个大小。如果尝试写入比缓冲块更大的数据,必须拆开放到多个非阻塞写操作中。太大的单个写操作可能被阻塞。


清单 5. 模拟非阻塞的写操作
int x, y = s.getSendBufferSize(), z = 0;
            int expectedWrite;
            byte [] p = buffer.array();
            ByteBuffer buf = ByteBuffer.allocateDirect(y);
            /* If there isn't any data to write, return, otherwise flush the stream */
            if(buffer.remaining() == 0) return 0;
            os.flush()
            for(x = 0; x < p.length; x += y)
            {
            if(p.length - x < y)
            {
            buf.put(p, x, p.length - x);
            expectedWrite = p.length - x;
            }
            else
            {
            buf.put(p, x, y);
            expectedWrite = y;
            }
            /* Check the status of the socket to make sure it's still open */
            if(!s.isConnected()) break;
            /* Write the data to the stream, flushing immediately afterward */
            buf.flip();
            z = wbc.write(buf); os.flush();
            if(z < expectedWrite) break;
            buf.clear();
            }
            if(x > p.length) return p.length;
            else if(x == 0) return -1;
            else return x + z;
            

与读操作类似,首先要检查套接字是否仍然连接。但是如果把数据写入 WritableByteBuffer 对象,就像清单 5 那样,该对象将自动进行检查并在没有连接时抛出必要的异常。在这个动作之后开始写数据之前,流必须立即被清空,以保证发送缓冲区中有发送数据的空间。任何写操作都要这样做。发送到块中的数据与发送缓冲区的大小相同。执行清除操作可以保证发送缓冲不会溢出而导致写操作被阻塞。

因为假定写操作只能写入能够写的内容,这个过程还必须检查套接字保证它在每个数据块写入后仍然是打开的。如果在写入数据时套接字被关闭,则必须中止写操作并返回套接字关闭之前能够发送的数据量。

BufferedOutputReader 可用于模拟非阻塞写操作。如果试图写入超过缓冲区两倍长度的数据,则直接写入缓冲区整倍数长度的数据(缓冲余下的数据)。比如说,如果缓冲区的长度是 256 字节而需要写入 529 字节的数据,则该对象将清除当前缓冲区、发送 512 字节然后保存剩下的 17 字节。

对于非阻塞写而言,这并非我们所期望的。我们希望分次把数据写入同样大小的缓冲区中,并最终把全部数据都写完。如果发送的大块数据留下一些数据被缓冲,那么在所有数据被发送的时候,写操作就会被阻塞。

模拟层类模板

整个模拟层可以放到一个类中,以便更容易和应用程序集成。如果要这样做,我建议从 ByteChannel 派生这个类。这个类可以强制转换成单独的 ReadableByteChannelWritableByteChannel 类。

清单 6 给出了从 ByteChannel 派生的模拟层类模板的一个例子。本文后面将一直使用这个类表示通过阻塞连接执行的非阻塞操作。


清单 6. 模拟层的类模板
public class nbChannel implements ByteChannel
            {
            Socket s;
            InputStream is; OutputStream os;
            ReadableByteChannel rbc;
            WritableByteChannel wbc;
            public nbChannel(Socket socket);
            public int read(ByteBuffer dest);
            public int write(ByteBuffer src);
            public void close();
            protected int checkConnection();
            }
            

使用模拟层创建套接字

使用新建的模拟层创建套接字非常简单。只要像通常那样创建 Socket 对象,然后创建 nbChannel 对象就可以了,如清单 7 所示:


清单 7. 使用模拟层
Socket s = new Socket("www.ibm.com", 80);
            nbChannel socketChannel = new nbChannel(s);
            ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
            WritableByteChannel wbc = (WritableByteChannel)socketChannel;
            





回页首


创建传统的非阻塞服务器套接字

服务器端的非阻塞套接字和客户端上的没有很大差别。稍微麻烦一点的只是建立接受输入连接的套接字。套接字必须通过从服务器套接字通道派生一个阻塞的服务器套接字绑定到阻塞模式。清单 8 列出了需要做的步骤。


清单 8. 创建非阻塞的服务器套接字(SocketChannel)
ServerSocketChannel ssc = ServerSocketChannel.open();
            ServerSocket ss = ssc.socket();
            ss.bind(new InetSocketAddress(port));
            SocketChannel sc = ssc.accept();
            

与客户机套接字通道相似,服务器套接字通道也必须打开而不是使用 new 操作符或者构造函数。在打开之后,必须派生服务器套接字对象以便把套接字通道绑定到一个端口。一旦套接字被绑定,服务器套接字对象就可以丢弃了。

通道使用 accept() 方法接收到来的连接并把它们转给套接字通道。一旦接收了到来的连接并转给套接字通道对象,通信就可以通过 read()write() 方法开始进行了。





回页首


创建替代的非阻塞服务器套接字

实际上,并非真正的替代。因为服务器套接字通道必须使用服务器套接字对象绑定,为何不完全绕开服务器套接字通道而仅使用服务器套接字对象呢?不过这里的通信不使用 SocketChannel ,而要使用模拟层 nbChannel。


清单 9. 建立服务器套接字的另一种方法
ServerSocket ss = new ServerSocket(port);
            Socket s = ss.accept();
            nbChannel socketChannel = new nbChannel(s);
            ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
            WritableByteChannel wbc = (WritableByteChannel)socketChannel;
            





回页首


创建 SSL 连接

创建SSL连接,我们要分别从客户端和服务器端考察。

从客户端

创建 SS L连接的传统方法涉及到使用套接字工厂和其他一些东西。我将不会详细讨论如何创建SSL连接,不过有一本很好的教程,“Secure your sockets with JSSE”(请参阅 参考资料),从中您可以了解到更多的信息。

创建 SSL 套接字的默认方法非常简单,只包括几个很短的步骤:

  1. 创建套接字工厂。
  2. 创建连接的套接字。
  3. 开始握手。
  4. 派生流。
  5. 通信。

清单 10 说明了这些步骤:


清单 10. 创建安全的客户机套接字
SSLSocketFactory sslFactory =
            (SSLSocketFactory)SSLSocketFactory.getDefault();
            SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port);
            ssl.startHandshake();
            InputStream is = ssl.getInputStream();
            OutputStream os = ssl.getOutputStream();
            

默认方法不包括客户验证、用户证书和其他特定连接可能需要的东西。

从服务器端

建立SSL服务器连接的传统方法稍微麻烦一点,需要加上一些类型转换。因为这些超出了本文的范围,我将不再进一步介绍,而是说说支持SSL服务器连接的默认方法。

创建默认的 SSL 服务器套接字也包括几个很短的步骤:

  1. 创建服务器套接字工厂。
  2. 创建并绑定服务器套接字。
  3. 接受传入的连接。
  4. 开始握手。
  5. 派生流。
  6. 通信。

尽管看起来似乎与客户端的步骤相似,要注意这里去掉了很多安全选项,比如客户验证。

清单 11 说明这些步骤:


清单 11. 创建安全的服务器套接字
SSLServerSocketFactory sslssf =
            (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
            SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port);
            SSLSocket ssls = (SSLSocket)sslss.accept();
            ssls.startHandshake();
            InputStream is = ssls.getInputStream();
            OutputStream os = ssls.getOutputStream();
            





回页首


创建安全的非阻塞连接

要精心实现安全的非阻塞连接,也需要分别从客户端和服务器端来看。

从客户端

在客户端建立安全的非阻塞连接非常简单:

  1. 创建并连接 Socket 对象。
  2. Socket 对象添加到模拟层上。
  3. 通过模拟层通信。

清单 12 说明了这些步骤:


清单 12. 创建安全的客户机连接
/* Create the factory, then the secure socket */
            SSLSocketFactory sslFactory =
            (SSLSocketFactory)SSLSocketFactory.getDefault();
            SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port);
            /* Start the handshake.  Should be done before deriving channels */
            ssl.startHandshake();
            /* Put it into the emulation layer and create separate channels */
            nbChannel socketChannel = new nbChannel(ssl);
            ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
            WritableByteChannel wbc = (WritableByteChannel)socketChannel;
            

利用前面给出的 模拟层类 就可以实现非阻塞的安全连接。因为安全套接字通道不能使用 SocketChannel 类打开,而 Java API 中又没有完成这项工作的类,所以创建了一个模拟类。模拟类可以实现非阻塞通信,无论使用安全套接字连接还是非安全套接字连接。

列出的步骤包括默认的安全设置。对于更高级的安全性,比如用户证书和客户验证, 参考资料 部分提供了说明如何实现的文章。

从服务器端

在服务器端建立套接字需要对默认安全稍加设置。但是一旦套接字被接收和路由,设置必须与客户端的设置完全相同,如清单 13 所示:


清单 13. 创建安全的非阻塞服务器套接字
/* Create the factory, then the socket, and put it into listening mode */
            SSLServerSocketFactory sslssf =
            (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
            SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port);
            SSLSocket ssls = (SSLSocket)sslss.accept();
            /* Start the handshake on the new socket */
            ssls.startHandshake();
            /* Put it into the emulation layer and create separate channels */
            nbChannel socketChannel = new nbChannel(ssls);
            ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
            WritableByteChannel wbc = (WritableByteChannel)socketChannel;
            

同样,要记住这些步骤使用的是默认安全设置。





回页首


集成安全的和非安全的客户机连接

多数 Internet 客户机应用程序,无论使用 Java 语言还是其他语言编写,都需要提供安全和非安全连接。Java Secure Socket Extensions 库使得这项工作非常容易,我最近在编写一个 HTTP 客户库时就使用了这种方法。

SSLSocket 类派生自 Socket。 您可能已经猜到我要怎么做了。所需要的只是该对象的一个 Socket 指针。如果套接字连接不使用SSL,则可以像通常那样创建套接字。如果要使用 SSL,就稍微麻烦一点,但此后的代码就很简单了。清单 14 给出了一个例子:


清单 14. 集成安全的和非安全的客户机连接
Socket s;
            ReadableByteChannel rbc;
            WritableByteChannel wbc;
            nbChannel socketChannel;
            if(!useSSL) s = new Socket(host, port);
            else
            {
            SSLSocketFactory sslsf = SSLSocketFactory.getDefault();
            SSLSocket ssls = (SSLSocket)SSLSocketFactory.createSocket(host, port);
            ssls.startHandshake();
            s = ssls;
            }
            socketChannel = new nbChannel(s);
            rbc = (ReadableByteChannel)socketChannel;
            wbc = (WritableByteChannel)socketChannel;
            ...
            s.close();
            

创建通道之后,如果套接字使用了SSL,那么就是安全通信,否则就是普通通信。如果使用了 SSL,关闭套接字将导致握手中止。

这种设置的一种可能是使用两个单独的类。一个类负责处理通过套接字沿着与非安全套接字的连接进行的所有通信。一个单独的类应该负责创建安全的连接,包括安全连接的所有必要设置,无论是否是默认的。安全类应该直接插入通信类,只有在使用安全连接时被调用。

posted @ 2007-12-31 05:07 hk2000c 阅读(1496) | 评论 (0)编辑 收藏

 

使用J2SEAPI读取Properties文件的六种方法



  1。使用java.util.Properties类的load()方法示例:InputStreamin=lnewBufferedInputStream(newFileInputStream(name));Propertiesp=newProperties();p.load(in);

  2。使用java.util.ResourceBundle类的getBundle()方法示例:ResourceBundlerb=ResourceBundle.getBundle(name,Locale.getDefault());

  3。使用java.util.PropertyResourceBundle类的构造函数示例:InputStreamin=newBufferedInputStream(newFileInputStream(name));ResourceBundlerb=newPropertyResourceBundle(in);

  4。使用class变量的getResourceAsStream()方法示例:InputStreamin=JProperties.class.getResourceAsStream(name);Propertiesp=newProperties();p.load(in);

  5。使用class.getClassLoader()所得到的java.lang.ClassLoader的getResourceAsStream()方法示例:InputStreamin=JProperties.class.getClassLoader().getResourceAsStream(name);Propertiesp=newProperties();p.load(in);

  6。使用java.lang.ClassLoader类的getSystemResourceAsStream()静态方法示例:InputStreamin=ClassLoader.getSystemResourceAsStream(name);Propertiesp=newProperties();p.load(in);

  补充

  Servlet中可以使用javax.servlet.ServletContext的getResourceAsStream()方法示例:InputStreamin=context.getResourceAsStream(path);Propertiesp=newProperties();p.load(in);

 


posted @ 2007-12-27 00:00 hk2000c 阅读(279) | 评论 (0)编辑 收藏

 

Windows和浏览器快捷键

使用计算机和软件的最大理由是可以提高工作效率。提高效率的关键,一是佳软,二是善用。熟练掌握热键,乃是高效工作之道的基础和不二法门。下文针对最经典的软件,列举了最实用的快捷键,涉及:操作系统、浏览器、播放器、交流工具、文件管理工具、文本编辑等。要注意的是,相同的全局热键,只能让一个程序生效。

Windows 及相应软件

1 最小化所有窗口(显示桌面)/恢复原状 Win + D

2 打开运行对话框 Win + R

3 打开系统属性 Win + Break/Pause

4 复制某一对象 按住CTRL拖动

5 选中/高亮文本块 CTRL+SHIFT+方向键

6 按打开的顺序在窗口间切换 Alt + Esc

7 复制文件 CTRL + C

8 粘贴文件 CTRL + V

9 剪切文件 CTRL + X

10 还原 CTRL + Z

11 撤消还原操作(如果可能的话) CTRL + Y

12 打开辅助工具 Win + U

13 打开资源管理器 Win + E

14 打开上下文菜单 Shift + F10

15 多页签时,在页签间切换 Ctrl + Tab

更多Windows全局热键: Windows keyboard shortcuts

更多Mac OS全局热键: Mac OS X keyboard shortcuts

更多Linux全局热键: Linux Keyboard Shortcuts You Should Know About

浏览器: Firefox

16 去除csS样式 Alt + V + Y + N (或 CTRL + Shift + S + Web Developer’s 工具栏)

17 恢复CSS样式 Alt + V + Y + B

18 查看源代码 Ctrl + U

19 查看选中内容的源代码 选取内容,Shift + F10,点“Show source code”

20 智能 DOM 侦测 Ctrl + Shift + I

21 启动Firebug F12

22 添加书签 Ctrl + D

23 书签 Ctrl + B

24 历鸣 Ctrl + H

25 找开刚关闭的标签 CTRL+SHIFT+T

26 全部标签加入书签 CTRL+SHIFT+D

27 返回 Alt + Left Arrow

28 前进 Alt + Right Arrow

29 访问历史中后退一步 Backspace

30 为书签添加关键词 这样做可以更快的访问书签。右击书签,选择属性,输入合适的关键词。保存后,你只要在地址栏输入关键词并回车,就可以访问书签了。

31 跳转到地址栏 Ctrl + L 或 F6

32 回到主页 Alt + Home

33 减小文本字号 Ctrl + -

34 增大文本字号 Ctrl + +

35 回到主页(与32重复) Alt + Home

36 快速搜索 /

37 跳转到搜索条 Ctrl + K

38 在标签历史上跳转 ALT + ← (后退), ALT + → (前进)

39 新建标签 Ctrl + T (键盘), 双击标签条 (鼠标)

40 关闭当前书签 Ctrl + W (键盘), 中键标签 (鼠标)

41 到下一标签 Ctrl + Page up 或 CTRL + Tab

42 到前一标签 Ctrl + Page Dn 或 Ctrl + Shift + Tab

43 新标签中打开链接 Ctrl + 左击

43 选择标签 Ctrl + [1 - 9]

45 到下一链接 Tab

46 到前一链接 Shift + Tab

47 输入框中显示输入过的文字,或下拉菜单中显示可选项 Alt + ↓

浏览器: Internet Explorer 7



48 在新的后台标签打开链接 CTRL+鼠标左键或中键

49 在新的前台标签打开链接 CTRL+SHIFT+鼠标左键或中键

50 打开快速标签视图 CTRL+Q

51 显示已打开标签列表 CTRL+SHIFT+Q

52 转到地址栏 Alt + D

53 在新标签中打开输入的地址 Address Bar in new tab Alt + Enter

54 转到搜索条 Ctrl + E

55 新建标签 Ctrl + T (keyboard), Double Click on Tab Bar (mouse)

56 关闭当前标签 Ctrl + W (keyboard), Middle Click on Tab (mouse)

57 到下一标签 Ctrl + Tab

58 到前一标签 Ctrl + Shift + Tab

59 到某一标签 Ctrl + [1 - 9]

60 打开feeds CTRL+J

61 到下一标签 Tab

62 到前一标签 Shift + Tab

Safari 热键

更多 MSIE 7热键

播放器快捷键

音乐播放: Winamp

要使用全局热键,须设置:Main Windows -> Options > Preferences > Global Hotkeys。其热键可自定义。

63 增大音量 CTRL + ALT + ↑

64 减小音量 CTRL + ALT + ↓

65 播放、重新开始或恢复播放 Winamp 窗口: x

66 暂停 CTRL + ALT + Home (Winamp 窗口: c)

67 播放 CTRL + ALT + Insert (Winamp 窗口: x)

68 停止 CTRL + ALT + End (Winamp 窗口: v)

69 前一首 CTRL + ALT + PgUp (Winamp 窗口: z)

70 下一首 CTRL + ALT + PgDn (Winamp 窗口: b)

71 返回5秒 CTRL + ALT + ← (Winamp 窗口: ←)

72 快进5秒 CTRL + ALT + → (Winamp 窗口: →)

73 切换重复 r

74 切换随机 s

75 添加文件 l

76 添加目录 Shift + l

77 随机播放列表 CTRL + Shift + r

更多热键: Winamp Shortcuts

Winamp Global Hotkeys

iTunes 热键: Hotkeys for iTunes

Last.FM 热键: Autohotkey Script for Last.FM

通讯: Thunderbird

在Skype中可自定义热键: Main Window -> Tools -> Options -> Hotkeys.

在Thunderbird中使用热键: Thunderbird Help: Keyboard Shortcuts 和 All hotkeys: Mozilla Thunderbird Hotkeys

xbeta提醒:Thunderbird和Firefox很多热键是相通的,并且可用keyconfig扩展更改和自定义更多热键。

78 转到下一封信 F

79 转到下一封未读信件 N

80 转到上一封未读信件 B

81 增大字号 Ctrl + +

82 减小字号 CTRL + -

83 标记信件为已读/未读 M

84 标记为垃圾邮件 J

85 标记为非垃圾邮件 SHIFT + J

86 查看信件源文件 CTRL + U

87 新建信件 CTRL + M, CTRL + N, Cmd + Shift + M (Mac)

88 回复信件 Ctrl + R

89 收取当前账户的信件 CTRL+T

90 收取所有账户的信件 CTRL+SHIFT+T

91 打开已收的信件 CTRL + O

92 发送信件 CTRL + Enter/Return

通讯: Google Mail

93 写邮件 c, + c 在新窗口中写信

94 回信 r, + r 在新窗口中回信

95 转发信件 f, + f 在新窗口中转发信件

96 转到收件箱 g 然后按 i

97 定位到搜索框 /

98 转到下一封信 n

99 转到上一封信 p

100 报告垃圾邮件 !

Total Commander快捷键

文件管理: Total CoMMander

xbeta注:Total Commander是Windows 下键盘操作的典范和极致。用TC不会热键,等于不懂TC。喜欢热键操作而未用TC——xbeta想不出会有这种情况。TC的热键是高度可以自定义的。如下举例仅是管中窥豹,还有更多热键需要你的发现,比如很重要的 alt+F5, alt+F9, alt+enter, →,space, alt+shift+enter, backspace,+, alt+, alt+F7, 及中国用户广泛自定义的ctrl+1,ctrl+2, ctrl+3等。

101 激活或取消激活左侧菜单 F10

102 比较和同步文件夹 SHIFT+F2

103 新建文本文件,并用编辑器打开 SHIFT + F4

104 在同一目录复制文件并改名 SHIFT + F5

105 重命名文件 SHIFT + F6

106 缩小到系统托盘图标 SHIFT + Esc

107 转到刚访问的前/后一个目录 ALT+left/right

108 显示已访问目录历史清单 ALT + Arrow Down

109 全选 CTRL+NUM +, CTRL + A

110 全不选 CTRL+NUM -

111 返回上级目录 (cd ..) CTRL+PgUp or Backspace

112 返回根目录 (多数欧洲键盘)(xbeta:适用于中国用户) CTRL+

113 返回根目录 (美国键盘布局)(xbeta注:对中国用户是新建目录) F7

114 按文件名排序 CTRL+F3

115 按后缀排序 CTRL+F4

116 按时间/日期排序 CTRL+F5

117 按大小排序 CTRL+F6

118 显示所有文件 CTRL+F10

119 仅显示可执行文件 CTRL + F11

120 显示用户自定义的文件 CTRL + F12

121 显示文件属性 ALT+ENTER

122 并列显示目录及所有子目录下的全部文件(xbeta:这是一个非常实用的功能,不用TC的人体会不到) CTRL + B

123 进入目录收藏列表(书签)(xbeta:TC用户就称之为ctrl+d) CTRL + D

124 连接FTP CTRL + F (断开: CTRL + SHIFT + F)

125 批量改名(xbeta:极其强大) CTRL + M

126 进入命令行对话框并带入当前路径 CTRL + P

127 快速浏览(xbeta:这是TC最常用、最实用的热键。相似功能F3) CTRL + Q

128 新建目录标签并激活它 CTRL + T (CTRL + SHIFT + T是新建标签但不激活)

129 左右窗口交换目录 CTRL + U

130 左右窗口交换目录和标签 CTRL + SHIFT + U

131 把光标处目录在新标签打开 CTRL + Arrow Up

132 转到下一标签 CTRL + TAB

133 转到上一标签 CTRL+SHIFT+TAB

134 定位到某一目录/文件(xbeta强烈建议定位框模式) CTRL+ALT+字母(s)

Wordpress

135 加粗 Alt + Shift + B

136 斜体 Alt + Shift + I

137 引用 Alt + Shift + Q

138 列表 (ul) Alt + Shift + U



139 序号列表 (ol) Alt + Shift + O

140 列表项 (li) Alt + Shift + L

141 代码 Alt + Shift + C

142 插入 Alt + Shift + S

143 删除 Alt + Shift + D

144 链接 Alt + Shift + A

145 more (Read More tag) Alt + Shift + T

146 发表文章 Alt + Shift + P

通讯: microsoft Office Outlook

147 切换到邮件 CTRL + 1

148 切换到日历 CTRL + 2

149 切换到通讯录 Ctrl + 3

150 切换到任务 Ctrl + 4

151 切换到笔记 Ctrl + 5

152 新建约会 CTRL+SHIFT+A

153 新建联系人 CTRL+SHIFT+C

154 新建日志项 CTRL+SHIFT+J

155 新建会议 CTRL+SHIFT+Q

156 新建信件 CTRL+SHIFT+M

157 新建笔记 CTRL+SHIFT+N

158 新建任务 CTRL + SHIFT + K

159 拼写检查 F7

160 转发信件 CTRL + F

161 搜索 F4

162 切换到收件箱 CTRL+SHIFT+I

163 切换到发件箱 CTRL+SHIFT+O

164 发送 Alt + S

165 回信 Ctrl + SHIFT + R

166 收信 CTRL+M or F9

167 写信 CTRL + N

168 打开信件 CTRL + O

169 标为已读 CTRL + Q

Outlook的全局热键: Keyboard shortcuts for Outlook

ICQ、ACDSee

通讯: ICQ

170 模仿在系统托盘中双击 CTRL + SHIFT + I

171 激活/取消激活用户窗口 CTRL + SHIFT + A

172 关闭 ICQ Control + Shift + I and then Alt + F4

173 发送url给联系人 Control + Shift + F6

174 改变状态 Alt + S +

175 接收信息 Control + Shift + I

176 选择好友 Insert +

图像管理: ACDSee Viewer

177 缩小显示比例(xbeta:推荐用免费软件IrfanView或XnView代替ACDSee) - (就是减号)

178 增加显示比例 +

179 复制当前项到某目录 ALT + C

180 移动当前项到某目录 ALT + M

181 改名当前项到某目录 ALT + R

182 打开属性面板 Alt + Enter

183 显/隐状态栏 b

184 显/隐菜单栏 Ctrl + Shift + M

185 用默认程序打开当前图像 CTRL + E

186 进入转换对话框 CTRL + F

187 复制选中部分到剪贴板 Ctrl + Insert

188 打开旋转/镜像对话框(xbeta:还是IrfanView方便) CTRL + J

189 打开当前图像,进入编辑模式,激活调节曝光工具 Ctrl + L

190 打开当前图像,进入编辑模式,激活调节大小工具 CTRL+R

191 清空选中内容 CTRL+Q

192 保存图像 CTRL + S

193 关闭查看窗口 CTRL + W


194 90度顺时针旋转当前图片(xbeta注:IrfanView只要按一下r即可) Ctrl + Alt +

195 改变图像为黑白两色 Ctrl + Shift + 1

196 进入批量处理菜单(xbeta注:IrfanView按B即可) CTRL + Alt + B

197 以默认程序打开当前文件 Shift + E

在小巧但极其强大的IrfanView中使用热键: Irfanview

编辑: Ultraedit

说到编辑器的快捷键,VIM是无与伦比的。要反对,也得是带脚踏板的Emacs。UE还是有差距的,很大差距。注意:VIM是开源、免费的,而UE则需要注册。UE是Windows下最好的编辑器——如果没有GVIM和Emacs的话。而VIM和Emacs则是任何操作系统下最好的编辑器。

198 自动换行 CTRL + W

199 插入当前日期/时间 F7

200 找到匹配的括号 (,[,{ or },],) CTRL + B

201 段落重新格式化 CTRL + T

202 Tag 列表 CTRL + F8

203 转换所选文字为小写 CTRL + F5

204 转换所选文字为大写 Alt + F5

205 激活拼写检查 CTRL + K

206 切换列/块模式 ALT + C

207 设定书签 CTRL + F2

208 转到下一书签 F2

209 插入用户定义的模板 Alt+0-9 or Shift+Alt+0-9

210 上滚一行,光标不变 CTRL + Up

211 下滚一行,光标不变 CTRL + Down

212 显示函数列表 F8

213 到下一段 Alt + Right

214 到上一段 Alt + Left




posted @ 2007-12-21 17:54 hk2000c 阅读(328) | 评论 (0)编辑 收藏

雪绒花(Edelweiss),又叫高山火绒草(德语中意为:高贵的白色),只生长在阿尔卑斯山脉的林线以上。雪绒花约有40个种类,分布在亚洲和欧洲的阿尔卑斯山脉一带。这种花在阿尔卑斯山脉中通常生长在海拔1700米以上的地方,由于它只生长在非常少有的岩石地表上,因而极为稀少。


在奥地利,雪绒花象征着勇敢,因为野生的雪绒花生长在条件艰苦的山上,常人难以得见其美丽容颜,所以见过雪绒花的人都是英雄。从前,奥地利许多年轻人冒着生命危险,攀上陡峭的山崖,只为摘下一朵雪绒花献给自己的心上人,只有雪绒花才能代表为爱牺牲一切的决心。关于雪绒花,奥地利有许多传说。人们相信雪绒花王可以指引那些采摘者们找到他们寻觅已久的花,但如果谁将花儿连根拔去,这个人将会坠入万丈深渊。


浪漫的传说固然令人神往,当雪绒花与军人联系起来的时候,它才被赋予新的意义,当佩带雪绒花的第三帝国军人凭借非凡的勇气征服一座座雪山的时候,渺小者的力量得到了充分的体现,德军用它作为自己山地部队的标志。传说每个志在加入山地部队的年轻人,都必须只携带最少的装备爬上阿尔卑斯山,采下一朵雪绒花,以证明自己适合做一个真正的山地军人。





posted @ 2007-12-20 23:57 hk2000c 阅读(181) | 评论 (0)编辑 收藏

仅列出标题
共11页: 上一页 1 2 3 4 5 6 7 8 9 下一页 Last