2007年04月17日

IBatisNet的配置文件的格式采用了标准的XML的命名空间,如果没有相应的命名空间定义,系统在载入配置文件时会因为验证通不过而出错:
   原来的providers.config 文件
      <?xml version="1.0" encoding="utf-8" ?>

            <providers>
               <clear/>
                 <provider
                     ……
             </providers>
   更改为
   <?xml version="1.0" encoding="utf-8"?>
          <providers xmlns="http://ibatis.apache.org/providers" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <clear/>
             <provider
                     ……
        </providers>
  原来的SqlMap.config文件
 <?xml version="1.0" encoding="utf-8"?>
<sqlMapConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:noNamespaceSchemaLocation="SqlMapConfig.xsd">
……
</sqlMapConfig>
更改为
<?xml version="1.0" encoding="utf-8"?>
<sqlMapConfig xmlns="http://ibatis.apache.org/dataMapper" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
原来的PO.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<sqlMap namespace="Sequence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:noNamespaceSchemaLocation="SqlMap.xsd">
……
</sqlMap>
更改为
<?xml version="1.0" encoding="UTF-8" ?>
<sqlMap namespace="Admin" xmlns="http://ibatis.apache.org/mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
……
</sqlMap>
为了让Vs2005 帮助验证配置文件的正确性,在编写配置文件时提供帮助,需要把SqlMap.xsd,SqlMapConfig.xsd,provider.xsd拷贝到C:\Program Files\Microsoft Visual Studio 8\Xml\Schemas下
2006年11月14日

把GridView控件的内容输出到Excel的方式网上有很多文章,其代码如下所示:

ExcelExport.aspx.cs 

using System;

using System.Data;

using System.Configuration;

using System.Collections;

using System.Web;

using System.Web.Security;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using System.Web.UI.HtmlControls;

using System.Text;

using System.IO;

 

public partial class DeleteConfirm : System.Web.UI.Page

{

 

    protected void Page_Load(object sender, EventArgs e)

    {

    }

 

    protected void Button1_Click(object sender, EventArgs e)

    {

        //Export the GridView to Excel

        PrepareGridViewForExport(GridView1);

        ExportGridView();

    }

 

    private void ExportGridView()

    {

        string attachment = "attachment; filename=Contacts.xls";

        Response.ClearContent();

        Response.AddHeader("content-disposition", attachment);

        Response.ContentType = "application/ms-excel";

        StringWriter sw = new StringWriter();

        HtmlTextWriter htw = new HtmlTextWriter(sw);

        GridView1.RenderControl(htw);

        Response.Write(sw.ToString());

        Response.End();

    }

 

    public override void VerifyRenderingInServerForm(Control control)

    {

    }

 

    private void PrepareGridViewForExport(Control gv)

    {

        LinkButton lb = new LinkButton();

        Literal l = new Literal();

        string name = String.Empty;

        for (int i = 0; i < gv.Controls.Count; i++)

        {

            if (gv.Controls[i].GetType() == typeof(LinkButton))

            {

                l.Text = (gv.Controls[i] as LinkButton).Text;

                gv.Controls.Remove(gv.Controls[i]);

                gv.Controls.AddAt(i, l);

            }

            else if (gv.Controls[i].GetType() == typeof(DropDownList))

            {

                l.Text = (gv.Controls[i] as DropDownList).SelectedItem.Text;

                gv.Controls.Remove(gv.Controls[i]);

                gv.Controls.AddAt(i, l);

            }

            else if (gv.Controls[i].GetType() == typeof(CheckBox))

            {

                l.Text = (gv.Controls[i] as CheckBox).Checked ? "True" : "False";

                gv.Controls.Remove(gv.Controls[i]);

                gv.Controls.AddAt(i, l);

            }

            if (gv.Controls[i].HasControls())

            {

                PrepareGridViewForExport(gv.Controls[i]);

            }

        }

    }

}

通过对这段代码的测试,有两个地方应该注意,一个是重载VerifyRenderingInServerForm(Control control)方法,如果没有这个重载函数的话,GridView1.RenderControl(htw)这个语句会报一个“控件必须包含在runat=server的窗体内”错误,即使你的gridview已经包含在一个<form runat=server>的form中;另一个问题是必须在页面设置EnableEventValidation="false",否则会有一个"registerEventValidation必须在render中"的错误。

2006年10月11日

   在软件中经常需要把数据输出,形成报表或其他的表格,最常用的方式就是输出到Excel.

        在.Net中把数据集输出到Excel是很简单的.我们需要建立一个没有任何内容,只要最上面的页面定义的ASP.net页面,然后在Page_Load方法中把页面的ContentType属性设置为application/vnd.ms-excel.具体的代码如下:

protected void Page_Load(object sender, EventArgs e)
{
       SqlConnection cn = new SqlConnection ("yourconnectionstring");
       cn.Open();
       SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Users", cn);
       DataTable dt = new DataTable();
       da.Fill(dt);
       cn.Close();

       Response.Clear();
       Response.ContentType = "application/vnd.ms-excel";
       string sep = "";
       foreach (DataColumn dc in dt.Columns)
       {
              Response.Write(sep + dc.ColumnName);
              sep = "\t";
       }
       Response.Write("\n");

       int i;
       foreach (DataRow dr in dt.Rows)
       {
              sep = "";
              for (i = 0; i < dt.Columns.Count; i++)
              {
                     Response.Write(sep + dr[i].ToString());
                     sep = "\t";
              }
              Response.Write("\n");
       }

}
2006年07月14日

