Hello-_-World님의 프로필撕裂空间的技术会所블로그리스트 도구 도움말
6월 16일

[转]谈谈Java语言的垃圾收集器

谈谈Java语言的垃圾收集器

2004-04-06 17:11:27
垃圾收集器是Java语言区别于其他程序设计语言的一大特色。它把程序员从手工回收内存空间的繁重工作中解脱了出来。在SUN公司的Java程序员(Java Programmer)认证考试中,垃圾收集器是必考的内容,一般最多可以占总分值的6%左右。但是由于SUN公司的Java Programming Language SL-275 课程的标准教材中,对有关垃圾收集器的内容只做了非常简单的介绍,而另外的一些关于Java技术的书籍,比如《Java 2 核心技术》(Core Java 2)、《Java编程思想》(Thinking in Java)、《精通Java 2》等等,里面关于垃圾收集器的内容也几乎没有,或者只是简单地提两句,所以很多参加Java Programmer认证考试的中国考生,在垃圾收集器这一部分的得分都为0分(笔者曾认识一位SUN公司授权的中国Java培训班的老师,其考试总分为89%,但垃圾收集器的部分竟然也为0分)。鉴于此,笔者总结了这个垃圾收集器的专题,希望对广大Java技术的爱好者和准备认证考试的考生们有所帮助。
我们知道,许多程序设计语言都允许在程序运行期动态地分配内存空间。分配内存的方式多种多样,取决于该种语言的语法结构。但不论是哪一种语言的内存分配方式,最后都要返回所分配的内存块的起始地址,即返回一个指针到内存块的首地址。
当已经分配的内存空间不再需要时,换句话说当指向该内存块的句柄超出了使用范围的时候,该程序或其运行环境就应该回收该内存空间,以节省宝贵的内存资源。
在C,C++或其他程序设计语言中,无论是对象还是动态配置的资源或内存,都必须由程序员自行声明产生和回收,否则其中的资源将消耗,造成资源的浪费甚至死机。但手工回收内存往往是一项复杂而艰巨的工作。因为要预先确定占用的内存空间是否应该被回收是非常困难的!如果一段程序不能回收内存空间,而且在程序运行时系统中又没有了可以分配的内存空间时,这段程序就只能崩溃。通常,我们把分配出去后,却无法回收的内存空间称为"内存渗漏体(Memory Leaks)"。
以上这种程序设计的潜在危险性在Java这样以严谨、安全著称的语言中是不允许的。但是Java语言既不能限制程序员编写程序的自由性,又不能把声明对象的部分去除(否则就不是面向对象的程序语言了),那么最好的解决办法就是从Java程序语言本身的特性入手。于是,Java技术提供了一个系统级的线程(Thread),即垃圾收集器线程(Garbage Collection Thread),来跟踪每一块分配出去的内存空间,当Java 虚拟机(Java Virtual Machine)处于空闲循环时,垃圾收集器线程会自动检查每一快分配出去的内存空间,然后自动回收每一快可以回收的无用的内存块。
垃圾收集器线程是一种低优先级的线程,在一个Java程序的生命周期中,它只有在内存空闲的时候才有机会运行。它有效地防止了内存渗漏体的出现,并极大可能地节省了宝贵的内存资源。但是,通过Java虚拟机来执行垃圾收集器的方案可以是多种多样的。
下面介绍垃圾收集器的特点和它的执行机制:
垃圾收集器系统有自己的一套方案来判断哪个内存块是应该被回收的,哪个是不符合要求暂不回收的。垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用System. gc 方法来"建议"执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。
垃圾收集器的主要特点有:
1.垃圾收集器的工作目标是回收已经无用的对象的内存空间,从而避免内存渗漏体的产生,节省内存资源,避免程序代码的崩溃。
2.垃圾收集器判断一个对象的内存空间是否无用的标准是:如果该对象不能再被程序中任何一个"活动的部分"所引用,此时我们就说,该对象的内存空间已经无用。所谓"活动的部分",是指程序中某部分参与程序的调用,正在执行过程中,尚未执行完毕。
3.垃圾收集器线程虽然是作为低优先级的线程运行,但在系统可用内存量过低的时候,它可能会突发地执行来挽救内存资源。当然其执行与否也是不可预知的。
4.垃圾收集器不可以被强制执行,但程序员可以通过调用System. gc方法来建议执行垃圾收集器。
5.不能保证一个无用的对象一定会被垃圾收集器收集,也不能保证垃圾收集器在一段Java语言代码中一定会执行。因此在程序执行过程中被分配出去的内存空间可能会一直保留到该程序执行完毕,除非该空间被重新分配或被其他方法回收。由此可见,完全彻底地根绝内存渗漏体的产生也是不可能的。但是请不要忘记,Java的垃圾收集器毕竟使程序员从手工回收内存空间的繁重工作中解脱了出来。设想一个程序员要用C或C++来编写一段10万行语句的代码,那么他一定会充分体会到Java的垃圾收集器的优点!
6.同样没有办法预知在一组均符合垃圾收集器收集标准的对象中,哪一个会被首先收集。
7.循环引用对象不会影响其被垃圾收集器收集。
8.可以通过将对象的引用变量(reference variables,即句柄handles)初始化为null值,来暗示垃圾收集器来收集该对象。但此时,如果该对象连接有事件监听器(典型的 AWT组件),那它还是不可以被收集。所以在设一个引用变量为null值之前,应注意该引用变量指向的对象是否被监听,若有,要首先除去监听器,然后才可以赋空值。
9.每一个对象都有一个finalize( )方法,这个方法是从Object类继承来的。
10.finalize( )方法用来回收内存以外的系统资源,就像是文件处理器和网络连接器。该方法的调用顺序和用来调用该方法的对象的创建顺序是无关的。换句话说,书写程序时该方法的顺序和方法的实际调用顺序是不相干的。请注意这只是finalize( )方法的特点。
11.每个对象只能调用finalize( )方法一次。如果在finalize( )方法执行时产生异常(exception),则该对象仍可以被垃圾收集器收集。
12.垃圾收集器跟踪每一个对象,收集那些不可到达的对象(即该对象没有被程序的任何"活的部分"所调用),回收其占有的内存空间。但在进行垃圾收集的时候,垃圾收集器会调用finalize( )方法,通过让其他对象知道它的存在,而使不可到达的对象再次"复苏"为可到达的对象。既然每个对象只能调用一次finalize( )方法,所以每个对象也只可能"复苏"一次。
13.finalize( )方法可以明确地被调用,但它却不能进行垃圾收集。
14.finalize( )方法可以被重载(overload),但只有具备初始的finalize( )方法特点的方法才可以被垃圾收集器调用。
15.子类的finalize( )方法可以明确地调用父类的finalize( )方法,作为该子类对象的最后一次适当的操作。但Java编译器却不认为这是一次覆盖操作(overriding),所以也不会对其调用进行检查。
16.当finalize( )方法尚未被调用时,System. runFinalization( )方法可以用来调用finalize( )方法,并实现相同的效果,对无用对象进行垃圾收集。
17.当一个方法执行完毕,其中的局部变量就会超出使用范围,此时可以被当作垃圾收集,但以后每当该方法再次被调用时,其中的局部变量便会被重新创建。
18.Java语言使用了一种"标记交换区的垃圾收集算法"。该算法会遍历程序中每一个对象的句柄,为被引用的对象做标记,然后回收尚未做标记的对象。所谓遍历可以简单地理解为"检查每一个"。
19.Java语言允许程序员为任何方法添加finalize( )方法,该方法会在垃圾收集器交换回收对象之前被调用。但不要过分依赖该方法对系统资源进行回收和再利用,因为该方法调用后的执行结果是不可预知的。
通过以上对垃圾收集器特点的了解,你应该可以明确垃圾收集器的作用,和垃圾收集器判断一块内存空间是否无用的标准。简单地说,当你为一个对象赋值为null并且重新定向了该对象的引用者,此时该对象就符合垃圾收集器的收集标准。
判断一个对象是否符合垃圾收集器的收集标准,这是SUN公司程序员认证考试中垃圾收集器部分的重要考点(可以说,这是唯一的考点)。所以,考生在一段给定的代码中,应该能够判断出哪个对象符合垃圾收集器收集的标准,哪个不符合。下面结合几种认证考试中可能出现的题型来具体讲解:
Object obj = new Object ( ) ;
我们知道,obj为Object的一个句柄。当出现new关键字时,就给新建的对象分配内存空间,而obj的值就是新分配的内存空间的首地址,即该对象的值(请特别注意,对象的值和对象的内容是不同含义的两个概念:对象的值就是指其内存块的首地址,即对象的句柄;而对象的内容则是其具体的内存块)。此时如果有 obj = null; 则obj指向的内存块此时就无用了,因为下面再没有调用该变量了。
请再看以下三种认证考试时可能出现的题型:

程序段1:
1.fobj = new Object ( ) ;
2.fobj. Method ( ) ;
3.fobj = new Object ( ) ;
4.fobj. Method ( ) ;
问:这段代码中,第几行的fobj 符合垃圾收集器的收集标准?
答:第3行。因为第3行的fobj被赋了新值,产生了一个新的对象,即换了一块新的内存空间,也相当于为第1行中的fobj赋了null值。这种类型的题在认证0考试中是最简单的。
程序段2:
1.Object sobj = new Object ( ) ;
2.Object sobj = null ;
3.Object sobj = new Object ( ) ;
4.sobj = new Object ( ) ;
问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准?
答:第1行和第3行。因为第2行为sobj赋值为null,所以在此第1行的sobj符合垃圾收集器的收集标准。而第4行相当于为sobj赋值为null,所以在此第3行的sobj也符合垃圾收集器的收集标准。
如果有一个对象的句柄a,且你把a作为某个构造器的参数,即 new Constructor ( a )的时候,即使你给a赋值为null,a也不符合垃圾收集器的收集标准。直到由上面构造器构造的新对象被赋空值时,a才可以被垃圾收集器收集。
程序段3:
1.Object aobj = new Object ( ) ;
2.Object bobj = new Object ( ) ;
3.Object cobj = new Object ( ) ;
4.aobj = bobj;
5.aobj = cobj;
6.cobj = null;
7.aobj = null;
问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准?
答:第7行。注意这类题型是认证考试中可能遇到的最难题型了。
行1-3分别创建了Object类的三个对象:aobj,bobj,cobj
行4:此时对象aobj的句柄指向bobj,所以该行的执行不能使aobj符合垃圾收集器的收集标准。
行5:此时对象aobj的句柄指向cobj,所以该行的执行不能使aobj符合垃圾收集器的收集标准。
行6:此时仍没有任何一个对象符合垃圾收集器的收集标准。
行7:对象cobj符合了垃圾收集器的收集标准,因为cobj的句柄指向单一的地址空间。在第6行的时候,cobj已经被赋值为null,但由cobj同时还指向了aobj(第5行),所以此时cobj并不符合垃圾收集器的收集标准。而在第7行,aobj所指向的地址空间也被赋予了空值null,这就说明了,由cobj所指向的地址空间已经被完全地赋予了空值。所以此时cobj最终符合了垃圾收集器的收集标准。 但对于aobj和bobj,仍然无法判断其是否符合收集标准。
总之,在Java语言中,判断一块内存空间是否符合垃圾收集器收集标准的标准只有两个:
1.给对象赋予了空值null,以下再没有调用过。
2.给对象赋予了新值,既重新分配了内存空间。
最后再次提醒一下,一块内存空间符合了垃圾收集器的收集标准,并不意味着这块内存空间就一定会被垃圾收集器收集。