theme就是一系列属性设置,利用它可以定义页面和空间的外观。它控制的范围可以是某些网页、整个web应用程序,甚至可以是一个服务器上的所有Web应用。

theme和控件皮肤
theme由一系列元素组成:皮肤、css、图片以及其它资源。它至少包含皮肤。theme在web server或web site的特殊目录中定义。

皮肤
皮肤文件具有.skin的文件扩展名,包含诸如:button,label,textbox或calender等空间的属性设置。空间皮肤设置就像空间自己的化妆,但只包含你要求设置的那些属性的值。下面是一个button的皮肤设置:
<asp:button runat="server" BackColor="lightblue" ForeColor="black" />
 
你应该在theme目录下建立.skin文件,一个.skin文件可以包含为一种或种控件设置的一个或多个皮肤,你可以为每个控件定义一个单独的文件,也可以只定义一个文件包含所有空间皮肤的设置。

有两种控件皮肤缺省皮肤和命名皮肤:
在theme应用到一个页面时,缺省皮肤会自动应用到该页面上的所有同类型的控件。什么样的皮肤是缺省皮肤呢?判断的标准就是他是否有SkinID属性,如果没有,就是缺省皮肤。举例来说,假如你为日历控件创建了一个缺省皮肤,这个皮肤会应用到使用theme的页面上的所有日历控件。(缺省皮肤通过控件类型匹配,这样,button皮肤会应用到所有的button,但不会应用到LinkButton等继承button的控件对象)。

一个命名皮肤具有一个SkinID属性。命名皮肤不会自动通过类型应用到控件。你必须通过设置控件的SkinID属性来显式地应用一个命名皮肤。使用命名皮肤允许你为相同控件的不同实例设置不同的皮肤。

层叠样式表
theme同样可以包含层叠样式表(.css)文件。当你把一个.css文件放在theme目录下时,其样式表会自动成为theme的一部分。你可以通过在与skin文件相同的目录下创建.css文件来定义样式表。

图片和其它资源
Theme可以包含图片和其它资源,如脚本文件或声音文件。例如,你的theme的包含了一个TreeView控件的皮肤,你可以包含一些图片用来表示其扩展或收起的按钮。

一般情况下资源文件与skin文件在同一个目录下,但他们可以在Web应用的任何地方,例如在theme目录的一个子目录。如果引用一个在theme目录的子目录下的资源文件,用一个类似下面Image控件所示的路径:
<asp:Image runat="server" ImageUrl="ThemeSubfolder/filename.ext" />
 
你也可以把你的资源文件存储在theme目录之外的其他目录。如果你使用(~)来引用资源文件,web应用会自动找到这些图片。举例来说,如果你把资源文件放在你的应用程序的一个子目录,你可以用这样的路径:~/SubFolder/filename.ext 来引用资源文件,就像下面这样:
<asp:Image runat="server" ImageUrl="~/AppSubfolder/filename.ext" />
 
Theme的范围

你可以为一个单独的Web应用定义theme,也可以为Web Server上的所有应用定义一个全局的theme。在theme定义后,你可以通过@Page设置页面的Theme或StyleSheetTheme属性来应用,你也可以通过在应用程序的设置文件中设置Pages元素来把theme应用到所有的页面。如果<pages>元素定义在Machine。config文件,这个theme将会应用到这台服务器的所有应用程序的所有页面。

Page Theme
Page Theme是\App_Themes 的一个子目录,包含控件皮肤,样式表,图片文件以及其它资源。每个Theme是\App_Themes的一个不同的子目录。下面的例子代码是一个典型的Page Theme,定义了两个Theme,分别命名为BlueTheme和PinkTheme。
MyWebSite
  App_Themes
    BlueTheme
      Controls.skin
      BlueTheme.css
    PinkTheme
      Controls.skin
      PinkTheme.css
 

Global Theme
    global theme是一个可以应用到服务器上所有Web site的theme。在你在一台服务器上维护多个网站时,global theme允许你定义这些网站的总体风格。
    global theme与Page theme很相似,也包含属性设置,样式表设置和图片。但是,global theme存储在Web Server的全局目录,命名为\Themes。在这台服务器上的任何网站,网站中的任何页面,都可以引用这个global theme。

Theme设置的先后顺序
你可以指定theme设置的先后顺序,设置theme设置接管本地控件设置。

如果你设置一个页面的Theme属性,在theme中的控件设置和页面上的页面设置将合并成为该控件的最终设置。如果一个控件属性在页面和theme中都进行了设置,theme中的控件设置将覆盖页面上的控件设置。这样的策略使得各个页面能够保持一致的风格。例如,它可以允许你把theme应用到你在以前版本的ASP.NET中创建的页面。

另外,你还可以把theme以样式表的方式应用到页面,通过设置页面的StyleSheetTheme属性。在这种情况下,本地页面设置优先于theme中的设置。这是使用层叠样式表的模型。如果你想设置单个的控件属性,同时应用theme控制整体表现,你就可以把theme作为样式表来应用。

使用Theme可以设置的属性
作为一个通用规则,你可以使用theme定义有关page或控件外表或静态内容的属性。你只可以设置那些ThemeableAttribute属性设置为true的控件类。
显式定义空间行为的属性不接受theme设置的值,例如,你不能通过Theme设置一个Button的CommandName属性,类似的,你不能使用theme来设置GridView控件的AllowPaging属性或DataSource属性。