作者:yeelf

6월 5일

做了个点击率刷新工具

  今天上百合系版,惊见30w人气的大作。整个世界震惊了!左右一想,不对,以人为的力量这得多少人多大的耐心呐。嗯,是了,肯定是借助了某种共具。
  受了启发,我已迅雷不及掩耳盗铃之势,打开可爱的eclipse。四处搜刮了一些资料,真是顺呐,这年头想在茫茫85899345920字节海的电脑里找到我想要的东东,居然就是信手拈来。。。
  立马开工,很快,即两个多小时后,页面刷新工具新鲜出炉。为了提高实用性,又增加了一些功能及参数,可谓能够在各大高低速不等的BBS间游刃有余得骗取点击率了。
  说了这么多废话,总得把简单的原理记下,要不怎对得起这空间?我是利用了URL封装类,直接创建连接,但是不必从连接中获取详细数据,每创建一个连接相当于页面刷新一次。同时为了防止界面失去响应,按钮事件里加入线程处理刷新页面事件;为了多个连接并行工作,为每个URL连接请求创建一个线程。另外,防止连接超时及刷新效率以及对网站增加负载的可控性,分“刷新块”(我创的名词,凑活用吧)来处理,比如一个块50次刷新,下一个块必须等待这个块全部刷新结束,即50个连接线程终止,包括异常在内。
  本次设计基于的URL类还是太高级了,可控制能力弱。考虑用并行连接方式发送HTTP报头来创建连接,估计效率还会提高。
  今天原想早点睡的,看看时间都2点快40了,明天还得早起,看样子考试期间的睡眠百分比要低于25%了。。。
5월 21일

JavaSoundAudioClip播放音频剪辑

Java中有许多可以制作音频播放的包和类,包括java.applet.AudioClip、com.sun.media.sound.JavaSoundAudioClip和javax.sound.midi.spi&javaw.sound.sampled.spi等,其中java.applet.AudioClip是从JDK1.0就存在的一个接口,其余的多个类是对这个接口的实现及丰富其功能。
 
构造AudioPlay类--音频播放wav,注意异常处理

//
import com.sun.media.sound.JavaSoundAudioClip;
import java.io.*;
//生成音频流
 private JavaSoundAudioClip locate;
 
  try {
   FileInputStream ff = new FileInputStream(f);
   locate = new JavaSoundAudioClip(ff);
  }
  catch (Exception e) {
   System.out.println("error");
  }
//播放
    try {
     locate.play();
         }
    catch (Exception ee) {}

//停止
    locate.stop();

5월 20일

[整理]利用“同时开放TCP连接”建立基于TCP的P2P

Simultaneous TCP open   同时开放TCP连接
 
  这里有一种方法能够在某种情况下建立一个穿透NAT的端对端TCP直连。我们知道,绝大多数的TCP会话的建立,都是通过一端先发送一个SYN包开始,另一方则回发一个SYN-ACK包的过程。然而,这里确实存在另外一种情况,就是P2P的双方各自同时地发出一个SYN包到对方的公网地址上,然后各自都单独地返回一个ACK响应来建立一个TCP会话,这个过程被称之为:“Simultaneous open”(“同时开放连接”)。
    如果一个NAT接收到一个来自私有网络外面的 TCP SYN 包,这个包想发起一个“引入”的 TCP 连接,一般来说,NAT会拒绝这个连接请求并扔掉这个SYN 包,或者回送一个TCP RST(connection reset,重建连接)包给请求方。但是,有一种情况,当这个接收到的 SYN 包 中的源IP地址和端口、目标IP地址和端口都与NAT登记的一个已经激活的TCP会话中的地址信息相符时,NAT将会放行这个SYN 包,让它进入NAT内部。特别要指出,如果NAT恰好看到一个刚刚发送出去的一个SYN包也和上面接收到的SYN包中的地址信息相符合的话,那么NAT将会认为这个TCP连接已经被激活,并将允许这个方向的SYN包进入NAT内部。