Theme和层叠样式表
theme与层叠样式表类似,他们都定义了一个通用属性集,可以应用到任何页面。但是,theme与样式表有以下不同:

  • theme可以定义一个控件或页面的许多属性,不仅是style属性。例如,使用theme,你可以为TreeView控件设定图片,为GridView控件设置模版设计,等等。
  • Theme可以包含图片
  • Theme与样式表的覆盖方式不一样。例如,确省情况下,属性值会覆盖本地属性值除非你把theme作为样式表应用。
  • 每个页面只能应用一个Theme,你不能应用多个theme到一个页面,不象样式表那样,可以应用多个到一个页面。
2006年07月04日

        SiteMapDatasource的设置是很简单的,让人容易误解的是微软对这个控件的设计。从原理上来讲,这个控件就是从一个特殊格式的xml文件读取数据,所以用这个控件时,第一个念头就是找一个设置这个xml文件的属性,但找了半天,竟然没有,让人很费解。看了微软的文档,才知道微软对这个控件的设计,竟然是要求这个xml文件必须命名为Web.Sitemap,并且必须在网站的根目录下。

        也许微软有足够的理由,认为一个网站只应该有一个网站地图,所以应该这样设计,但做惯了java,真的对这种规定文件名、规定目录,而不是由开发人员自己设置的方式很不习惯,总觉得这样的设计不够漂亮,当然,功能是没有问题的。

2006年05月25日

        为了配置struts的数据源,搞了两天,都快疯了。查了n多的资料,试验了无数次,终于搞成了,最终的配置如下:
  <data-sources>
     <data-source key="mysql datasource" type="org.apache.commons.dbcp.BasicDataSource">
         <set-property property="driverClassName" value="org.gjt.mm.mysql.Driver" />
         <set-property property="url" value="jdbc:mysql://localhost/stocks" />
         <set-property property="username" value="root" />
         <set-property property="password" value="2131018" />
         <set-property property="minCount" value="2" />
         <set-property property="maxCount" value="10" />
      </data-source>
 </data-sources>
       不过成功是成功了,还是有些问题没搞清楚。主要是属性名称方面。很多资料里面JDBC驱动的属性名为driverClass,设置用户的属性名为user。不知道具体如何,还是这两种都可以呢?

    

struts-config.xml中的元素有严格的顺序,如果元素的顺序出错,系统会抛出一个错误,描述如下:
The content of element type "struts-config" must match "(display-name?,description?,data-sources?,form-beans?,global-exceptions?,global-forwards?,
action-mappings?,controller?,message-resources*,plug-in*)". [109]
刚看到这个错误,我总以为是某个元素的拼写出了错误,把整个文件仔细检查了一遍,没有发现任何拼写作错误。查了资料才知道,struts-config.xml中的元素有严格的顺序,他们的顺序应该是象下面这个样子:

<struts−config>
<data−sources>
<data−source>
</data−source>

</data−sources>
<form−beans>

<form−bean />

</form−beans>
<global−forwards>

<forward />

</global−forwards>
<action−mappings>

<action/>

</action−mappings>
<controller />
<message−resource />
<plug−in />

</struts−config>
上面的错误提示不是说元素有拼写错误,而是说struts-config.xml必须按照display-name–>description–>datasources–>form-beans–>global-forwards–>action-mappings–>controller–>message_resources–>plug-in的顺序书写。


2006年02月23日

        随着互联网的成长和普及,软件分发的费用在不断降低,当前基本上已经接近或达到了零费用。同时,互联网又使得软件的潜在用户规模极为庞大,因而软件开发费用平均到每个用户是微乎其微的,这也就是当前互联网上这么多免费软件的根本原因。在当前这个信息极度发达的时代,一个好的产品、服务或内容的传播速度是非常快的,能在几乎一夜之间扩散到世界的各个角落,扩散到成千上万的网络用户。Napster、Skype等一直在为我们展现着这样的神话,因而,当前的互联网,越来越呈现出一种只要第一,不要第二的趋势,我们试想一下,如果一个做的很好的软件或服务,已经占领了几乎所有上网的电脑,开发一个与此相同的,甚至在质量上还略差的东西,还有没有可能?

        互联网带来的这些特点,似乎意味着一个新的互联网帝国产生的可能性,就如当初微软帝国的诞生和成长一样。当前Google隐隐然已经有了这种野心,说了这么多,其实我想说的是,我们中国的软件行业将会如何?我们一直以来就是以所谓的中国特色为借口,生产销售一些别人没有占领领域的所谓软件。昨天,一位软件公司的经理还对我说,我们的产品架构在5年内是领先的,不需要做出什么改变,我真的很悲哀,我们也许已经失去了又一个机会,并且不知道是不是最后的机会。

2005年10月08日

volatile —多线程程序员的最好朋友  
           作者:Andrei Alexandrescu
           原文:http://www.cuj.com/documents/s=7998/cujcexp1902alexandr/

——————————————————————————–
   就像广为人知的const一样,volatile也是一个类型标志符。它与那些需要在不同线程中访问和修改的变量一起使用。没有volatile,即使不能说编写多线程程序是不可能的,但使编译器浪费大量的优化机会却是不争的事实。下面让我一一道来。
   考虑下面的代码:
class Gadget
{
public:
    void Wait()
    {
        while (!flag_)
        {
            Sleep(1000); // sleeps for 1000 milliseconds
        }
    }
    void Wakeup()
    {
        flag_ = true;
    }
    …
private:
    bool flag_;
};
上面Gadget::Wait的目标是每秒检查flag_成员变量,在另一个线程把flag_设置为true时返回,最少编写这段代码的程序员是这个意思,但是,这是不正确的。
假设编译器发现Sleep(1000)调用的是外部库的函数并且不可能修改成员变量flag_,编译器觉得自己应该把flag_缓存到寄存器中,每次访问这个变量都访问寄存器而不是慢速的内存,这对单线程代码是一个优秀的优化设计,但在这种情况下,它损害了正确性:在你调用Wait后,尽管另一个线程调用了Wakeup,Wait还是会永远循环下去。这是因为flag_的变化并不会反映到缓存flag_的寄存器中。这个优化有点……"太优化"了。

把变量缓存到寄存器中在大多数时间是一个非常有价值的优化,浪费掉它是不可取的。C和C++给了你一个机会,通过显式的声明来禁止这种缓存。如果你用volatile标志符来修饰一个变量,编译器就不会把这个变量缓存到寄存器中-每次访问这个变量都会访问它在内存中的实际位置。这样,要使Gadget的Wait/Wakeup能够工作,你需要做的就只是正确的标识flag_。 

class Gadget
{
public:
    … as above …
private:
    volatile bool flag_;
};
volatile的基本原理和使用就解释到这儿,建议你在多线程编程时用volatile标识基本类型变量。然而,你还可以用volatile做多得多的事情,因为它是C++的丰富多彩的类型系统的一部分。

对用户自定义类型使用volatile
你不仅可以用volatile标识基本类型,也可以用它标识用户自定义类型,在这种情况下,volatile使用和const类似的方式修饰变量。(你可以同时使用const和volatile来修饰一个变量)
与const不同,volatile区分基本类型和用户自定义类型,也就是说,与类不同,在被volatile修饰时,基本类型还支持所有与其相关的操作(加,乘,赋值等)。例如,你可以把一个非volatile的整型变量赋给一个volatile的整型变量,但是你不能把一个非volatile对象赋给一个volatile对象。
我们来看一个volatile用于用户自定义类型的例子

class Gadget
{
public:
    void Foo() volatile;
    void Bar();
    …
private:
    String name_;
    int state_;
};

Gadget regularGadget;
volatile Gadget volatileGadget;
如果你认为volatile不是那么有用的话,就准备着被吓一跳吧

volatileGadget.Foo(); // ok, volatile对象调用volatile方法
regularGadget.Foo();  // ok, 非volatile对象调用volatile方法
volatileGadget.Bar(); // 错误! volatile对象不能调用非volatile方法!

从非volatile类型变换到volatile类型是隐含的,但是,与const一样,你不能直接把一个volatile类型转换成非volatile类型,在这种情况下,你必须使用强制转换

Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok

一个volatile标识的类只把访问权限给与自己接口的一个子集,一个在实现者的控制之下的一个子集。用户只有使用一个const_cast才能得到对这个类型接口的完全访问权限。另外,与const相似,volatile从类扩展到类的成员(例如,volatileGadget.name_和volatileGadget.state_ 就是volatile变量)

volatile,关键区和竞争条件
在多线程编程中最简单也是最常用的同步机制是mutex。一个mutex向外提供Acquire和Realse两个方法,一旦你在某个线程中调用了Acquire方法,其它线程调用Acquire方法将会被阻塞。以后,当这个线程调用Release后,以前阻塞的线程中的一个的Acquire调用将会被释放,也就是说,对于一个mutex,在调用Acquire和Release之间的代码,同时只有一个线程能够获得处理器时间,在调用Acquire和调用Release之间的代码叫做一个关键区。(Windows的术语稍微有些混淆,它把mutex自己叫做关键区,而实际上mutex只是进程之间的互斥锁,如果它们被称为线程互斥体或进程互斥体的话就比较明确了)

mutex用来在线程或进程竞争过程中保护数据。根据定义,当多个线程对数据的操作结果取决于线程的调度过程时就会发生竞争。当两个或多个线程要求使用同一份数据时,竞争就出现了。因为线程相互之间可以在一个随机的时间中断对方,这样数据就可能被损坏,所以,更改数据,甚至在有些情况下访问数据都必须用关键区小心地保护起来。在面向对象编程中,这往往意味着你把一个mutex存在一个类的成员变量里,任何时候你访问类的状态时,都要首先使用它。

有经验的多线程程序员可能觉得上面两段很无聊,但他们的目的是提供一个智力测验,因为我们现在就要联系volatile,我们这样做就引出了C++类型和线程术语这两个看似相互无关的东西。

在关键区外部,任何线程都可以在任何时间中断其它线程,没有什么控制,这样多个线程都可以访问的变量的值是不可预测的。这就是最初提出volatile的意图-阻止编译器无意的缓存多线程使用的变量的值。

在由mutex定义的关键区内部,同时只有一个线程可以访问。所以,在关键区内部的代码,具有但线程的语境,变量不再是不可预测的了-你可以去掉volatile标识符。简单的说,多线程间共享的数据,在关键区外是不可预测的,而在关键区内则不是。

你可以通过锁住一个mutex进入一个关键区,通过使用const_cast去掉一个类型的volatile标识符。如果我们试着把这两个操作放在一起,我们就在c++的类型系统和应用程序的线程术语之间建立了一个连接,我们可以让编译器为我们检查竞争。

LockingPtr
我们需要一个工具来收集mutex和const_cast。我们来写一个LockingPtr的类模版,用一个volatile对象obj和一个mutex mtx初始化,在他的生命周期内,LockingPtr保持着要求mtx的状态。LockingPtr也提供到剥离了volatile标识符的对象的访问。这个访问使用一个安全指针,通过操作符->和*来访问。const_cast在LockingPtr内部执行。因为LockingPtr在自己的生命周期内都保持着木特性,这个强制转换在语法上是合法的。

首先,我们定义Mutex类的骨架

class Mutex
{
public:
    void Acquire();
    void Release();
    …   
};