如果 Client A 和 Client B 能够彼此正确的预知对方的NAT将会给下一个TCP连接分配的公网TCP端口,并且两个客户端能够同时地发起一个“外出”的TCP连接,并在对方的 SYN 包到达之前,自己刚发送出去的SYN包都能顺利的穿过自己的NAT的话,一条端对端的TCP连接就成功地建立了。
不幸的是,这个诡计比3.4节所讲的UDP端口预言更容易被粉碎,并且对时间的敏感性的依赖更多!首先,除非双方的NAT都是Simple firewalls 或者都像cone NAT那样处理TCP通信,否则两个客户端还是无法建立这个TCP直连,因为它们无法预知对方的NAT会分配给新的会话的端口号是多少。 另外,如果双方的 SYN 到达对方的NAT 速度太快(举例来说,就是SYN A的还未穿过NAT A时,SYN B已经提早到达了NAT A),对方的NAT会将这个SYN扔掉,并返回一个 RST 包,这样就使得这个发快了的一方NAT关闭原来的会话,又重新建立一个新的会话,再利用这个新的会话重发一个SYN,这时端口预言就失效了,因为再次分配到相同的端口地址的几率太小了。
       最后,尽管在“TCP规范”中说明了“Simultaneous open”是一种支持的标准技术,但是在一些公共操作系统中,对这种技术的支持并不好。基于这个原因,我们也在这里郑重申明,并不推荐使用这个技术。如果您的应用程序想要穿透NAT并进行高效率高性能的P2P的通信,应该使用UDP技术!
 

 

过程详细描述:

Client A发送一个TCP SYN 包给 Client B我们把这个SYN包叫做 SYN A,包含的信息如下

SrcAddress10.0.0.1                 Tcp port 1234 

DestAddress138.76.29.7          Tcp port310000

 

同时Client B发送一个TCP SYN包给Client A我们把这个包叫做 SYN B,包含的信息如下

SrcAddress10.1.1.3                Tcp port 1234

DestAddress155.99.25.11       Tcp port620000

 

SYN A首先通过NAT A(必须在SYN B到达NAT A之前),NAT A看到这个包并将其地址信息进行转换为:

SrcAddress155.99.25.11         Tcp port 620000

DestAddress138.76.29.7         Tcp port310000

我们把这个经过 NAT A转换的包叫做 SYN A’

 

同样,SYN B首先通过NAT B(也必须在SYN A到达NAT B之前),NAT B看到这个包并进行地址转换为:

SrcAddress138.76.29.7           Tcp port310000

DestAddress155.99.25.11       Tcp port 620000

我们把这个经过NAT B转换的包叫做 SYN B’

 

这时,NAT ANAT B都在自己的TCP连接表中存储了含有上面两个公网IP地址和端口信息,因此,只要看到包含这两个信息的SYN包,都会让其通过。

 

就在这个瞬间,SYN A’到达了NAT BNAT B检查了一下SYN A’,发现它的地址信息和自己TCP连接表中的信息相符,便让SYN A’通过了,并将SYN A’的地址信息转换为:我们称这个包为 SYN A’’

SrcAddress155.99.25.11      Tcp port 620000

DestAddress10.1.1.3           Tcp port1234   

以使这个包能够到达内部网中Client B

 

也就在这个瞬间,SYN B’到达了NAT ANAT A检查了一下SYN B’,发现它的地址信息和自己TCP连接表中的信息相符,便让SYN B’通过了,并将SYN B’的地址信息转换为:我们称这个包为 SYN B’’

SrcAddress138.76.29.7       Tcp port 310000

DestAddress10.0.0.1          Tcp port1234    

以使这个包能够到达内部网中Client A

 

这时,Client A收到了SYN B’’Client B收到了SYN A’’,并返回给对方ACK,经过三次握手,这个P2PTCP连接就建立了。

[整理][续]基于UDP的P2P问题拾遗

UDP协议包经NAPT透明传输的说明:

NAPT为每一个Session分配一个NAPT自己的端口号,依据此端口号来判断将收到的公网IP主机返回的TCP/IP数据包转发给那台内网IP地址的计算机。在这里Session是虚拟的,UDP通讯并不需要建立连接,但是对于NAPT而言,的确要有一个Session的概念存在。NAPT对于UDP协议包的透明传输面临的一个重要的问题就是如何处理这个虚拟的Session。我们都知道TCP连接的Session以SYN包开始,以FIN包结束,NAPT可以很容易的获取到TCP Session的生命周期,并进行处理。但是对于UDP而言,就麻烦了,NAPT并不知道转发出去的UDP协议包是否到达了目的主机,也没有办法知道。而且鉴于UDP协议的特点,可靠很差,因此NAPT必须强制维持Session的存在,以便等待将外部送回来的数据并转发给曾经发起请求的内网IP地址的计算机。NAPT具体如何处理UDP Session的超时呢?不同的厂商提供的设备对于NAPT的实现不近相同,也许几分钟,也许几个小时,些NAPT的实现还会根据设备的忙碌状态进行智能计算超时时间的长短。