为了使用LockingPtr,你必须用本地操作系统的数据结构和函数实现Mutex。
LockingPtr是以要控制的变量的类型为参数的模版,例如,如果你想要控制Widget,使用LockingPtr<Widget>,然后用一个volatile类型的Widget变量初始化。

LockingPtr的定义非常简单。LockingPtr实现了一个简单的安全指针,他只是简单地收集const_cast和关键区。

template <typename T>
class LockingPtr {
public:
   // Constructors/destructors
   LockingPtr(volatile T& obj, Mutex& mtx)
       : pObj_(const_cast<T*>(&obj)),
        pMtx_(&mtx)
   {    mtx.Lock();    }
   ~LockingPtr()
   {    pMtx_->Unlock();    }
   // Pointer behavior
   T& operator*()
   {    return *pObj_;    }
   T* operator->()
   {   return pObj_;   }
private:
   T* pObj_;
   Mutex* pMtx_;
   LockingPtr(const LockingPtr&);
   LockingPtr& operator=(const LockingPtr&);
};
虽然很简单,LockingPtr对于书写正确的多线程代码很有帮助。你应该把多线程之间共享的对象定义为volatile,并且任何时候都不对他们使用const_cast-总是使用LockingPtr,我们下面用一个例子说明。
假设你有两个线程共享 vector<char> 对象:
class SyncBuf {
public:
    void Thread1();
    void Thread2();
private:
    typedef vector<char> BufT;
    volatile BufT buffer_;
    Mutex mtx_; // controls access to buffer_
};

在一个线程的函数中,只是使用LockingPtr<BufT>来得到对于成员变量buffer_的访问控制权。
void SyncBuf::Thread1() {
    LockingPtr<BufT> lpBuf(buffer_, mtx_);
    BufT::iterator i = lpBuf->begin();
    for (; i != lpBuf->end(); ++i) {
        … use *i …
    }
}
这里的代码很容易写也很容易理解–任何时候当你需要使用buff_时,你必须创建一个LockingPtr<BufT>指向它,如果你创建成功,你就可以访问vector的全部接口。
这段代码的优点是如果你有错误,编译器会指出来
void SyncBuf::Thread2() {
    // Error! Cannot access ‘begin’ for a volatile object
    BufT::iterator i = buffer_.begin();
    // Error! Cannot access ‘end’ for a volatile object
    for (; i != lpBuf->end(); ++i) {
        … use *i …
    }
}

2005年08月31日

2 核心协议
2.1 启动
   在早期的Gnutella协议中,有少数几个固定地址的主机提供Gnutella主机列表,方便servent连接,这些主机叫做“主机cache”。但现在已经不用了。
   为了连接Gnutella网络,servent需要发现并存储多个主机的地址,有四种方法来得到这些主机地址:
    1、调用GWebCache
    2、在握手时(不管握手是否成功)存储从X-Try和X-Try-Ultrapeers头中读取的主机地址
    3、存储返回信息中的主机地址(在至少一个连接建立以后)
    4、存储从QueryHit消息中读取的主机地址(在至少建立两个连接以后),这种情况假设远端的servent对上传部分和GNet部分使用相同的端口(大多数情况都是这样)
   GWebCache (GWC)协议允许通过向GWebCache服务器(参见3.2节 WebCache 系统 )发送一个HTTP请求来得到主机地址,但是,应该非常小心不要向web cache发送太多的请求,一般情况下,在一个会话期内,查询GWebCache的次数不应该超过一次。这就意味着,如果一个用户掉线需要重新连接时,servent不应该查询GWebCache,作为替代,servent应该实现本机cache(参见 3.1节 本地主机cache)来存储以上四种方式发现的主机地址,并和GWebCache一起使用。
    以下是推荐的启动算法,应该能够保证每个session调用GWebCache 的次数接近1(稍微大一点)
   * 启动客户端并调入本地主机cache
   * 使用本地主机cache连接GNet.
   * 如果在X秒后还没有连接到GNet,查询一个随机的GWebCache.
   * 等待最少Y秒后,向另一个GWebCache发送请求。(在第一次选取的GWebCache宕机或反应非常慢的情况下).
   * 在Y秒后,如果第一次GWC调用还没有返回结果,每隔Z秒调用一个新的GWC,直到收到其中一个的响应。
   * 一旦至少收到一个GWC的返回结果,servent就不应该在接下来的session中发送新的请求,除非本地主机cache为空(如果软件提供初始的主机cache,并且没有清除主机cache的选项,这种情况就不应该发生).
    假设X,Y和Z的值分别为5秒、10秒和2秒,这样应该能够确保servent能够很快连接到GNet并把查询GWebCache的次数控制到最低。
启动过程还应该确保servent不用经常连接一个远程主机,每秒钟少于一次是一个合理的界限。为了达到这个要求,一个解决办法就是确保本地主机cache中总是有足够多的主机,这样就能保证任何一个主机都不会在一分钟之内被再次试图连接。
    每一个servent在握手时都应该发送一个X-Try头,这个头给出这个servent可以尝试连接的主机列表,允许它在不用调用GWebCache的情况下得到新的主机地址。例如:
   X-Try:  1.2.3.4:1234, 1.2.3.5:6346, 1.2.3.6:6347
    servent应该发送适当数量的主机,一般在10到20之间。这些随着X-Try头发送的主机信息应该是确切的。所以,servent应该只选择那些最近(在发送X-Try头之前几分钟内)所知还在运行的主机。如果最近发现还在运行的主机数量少于10个,那发送时就少发送一些。如果servent实现了响应cache(参见3.4节),在响应信息中发现的主机可以认为具有空闲的时隙。但这不适用于在监控QueryHits中得到的主机信息。这样,在返回信息中得到的主机信息可以替换X-Try头中主机的位置,而QueryHits中发现的主机却不行。

2.2 握手协议
来源 : 初始化握手建议
    一个Gnutella servent通过与当前在网络上的另一个servent建立连接的方式把自己连接到Gnutella网络。发现第一个在网上的主机的技术已经在2.1节描述过了。一旦第一个连接建立起来,网络可以提供更多的当前连接在网上上的主机地址。Gnutella的缺省端口是6346,但servent可以使用其他任何当前没有使用的端口。如果这个端口(指6346端口)正在使用(可能由另一个Gnutella servent应用占用),servent应该尝试监听其他端口,正在监听的端口的信息可以通过应答信息在网络中进行广告。
    关于如何选择连接其他Gnutella主机、何时接受连接请求的规则和技术参见附件4
    一旦得到当前在网上的另一个servent的地址,首先建立一个到servent的TCP/IP连接,然后开始一个握手的消息序列。客户端是发起连接的主机,服务器是接受连接的主机。<cr>代表ASCII码的13(回车),<lf>代表10(换行)。
    1.客户端建立一个到服务器的TCP连接
    2.客户端发送“GNUTELLA CONNECT/0.6<cr><lf>”
    3.客户端发送带有所有域的头,除了供应商自定义的特殊域,每个域以"<cr><lf>"结束,在整个头的最后添加一个额外的"<cr><lf>"
    4.服务器以"GNUTELLA/0.6 200 <string><cr><lf>"应答,这里<string>应该是"ok",但是servent应该只查找"200"这个代码作为状态标志。
    5.服务器发送自己信息头的全部,按照3规定的格式
    6.如果客户端在解析完服务器发送的信息头以后,还希望继续,就发送"GNUTELLA/0.6 200 OK <cr><lf>"给服务器作为应答,就象4一样。否则,它需要返回一个错误码,并关闭这个连接。
    7.客户端发送供应商自定义的信息头,与3的格式一样。
    8.客户端和服务器按照3和5中得到的信息,按照自己的要求发送和接收二进制消息。
    所有的信息头应该在http://groups.yahoo.com/group/the_gdf/database?method=reportRows&tbl=9的GDF数据库中注册(要求是GDF的成员)
    信息头遵循RFC822和RFC2616标准。每个域由字段名称和字段值组成,中间用冒号隔开。每行以<cr><lf>结束,整个信息头以一个单独的<cr><lf>行结束。正常情况下每行起始都是一个域,也可以是空格或水平制表符(分别是ASCII码的32和9),后面紧接着信息头的域。多余的空格和制表符可以把多行的头域的值压缩到一个空间中,例如:
   First-Field: this is the value of the first field<cr><lf>
    Second-Field: this is the value<cr><lf>
        of the<cr><lf>
        second field<cr><lf>
    <cr><lf>
上面的信息头由两个域组成,"First-Field" 和 "Second-Field",它们的值分别是“this is the value of the first   
field”和“this is the value of the second field”(前面起连接作用的空格被忽略了)。注意,在":"之后,在字段名结束之后,在字段值开始之前的前导空格并不被忽略。
    多个具有相同字段名的头域行与一个头域行,其字段名之间与","隔开是等价的。只意味着:
    Field: first<cr><lf>
    Field: second<cr><lf>
    与Field: first,second<cr><lf>具有完全相同的含义,也就是说,在这种情况下,各行之间的顺序是严格要求的。
    下面是一个客户端和服务器交互的例子。从客户端发送到服务器的数据列在左边,从服务器发送到客户端的数据列在右边。
     Client                          Server
       ———————————————————–
       GNUTELLA CONNECT/0.6<cr><lf>
       User-Agent: BearShare/1.0<cr><lf>
       Pong-Caching: 0.1<cr><lf>
       GGEP: 0.5<cr><lf>
       <cr><lf>
                                       GNUTELLA/0.6 200 OK<cr><lf>
                                       User-Agent: BearShare/1.0<cr><lf>
                                       Pong-Caching: 0.1<cr><lf>
                                       GGEP: 0.5<cr><lf>
                                       Private-Data: 5ef89a<cr><lf>
                                       <cr><lf>
       GNUTELLA/0.6 200 OK<cr><lf>
       Private-Data: a04fce<cr><lf>
       <cr><lf>
      [binary messages]                [binary messages]

    一些需要注意的地方:首先,客户端(或服务器)在第4步收到除了"200"代码以外的其它响应时,应该断开连接,当前没有必要定义这些错误代码。第二,servent应该忽略第2步、第4步和第6步可能出现的高版本。例如,将来可能出现一个客户端发出"GNUTELLA CONNECT/0.7"来连接服务器,这是完全可以接受的,服务器如果支持0.7版的协议,应该返回"GNUTELLA/0.7 200 OK",否则返回"GNUTELLA/0.6 200 OK"。
   关于信息头一些应该注意的地方:servent应该在任何时候使用标准的HTTP头,例如,servent应该使用标准的"User-Agent",而不是创造一个"Servent-Vendor"。但是,如果没有合适的HTTP头可以使用,完全可以添加新的头(如:"Query-Routing"),只要这些新的头符合HTTP的语法就行。servent不认识的头必须被忽略。一些旧的servent会发送"GNUTELLA CONNECT/0.4<lf><lf>"来初始化一个握手过程,服务器如果可以接受这个连接请求,应该以"GNUTELLA OK<lf><lf>"应答,后面跟随二进制消息。servent在以0.6协议连接被拒绝后,可能使用0.4协议尝试连接。在0.4协议中,不使用任何握手信息头。
    servent拒绝一个连接时,如果可能的话,必须向要求连接的远程主机提供一个其它Gnutella主机的列表,这样,远程主机可以尝试连接这些主机。这个列表应该使用X-Try头来提供,一个X-Try头与以下类似:
    X-Try:1.2.3.4:1234,3.4.5.6:3456
    在","和"."之后,可能会有一个空格。在一个信息头集中,可能包含多个X-Try头。整个信息头可能以一个额外的"."结束。整个信息头可能通过连接符格式化在多行中。每个X-Try头给出一个servent的IP地址和监听端口。有时候这被称作“connection pong”。如果发送X-Try的服务器实现了响应cache机制,每个被发送的连接响应必须是最新的。
    拒绝一个连接的一般原因是该servent忙,代码"503",后面紧跟着“busy”或其它描述信息。