现在来看一下NAPT是依据什么策略来判断是否要为一个请求发出的UDP数据包建立Session的主要有一下几个策略:

A. 源地址(内网IP地址)不同,忽略其它因素, 在NAPT上肯定对应不同的Session
B. 源地址(内网IP地址)相同,源端口不同,忽略其它的因素,则在NAPT上也肯定对应不同的Session
C. 源地址(内网IP地址)相同,源端口相同,目的地址(公网IP地址)相同,目的端口不同,则在NAPT上肯定对应同一个Session
D. 源地址(内网IP地址)相同,源端口相同,目的地址(公网IP地址)不同,忽略目的端口,则在NAPT上是如何处理Session的呢?

D的情况正式我们关心和要讨论的问题。依据目的地址(公网IP地址)对于Session的建立的决定方式我们将NAPT设备划分为两大类:

Symmetric NAPT:
对于到同一个IP地址,任意端口的连接分配使用同一个Session; 对于到不同的IP地址, 任意端口的连接使用不同的Session.
我们称此种NAPT为 Symmetric NAPT. 也就是只要本地绑定的UDP端口相同, 发出的目的IP地址不同,则会建立不同的Session.

Cone NAPT:
对于到同一个IP地址,任意端口的连接分配使用同一个Session; 对于到不同的IP地址,任意端口的连接也使用同一个Session.
我们称此种NAPT为 Cone NAPT. 也就是只要本地绑定的UDP端口相同, 发出的目的地址不管是否相同, 都使用同一个Session.

客户端都处于相同的NAT之后

我们假设,Client A 和 Client B 要使用上一节我们所描述的 “UDP打洞技术”,并通过服务器S这个“媒人”来认识,这样Client A 和Client B首先从服务端S得到了彼此的公网IP地址和端口,然后就往对方的公网IP地址和端口上发送消息。在这种情况下,如果NAT 仅仅允许在 内部网主机与其他内部网主机(处于同一个NAT之后的网络主机)之间打开UDP会话通信通道,而内部网主机与其他外部网主机就不允许的话,那么Client A 和Client B就可以通话了。我们把这种情形叫做“loopback translation”(“回环转换”),因为数据包首先从局域网的私有IP发送到NAT转换,然后“绕一圈”,再回到局域网中来,但是这样总比这些数据通过公网传送好。举例来说,当 Client A发送了一个UDP数据包到 Client B的公网IP地址,这个数据包的报头中就会有一个源地址10.0.0.1:124和一个目标地址155.99.25.11:62001。NAT接收到这个包以后,就会(进行地址转换)解析出这个包中有一个公网地址源地址155.99.25.11:62000和一个目标地址10.1.1.3:1234,然后再发送给B,虽说NAT支持“loopback translation”,我们也发现,在这种情形下,这个解析和发送的过程有些多余,并且这个Client A 和Client B 之间的对话可能潜在性地给NAT增加了负担。

 UDP打洞技术有很多实用的地方

第一,一旦这种处于NAT之后的端对端的直连建立之后,连接的双方可以轮流担任 对方的“媒人”,把对方介绍给其他的客户端,这样就极大的降低了服务器S的工作量;第二,应用程序不用关心这个NAT是属于cone还是symmetric,即便要,如果连接的双方有一方或者双方都恰好不处于NAT之后,基于上叙的步骤,他们之间还是可以建立很好的通信通道;第三,打洞技术能够自动运作在多重NAT之后,不论连接的双方经过多少层NAT才到达Internet,都可以进行通信。

UPD端口号预言
明显的,有许多因素会导致这个方法失败:如果这个预言的新端口(62001和31001) 恰好已经被一个不相关的会话所使用,那么NAT就会跳过这个端口号,这个连接就会宣告失败;如果两个NAT有时或者总是不按照顺序来生成新的端口号,那么这个方法也是行不通的。

如果隐藏在NAT A后的一个不同的客户端X(或者在NAT B后)打开了一个新的“外出”UDP 连接,并且无论这个连接的目的如何;只要这个动作发生在 客户端A 建立了与服务器S 的连接之后,客户端A 与 客户端B 建立连接之前;那么这个无关的客户端X 就会趁人不备地“偷” 到这个我们渴望分配的端口。所以,这个方法变得如此脆弱而且不堪一击,只要任何一个NAT方包含以上碰到的问题,这个方法都不会奏效。
 自从使用这种方法来实践P2P的应用程序以来,在处于 cone NAT 系列的网络环境中这个方法还是实用的;如果有一方为 cone NAT 而另外一方为 symmetric NAT,那么应用程序就应该预先发现另外一方的 NAT 是什么类型,再做出正确的行为来处理通信,这样就增大了算法的复杂度,并且降低了在真实网络环境中的普适性。

    最后,如果P2P的一方处在两级或者两级以上的NAT下面,并且这些NATs 接近这个客户端是 symmetric的话,端口号预言 是无效的!

 