2.3 叶子模式和超级节点模式
来源 – Ultrapeer Proposal v1.0.

最初,所有的Gnutella节点随机地连接在一起,这样的网络对宽带用户工作地很好,但对于使用很慢的猫的用户来说就不行了。我们可以通过对网络进行组织来解决这个问题。

2.3.1 超级节点系统

        超级节点系统对解决这个问题是很有效的。这种系统是通过把节点分为叶子节点和超级节点从而使整个网络形成一种层次结构,一个叶子节点只维持少量对超级节点的连接。一个超级节点作为连接到自己的叶子节点的代理服务器。这对提高Gnutella网络的灵活性非常有好处,降低了在消息处理和路由过程中涉及的节点数量,同时也降低了节点之间的实际流量。
      超级节点只有在认为叶节点能够应答的情况下,才会向该节点转发查询请求,而叶节点从来不会向超级节点转发请求。超级节点相互连接在一起,并和“一般的”Gnutella主机(没有实现超级节点系统的主机)连接在一起。
      超级节点使用查询路由协议(Query Routing Protocol, QRP参见3.8)决定把什么样的查询发送给叶节点,如果超级节点和叶节点都支持其他协议,也可以使用该协议决定转发哪些查询。QRP协议不用在超级节点/普通节点之间。
     推荐servent实现超级节点系统,或将来发展的其他类似系统,来降低占用modem用户带宽的数量。
     超级节点选举是自组织的,选择叶节点模式和超级节点模式的规则细节可以在3.7 超级节点选举法则中找到。
    更多的细节请参考最初协议规定。
2.3.2 超级节点握手
    超级节点的容量和信息在试图建立新的Gnutella连接时,通过握手序列(参见2.2)进行交换,使用下面新的信息头:
     * X-Ultrapeer: "True" 通知该节点是一个超级节点,"False"通知该节点是一个受保护的叶节点。
     * X-Ultrapeer-Needed: 用来平衡超级节点的数量
     * X-Try-Ultrapeers: 与X-Try(参见2.1)相似,但只包含超级节点的地址。
     * X-Query-Routing: 表示支持查询路由协议(参见3.8)。该头域的值应该是QRP的版本号(当前是0.1)
    特别注意,这些信息头域可以以任何顺序发送,"True","false"是大小写无关的。
    下面是一个叶节点连接超级节点的交互的例子:

   叶节点                            超级节点
  ———————————————————–
   GNUTELLA CONNECT/0.6
   User-Agent: LimeWire/1.0
   X-Ultrapeer: False
   X-Query-Routing: 0.1
  
                                    GNUTELLA/0.6 200 OK
                                    User-Agent: LimeWire/1.0
                                    X-Ultrapeer: True
                                    X-Ultrapeer-Needed: False
                                    X-Query-Routing: 0.1
                                    X-Try: 24.37.144:6346,
                                     193.205.63.22:6346
                                    X-Try-Ultrapeers: 23.35.1.7:6346,
                                     18.207.63.25:6347
                                   
   GNUTELLA/0.6 200 OK
  

   [二进制消息]                [二进制消息]

现在叶节点在超级节点的掩盖之下了,也节点应该断掉所有的与不是超级节点的连接并发送一个QRP路由表(假设使用QRP)。如果一个叶节点接收到连接请求,它应该返回一个503的错误码,拒绝接受这个连接,同时发送X-Try和X-Try-Ultrapeer头把远程主机转到其他地址。例如,当一个叶节点试图连接另一个叶节点时,应该看起来像下面这样,不重要de信息头在这个和以后的例子中都被去掉了。 

   叶节点1                            叶节点2
   ———————————————————–
   GNUTELLA CONNECT/0.6
   X-Ultrapeer: False
  
                                    GNUTELLA/0.6 503 I am a leaf
                                    X-Ultrapeer: False
                                    X-Try: 24.37.144:6346
                                    X-Try-Ultrapeers: 23.35.1.7:6346
                                   

                                    [中断连接]


   有时一些节点无法作为超级节点,但又不能找到超级节点。在这种情况下,他们就像老的,没有路由功能的Gnutella 0.4连接一样工作。
   叶节点1                            叶节点2
   ———————————————————–
   GNUTELLA CONNECT/0.6
   X-Ultrapeer: False
  
                                    GNUTELLA/0.6 200 OK
                                    X-Ultrapeer: False
                                   
   GNUTELLA/0.6 200 OK
  

   [二进制消息]                      [二进制消息]


     当两个超级节点相遇,两个都设X-Ultrapeer: true。如果两个超级节点都有叶节点,他们在交互结束后仍然是超级节点。注意这种情况下,在连接建立后相互之间并不发送路由表,下面是握手的例子:
   UltrapeerA                       UltrapeerB
   ———————————————————–
   GNUTELLA CONNECT/0.6
   X-Ultrapeer: True
  
                                    GNUTELLA/0.6 200 OK
                                    X-Ultrapeer: True
                                   

   GNUTELLA/0.6 200 OK
  

   [二进制消息]                [二进制消息]