保持端口绑定
在使用“UDP打洞技术”时有一点必须要注意:它只能在双方的NAT都是cone NAT(或者干脆没有NAT)时才能正常工作;这些NAT在自己的公网UDP端口被使用时保持着端口的绑定——[私有IP,私有UDP端口]对和[公网IP,公网UDP端口]对的一一对应。如果像 symmetricNAT那样给每个新的会话都分配一个新的公网端口,那么UDP应用程序想要与其他外部客户端进行通话,就无法重复使用已经建立好的通信转换。

伴随着 cone NAT 的推广,“UDP打洞技术”也被越来越广泛的应用。然而,仍存在一小部分使用 symmetric NAT 的网络,那么在这小部分网络环境中,就不能使用“UDP打洞技术”。

(注:因为我国的国情,网络技术应用得比较晚,所以可以说绝大部分的网络都是cone NAT,所以 UDP打洞技术基本上可以畅通无阻的使用,只是还要注意对NAT是否支持“loopback translation”的测试)

5월 17일

Java JList及其CellRenderer的使用

  这是一个视图操作与数据操作相分离的组件。
  对视图操作如下,用一个视图图类定义,实现ListCellRenderer接口即可。
  public class BWCRoomCellRenderer extends JLabel implements ListCellRenderer
  {
    private BWCRoomListData data = null;
    public Component getListCellRendererComponent(JList list, Object value,
        int index, boolean isSelected, boolean hasFocus)
    {
      // 这里就是列表框的数据,其中BWCRoomListData是另外一个数据类.
      data = (BWCRoomListData)value;
      //在JLabel上放上图标和字符串 并设置文本在右边,以及两者的间距 
     setText(String.valueOf(data.getRoomIndex() + "  " + data.getRoomName()
          + "  (" + data.getReadyUsers() + "/" + data.getTotalUsers() + ")"));
      if(data.getReadyUsers() == 2)
        setIcon(startIcon);
      else setIcon(waitIcon);
      setHorizontalTextPosition(SwingConstants.RIGHT);
      setIconTextGap(5);
      // setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
      // 设置项目被选中时的颜色
       
      if(isSelected)
      {
    //    setBackground(list.getSelectionBackground());
        setBackground(new Color(252, 209, 0));
        setForeground(Color.BLACK);
      ///  setForeground(list.getSelectionForeground());
      }
      else
      {
        setBackground(list.getBackground());
        setForeground(list.getForeground());
      }
      setEnabled(list.isEnabled());
      setFont(list.getFont());
      setOpaque(true);
      return this;
    }
  }
 
  再添加数据类,如下即可
 public class BWCRoomListData
  {
    private Icon icon;
    private int roomIndex;
    private int readyUsers = 0;
    private int totalUsers = 0;
    private String roomName;
    BWCRoomListData(int roomIndex, String roomName, int readyUsers,
        int totalUsers)
    {
      this.roomIndex = roomIndex;
      this.readyUsers = readyUsers;
      this.totalUsers = totalUsers;
      this.roomName = roomName;
      // this.icon = icon;
    }
    public int getRoomIndex()
    {
      return this.roomIndex;
    }
    public int getReadyUsers()
    {
      return this.readyUsers;
    }
    public int getTotalUsers()
    {
      return this.totalUsers;
    }
    public String getRoomName()
    {
      return this.roomName;
    }   
  }
  最后设置好数据,可以为上述数据类对象的数组,然后调用JList的函数
  jlRoomList.setCellRenderer(new BWCRoomCellRenderer());
再添加数据jlRoomList.setListData(data);即可。
注意这里会实际更新data里的数据并显示出来,如果data=null,列表视图也为空
5월 15일

Java多线程关于读对象流的一个注意事项

  我在写黑白棋网络版,大厅与游戏界面(两个JFrame)切换时遇到一个很隐藏的问题,花了半天时间才搞明白。
  具体现象是,游戏界面dispose并调出大厅界面,然后大厅中的readObject()完全读不到东西,异常大概是EOFException。经过长时间分析(怎么解决bug的过程在此略过了) ,确定原因是由于界面类(BWCClient)通过关闭窗口的事件响应里开启大厅线程,而BWCClient里另外一个处理接受消息的线程,包含readObject指令。而大厅线程启动后也开启了监听消息线程,此时原来的BWCClient里并没有完全释放掉资源,线程也未退出,即同时有了两个readObject()函数在执行,造成了异常。
  解决方案当然多种多样,我只试了一下通过服务器来转发通知数据包给本界面线程的readObject循环,当读到该通知信息后,线程退出并解除循环等待(Flag = false),窗口监听事件用一个循环等待
while(Flag)
{
   Thread.sleep(100);

}

Flag = true;

收到解除指令后再开启下一窗口。

   另外,用此方法,也可以保证窗口退出时保证先结束监听线程再关闭窗口,就没有异常产生了。

 

Java 的几个tips与困惑

1,ObjectOutputStream osToServer = new ObjectOutputStream(connectToServer
            .getOutputStream());
        ObjectInputStream isFromServer = new ObjectInputStream(connectToServer
            .getInputStream());
这两句话貌似对顺序有一定的要求,换一下会读不出Stream
 
2,getOutputStream().reset() 在输出一个对象流的时候 往往另一端会读到null(一般不会全是null,而是某些如ArrayList的对象会读出来null),这时候用输出对象的reset可以保证流的传输正确。
-------------------------我素华丽的分割线----------------------
以上两点有些困惑。
 