有时有太多的可以成为超级节点的节点。考虑一个超级节点A正在连接超级节点B,如果B并没有很多节点,它可能指示A变成一个叶节点。如果A没有叶节点连接,它停止接收新的连接,断掉任何Gnutella 0.4的连接后,发送一个QFP路由表给B。然后B屏蔽A的所有流量。如果A有叶节点的连接,它忽略B的指示,就像上面的例子一样工作。
   超级节点A                         超级节点B
   ———————————————————–
   GNUTELLA CONNECT/0.6
   X-Ultrapeer: True
  
                                    GNUTELLA/0.6 200 OK
                                    X-Ultrapeer: True
                                    X-Ultrapeer-Needed: False
                                   

   GNUTELLA/0.6 200 OK
   X-Ultrapeer: False
  

   [二进制消息]                [二进制消息]


2.5 交换网络拓扑信息

2.5.1 使用Ping和Pong信息

        在早期版本的Gnutella中,ping消息在网络中广播,pong消息通过与查询命中消息相同的方式路由到ping消息的发起者(参见2.7节)。这种做法要求网络有足够的带宽,所以现在的Gnutella servent缓存pong消息,或使用其他方式尽量减少ping和pong消息所占用的带宽。这样,pong消息就不能准确反映网络的拓扑结构,另外,没有可用GNet连接的servent根本不对ping消息发出应答。所以,pong消息在将来可能被更有效率的通信方式取代。截至本文发表时,仍然没有发现Gnutella网络拓扑结构的替代方案。


2.7 查询

本章描述查询和查询命中消息的使用。

2.7.1 转发、路由查询和查询命中消息
    Servent应该向所有与自己直接连接的servent转发接收到的查询消息,传入这个消息的servent除外。使用Flow控制和超级节点(3.1节和3.2节)的servent并不总是转发所有的消息到所有的连接。
    servent在把一个消息转发给任何直接连接的servent之前,必须递减消息头中的TTL域,递增Hops域。如果在递减TTL域后,发现其值为0,这条消息就不能转发到任何连接。
    servent在接收到与以前接收到的某个消息具有相同的内容和消息ID的消息时,必须丢弃该消息,因为这意味这该消息已经被处理了。
    查询命中消息只能沿着查询消息进入的路径发送,这样能够保证只有那些曾经路由过该查询消息的servent才能收到查询的应答。如果一个servent收到一个消息ID=n的查询命中消息,但并没有收到过消息ID=n的查询消息,它应该把该查询命中消息从网络上去掉。

2.7.2 何时和如何发送新的查询消息
    查询消息通常在用户发起一个搜索时发出,一个servent也可能自动产生查询消息,例如在查找一个资源的更多位置时。如果这样做,servent必须小心不能阻塞网络。一个servent在一小时内不应该发送一条以上的自动查询消息。
    servent不应该允许用户通过重复点击按钮来产生大量的查询。
    servent应该查看与它相临的servent发起的查询(Hops=0),如果这些查询过于频繁,是重复发送的消息或其他方式的非正常操作,就应该丢弃这些查询甚至关闭与发送他们的servent的连接。
    一个新的查询的TTL值不应该大于7,绝对不能大于10。Hops值必须设为0。

2.7.3 何时和如何用查询命中消息应答
    当一个servent接收到查询消息,它应该按照查询标准搜索本地共享的文件。搜索条件是文本的,从来没有定义这些文本应该用什么字符集,所以,servent必须假设他们是纯ASCII的。如果有字节的第7位(最高位)不是0,那么要么是一个GGEP扩展,指定了使用的字符集,要么就由servent来猜出正确的字符集,一般情况下,字符集是ISO-latin-1或UTF-8
    如何处理搜索条件也没有准确规定,但是有一些在servent之间互操作的指南:
    搜索条件是一个关键字组成的字符串,servent应该只选择具有全部关键字的文件作为应答;推荐在非数字文字字符(即不是字母也不是数字的字符)处分词;空格是关键词之间的标准分隔符。
    servent可能要求只有与搜索条件中关键字出现相同次数和相同顺序的才是满足条件的条目。
    不支持正则表达式,一般的通配符如"*"和".",要不代表它们自己要不被忽略,查询匹配时应该是大小写不敏感的,空查询或只包含一个字符的查询应该被忽略。
    GGEP扩展可以用来提供如何解析搜索条件的细节(例如指定可以使用正则表达式进行匹配),但是一个servent无法确定其他servent是否理解GGEP扩展。
    为了避免过于广泛的查询,servent可能忽略那些搜索条件短于一定长度的查询。
    TTL=1、Hops=0,搜索条件等于“    ”(四个空格)的查询消息用来列出一个主机共享的所有文件,servent应该用自己共享的所有文件来应答,如果共享的文件很多,应该使用多个查询命中消息。如果因为隐私或带宽限制,可以不应答这种列表查询请求。
    查询命中消息必须具有与查询消息相同的消息ID,TTL值最少应该设为对应查询消息的Hops值加2,这样如果需要的话,就允许查询命中消息使用一个相对较长的路径返回。TTL值最少必须设置为对应查询的Hops值,查询命中消息的Hops值的初值必须设为0。有些servent使用(2 * Query_TTL + 2)的TTL值,以保证应答能够到达目的地。具有高的TTL值的应答应该被允许通过。