3, JTextArea.setCaretPosition(jChat.getText().length());通过每次添加字符串后调用此方法,可以使光标移至最后,保证其自动滚动
 

4,JTextArea.setLineWrap(boolean wrap)//一般的换行。
     JTextArea.setWrapStyleWorld(boolean world)//以单词边缘为界限分行。

5월 13일

[转]P2P 之 UDP穿透NAT的原理与实现

P2P 之 UDP穿透NAT的原理与实现(附源代码)
原创:shootingstars
参考:http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt

论坛上经常有对P2P原理的讨论,但是讨论归讨论,很少有实质的东西产生(源代码)。呵呵,在这里我就用自己实现的一个源代码来说明UDP穿越NAT的原理。

首先先介绍一些基本概念:
    NAT(Network Address Translators),网络地址转换:网络地址转换是在IP地址日益缺乏的情况下产生的,它的主要目的就是为了能够地址重用。NAT分为两大类,基本的NAT和NAPT(Network Address/Port Translator)。
    最开始NAT是运行在路由器上的一个功能模块。
   
    最先提出的是基本的NAT,它的产生基于如下事实:一个私有网络(域)中的节点中只有很少的节点需要与外网连接(呵呵,这是在上世纪90年代中期提出的)。那么这个子网中其实只有少数的节点需要全球唯一的IP地址,其他的节点的IP地址应该是可以重用的。
    因此,基本的NAT实现的功能很简单,在子网内使用一个保留的IP子网段,这些IP对外是不可见的。子网内只有少数一些IP地址可以对应到真正全球唯一的IP地址。如果这些节点需要访问外部网络,那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP然后发送出去。(基本的NAT会改变IP包中的原IP地址,但是不会改变IP包中的端口)
    关于基本的NAT可以参看RFC 1631
   
    另外一种NAT叫做NAPT,从名称上我们也可以看得出,NAPT不但会改变经过这个NAT设备的IP数据报的IP地址,还会改变IP数据报的TCP/UDP端口。基本NAT的设备可能我们见的不多(呵呵,我没有见到过),NAPT才是我们真正讨论的主角。看下图:
                                Server S1                        
                         18.181.0.31:1235                         
                                      |
          ^  Session 1 (A-S1)  ^      | 
          |  18.181.0.31:1235  |      |  
          v 155.99.25.11:62000 v      |   
                                      |
                                     NAT
                                 155.99.25.11
                                      |
          ^  Session 1 (A-S1)  ^      | 
          |  18.181.0.31:1235  |      | 
          v   10.0.0.1:1234    v      | 
                                      |
                                   Client A
                                10.0.0.1:1234
    有一个私有网络10.*.*.*,Client A是其中的一台计算机,这个网络的网关(一个NAT设备)的外网IP是155.99.25.11(应该还有一个内网的IP地址,比如10.0.0.10)。如果Client A中的某个进程(这个进程创建了一个UDP Socket,这个Socket绑定1234端口)想访问外网主机18.181.0.31的1235端口,那么当数据包通过NAT时会发生什么事情呢?
    首先NAT会改变这个数据包的原IP地址,改为155.99.25.11。接着NAT会为这个传输创建一个Session(Session是一个抽象的概念,如果是TCP,也许Session是由一个SYN包开始,以一个FIN包结束。而UDP呢,以这个IP的这个端口的第一个UDP开始,结束呢,呵呵,也许是几分钟,也许是几小时,这要看具体的实现了)并且给这个Session分配一个端口,比如62000,然后改变这个数据包的源端口为62000。所以本来是(10.0.0.1:1234->18.181.0.31:1235)的数据包到了互联网上变为了(155.99.25.11:62000->18.181.0.31:1235)。
    一旦NAT创建了一个Session后,NAT会记住62000端口对应的是10.0.0.1的1234端口,以后从18.181.0.31发送到62000端口的数据会被NAT自动的转发到10.0.0.1上。(注意:这里是说18.181.0.31发送到62000端口的数据会被转发,其他的IP发送到这个端口的数据将被NAT抛弃)这样Client A就与Server S1建立以了一个连接。

    呵呵,上面的基础知识可能很多人都知道了,那么下面是关键的部分了。
    看看下面的情况:
    Server S1                                     Server S2
 18.181.0.31:1235                              138.76.29.7:1235
        |                                             |
        |                                             |
        +----------------------+----------------------+
                               |
   ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
   |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
   v 155.99.25.11:62000 v      |      v 155.99.25.11:62000 v
                               |
                            Cone NAT
                          155.99.25.11
                               |
   ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
   |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
   v   10.0.0.1:1234    v      |      v   10.0.0.1:1234    v
                               |
                            Client A
                         10.0.0.1:1234
    接上面的例子,如果Client A的原来那个Socket(绑定了1234端口的那个UDP Socket)又接着向另外一个Server S2发送了一个UDP包,那么这个UDP包在通过NAT时会怎么样呢?
    这时可能会有两种情况发生,一种是NAT再次创建一个Session,并且再次为这个Session分配一个端口号(比如:62001)。另外一种是NAT再次创建一个Session,但是不会新分配一个端口号,而是用原来分配的端口号62000。前一种NAT叫做Symmetric NAT,后一种叫做Cone NAT。我们期望我们的NAT是第二种,呵呵,如果你的NAT刚好是第一种,那么很可能会有很多P2P软件失灵。(可以庆幸的是,现在绝大多数的NAT属于后者,即Cone NAT)
  
    好了,我们看到,通过NAT,子网内的计算机向外连结是很容易的(NAT相当于透明的,子网内的和外网的计算机不用知道NAT的情况)。
    但是如果外部的计算机想访问子网内的计算机就比较困难了(而这正是P2P所需要的)。
    那么我们如果想从外部发送一个数据报给内网的计算机有什么办法呢?首先,我们必须在内网的NAT上打上一个“洞”(也就是前面我们说的在NAT上建立一个Session),这个洞不能由外部来打,只能由内网内的主机来打。而且这个洞是有方向的,比如从内部某台主机(比如:192.168.0.10)向外部的某个IP(比如:219.237.60.1)发送一个UDP包,那么就在这个内网的NAT设备上打了一个方向为219.237.60.1的“洞”,(这就是称为UDP Hole Punching的技术)以后219.237.60.1就可以通过这个洞与内网的192.168.0.10联系了。(但是其他的IP不能利用这个洞)。

呵呵,现在该轮到我们的正题P2P了。有了上面的理论,实现两个内网的主机通讯就差最后一步了:那就是鸡生蛋还是蛋生鸡的问题了,两边都无法主动发出连接请求,谁也不知道谁的公网地址,那我们如何来打这个洞呢?我们需要一个中间人来联系这两个内网主机。
    现在我们来看看一个P2P软件的流程,以下图为例:

                       Server S (219.237.60.1)
                          |
                          |
   +----------------------+----------------------+
   |                                             |
 NAT A (外网IP:202.187.45.3)                 NAT B (外网IP:187.34.1.56)
   |   (内网IP:192.168.0.1)                      | (内网IP:192.168.0.1)
   |                                             |
Client A  (192.168.0.20:4000)             Client B (192.168.0.10:40000)

    首先,Client A登录服务器,NAT A为这次的Session分配了一个端口60000,那么Server S收到的Client A的地址是202.187.45.3:60000,这就是Client A的外网地址了。同样,Client B登录Server S,NAT B给此次Session分配的端口是40000,那么Server S收到的B的地址是187.34.1.56:40000。
    此时,Client A与Client B都可以与Server S通信了。如果Client A此时想直接发送信息给Client B,那么他可以从Server S那儿获得B的公网地址187.34.1.56:40000,是不是Client A向这个地址发送信息Client B就能收到了呢?答案是不行,因为如果这样发送信息,NAT B会将这个信息丢弃(因为这样的信息是不请自来的,为了安全,大多数NAT都会执行丢弃动作)。现在我们需要的是在NAT B上打一个方向为202.187.45.3(即Client A的外网地址)的洞,那么Client A发送到187.34.1.56:40000的信息,Client B就能收到了。这个打洞命令由谁来发呢,呵呵,当然是Server S。
    总结一下这个过程:如果Client A想向Client B发送信息,那么Client A发送命令给Server S,请求Server S命令Client B向Client A方向打洞。呵呵,是不是很绕口,不过没关系,想一想就很清楚了,何况还有源代码呢(侯老师说过:在源代码面前没有秘密 8)),然后Client A就可以通过Client B的外网地址与Client B通信了。
   
    注意:以上过程只适合于Cone NAT的情况,如果是Symmetric NAT,那么当Client B向Client A打洞的端口已经重新分配了,Client B将无法知道这个端口(如果Symmetric NAT的端口是顺序分配的,那么我们或许可以猜测这个端口号,可是由于可能导致失败的因素太多,我们不推荐这种猜测端口的方法)。
   
    下面是一个模拟P2P聊天的过程的源代码,过程很简单,P2PServer运行在一个拥有公网IP的计算机上,P2PClient运行在两个不同的NAT后(注意,如果两个客户端运行在一个NAT后,本程序很可能不能运行正常,这取决于你的NAT是否支持loopback translation,详见http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt,当然,此问题可以通过双方先尝试连接对方的内网IP来解决,但是这个代码只是为了验证原理,并没有处理这些问题),后登录的计算机可以获得先登录计算机的用户名,后登录的计算机通过send username message的格式来发送消息。如果发送成功,说明你已取得了直接与对方连接的成功。
    程序现在支持三个命令:send , getu , exit
   
    send格式:send username message
    功能:发送信息给username
   
    getu格式:getu
    功能:获得当前服务器用户列表
   
    exit格式:exit
    功能:注销与服务器的连接(服务器不会自动监测客户是否吊线)
       
    代码很短,相信很容易懂,如果有什么问题,可以给我发邮件zhouhuis22@sina.com  或者在CSDN上发送短消息。同时,欢迎转发此文,但希望保留作者版权8-)。
   
    最后感谢CSDN网友 PiggyXP 和 Seilfer的测试帮助

 
추가한 리스트 항목이 없습니다.

Hello-_-World