2007年5月29日星期二

ASP.NET调用DLL(C#)

首先建立一个bin文件夹,然后将dll文件拷贝到该bin文件夹下,
然后在程序里使用
using namespace;
或者
<%@ import namespace="your-namespace" %>
的方式导入该命名空间,接下来就可以使用dll里面的函数了。


、创建C# 类库 (Dll)
以前在VC++中创建一个dll文件不能说简单,但在Visual C# 中,这将同样是轻而易举的事情。下面的介绍分成两部分:1、创建DLL,2、在客户端测试dll。
(1)创建DLL
首先创建一个空的类库工程。在VS.NET集成环境(IDE)中选择“文件->新建->工程文件->Visual C# 工程->类库”,点击Browse(浏览)按钮选择工程文件名和相应的目录,再点击 OK。
接着看看工程和它的相关文件。Solution Explorer(解决方案探测器)向工程中增加两个C# 类,第一个是 AssemblyInfo.cs ,第二个是Class1.cs。我们不讨论AssemblyInfo,重点介绍 Class1.cs。


双击Class1.cs,就能看到一个名称空间mcMath。我们将在客户机引用这个名称空间以使用这个类库:
namespace mcMath
{
using System;
///
/// Summary description for Class1.
///

public class Class1
{
public Class1()
{
//
// TODO: Add Constructor Logic here
//
}
}
}
现在就可以Build(构造)这个工程了。Build(构造)完毕后,就会在工程文件的bin/debug 目录中生成mcMath.dll文件。
增加一个方法
从View (视图)菜单中打开ClassView(类视图),开始只显示Class1,没有方法和属性。现在来增加一个方法和一个属性。
用鼠标右键单击“Class1”,选择“Add(增加)-> Add Method(增加方法)”,这时将弹出C# 方法生成向导:


在这个窗口中增加方法名、存取类型、返回类型、参数以及注释信息。使用Add(增加)和Remove(取消)按钮可分别从参数列表中增加和取消参数。这里增加了一个方法long Add( long val1, long val2 ),它负责将两个数字相加并返回和。
增加一个属性
同理可以通过C#属性生成向导,向类中增加一个属性:


增加了一个方法和一个属性后, Class1变成下图所示的样子:


仔细观察这个 Class1,你会发现C#的向导程序向类中增加了如下两个函数:
public long Add (long val1, long val2)
{
return 0;
}

public bool Extra
{
get
{
return true;
}
set
{
}
}
向类中增加代码
这里把Class1修改成为 mcMathComp ,因为 Class1是个容易造成混淆的名字,当想将这个类用在一个客户应用程序中时会造成问题。下面的代码对上面的做了些调整:
namespace mcMath
{
using System;
public class mcMathComp
{
private bool bTest = false;
public mcMathComp()
{
}
public long Add (long val1, long val2)
{
return val1 + val2;
}
public bool Extra
{
get
{
return bTest;
}
set
{
bTest = Extra ;
}
}
}
}
构造 dll
选择Build菜单创建dll文件,如果一切OK,就会在工程文件的 bin\debug目录生成dll文件。
(2)在客户端测试 dll
在客户端调用dll的方法和属性也是非常简单的工作,请遵照下面的步骤执行:
① 创建控制台应用程序
在VS.NET IDE集成环境中选择“文件-> 新建->工程文件->Visual C#工程文件->控制台应用程序”,最终将在这个控制台应用程序中测试dll。
② 增加名称空间的引用
选择“工程->添加引用”(Project->Add reference),然后浏览文件找到dll,点击 Ok:




引用添加向导程序将向当前工程文件中增加对相关库的引用:


③ 调用mcMath名称空间,创建 mcMathComp 的对象,并调用其方法和属性。
现在距离调用组件的方法和属性只有一步之遥了。请按照以下步骤进行:
●引用名称空间:using mcMath
●创建一个 mcMathComp的对象:mcMathComp cls = new mcMathComp();
●调用方法和属性
mcMathComp cls = new mcMathComp();
long lRes = cls.Add( 23, 40 );
cls.Extra = false;
以下是完整的工程文件代码:
namespace mcClient
{
using System;
using mcMath;
///
/// Summary description for Class1.
///

public class Class1
{
public Class1()
{
//
// TODO: Add Constructor Logic here
//
}
public static int Main(string[] args)
{
mcMathComp cls = new mcMathComp();
long lRes = cls.Add( 23, 40 );
cls.Extra = false;
return 0;
}
}

IIS报Service Unavailable错的解决方案

过检测,发现我的windows2003的应用程序池被改成DefaultAppPool了!改成原来的MsSharePointAppPool就可以了。

转载一篇详细的文章:

浏览 Windows SharePoint Services Web 站点时收到“Service Unavailable”(服务不可用)错误信息

症状
当您浏览一个 Windows SharePoint Services Web 站点时,您可能会收到下面的错误信息:
Service Unavailable
原因
如果 Microsoft Internet 信息服务 (IIS) 6.0 中没有正确地配置用于虚拟服务器的应用程序池,就可能会发生此问题。此问题可能会在存在下列一种或多种情况时发生: •应用程序池没有运行。
•应用程序池帐户使用的密码不正确。
•应用程序池帐户不是服务器上的 IIS_WPG 和 STS_WPG 这两个组的公共成员。

解决方案
要解决此问题,请按照下列步骤操作: 1.验证是否已为虚拟服务器配置了应用程序池。默认的应用程序池是 MSSharePointPortalAppPool。

请按照下列步骤来确定虚拟服务器正在使用的应用程序池。 a. 单击“开始”,指向“管理工具”,然后单击“Internet 信息服务 (IIS) 管理器”。
b. 展开“ServerName”,展开“Web 站点”,右键单击虚拟服务器,然后单击“属性”。
c. 单击“主目录”选项卡。

为虚拟服务器配置的应用程序池列在“应用程序池”框中。
d. 单击“确定”。

2.验证应用程序池帐户使用的密码是否正确。IIS 不会自动轮询 Active Directory 目录服务中的密码更改。如果应用程序池帐户是一个域帐户,其密码已过期,则在为此帐户重新指定一个新密码后,您可能会收到本文“症状”部分所描述的错误信息。

请按照下列步骤来验证应用程序池帐户所用的密码是否正确: a. 在 Internet 信息服务 (IIS) 管理器中,展开“应用程序池”。
b. 右键单击为虚拟服务器配置的应用程序池(例如,右键单击“MSSharePointPortalAppPool”),然后单击“属性”。
c. 单击“标识”选项卡。
d. 在“密码”框中,键入列在“用户名”框中的应用程序池帐户所用的密码,然后单击“确定”。
e. 在“确认密码”对话框中,再次键入密码,然后单击“确定”。

3.验证应用程序池帐户是服务器上的 IIS_WPG 组和 STS_WPG 组的成员。

根据您的具体情况选用下列方法之一。 a. 在成员服务器上安装了 SharePoint Portal Server 的情况下: 1.单击“开始”,指向“管理工具”,然后单击“计算机管理”。
2.展开“本地用户和组”,然后展开“用户”。
3.右键单击虚拟服务器的应用程序池使用的帐户,然后单击“属性”。
4.单击“成员属于”选项卡。

验证 IIS_WPG 和 STS_WPG 是否都出现在“成员属于”列表中。如果其中之一没有列出或者两者均未列出,请根据具体情况将 IIS_WPG 组、STS_WPG 组或者这两个组添加到列表中。

b. 在域控制器上安装了 SharePoint Portal Server 的情况下: 1.启动“Active Directory 用户和计算机”。
2.展开“用户”。
3.右键单击虚拟服务器的应用程序池使用的帐户,然后单击“属性”。
4.单击“成员属于”选项卡。

验证 IIS_WPG 和 STS_WPG 都出现在“成员属于”列表中。如果其中之一没有列出或者两者均未列出,请根据具体情况将 IIS_WPG 组、STS_WPG 组或者这两个组添加到列表中。


4.重新启动 IIS 以回收应用程序池: a. 在 Internet 信息服务 (IIS) 管理器中,右键单击“ServerName”,指向“所有任务”,然后单击“重新启动 IIS”。
b. 单击“在 ServerName 上重新启动 Internet 信息服务”,然后单击“确定”。

2007年5月16日星期三

文本分类子模块项目开发心得

1 第一次遇到MySQL数据库的BUG,晕翻>_<,害得我调试半天,原来是MYSQL把中文的“读”和“多”当成相同的字符串了,真是烂货数据库。。。
2 后来才搞清楚,是MySQl的字符编码设置的问题,搞了一上午和一下午,发现要改:
(1在my.ini处的2处都要改为default character set,
(2建的所有的表都要在最后加上default charset = gbk,
(3 string useGBK = "set names gbk";//成功调试必备的
DBComm = new MySQLCommand(useGBK, DBConn);
DBComm.ExecuteNonQuery();
3 一些常用的MySQLConnection 的函数集
static void Main(string[] args)
{
string sqlstr = "select * from manavatar";
MySQLConnection DBConn = new MySQLConnection(new MySQLConnectionString("192.168.0.13", "flashdata", "root", "root", 3306).AsString);
DBConn.Open();
//MySQLDataAdapter myadap = new MySQLDataAdapter(sqlstr, conn);
MySQLCommand DBComm = new MySQLCommand(sqlstr,DBConn);
MySQLDataReader DBReader = DBComm.ExecuteReaderEx(); //DBComm.ExecuteReaderEx();
MySQLDataAdapter DTAdapter = new MySQLDataAdapter(sqlstr,DBConn);

DataSet myDataSet = new DataSet();
DTAdapter.Fill(myDataSet,"manavatar");


try
{
while (DBReader.Read())
{
//Console.WriteLine("11");
Console.WriteLine("DBReader:{0},\t\t\tddddd:{1},\t\t {2}",DBReader.GetString(0), DBReader.GetString(1),DBReader.GetString(3));
}
Console.WriteLine("0000");
}
catch (Exception e)
{
Console.WriteLine("读入失败!"+e.ToString());
}
finally
{
Console.WriteLine("DBReader关闭");
Console.WriteLine("DBConn关闭");
DBReader.Close();
//DBConn.Close();
}

for (int i = 0; i < myDataSet.Tables["manavatar"].Rows.Count; i++)
{
Console.WriteLine("{0}",myDataSet.Tables["manavatar"].Rows[2]["user"]);
}


}

这是一个简单的例子。
在这里有个问题:dataset如果没设主键的话,可能会引起一些对数库操作的问题,比如会造成updata出现错误。

4 项目中导入了一个开源的DLL,MySQLDriverCS,他对C#调用MySQL做了很好的封装
5出现了诡异的语句INSERT INTO dic VALUES('/|\','军事20',9,1,0.181818181818182);
主要'/|\'是转义字符,暂时没有很好的解决方法

5 如果要调用generic 的Sort方法,必须实现CompareTo接口。这个接口有一个叫CompareTo(object)方法,如果“this”大于、小于 ...
如果想使用Array等的Find方法,必须重载Equals()方法,因为任何类都是继承Object类的。

6 无法显示 XML 页。
使用 XSL 样式表无法查看 XML 输入。请更正错误然后单击 刷新按钮,或以后重试。
--------------------------------------------------------------------------------
名称以无效字符开头。处理资源 'http://localhost/Asp.net/Default.aspx' 时出错。第 1 行,位置: 2
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
===================

解决办法:运行vs2005命令行(开始、所有程序、vs2005、tools,写得不准确),之后找到.net2.0的路径,我的是在C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727,之后运行aspnet_regiis.exe /i,就ok了。

(待续)

2007年5月14日星期一

MySQL常用命令(ZZ)

MySQL常用命令

建立库:
create database sss

使用库:
use sss

显示库:
show databases

删除库:
drop database sss

建立表:
create table object (obj_id numeric(10),obj_ra numeric(8,5),obj_de numeric(8,5),obj_mag numeric(4,2))

显示表:
show tables

显示表的结构:
describe object

删除表:
drop table object

授权外部连接:
grant all on sss.object to sss@192.168.1.12 identified by 'mysql'




# casularm 发表于2006-04-06 14:59:00 IP: 218.104.71.*
MySQL常用操作

启动:
/etc/init.d/mysql start

关闭:
/usr/bin/mysqladmin -u root -p shutdown

登陆:
mysql -uroot -p




# casularm 发表于2006-08-13 21:13:00 IP: 211.162.10.*
从外部文件向表中批量导入数据
load data infile 'e:/Data/minifiber.csv' into table minifiber fields terminated by ',';



# casularm 发表于2006-08-30 11:16:00 IP: 211.162.6.*
1.备份服务器数据:

mysqldump -h服务器IP地址 -u用户名 -p密码 --opt 数据库名>备份数据库名

例如:

mysqldump -h 192.168.1.100 -u sss -p --opt sss >sss_backup.sql

这条命令将服务器192.168.1.100上的sss这个数据库备份到本地计算机当前目录的sss_backup.sql这个文件中,这样,当数据库不小心损坏或数据丢失时,就可以由sss_backup.sql这个备份文件恢复了

2. 导入.sql数据到mysql数据库

mysql -h服务器IP地址 -u用户名 -p密码 -f -D 数据库名
备份文件名

例如:

mysql -h 192.168.1.100 -u sss -p -f -D sss 这条命令会将sss_backup.sql这个文件中的数据重新恢复到服务器sss数据库中。

3.注意事项
如果是新安装的数据库,需要新建个sss库
create database sss
备份和导入都要有sss数据库的权限
grant all on sss.* to sss@192.168.1.100 identified by 'mysql'




# casularm 发表于2006-10-29 17:06:00 IP: 211.162.10.*
使用PowerDesigner设计建造MySQL数据库

一、使用PowerDesigner制作建库脚本
1、设计CDM(Conceptual Data Model)
2、选择 Tools -> Generate Physical Data Model ,选择对应的DBMS为MySQL,生成PDM
3、选择 Database -> Generate Database ,在弹出的 Database Generation 对话框中选择脚本存取路径及脚本文件名称
4、点击确定后生成数据库建库脚本(*.sql)

二、使用建库脚本建立数据库
1、登陆 mysql -u root -p
2、建立空的databse create databse sss;
3、建立用户 grant all on sss.* to sss@192.168.1.100 identified by 'mysql';
4、退出 exit;
5、在终端中输入 mysql -h 192.168.1.100 -u sss -p < Script.sql
6、回车后输入密码即可

修改某个表的字段类型及指定为空或非空
>alter table 表名称 change 字段名称 字段名称 字段类型 [是否允许非空];
>alter table 表名称 modify 字段名称 字段类型 [是否允许非空];

修改某个表的字段名称及指定为空或非空
>alter table 表名称 change 字段原名称 字段新名称 字段类型 [是否允许非空];

例如:
修改表expert_info中的字段birth,允许其为空
>alter table expert_info change birth birth varchar(20) null;
mysql> ALTER TABLE t2 MODIFY a TINYINT NOT NULL, CHANGE b c CHAR(20);
添加新字段 d:
mysql> ALTER TABLE t2 ADD d TIMESTAMP;
在a d 上增加索引:
mysql> ALTER TABLE t2 ADD INDEX (d), ADD INDEX (a);
删除字段c:
mysql> ALTER TABLE t2 DROP COLUMN c;
添加一个自动增长的字段c ,并且添加c 为主健:
mysql> ALTER TABLE t2 ADD c INT UNSIGNED NOT NULL AUTO_INCREMENT,
-> ADD PRIMARY KEY (c);

2007年5月13日星期日

Equals() 和运算符 == 的重载准则(C# 编程指南) ZZ

C# 程序员参考
Equals() 和运算符 == 的重载准则(C# 编程指南)

C# 中有两种不同的相等:引用相等和值相等。值相等是大家普遍理解的意义上的相等:它意味着两个对象包含相同的值。例如,两个值为 2 的整数具有值相等性。引用相等意味着要比较的不是两个对象,而是两个对象引用,这两个对象引用引用的是同一个对象。这可以通过简单的赋值来实现,如下面的示例所示:

C# 复制代码
System.Object a = new System.Object();
System.Object b = a;
System.Object.ReferenceEquals(a, b); //returns true

在上面的代码中,只存在一个对象,但存在对该对象的多个引用:a 和 b。由于它们引用的是同一个对象,因此具有引用相等性。如果两个对象具有引用相等性,则它们也具有值相等性,但是值相等性不能保证引用相等性。

若要检查引用相等性,应使用 ReferenceEquals。若要检查值相等性,应使用 Equals 或 Equals。

重写 Equals

Equals 是一个虚方法,允许任何类重写其实现。表示某个值(本质上可以是任何值类型)或一组值(如复数类)的任何类都应该重写 Equals。如果类型要实现 IComparable,则它应该重写 Equals。

Equals 的新实现应该遵循 Equals 的所有保证:

x.Equals(x) 返回 true。

x.Equals(y) 与 y.Equals(x) 返回相同的值。

如果 (x.Equals(y) && y.Equals(z)) 返回 true,则 x.Equals(z) 返回 true。

只要不修改 x 和 y 所引用的对象,x.Equals(y) 的后续调用就返回相同的值。

x.Equals(null) 返回 false。

Equals 的新实现不应该引发异常。建议重写 Equals 的任何类同时也重写 System.Object.GetHashCode。除了实现 Equals(对象)外,还建议所有的类为自己的类型实现 Equals(类型)以增强性能。例如:

C# 复制代码
class TwoDPoint : System.Object
{
public readonly int x, y;

public TwoDPoint(int x, int y) //constructor
{
this.x = x;
this.y = y;
}

public override bool Equals(System.Object obj)
{
// If parameter is null return false.
if (obj == null)
{
return false;
}

// If parameter cannot be cast to Point return false.
TwoDPoint p = obj as TwoDPoint;
if ((System.Object)p == null)
{
return false;
}

// Return true if the fields match:
return (x == p.x) && (y == p.y);
}

public bool Equals(TwoDPoint p)
{
// If parameter is null return false:
if ((object)p == null)
{
return false;
}

// Return true if the fields match:
return (x == p.x) && (y == p.y);
}

public override int GetHashCode()
{
return x ^ y;
}
}

可调用基类的 Equals 的任何派生类在完成其比较之前都应该这样做。在下面的示例中,Equals 调用基类 Equals,后者将检查空参数并将参数的类型与派生类的类型做比较。这样就把检查派生类中声明的新数据字段的任务留给了派生类中的 Equals 实现:

C# 复制代码
class ThreeDPoint : TwoDPoint
{
public readonly int z;

public ThreeDPoint(int x, int y, int z)
: base(x, y)
{
this.z = z;
}

public override bool Equals(System.Object obj)
{
// If parameter cannot be cast to ThreeDPoint return false:
ThreeDPoint p = obj as ThreeDPoint;
if ((object)p == null)
{
return false;
}

// Return true if the fields match:
return base.Equals(obj) && z == p.z;
}

public bool Equals(ThreeDPoint p)
{
// Return true if the fields match:
return base.Equals((TwoDPoint)p) && z == p.z;
}

public override int GetHashCode()
{
return base.GetHashCode() ^ z;
}
}

2007年5月12日星期六

近期程序设计竞赛和公司面试的总结

1 树型DP的问题:


2 C++中讲string 快速转化到Int的问题:
stringstream是用于C++风格的字符串的输入输出的。
stringstream的构造函数原形如下: stringstream::stringstream(string str); #include

string s;
int a;
istringstream ss(s);
ss>>a;



3 C++中常用的字符串初始化函数
void *memset(void *s, int c, size_t n); Sets n bytes of a block of memory to byte c. memset sets the first n bytes of the array s to the character c.
用这个东东可以快速将字符串初始化.


4数学问题(发现好久没看数学了,连一些基本的东东都忘了)
递归方程组解的渐进阶的求法——差分方程法
这里只考虑形如:
T(n)=c1T(n-1)+c2T(n-2)+…+ ckT(n-k)+f(n),n≥k (6.18)
的递归方程。其中ci (i=l,2,…,k)为实常数,且ck≠0。它可改写为一个线性常系数k阶非齐次的差分方程:
T(n)-c1T(n-1)- c2T(n-2)-…-ckT(n-k)=f(n),n≥k (6.19)
(6.19)与线性常系数k阶非齐次常微分方程的结构十分相似,因而解法类同。限于篇幅,这里直接给出(6.19)的解法,略去其正确性的证明。
第一步,求(6.19)所对应的齐次方程:
T(n)-c1T(n-1)- c2T(n-2)-…-ckT(n-k)=0 (6.20)
的基本解系:写出(6.20)的特征方程:
C(t)=tk-c1tk-1-c2tk-2 -…-ck=0 (6.21)
若t=r是(6.21)的m重实根,则得(6.20)的m个基础解rn,nrn,n2rn,…,nm-1rn;若ρeiθ和ρe-iθ是(6.21)的一对l重的共扼复根,则得(6.20)的2l个基础解ρncosnθ,ρnsinnθ,nρncosnθ,nρnsinnθ,…,nl-1ρncosnθ,nl-1ρncosnθ。如此,求出(6.21)的所有的根,就可以得到(6.20)的k个的基础解。而且,这k个基础解构成了(6.20)的基础解系。即(6.20)的任意一个解都可以表示成这k个基础解的线性组合。
第二步,求(6.19)的一个特解。理论上,(6.19)的特解可以用Lagrange常数变易法得到。但其中要用到(6.20)的通解的显式表达,即(6.20)的基础解系的线性组合,十分麻烦。因此在实际中,常常采用试探法,也就是根据f(n)的特点推测特解的形式,留下若干可调的常数,将推测解代人(6.19)后确定。由于(6.19)的特殊性,可以利用迭加原理,将f(n)线性分解为若干个单项之和并求出各单项相应的特解,然后迭加便得到f(n)相应的特解。这使得试探法更为有效。为了方便,这里对三种特殊形式的f(n),给出(6.19)的相应特解并列在表6-1中,可供直接套用。其中pi,i=1,2,…,s是待定常数

2007年5月7日星期一

在 C# 中通过 P/Invoke 调用Win32 DLL(ZZ)

下载本文的代码: NET0307.exe (133KB)
我在自己最近的编程中注意到一个趋势,正是这个趋势才引出本月的专栏主题。最近,我在基于 Microsoft® .NET Framework 的应用程序中完成了大量的 Win32® Interop。我并不是要说我的应用程序充满了自定义的 interop 代码,但有时我会在 .NET Framework 类库中碰到一些次要但又繁絮、不充分的内容,通过调用该 Windows® API,可以快速减少这样的麻烦。
因此我认为,.NET Framework 1.0 或 1.1 版类库中存在任何 Windows 所没有的功能限制都不足为怪。毕竟,32 位的 Windows(不管何种版本)是一个成熟的操作系统,为广大客户服务了十多年。相比之下,.NET Framework 却是一个新事物。
随着越来越多的开发人员将生产应用程序转到托管代码,开发人员更频繁地研究底层操作系统以图找出一些关键功能显得很自然 — 至少目前是如此。
值得庆幸的是,公共语言运行库 (CLR) 的 interop 功能(称为平台调用 (P/Invoke))非常完善。在本专栏中,我将重点介绍如何实际使用 P/Invoke 来调用 Windows API 函数。当指 CLR 的 COM Interop 功能时,P/Invoke 当作名词使用;当指该功能的使用时,则将其当作动词使用。我并不打算直接介绍 COM Interop,因为它比 P/Invoke 具有更好的可访问性,却更加复杂,这有点自相矛盾,这使得将 COM Interop 作为专栏主题来讨论不太简明扼要。
走进 P/Invoke
首先从考察一个简单的 P/Invoke 示例开始。让我们看一看如何调用 Win32 MessageBeep 函数,它的非托管声明如以下代码所示:
BOOL MessageBeep( UINT uType // beep type);
为了调用 MessageBeep,您需要在 C# 中将以下代码添加到一个类或结构定义中:
[DllImport("User32.dll")]static extern Boolean MessageBeep(UInt32 beepType);
令人惊讶的是,只需要这段代码就可以使托管代码调用非托管的 MessageBeep API。它不是一个方法调用,而是一个外部方法定义。(另外,它接近于一个来自 C 而 C# 允许的直接端口,因此以它为起点来介绍一些概念是有帮助的。)来自托管代码的可能调用如下所示:
MessageBeep(0);
请注意,现在 MessageBeep 方法被声明为 static。这是 P/Invoke 方法所要求的,因为在该 Windows API 中没有一致的实例概念。接下来,还要注意该方法被标记为 extern。这是提示编译器该方法是通过一个从 DLL 导出的函数实现的,因此不需要提供方法体。
说到缺少方法体,您是否注意到 MessageBeep 声明并没有包含一个方法体?与大多数算法由中间语言 (IL) 指令组成的托管方法不同,P/Invoke 方法只是元数据,实时 (JIT) 编译器在运行时通过它将托管代码与非托管的 DLL 函数连接起来。执行这种到非托管世界的连接所需的一个重要信息就是导出非托管方法的 DLL 的名称。这一信息是由 MessageBeep 方法声明之前的 DllImport 自定义属性提供的。在本例中,可以看到,MessageBeep 非托管 API 是由 Windows 中的 User32.dll 导出的。
到现在为止,关于调用 MessageBeep 就剩两个话题没有介绍,请回顾一下,调用的代码与以下所示代码片段非常相似:
[DllImport("User32.dll")]static extern Boolean MessageBeep(UInt32 beepType);
最后这两个话题是与数据封送处理 (data marshaling) 和从托管代码到非托管函数的实际方法调用有关的话题。调用非托管 MessageBeep 函数可以由找到作用域内的extern MessageBeep 声明的任何托管代码执行。该调用类似于任何其他对静态方法的调用。它与其他任何托管方法调用的共同之处在于带来了数据封送处理的需要。
C# 的规则之一是它的调用语法只能访问 CLR 数据类型,例如 System.UInt32 和 System.Boolean。C# 显然不识别 Windows API 中使用的基于 C 的数据类型(例如 UINT 和 BOOL),这些类型只是 C 语言类型的类型定义而已。所以当 Windows API 函数 MessageBeep 按以下方式编写时
BOOL MessageBeep( UINT uType )
外部方法就必须使用 CLR 类型来定义,如您在前面的代码片段中所看到的。需要使用与基础 API 函数类型不同但与之兼容的 CLR 类型是 P/Invoke 较难使用的一个方面。因此,在本专栏的后面我将用完整的章节来介绍数据封送处理。
样式
在 C# 中对 Windows API 进行 P/Invoke 调用是很简单的。但如果类库拒绝使您的应用程序发出嘟声,应该想方设法调用 Windows 使它进行这项工作,是吗?
是的。但是与选择的方法有关,而且关系甚大!通常,如果类库提供某种途径来实现您的意图,则最好使用 API 而不要直接调用非托管代码,因为 CLR 类型和 Win32 之间在样式上有很大的不同。我可以将关于这个问题的建议归结为一句话。当您进行 P/Invoke 时,不要使应用程序逻辑直接属于任何外部方法或其中的构件。如果您遵循这个小规则,从长远看经常会省去许多的麻烦。
图 1 中的代码显示了我所讨论的 MessageBeep 外部方法的最少附加代码。图 1 中并没有任何显著的变化,而只是对无包装的外部方法进行一些普通的改进,这可以使工作更加轻松一些。从顶部开始,您会注意到一个名为 Sound 的完整类型,它专用于 MessageBeep。如果我需要使用 Windows API 函数 PlaySound 来添加对播放波形的支持,则可以重用 Sound 类型。然而,我不会因公开单个公共静态方法的类型而生气。毕竟这只是应用程序代码而已。还应该注意到,Sound 是密封的,并定义了一个空的私有构造函数。这些只是一些细节,目的是使用户不会错误地从 Sound 派生类或者创建它的实例。
图 1 中的代码的下一个特征是,P/Invoke 出现位置的实际外部方法是 Sound 的私有方法。这个方法只是由公共 MessageBeep 方法间接公开,后者接受 BeepTypes 类型的参数。这个间接的额外层是一个很关键的细节,它提供了以下好处。首先,应该在类库中引入一个未来的 beep 托管方法,可以重复地通过公共 MessageBeep 方法来使用托管 API,而不必更改应用程序中的其余代码。
该包装方法的第二个好处是:当您进行 P/Invoke 调用时,您放弃了免受访问冲突和其他低级破坏的权利,这通常是由 CLR 提供的。缓冲方法可以保护您的应用程序的其余部分免受访问冲突及类似问题的影响(即使它不做任何事而只是传递参数)。该缓冲方法将由 P/Invoke 调用引入的任何潜在的错误本地化。
将私有外部方法隐藏在公共包装后面的第三同时也是最后的一个好处是,提供了向该方法添加一些最小的 CLR 样式的机会。例如,在图 1 中,我将 Windows API 函数返回的 Boolean 失败转换成更像 CLR 的异常。我还定义了一个名为 BeepTypes 的枚举类型,它的成员对应于同该 Windows API 一起使用的定义值。由于 C# 不支持定义,因此可以使用托管枚举类型来避免幻数向整个应用程序代码扩散。
包装方法的最后一个好处对于简单的 Windows API 函数(如 MessageBeep)诚然是微不足道的。但是当您开始调用更复杂的非托管函数时,您会发现,手动将 Windows API 样式转换成对 CLR 更加友好的方法所带来的好处会越来越多。越是打算在整个应用程序中重用 interop 功能,越是应该认真地考虑包装的设计。同时我认为,在非面向对象的静态包装方法中使用对 CLR 友好的参数也并非不可以。
DLL Import 属性
现在是更深入地进行探讨的时候了。在对托管代码进行 P/Invoke 调用时,DllImportAttribute 类型扮演着重要的角色。DllImportAttribute 的主要作用是给 CLR 指示哪个 DLL 导出您想要调用的函数。相关 DLL 的名称被作为一个构造函数参数传递给 DllImportAttribute。
如果您无法肯定哪个 DLL 定义了您要使用的 Windows API 函数,Platform SDK 文档将为您提供最好的帮助资源。在 Windows API 函数主题文字临近结尾的位置,SDK 文档指定了 C 应用程序要使用该函数必须链接的 .lib 文件。在几乎所有的情况下,该 .lib 文件具有与定义该函数的系统 DLL 文件相同的名称。例如,如果该函数需要 C 应用程序链接到 Kernel32.lib,则该函数就定义在 Kernel32.dll 中。您可以在 MessageBeep 中找到有关 MessageBeep 的 Platform SDK 文档主题。在该主题结尾处,您会注意到它指出库文件是 User32.lib;这表明 MessageBeep 是从 User32.dll 中导出的。
可选的 DllImportAttribute 属性
除了指出宿主 DLL 外,DllImportAttribute 还包含了一些可选属性,其中四个特别有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。
EntryPoint 在不希望外部托管方法具有与 DLL 导出相同的名称的情况下,可以设置该属性来指示导出的 DLL 函数的入口点名称。当您定义两个调用相同非托管函数的外部方法时,这特别有用。另外,在 Windows 中还可以通过它们的序号值绑定到导出的 DLL 函数。如果您需要这样做,则诸如“#1”或“#129”的 EntryPoint 值指示 DLL 中非托管函数的序号值而不是函数名。
CharSet 对于字符集,并非所有版本的 Windows 都是同样创建的。Windows 9x 系列产品缺少重要的 Unicode 支持,而 Windows NT 和 Windows CE 系列则一开始就使用 Unicode。在这些操作系统上运行的 CLR 将Unicode 用于 String 和 Char 数据的内部表示。但也不必担心 — 当调用 Windows 9x API 函数时,CLR 会自动进行必要的转换,将其从 Unicode转换为 ANSI。
如果 DLL 函数不以任何方式处理文本,则可以忽略 DllImportAttribute 的 CharSet 属性。然而,当 Char 或 String 数据是等式的一部分时,应该将 CharSet 属性设置为 CharSet.Auto。这样可以使 CLR 根据宿主 OS 使用适当的字符集。如果没有显式地设置 CharSet 属性,则其默认值为 CharSet.Ansi。这个默认值是有缺点的,因为对于在 Windows 2000、Windows XP 和 Windows NT® 上进行的 interop 调用,它会消极地影响文本参数封送处理的性能。
应该显式地选择 CharSet.Ansi 或 CharSet.Unicode 的 CharSet 值而不是使用 CharSet.Auto 的唯一情况是:您显式地指定了一个导出函数,而该函数特定于这两种 Win32 OS 中的某一种。ReadDirectoryChangesW API 函数就是这样的一个例子,它只存在于基于 Windows NT 的操作系统中,并且只支持 Unicode;在这种情况下,您应该显式地使用 CharSet.Unicode。
有时,Windows API 是否有字符集关系并不明显。一种决不会有错的确认方法是在 Platform SDK 中检查该函数的 C 语言头文件。(如果您无法肯定要看哪个头文件,则可以查看 Platform SDK 文档中列出的每个 API 函数的头文件。)如果您发现该 API 函数确实定义为一个映射到以 A 或 W 结尾的函数名的宏,则字符集与您尝试调用的函数有关系。Windows API 函数的一个例子是在 WinUser.h 中声明的 GetMessage API,您也许会惊讶地发现它有 A 和 W 两种版本。
SetLastError 错误处理非常重要,但在编程时经常被遗忘。当您进行 P/Invoke 调用时,也会面临其他的挑战 — 处理托管代码中 Windows API 错误处理和异常之间的区别。我可以给您一点建议。
如果您正在使用 P/Invoke 调用 Windows API 函数,而对于该函数,您使用 GetLastError 来查找扩展的错误信息,则应该在外部方法的 DllImportAttribute 中将 SetLastError 属性设置为 true。这适用于大多数外部方法。
这会导致 CLR 在每次调用外部方法之后缓存由 API 函数设置的错误。然后,在包装方法中,可以通过调用类库的 System.Runtime.InteropServices.Marshal 类型中定义的 Marshal.GetLastWin32Error 方法来获取缓存的错误值。我的建议是检查这些期望来自 API 函数的错误值,并为这些值引发一个可感知的异常。对于其他所有失败情况(包括根本就没意料到的失败情况),则引发在 System.ComponentModel 命名空间中定义的 Win32Exception,并将 Marshal.GetLastWin32Error 返回的值传递给它。如果您回头看一下图 1 中的代码,您会看到我在 extern MessageBeep 方法的公共包装中就采用了这种方法。
CallingConvention 我将在此介绍的最后也可能是最不重要的一个 DllImportAttribute 属性是 CallingConvention。通过此属性,可以给 CLR 指示应该将哪种函数调用约定用于堆栈中的参数。CallingConvention.Winapi 的默认值是最好的选择,它在大多数情况下都可行。然而,如果该调用不起作用,则可以检查 Platform SDK 中的声明头文件,看看您调用的 API 函数是否是一个不符合调用约定标准的异常 API。
通常,本机函数(例如 Windows API 函数或 C- 运行时 DLL 函数)的调用约定描述了如何将参数推入线程堆栈或从线程堆栈中清除。大多数 Windows API 函数都是首先将函数的最后一个参数推入堆栈,然后由被调用的函数负责清理该堆栈。相反,许多 C-运行时 DLL 函数都被定义为按照方法参数在方法签名中出现的顺序将其推入堆栈,将堆栈清理工作交给调用者。
幸运的是,要让 P/Invoke 调用工作只需要让外围设备理解调用约定即可。通常,从默认值 CallingConvention.Winapi 开始是最好的选择。然后,在 C 运行时 DLL 函数和少数函数中,可能需要将约定更改为 CallingConvention.Cdecl。
数据封送处理
数据封送处理是 P/Invoke 具有挑战性的方面。当在托管和非托管代码之间传递数据时,CLR 遵循许多规则,很少有开发人员会经常遇到它们直至可将这些规则记住。除非您是一名类库开发人员,否则在通常情况下没有必要掌握其细节。为了最有效地在 CLR 上使用 P/Invoke,即使只偶尔需要 interop 的应用程序开发人员仍然应该理解数据封送处理的一些基础知识。
在本月专栏的剩余部分中,我将讨论简单数字和字符串数据的数据封送处理。我将从最基本的数字数据封送处理开始,然后介绍简单的指针封送处理和字符串封送处理。
封送数字和逻辑标量
Windows OS 大部分是用 C 编写的。因此,Windows API 所用到的数据类型要么是 C 类型,要么是通过类型定义或宏定义重新标记的 C 类型。让我们看看没有指针的数据封送处理。简单起见,首先重点讨论的是数字和布尔值。
当通过值向 Windows API 函数传递参数时,需要知道以下问题的答案:
• 数据从根本上讲是整型的还是浮点型的? • 如果数据是整型的,则它是有符号的还是无符号的? • 如果数据是整型的,则它的位数是多少? • 如果数据是浮点型的,则它是单精度的还是双精度的?
有时答案很明显,但有时却不明显。Windows API 以各种方式重新定义了基本的 C 数据类型。图 2 列出了 C 和 Win32 的一些公共数据类型及其规范,以及一个具有匹配规范的公共语言运行库类型。
通常,只要您选择一个其规范与该参数的 Win32 类型相匹配的 CLR 类型,您的代码就能够正常工作。不过也有一些特例。例如,在 Windows API 中定义的 BOOL 类型是一个有符号的 32 位整型。然而,BOOL 用于指示 Boolean 值 true 或 false。虽然您不用将 BOOL 参数作为 System.Int32 值封送,但是如果使用 System.Boolean 类型,就会获得更合适的映射。字符类型的映射类似于 BOOL,因为有一个特定的 CLR 类型 (System.Char) 指出字符的含义。
在了解这些信息之后,逐步介绍示例可能是有帮助的。依然采用 beep 主题作为例子,让我们来试一下 Kernel32.dll 低级 Beep,它会通过计算机的扬声器发生嘟声。这个方法的 Platform SDK 文档可以在 Beep 中找到。本机 API 按以下方式进行记录:
BOOL Beep( DWORD dwFreq, // Frequency DWORD dwDuration // Duration in milliseconds);
在参数封送处理方面,您的工作是了解什么 CLR 数据类型与 Beep API 函数所使用的 DWORD 和 BOOL 数据类型相兼容。回顾一下图 2 中的图表,您将看到 DWORD 是一个 32 位的无符号整数值,如同 CLR 类型 System.UInt32。这意味着您可以使用 UInt32 值作为送往 Beep 的两个参数。BOOL 返回值是一个非常有趣的情况,因为该图表告诉我们,在 Win32 中,BOOL 是一个 32 位的有符号整数。因此,您可以使用 System.Int32 值作为来自 Beep 的返回值。然而,CLR 也定义了 System.Boolean 类型作为 Boolean 值的语义,所以应该使用它来替代。CLR 默认将 System.Boolean 值封送为 32 位的有符号整数。此处所显示的外部方法定义是用于 Beep 的结果 P/Invoke 方法:
[DllImport("Kernel32.dll", SetLastError=true)]static extern Boolean Beep( UInt32 frequency, UInt32 duration);
指针参数
许多 Windows API 函数将指针作为它们的一个或多个参数。指针增加了封送数据的复杂性,因为它们增加了一个间接层。如果没有指针,您可以通过值在线程堆栈中传递数据。有了指针,则可以通过引用传递数据,方法是将该数据的内存地址推入线程堆栈中。然后,函数通过内存地址间接访问数据。使用托管代码表示此附加间接层的方式有多种。
在 C# 中,如果将方法参数定义为 ref 或 out,则数据通过引用而不是通过值传递。即使您没有使用 Interop 也是这样,但只是从一个托管方法调用到另一个托管方法。例如,如果通过 ref 传递 System.Int32 参数,则在线程堆栈中传递的是该数据的地址,而不是整数值本身。下面是一个定义为通过引用接收整数值的方法的示例:
void FlipInt32(ref Int32 num){ num = -num;}
这里,FlipInt32 方法获取一个 Int32 值的地址、访问数据、对它求反,然后将求反过的值赋给原始变量。在以下代码中,FlipInt32 方法会将调用程序的变量 x 的值从 10 更改为 -10:
Int32 x = 10;FlipInt32(ref x);
在托管代码中可以重用这种能力,将指针传递给非托管代码。例如,FileEncryptionStatus API 函数以 32 位无符号位掩码的形式返回文件加密状态。该 API 按以下所示方式进行记录:
BOOL FileEncryptionStatus( LPCTSTR lpFileName, // file name LPDWORD lpStatus // encryption status);
请注意,该函数并不使用它的返回值返回状态,而是返回一个 Boolean 值,指示调用是否成功。在成功的情况下,实际的状态值是通过第二个参数返回的。它的工作方式是调用程序向该函数传递指向一个 DWORD 变量的指针,而该 API 函数用状态值填充指向的内存位置。以下代码片段显示了一个调用非托管 FileEncryptionStatus 函数的可能外部方法定义:
[DllImport("Advapi32.dll", CharSet=CharSet.Auto)]static extern Boolean FileEncryptionStatus(String filename, out UInt32 status);
该定义使用 out 关键字来为 UInt32 状态值指示 by-ref 参数。这里我也可以选择 ref 关键字,实际上在运行时会产生相同的机器码。out 关键字只是一个 by-ref 参数的规范,它向 C# 编译器指示所传递的数据只在被调用的函数外部传递。相反,如果使用 ref 关键字,则编译器会假定数据可以在被调用的函数的内部和外部传递。
托管代码中 out 和 ref 参数的另一个很好的方面是,地址作为 by-ref 参数传递的变量可以是线程堆栈中的一个本地变量、一个类或结构的元素,也可以是具有合适数据类型的数组中的一个元素引用。调用程序的这种灵活性使得 by-ref 参数成为封送缓冲区指针以及单数值指针的一个很好的起点。只有在我发现 ref 或 out 参数不符合我的需要的情况下,我才会考虑将指针封送为更复杂的 CLR 类型(例如类或数组对象)。
如果您不熟悉 C 语法或者调用 Windows API 函数,有时很难知道一个方法参数是否需要指针。一个常见的指示符是看参数类型是否是以字母 P 或 LP 开头的,例如 LPDWORD 或 PINT。在这两个例子中,LP 和 P 指示参数是一个指针,而它们指向的数据类型分别为 DWORD 或 INT。然而,在有些情况下,可以直接使用 C 语言语法中的星号 (*) 将 API 函数定义为指针。以下代码片段展示了这方面的示例:
void TakesAPointer(DWORD* pNum);
可以看到,上述函数的唯一一个参数是指向 DWORD 变量的指针。
当通过 P/Invoke 封送指针时,ref 和 out 只用于托管代码中的值类型。当一个参数的 CLR 类型使用 struct 关键字定义时,可以认为该参数是一个值类型。Out 和 ref 用于封送指向这些数据类型的指针,因为通常值类型变量是对象或数据,而在托管代码中并没有对值类型的引用。相反,当封送引用类型对象时,并不需要 ref 和 out 关键字,因为变量已经是对象的引用了。
如果您对引用类型和值类型之间的差别不是很熟悉,请查阅 2000 年 12 月 发行的 MSDN® Magazine,在 .NET 专栏的主题中可以找到更多信息。大多数 CLR 类型都是引用类型;然而,除了 System.String 和 System.Object,所有的基元类型(例如 System.Int32 和 System.Boolean)都是值类型。
封送不透明 (Opaque) 指针:一种特殊情况
有时在 Windows API 中,方法传递或返回的指针是不透明的,这意味着该指针值从技术角度讲是一个指针,但代码却不直接使用它。相反,代码将该指针返回给 Windows 以便随后进行重用。
一个非常常见的例子就是句柄的概念。在 Windows 中,内部数据结构(从文件到屏幕上的按钮)在应用程序代码中都表示为句柄。句柄其实就是不透明的指针或有着指针宽度的数值,应用程序用它来表示内部的 OS 构造。
少数情况下,API 函数也将不透明指针定义为 PVOID 或 LPVOID 类型。在 Windows API 的定义中,这些类型意思就是说该指针没有类型。
当一个不透明指针返回给您的应用程序(或者您的应用程序期望得到一个不透明指针)时,您应该将参数或返回值封送为 CLR 中的一种特殊类型 — System.IntPtr。当您使用 IntPtr 类型时,通常不使用 out 或 ref 参数,因为 IntPtr 意为直接持有指针。不过,如果您将一个指针封送为一个指针,则对 IntPtr 使用 by-ref 参数是合适的。
在 CLR 类型系统中,System.IntPtr 类型有一个特殊的属性。不像系统中的其他基类型,IntPtr 并没有固定的大小。相反,它在运行时的大小是依底层操作系统的正常指针大小而定的。这意味着在 32 位的 Windows 中,IntPtr 变量的宽度是 32 位的,而在 64 位的 Windows 中,实时编译器编译的代码会将 IntPtr 值看作 64 位的值。当在托管代码和非托管代码之间封送不透明指针时,这种自动调节大小的特点十分有用。
请记住,任何返回或接受句柄的 API 函数其实操作的就是不透明指针。您的代码应该将 Windows 中的句柄封送成 System.IntPtr 值。
您可以在托管代码中将 IntPtr 值强制转换为 32 位或 64 位的整数值,或将后者强制转换为前者。然而,当使用 Windows API 函数时,因为指针应是不透明的,所以除了存储和传递给外部方法外,不能将它们另做它用。这种“只限存储和传递”规则的两个特例是当您需要向外部方法传递 null 指针值和需要比较 IntPtr 值与 null 值的情况。为了做到这一点,您不能将零强制转换为 System.IntPtr,而应该在 IntPtr 类型上使用 Int32.Zero 静态公共字段,以便获得用于比较或赋值的 null 值。
封送文本
在编程时经常要对文本数据进行处理。文本为 interop 制造了一些麻烦,这有两个原因。首先,底层操作系统可能使用 Unicode 来表示字符串,也可能使用 ANSI。在极少数情况下,例如 MultiByteToWideChar API 函数的两个参数在字符集上是不一致的。
第二个原因是,当需要进行 P/Invoke 时,要处理文本还需要特别了解到 C 和 CLR 处理文本的方式是不同的。在 C 中,字符串实际上只是一个字符值数组,通常以 null 作为结束符。大多数 Windows API 函数是按照以下条件处理字符串的:对于 ANSI,将其作为字符值数组;对于 Unicode,将其作为宽字符值数组。
幸运的是,CLR 被设计得相当灵活,当封送文本时问题得以轻松解决,而不用在意 Windows API 函数期望从您的应用程序得到的是什么。这里是一些需要记住的主要考虑事项:
• 是您的应用程序向 API 函数传递文本数据,还是 API 函数向您的应用程序返回字符串数据?或者二者兼有? • 您的外部方法应该使用什么托管类型? • API 函数期望得到的是什么格式的非托管字符串?
我们首先解答最后一个问题。大多数 Windows API 函数都带有 LPTSTR 或 LPCTSTR 值。(从函数角度看)它们分别是可修改和不可修改的缓冲区,包含以 null 结束的字符数组。“C”代表常数,意味着使用该参数信息不会传递到函数外部。LPTSTR 中的“T”表明该参数可以是 Unicode 或 ANSI,取决于您选择的字符集和底层操作系统的字符集。因为在 Windows API 中大多数字符串参数都是这两种类型之一,所以只要在 DllImportAttribute 中选择 CharSet.Auto,CLR 就按默认的方式工作。
然而,有些 API 函数或自定义的 DLL 函数采用不同的方式表示字符串。如果您要用到一个这样的函数,就可以采用 MarshalAsAttribute 修饰外部方法的字符串参数,并指明一种不同于默认 LPTSTR 的字符串格式。有关 MarshalAsAttribute 的更多信息,请参阅位于 MarshalAsAttribute Class 的 Platform SDK 文档主题。
现在让我们看一下字符串信息在您的代码和非托管函数之间传递的方向。有两种方式可以知道处理字符串时信息的传递方向。第一个也是最可靠的一个方法就是首先理解参数的用途。例如,您正调用一个参数,它的名称类似 CreateMutex 并带有一个字符串,则可以想像该字符串信息是从应用程序向 API 函数传递的。同时,如果您调用 GetUserName,则该函数的名称表明字符串信息是从该函数向您的应用程序传递的。
除了这种比较合理的方法外,第二种查找信息传递方向的方式就是查找 API 参数类型中的字母“C”。例如,GetUserName API 函数的第一个参数被定义为 LPTSTR 类型,它代表一个指向 Unicode 或 ANSI 字符串缓冲区的长指针。但是 CreateMutex 的名称参数被类型化为 LTCTSTR。请注意,这里的类型定义是一样的,但增加一个字母“C”来表明缓冲区为常数,API 函数不能写入。
一旦明确了文本参数是只用作输入还是用作输入/输出,就可以确定使用哪种 CLR 类型作为参数类型。这里有一些规则。如果字符串参数只用作输入,则使用 System.String 类型。在托管代码中,字符串是不变的,适合用于不会被本机 API 函数更改的缓冲区。
如果字符串参数可以用作输入和/或输出,则使用 System.StringBuilder 类型。StringBuilder 类型是一个很有用的类库类型,它可以帮助您有效地构建字符串,也正好可以将缓冲区传递给本机函数,由本机函数为您填充字符串数据。一旦函数调用返回,您只需要调用 StringBuilder 对象的 ToString 就可以得到一个 String 对象。
GetShortPathName API 函数能很好地用于显示什么时候使用 String、什么时候使用 StringBuilder,因为它只带有三个参数:一个输入字符串、一个输出字符串和一个指明输出缓冲区的字符长度的参数。
图 3 所示为加注释的非托管 GetShortPathName 函数文档,它同时指出了输入和输出字符串参数。它引出了托管的外部方法定义,也如图 3 所示。请注意第一个参数被封送为 System.String,因为它是一个只用作输入的参数。第二个参数代表一个输出缓冲区,它使用了 System.StringBuilder。
小结
本月专栏所介绍的 P/Invoke 功能足够调用 Windows 中的许多 API 函数。然而,如果您大量用到 interop,则会最终发现自己封送了很复杂的数据结构,甚至可能需要在托管代码中通过指针直接访问内存。实际上,本机代码中的 interop 可以是一个将细节和低级比特藏在里面的真正的潘多拉盒子。CLR、C# 和托管 C++ 提供了许多有用的功能;也许以后我会在本专栏介绍高级的 P/Invoke 话题。
同时,只要您觉得 .NET Framework 类库无法播放您的声音或者为您执行其他一些功能,您可以知道如何向原始而优秀的 Windows API 寻求一些帮助。
将您要发给 Jason 的问题和意见发送到 dot-net@microsoft.com
Jason Clark 为 Microsoft 和 Wintellect (http://www.wintellect.com) 提供培训和咨询,以前是 Windows NT 和 Windows 2000 Server 团队的开发人员。他与人合著了 Programming Server-side Applications for Microsoft Windows 2000 (Microsoft Press, 2000)。您可以通过 JClark@Wintellect.com 与 Jason 取得联系。

2007年4月4日星期三

谈谈Web Mining研究者掌握动态脚本语言的必要性发信站: 水木社区 (Fri Mar 23 17:58:30 2007),

站内自己平时很喜欢在闲暇时学习一些新的计算机语言,它使我对计算机的兴趣不至于在书写各种申报材料和论文中完全泯灭。特别是在我把工作平台转向Linux后,学习各种开源语言更成为自己掌握在Linux下工作的一种必然选择,除了C/C++、JAVA、C#这些主流开发语言外,我尤其对Perl、Python和Ruby这样的动态脚本语言情有独衷,这里特别谈谈自己在学习它们的过程中的一些体会。 第一次接触Perl语言是自己在作WebMining研究时从CMU大学的著名网页数据集Web-KB开始的,它的代码里有一些Perl语言脚本,往往几行代码就能完成JAVA这样的开发语言几十行代码才能完成的任务。显然它比Bash这样的Linux中的shell脚本功能要强大得多。由于第一次读到Perl语言书写的代码,感觉真的像天书一样难懂,特别是那些到处可见的$、@、%等奇怪的符号。为了完全弄懂Web-KB的这些代码,我下决心开始学习Perl,结果没想到从此一发不可收拾,很快就成为Perl铁定的忠实拥户。下面是我总结的几点它最突出的优点: 1。实用:它像编码世界里的"瑞士军刀",很多常用的处理方式特别作了很大的简化,让你用最少的代码量完成最多的功能。在JAVA语言里你需要首先声明各种变量,在程序执行前需要进行编译,在PERL语言里这些都去掉了,这大大简化了程序开发人员的开发效率。在JAVA程序里即使你只是打印一行"HelloWorld"也要先定义一个类,再定义一个static voidmain函数作为程序调用的入口,在PERL程序中这些全都不必了;再比如从网上下载网页在JAVA语言里至少要写上几行的代码,在Perl里只用一句就可以: use LWP::Simple; $html =get('http://www.sina.com.cn);由于经常需要引入package,所以选择use而不是JAVA里的import,因为它比后者少敲三个键。由于编程时90%的情况是处理文本,它就把正则表达式作为语言的内置功能而不像JAVA那样首先需要importre;在所有动态脚本语言中它也是运行速度最快的。2。第三方类库有集中的CPAN网站及其镜像站点统一管理:这是另一个让JAVA语言开发者嫉妒的优势,浏览一下网页http://search.cpan.org/吧,它把十年来所有开源的第三方软件包分门别类地集中存储,如果你愿意,也可以把自己开发的觉的有价值的软件包与全世界共享。JAVA虽然有Jarkata这样的第三方类库,但绝大多数还是需要我们自己去寻找。3。它是最像自然语言的计算机语言,不要忘记它的作者以前曾经是一个语言学家,如果你牢记这一点,那学习Perl语言便容易得多。我认为Perl语言也是目前我学到的所有语言中最具创新性的语言(它直接影响了Ruby),深深影响了以后各种新的语言的设计。4。它是完全开源的语言,它的作者是计算机历史上大名鼎鼎的LarryWall,他发明了patch命令和一个新闻组阅读器,但更主要的是他发明了Perl和CGI动态网页技术,他对Web的迅猛发展有直接的影响力。如果你想学习一门很多年后依然保持生命力的语言,那最保险的办法就是学习一门开源的语言,因为如果你去学习Delphi这样的商业语言,等到Borland公司一旦在商业市场上份额下降,那这种语言可能就完全风光不再。现在最热门的语言JAVA是Sun公司的宠儿,虽然Sun公司总裁声称在今年年底会完全公开JAVA源码,但显然这门语言的发展完全掌握在商业的运营模式中。Perl语言当然有它的缺点:不像Python和Ruby一样易学;面向对象功能不是一开始就设计而是后来添加的;代码可读性较差,过于灵活。LarryWall在2000年每年一度的开源大会上宣称开发新一代perl语言(即Perl6)的计划正式发始启动,它将摆脱现在向下兼容的包袱,从内核上对语言进行重新设计,采纳当下各门语言的精华同时保留它最根本的特性。这是让无数perl爱好者兴奋到整夜难眠的事情,现在开发Perl6还没有实现,这中间台湾的唐宗汉(现变性后改为唐凤)起了力挽狂澜的作用,是我们华人在开源领域最骄傲的人物。(见http://www.linuxeden.com/doc/21782.html)我认为学习像perl这样的脚本语言对计算机专业的研究者来说尤其适合,因为它可以大大加快开发原型或验证算法的过程,特别是它对文本处理的支持至今是无与伦比的。以我个人经验来说,作WebMining研究有一个很漂亮的想法很容易,但想得到实验数据的支持必须经过大量的Coding,它占用整个过程99%以上的时间(只关心发论文不在意伪造数据的除外)。如果用C++或JAVA语言来实现,你要花大量的时间用在Debug上面,但用Perl这样的快速开发语言,只花费十分之一的精力就能将其搞定,它免去了声明和编译(包括make过程),同时由于有很多现成的第三方软件包可以使用(在cpan里很容易找到Naive Bayes,Support VectorMachine这些机器学习工具),它的功能却一点也不逊色。特别的,由于它的代码量只有前者的几分甚至十几分之一,debug也相对容易得多。另外,使用perl语言最好结合Unix/Linux系统(虽然它在跨平台特性上一点不比JAVA差),这更能发挥它应有的作用。事实上在Linux系统下Perl是必备的工具(很多系统管理命令甚至带窗口的程序都是用它编写的,比如synaptic),你甚至无须去安装它。Python是另一个非常好的选择,它的好处是代码可读性好,对面向对象支持得非常好,非常易学(一个下午的时间可以学会80%场合用到的20%命令),另外与C/C++和Java的互相调用也作得非常出色(所以它是一种更好的gluelanguage),还有一个Perl语言没有的交互式编译器,它类似于以前苹果机上的Basic,你可以无须编写程序而直接在终端一行行边输入程序边看到结果,所以非常有利于快速开发。它的作者现在为Google工作,据说Google很多的代码都是用Python书写的。Python的名字来自国外非常流行的一个黑色幽默电影,放假时中央电影频道还专门介绍过它(下次我再次学习它时会先那部电影看一遍,找找感觉),开源世界的一大特色就是这种无比轻松的幽默而不是像微软产品包含的那种浓浓的商业气息。 Ruby现在由于Ruby onRails的流行而成为亲的上升之星,有人甚至预言它是未来的JAVA,它从Perl语言和Ada语言吸取了很多东西,自己打算有空闲时间的时候好好学习它。它的发明人是个日本人,有些中国程序员便排斥这门优秀的语言,我当然对此不会在意的。据说敏捷开发之父非常推崇这种语言,因为它是比JAVA还要纯的面向对象语言,像字符串和数学这些都作为对象处理,可以直接调用各种方

2007年3月29日星期四

Worker Threads in C#(http://www.codeproject.com/csharp/workerthread.asp)

Introduction


The .NET framework provides a lot of ways to implement multithreading
programs. I want to show how we can run a worker thread which makes syncronous
calls to a user interface (for example, a thread that reads a long recordset and
fills some control in the form).

To run thread I use:


  • Thread instance and main thread function

  • Two events used to stop thread. First event is set when main thread
    wants to stop worker thread; second event is set by worker thread when it really
    stops.

.NET allows you to call System.Windows.Forms.Control functions only from the thread in
which the control was created. To run them from another thread we need to use
the Control.Invoke (synchronous call) or Control.BeginInvoke (asynchronous call) functions. For tasks like
showing database records we need Invoke.

To implement this we will use:


  • A Delegate type for calling the form function. Delegate instance and
    function called using this delegate

  • The Invoke call from the worker thread.

The next problem is to stop the worker thread correctly. The steps to
do this are:


  • Set the event "Stop Thread"
  • Wait for the event "Thread is stopped"
  • Wait for the event process messages using the Application.DoEvents function. This prevents deadlocks because
    the worker thread makes Invoke calls which are processed in
    the main thread.

The thread function checks every iteration whether the "Stop Thread"
event has been set. If the event is set the function invokes clean-up
operations, sets the event "Thread is stopped" and returns.

Demo project has two classes: MainForm and LongProcess. The LongProcess.Run function
runs in a thread and fills the list box with some lines. The worker thread may
finish naturally or may be stopped when user presses the "Stop Thread" button or
closes the form.

Code fragments


Collapse
// MainForm.cs

namespace WorkerThread
{
// delegates used to call MainForm functions from worker thread
public delegate void DelegateAddString(String s);
public delegate void DelegateThreadFinished();

public class MainForm : System.Windows.Forms.Form
{
// ...

// worker thread
Thread m_WorkerThread;

// events used to stop worker thread
ManualResetEvent m_EventStopThread;
ManualResetEvent m_EventThreadStopped;

// Delegate instances used to call user interface functions
// from worker thread:
public DelegateAddString m_DelegateAddString;
public DelegateThreadFinished m_DelegateThreadFinished;

// ...

public MainForm()
{
InitializeComponent();

// initialize delegates
m_DelegateAddString = new DelegateAddString(this.AddString);
m_DelegateThreadFinished = new DelegateThreadFinished(this.ThreadFinished);

// initialize events
m_EventStopThread = new ManualResetEvent(false);
m_EventThreadStopped = new ManualResetEvent(false);

}

// ...

// Start thread button is pressed
private void btnStartThread_Click(object sender, System.EventArgs e)
{
// ...

// reset events
m_EventStopThread.Reset();
m_EventThreadStopped.Reset();

// create worker thread instance
m_WorkerThread = new Thread(new ThreadStart(this.WorkerThreadFunction));

m_WorkerThread.Name = "Worker Thread Sample"; // looks nice in Output window

m_WorkerThread.Start();

}


// Worker thread function.
// Called indirectly from btnStartThread_Click
private void WorkerThreadFunction()
{
LongProcess longProcess;

longProcess = new LongProcess(m_EventStopThread, m_EventThreadStopped, this);

longProcess.Run();
}

// Stop worker thread if it is running.
// Called when user presses Stop button or form is closed.
private void StopThread()
{
if ( m_WorkerThread != null && m_WorkerThread.IsAlive ) // thread is active
{
// set event "Stop"
m_EventStopThread.Set();

// wait when thread will stop or finish
while (m_WorkerThread.IsAlive)
{
// We cannot use here infinite wait because our thread
// makes syncronous calls to main form, this will cause deadlock.
// Instead of this we wait for event some appropriate time
// (and by the way give time to worker thread) and
// process events. These events may contain Invoke calls.
if ( WaitHandle.WaitAll(
(new ManualResetEvent[] {m_EventThreadStopped}),
100,
true) )
{
break;
}

Application.DoEvents();
}
}
}

// Add string to list box.
// Called from worker thread using delegate and Control.Invoke
private void AddString(String s)
{
listBox1.Items.Add(s);
}

// Set initial state of controls.
// Called from worker thread using delegate and Control.Invoke
private void ThreadFinished()
{
btnStartThread.Enabled = true;
btnStopThread.Enabled = false;
}

}
}

// LongProcess.cs

namespace WorkerThread
{
public class LongProcess
{
// ...

// Function runs in worker thread and emulates long process.
public void Run()
{
int i;
String s;

for (i = 1; i <= 10; i++)
  {                 // make step                 
  s = "Step number " + i.ToString() + " executed";                  
  Thread.Sleep(400);  // Make synchronous call to main form.    
             // MainForm.AddString function runs in main thread.  
               // (To make asynchronous call use BeginInvoke)   
              m_form.Invoke(m_form.m_DelegateAddString, new Object[] {s});                   // check if thread is cancelled             
      if ( m_EventStop.WaitOne(0, true) )               
    {                     // clean-up operations may be placed here                     // ...                      // inform main thread that this thread stopped                  
     m_EventStopped.Set();                 
       return;               
    }             
  } // Make synchronous call to main form             // to inform it that thread finished            
   m_form.Invoke(m_form.m_DelegateThreadFinished, null);         
 }     
 }
 }  

2007年3月6日星期二

用Visual C#调用Windows API函数(ZZ)

Api函数是构筑Windws应用程序的基石,每一种Windows应用程序开发工具,它提供的底层函数都间接或直接地调用了Windows API函数,同时为了实现功能扩展,一般也都提供了调用WindowsAPI函数的接口, 也就是说具备调用动态连接库的能力。Visual C#和其它开发工具一样也能够调用动态链接库的API函数。.NET框架本身提供了这样一种服务,允许受管辖的代码调用动态链接库中实现的非受管辖函数,包括操作系统提供的Windows API函数。它能够定位和调用输出函数,根据需要,组织其各个参数(整型、字符串类型、数组、和结构等等)跨越互操作边界。

下面以C#为例简单介绍调用API的基本过程:
动态链接库函数的声明
 动态链接库函数使用前必须声明,相对于VB,C#函数声明显得更加罗嗦,前者通过 Api Viewer粘贴以后,可以直接使用,而后者则需要对参数作些额外的变化工作。

 动态链接库函数声明部分一般由下列两部分组成,一是函数名或索引号,二是动态链接库的文件名。
  譬如,你想调用User32.DLL中的MessageBox函数,我们必须指明函数的名字MessageBoxA或MessageBoxW,以及库名字User32.dll,我们知道Win32 API对每一个涉及字符串和字符的函数一般都存在两个版本,单字节字符的ANSI版本和双字节字符的UNICODE版本。

 下面是一个调用API函数的例子:
[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW", SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.StdCall)]
public static extern bool MoveFile(String src, String dst);

 其中入口点EntryPoint标识函数在动态链接库的入口位置,在一个受管辖的工程中,目标函数的原始名字和序号入口点不仅标识一个跨越互操作界限的函数。而且,你还可以把这个入口点映射为一个不同的名字,也就是对函数进行重命名。重命名可以给调用函数带来种种便利,通过重命名,一方面我们不用为函数的大小写伤透脑筋,同时它也可以保证与已有的命名规则保持一致,允许带有不同参数类型的函数共存,更重要的是它简化了对ANSI和Unicode版本的调用。CharSet用于标识函数调用所采用的是Unicode或是ANSI版本,ExactSpelling=false将告诉编译器,让编译器决定使用Unicode或者是Ansi版本。其它的参数请参考MSDN在线帮助.

 在C#中,你可以在EntryPoint域通过名字和序号声明一个动态链接库函数,如果在方法定义中使用的函数名与DLL入口点相同,你不需要在EntryPoint域显示声明函数。否则,你必须使用下列属性格式指示一个名字和序号。

[DllImport("dllname", EntryPoint="Functionname")]
[DllImport("dllname", EntryPoint="#123")]
值得注意的是,你必须在数字序号前加“#”
下面是一个用MsgBox替换MessageBox名字的例子:
[C#]
using System.Runtime.InteropServices;

public class Win32 {
[DllImport("user32.dll", EntryPoint="MessageBox")]
public static extern int MsgBox(int hWnd, String text, String caption, uint type);
}
许多受管辖的动态链接库函数期望你能够传递一个复杂的参数类型给函数,譬如一个用户定义的结构类型成员或者受管辖代码定义的一个类成员,这时你必须提供额外的信息格式化这个类型,以保持参数原有的布局和对齐。

C#提供了一个StructLayoutAttribute类,通过它你可以定义自己的格式化类型,在受管辖代码中,格式化类型是一个用StructLayoutAttribute说明的结构或类成员,通过它能够保证其内部成员预期的布局信息。布局的选项共有三种:

布局选项
描述
LayoutKind.Automatic
为了提高效率允许运行态对类型成员重新排序。
注意:永远不要使用这个选项来调用不受管辖的动态链接库函数。
LayoutKind.Explicit
对每个域按照FieldOffset属性对类型成员排序
LayoutKind.Sequential
对出现在受管辖类型定义地方的不受管辖内存中的类型成员进行排序。
传递结构成员
下面的例子说明如何在受管辖代码中定义一个点和矩形类型,并作为一个参数传递给User32.dll库中的PtInRect函数,
函数的不受管辖原型声明如下:
BOOL PtInRect(const RECT *lprc, POINT pt);
注意你必须通过引用传递Rect结构参数,因为函数需要一个Rect的结构指针。
[C#]
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public struct Point {
public int x;
public int y;
}

[StructLayout(LayoutKind.Explicit]
public struct Rect {
[FieldOffset(0)] public int left;
[FieldOffset(4)] public int top;
[FieldOffset(8)] public int right;
[FieldOffset(12)] public int bottom;
}

class Win32API {
[DllImport("User32.dll")]
public static extern Bool PtInRect(ref Rect r, Point p);
}
类似你可以调用GetSystemInfo函数获得系统信息:
? using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEM_INFO {
public uint dwOemId;
public uint dwPageSize;
public uint lpMinimumApplicationAddress;
public uint lpMaximumApplicationAddress;
public uint dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public uint dwProcessorLevel;
public uint dwProcessorRevision;
}




[DllImport("kernel32")]
static extern void GetSystemInfo(ref SYSTEM_INFO pSI);

SYSTEM_INFO pSI = new SYSTEM_INFO();
GetSystemInfo(ref pSI);

类成员的传递
同样只要类具有一个固定的类成员布局,你也可以传递一个类成员给一个不受管辖的动态链接库函数,下面的例子主要说明如何传递一个sequential顺序定义的MySystemTime类给User32.dll的GetSystemTime函数, 函数用C/C++调用规范如下:

void GetSystemTime(SYSTEMTIME* SystemTime);
不像传值类型,类总是通过引用传递参数.
[C#]
[StructLayout(LayoutKind.Sequential)]
public class MySystemTime {
public ushort wYear;
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
}
class Win32API {
[DllImport("User32.dll")]
public static extern void GetSystemTime(MySystemTime st);
}
回调函数的传递:
从受管辖的代码中调用大多数动态链接库函数,你只需创建一个受管辖的函数定义,然后调用它即可,这个过程非常直接。
如果一个动态链接库函数需要一个函数指针作为参数,你还需要做以下几步:
首先,你必须参考有关这个函数的文档,确定这个函数是否需要一个回调;第二,你必须在受管辖代码中创建一个回调函数;最后,你可以把指向这个函数的指针作为一个参数创递给DLL函数,.

回调函数及其实现:
回调函数经常用在任务需要重复执行的场合,譬如用于枚举函数,譬如Win32 API 中的EnumFontFamilies(字体枚举), EnumPrinters(打印机), EnumWindows (窗口枚举)函数. 下面以窗口枚举为例,谈谈如何通过调用EnumWindow 函数遍历系统中存在的所有窗口

分下面几个步骤:
1. 在实现调用前先参考函数的声明
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARMAM IParam)
显然这个函数需要一个回调函数地址作为参数.
2. 创建一个受管辖的回调函数,这个例子声明为代表类型(delegate),也就是我们所说的回调,它带有两个参数hwnd和lparam,第一个参数是一个窗口句柄,第二个参数由应用程序定义,两个参数均为整形。

  当这个回调函数返回一个非零值时,标示执行成功,零则暗示失败,这个例子总是返回True值,以便持续枚举。
3. 最后创建以代表对象(delegate),并把它作为一个参数传递给EnumWindows 函数,平台会自动地 把代表转化成函数能够识别的回调格式。

[C#]
using System;
using System.Runtime.InteropServices;

public delegate bool CallBack(int hwnd, int lParam);

public class EnumReportApp {

[DllImport("user32")]
public static extern int EnumWindows(CallBack x, int y);

public static void Main()
{
CallBack myCallBack = new CallBack(EnumReportApp.Report);
EnumWindows(myCallBack, 0);
}

public static bool Report(int hwnd, int lParam) {
Console.Write("窗口句柄为");
Console.WriteLine(hwnd);
return true;
}
}



指针类型参数传递:
 在Windows API函数调用时,大部分函数采用指针传递参数,对一个结构变量指针,我们除了使用上面的类和结构方法传递参数之外,我们有时还可以采用数组传递参数。

 下面这个函数通过调用GetUserName获得用户名
BOOL GetUserName(
LPTSTR lpBuffer, // 用户名缓冲区
LPDWORD nSize // 存放缓冲区大小的地址指针
);
 
[DllImport("Advapi32.dll",
EntryPoint="GetComputerName",
ExactSpelling=false,
SetLastError=true)]
static extern bool GetComputerName (
[MarshalAs(UnmanagedType.LPArray)] byte[] lpBuffer,
  [MarshalAs(UnmanagedType.LPArray)] Int32[] nSize );
 这个函数接受两个参数,char * 和int *,因为你必须分配一个字符串缓冲区以接受字符串指针,你可以使用String类代替这个参数类型,当然你还可以声明一个字节数组传递ANSI字符串,同样你也可以声明一个只有一个元素的长整型数组,使用数组名作为第二个参数。上面的函数可以调用如下:

byte[] str=new byte[20];
Int32[] len=new Int32[1];
len[0]=20;
GetComputerName (str,len);
MessageBox.Show(System.Text.Encoding.ASCII.GetString(str));
 最后需要提醒的是,每一种方法使用前必须在文件头加上:
 using System.Runtime.InteropServices;

2007年3月5日星期一

利用Lucene搜索Java源代码 (ZZ)

package com.infosys.lucene.code JavaSourceCodeAnalyzer.;import java.io.Reader;import java.util.Set;import org.apache.lucene.analysis.*;public class JavaSourceCodeAnalyzer extends Analyzer { private Set javaStopSet; private Set englishStopSet; private static final String[] JAVA_STOP_WORDS = { "public","private","protected","interface", "abstract","implements","extends","null""new", "switch","case", "default" ,"synchronized" , "do", "if", "else", "break","continue","this", "assert" ,"for","instanceof", "transient", "final", "static" ,"void","catch","try", "throws","throw","class", "finally","return", "const" , "native", "super","while", "import", "package" ,"true", "false" }; private static final String[] ENGLISH_STOP_WORDS ={ "a", "an", "and", "are","as","at","be" "but", "by", "for", "if", "in", "into", "is", "it", "no", "not", "of", "on", "or", "s", "such", "that", "the", "their", "then", "there","these", "they", "this", "to", "was", "will", "with" }; public SourceCodeAnalyzer(){ super(); javaStopSet = StopFilter.makeStopSet(JAVA_STOP_WORDS); englishStopSet = StopFilter.makeStopSet(ENGLISH_STOP_WORDS); } public TokenStream tokenStream(String fieldName, Reader reader) { if (fieldName.equals("comment")) return new PorterStemFilter(new StopFilter( new LowerCaseTokenizer(reader),englishStopSet)); else return new StopFilter( new LowerCaseTokenizer(reader),javaStopSet); }}

  编写类JavaSourceCodeIndexer
  第二步生成索引。用来建立索引的非常重要的类有IndexWriter、Analyzer、Document和Field。对每一个源代码文件建立Lucene的一个Document实例。解析源代码文件并且摘录出与代码相关的语法元素,主要包括:导入声明、类名称、所继承的类、实现的接口、实现的方法、方法使用的参数和每个方法的代码等。然后把这些句法元素添加到Document实例中每个独立的Field实例中。然后使用存储索引的IndexWriter实例将Document实例添加到索引中。

  下面列出了JavaSourceCodeIndexer类的源代码。该类使用了JavaParser类解析Java文件和摘录语法元素,也可以使用Eclipse3.0 ASTParser。这里就不探究JavaParser类的细节了,因为其它解析器也可以用于提取相关源码元素。在源代码文件提取元素的过程中,创建Filed实例并添加到Document实例中。
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import com.infosys.lucene.code.JavaParser.*;

public class JavaSourceCodeIndexer {
private static JavaParser parser = new JavaParser();
private static final String IMPLEMENTS = "implements";
private static final String IMPORT = "import";
...
public static void main(String[] args) {
File indexDir = new File("C:\\Lucene\\Java");
File dataDir = new File("C:\\JavaSourceCode ");
IndexWriter writer = new IndexWriter(indexDir,
new JavaSourceCodeAnalyzer(), true);
indexDirectory(writer, dataDir);
writer.close();
}
public static void indexDirectory(IndexWriter writer, File dir){
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++) {
File f = files[i];
// Create a Lucene Document
Document doc = new Document();
// Use JavaParser to parse file
parser.setSource(f);
addImportDeclarations(doc, parser);
addComments(doc, parser);
// Extract Class elements Using Parser
JClass cls = parser.getDeclaredClass();
addClass(doc, cls);
// Add field to the Lucene Document
doc.add(Field.UnIndexed(FILENAME, f.getName()));
writer.addDocument(doc);
}
}
private static void addClass(Document doc, JClass cls) {
//For each class add Class Name field
doc.add(Field.Text(CLASS, cls.className));
String superCls = cls.superClass;
if (superCls != null)
//Add the class it extends as extends field
doc.add(Field.Text(EXTENDS, superCls));
// Add interfaces it implements
ArrayList interfaces = cls.interfaces;
for (int i = 0; i < interfaces.size(); i++)
doc.add(Field.Text(IMPLEMENTS, (String) interfaces.get(i)));
//Add details on methods declared
addMethods(cls, doc);
ArrayList innerCls = cls.innerClasses;
for (int i = 0; i < innerCls.size(); i++)
addClass(doc, (JClass) innerCls.get(i));
}
private static void addMethods(JClass cls, Document doc) {
ArrayList methods = cls.methodDeclarations;
for (int i = 0; i < methods.size(); i++) {
JMethod method = (JMethod) methods.get(i);
// Add method name field
doc.add(Field.Text(METHOD, method.methodName));
// Add return type field
doc.add(Field.Text(RETURN, method.returnType));
ArrayList params = method.parameters;
for (int k = 0; k < params.size(); k++)
// For each method add parameter types
doc.add(Field.Text(PARAMETER, (String)params.get(k)));
String code = method.codeBlock;
if (code != null)
//add the method code block
doc.add(Field.UnStored(CODE, code));
}
}
private static void addImportDeclarations(Document doc, JavaParser parser) {
ArrayList imports = parser.getImportDeclarations();
if (imports == null) return;
for (int i = 0; i < imports.size(); i++)
//add import declarations as keyword
doc.add(Field.Keyword(IMPORT, (String) imports.get(i)));
}
}

  Lucene有四种不同的字段类型:Keyword,UnIndexed,UnStored和Text,用于指定建立最佳索引。
 Keyword字段是指不需要分析器解析但需要被编入索引并保存到索引中的部分。JavaSourceCodeIndexer类使用该字段来保存导入类的声明。
 UnIndexed字段是既不被分析也不被索引,但是要被逐字逐句的将其值保存到索引中。由于我们一般要存储文件的位置但又很少用文件名作为关键字来搜索,所以用该字段来索引Java文件名。
 UnStored字段和UnIndexed字段相反。该类型的Field要被分析并编入索引,但其值不会被保存到索引中。由于存储方法的全部源代码需要大量的空间。所以用UnStored字段来存储被索引的方法源代码。可以直接从Java源文件中取出方法的源代码,这样作可以控制我们的索引的大小。
 Text字段在索引过程中是要被分析、索引并保存的。类名是作为Text字段来保存。下表展示了JavaSourceCodeIndexer类使用Field字段的一般情况。



1. 用Lucene建立的索引可以用Luke预览和修改,Luke是用于理解索引很有用的一个开源工具。图1中是Luke工具的一张截图,它显示了JavaSourceCodeIndexer类建立的索引。


图1:在Luke中索引截图

如图所见,导入类的声明没有标记化或分析就被保存了。类名和方法名被转换为小写字母后,才保存的。

  查询Java源代码
  建立多字段索引后,可以使用Lucene来查询这些索引。它提供了这两个重要类分别是IndexSearcher和QueryParser,用于搜索文件。QueryParser类则用于解析由用户输入的查询表达式,同时IndexSearcher类在文件中搜索满足查询条件的结果。下列表格显示了一些可能发生的查询及它的含义:


用户通过索引不同的语法元素组成有效的查询条件并搜索代码。下面列出了用于搜索的示例代码。
public class JavaCodeSearch {
public static void main(String[] args) throws Exception{
File indexDir = new File(args[0]);
String q = args[1]; //parameter:JGraph code:insert
Directory fsDir = FSDirectory.getDirectory(indexDir,false);
IndexSearcher is = new IndexSearcher(fsDir);

PerFieldAnalyzerWrapper analyzer = new
PerFieldAnalyzerWrapper( new
JavaSourceCodeAnalyzer());

analyzer.addAnalyzer("import", new KeywordAnalyzer());
Query query = QueryParser.parse(q, "code", analyzer);
long start = System.currentTimeMillis();
Hits hits = is.search(query);
long end = System.currentTimeMillis();
System.err.println("Found " + hits.length() +
" docs in " + (end-start) + " millisec");
for(int i = 0; i < hits.length(); i++){
Document doc = hits.doc(i);
System.out.println(doc.get("filename")
+ " with a score of " + hits.score(i));
}
is.close();
}
}

  IndexSearcher实例用FSDirectory来打开包含索引的目录。然后使用Analyzer实例分析搜索用的查询字符串,以确保它与索引(还原词根,转换小写字母,过滤掉,等等)具有同样的形式。为了避免在查询时将Field作为一个关键字索引,Lucene做了一些限制。Lucene用Analyzer分析在QueryParser实例里传给它的所有字段。为了解决这个问题,可以用Lucene提供的PerFieldAnalyzerWrapper类为查询中的每个字段指定必要的分析。因此,查询字符串import:org.w3c.* AND code:Document将用KeywordAnalyzer来解析字符串org.w3c.*并且用JavaSourceCodeAnalyzer来解析Document。QueryParser实例如果查询没有与之相符的字段,就使用默认的字段:code,使用PerFieldAnalyzerWrapper来分析查询字符串,并返回分析后的Query实例。IndexSearcher实例使用Query实例并返回一个Hits实例,它包含了满足查询条件的文件。

  结束语

  这篇文章介绍了Lucene——文本搜索引擎,其可以通过加载分析器及多字段索引来实现源代码搜索。文章只介绍了代码搜索引擎的基本功能,同时在源码检索中使用愈加完善的分析器可以提高检索性能并获得更好的查询结果。这种搜索引擎可以允许用户在软件开发社区搜索和共享源代码。

  资源
这篇文章的示例Sample code
Matrix:http://www.matrix.org.cn/
Onjava:http://www.onjava.com/
Lucene home page
"Introduction to Text Indexing with Apache Jakarta Lucene:" 这是篇简要介绍使用Lucene的文章。
Lucene in Action: 在使用Lucene方面进行了深入地讲解。

Nutch爬虫工作流程及文件格式详细分析 (ZZ)

Nutch爬虫工作流程及文件格式详细分析


  Crawler和Searcher两部分尽量分开的目的主要是为了使两部分可以分布式配置在硬件平台上,例如将Crawler和Searcher分别放在两个主机上,这样可以提升性能。

  爬虫,Crawler:

  Crawler的重点在两个方面,Crawler的工作流程和涉及的数据文件的格式和含义。数据文件主要包括三类,分别是web database,一系列的segment加上index,三者的物理文件分别存储在爬行结果目录下的db目录下webdb子文件夹内,segments文件夹和index文件夹。那么三者分别存储的信息是什么呢?

  Web database,也叫WebDB,其中存储的是爬虫所抓取网页之间的链接结构信息,它只在爬虫Crawler工作中使用而和Searcher的工作没有任何关系。WebDB内存储了两种实体的信息:page和link。Page实体通过描述网络上一个网页的特征信息来表征一个实际的网页,因为网页有很多个需要描述,WebDB中通过网页的URL和网页内容的MD5两种索引方法对这些网页实体进行了索引。Page实体描述的网页特征主要包括网页内的link数目,抓取此网页的时间等相关抓取信息,对此网页的重要度评分等。同样的,Link实体描述的是两个page实体之间的链接关系。WebDB构成了一个所抓取网页的链接结构图,这个图中Page实体是图的结点,而Link实体则代表图的边。

  一次爬行会产生很多个segment,每个segment内存储的是爬虫Crawler在单独一次抓取循环中抓到的网页以及这些网页的索引。Crawler爬行时会根据WebDB中的link关系按照一定的爬行策略生成每次抓取循环所需的fetchlist,然后Fetcher通过fetchlist中的URLs抓取这些网页并索引,然后将其存入segment。Segment是有时限的,当这些网页被Crawler重新抓取后,先前抓取产生的segment就作废了。在存储中。Segment文件夹是以产生时间命名的,方便我们删除作废的segments以节省存储空间。

  Index是Crawler抓取的所有网页的索引,它是通过对所有单个segment中的索引进行合并处理所得的。Nutch利用Lucene技术进行索引,所以Lucene中对索引进行操作的接口对Nutch中的index同样有效。但是需要注意的是,Lucene中的segment和Nutch中的不同,Lucene中的segment是索引index的一部分,但是Nutch中的segment只是WebDB中各个部分网页的内容和索引,最后通过其生成的index跟这些segment已经毫无关系了。

  Crawler工作流程:

  在分析了Crawler工作中设计的文件之后,接下来我们研究一下Crawler的抓取流程以及这些文件在抓取中扮演的角色。Crawler的工作原理主要是:首先Crawler根据WebDB生成一个待抓取网页的URL集合叫做Fetchlist,接着下载线程Fetcher开始根据Fetchlist将网页抓取回来,如果下载线程有很多个,那么就生成很多个Fetchlist,也就是一个Fetcher对应一个Fetchlist。然后Crawler根据抓取回来的网页WebDB进行更新,根据更新后的WebDB生成新的Fetchlist,里面是未抓取的或者新发现的URLs,然后下一轮抓取循环重新开始。这个循环过程可以叫做“产生/抓取/更新”循环。

  指向同一个主机上Web资源的URLs通常被分配到同一个Fetchlist中,这样的话防止过多的Fetchers对一个主机同时进行抓取造成主机负担过重。另外Nutch遵守Robots Exclusion Protocol,网站可以通过自定义Robots.txt控制Crawler的抓取。

  在Nutch中,Crawler操作的实现是通过一系列子操作的实现来完成的。这些子操作Nutch都提供了子命令行可以单独进行调用。下面就是这些子操作的功能描述以及命令行,命令行在括号中。

  1. 创建一个新的WebDb (admin db -create).

  2. 将抓取起始URLs写入WebDB中 (inject).

  3. 根据WebDB生成fetchlist并写入相应的segment(generate).

  4. 根据fetchlist中的URL抓取网页 (fetch).

  5. 根据抓取网页更新WebDb (updatedb).

  6. 循环进行3-5步直至预先设定的抓取深度。

  7. 根据WebDB得到的网页评分和links更新segments (updatesegs).

  8. 对所抓取的网页进行索引(index).

  9. 在索引中丢弃有重复内容的网页和重复的URLs (dedup).

  10. 将segments中的索引进行合并生成用于检索的最终index(merge).

  Crawler详细工作流程是:在创建一个WebDB之后(步骤1), “产生/抓取/更新”循环(步骤3-6)根据一些种子URLs开始启动。当这个循环彻底结束,Crawler根据抓取中生成的segments创建索引(步骤7-10)。在进行重复URLs清除(步骤9)之前,每个segment的索引都是独立的(步骤8)。最终,各个独立的segment索引被合并为一个最终的索引index(步骤10)。

  其中有一个细节问题,Dedup操作主要用于清除segment索引中的重复URLs,但是我们知道,在WebDB中是不允许重复的URL存在的,那么为什么这里还要进行清除呢?原因在于抓取的更新。比方说一个月之前你抓取过这些网页,一个月后为了更新进行了重新抓取,那么旧的segment在没有删除之前仍然起作用,这个时候就需要在新旧segment之间进行除重。

  另外这些子操作在第一次进行Crawler运行时可能并不用到,它们主要用于接下来的索引更新,增量搜索等操作的实现。这些在以后的文章中我再详细讲。

  个性化设置:

  Nutch中的所有配置文件都放置在总目录下的conf子文件夹中,最基本的配置文件是conf/nutch-default.xml。这个文件中定义了Nutch的所有必要设置以及一些默认值,它是不可以被修改的。如果你想进行个性化设置,你需要在conf/nutch-site.xml进行设置,它会对默认设置进行屏蔽。

  Nutch考虑了其可扩展性,你可以自定义插件plugins来定制自己的服务,一些plugins存放于plugins子文件夹。Nutch的网页解析与索引功能是通过插件形式进行实现的,例如,对HTML文件的解析与索引是通过HTML document parsing plugin, parse-html实现的。所以你完全可以自定义各种解析插件然后对配置文件进行修改,然后你就可以抓取并索引各种类型的文件了。

2007年3月2日星期五

Nutch初体验(ZZ)

http://blog.csdn.net/setok/articles/499791.aspx

前几天看到卢亮的 Larbin 一种高效的搜索引擎爬虫工具 一文提到 http://hedong.3322.org/">竹笋炒肉中对 Nutch 进行了一下介绍

Nutch vs Lucene
Lucene 不是完整的应用程序,而是一个用于实现全文检索的软件库。
Nutch 是一个应用程序,可以以 Lucene 为基础实现搜索引擎应用。

Nutch vs GRUB
http://www.wespoke.com/archives/000879.php">这里
Nutch 则还可以存储到数据库并建立索引。
Nutch Architecture.png
[引自这里

Nutch 的早期版本不支持中文搜索,而最新的版本(2004-Aug-04 发布了 http://www.dbanotes.net)为例,先进行一下针对企业内部网的测试。

在 nutch 目录中创建一个包含该网站顶级网址的文件 urls ,包含如下内容:

http://www.dbanotes.net/

然后编辑conf/crawl-urlfilter.txt 文件,设定过滤信息,我这里只修改了MY.DOMAIN.NAME:

# accept hosts in MY.DOMAIN.NAME+^http://([a-z0-9]*\.)*dbanotes.net/

运行如下命令开始抓取分析网站内容:

[root@fc3 nutch]# bin/nutch crawl urls -dir crawl.demo -depth 2 -threads 4 >& crawl.log

depth 参数指爬行的深度,这里处于测试的目的,选择深度为 2 ;
threads 参数指定并发的进程 这是设定为 4 ;

在该命令运行的过程中,可以从 crawl.log 中查看 nutch 的行为以及过程:

......050102 200336 loading file:/u01/nutch/conf/nutch-site.xml050102 200336 crawl started in: crawl.demo 050102 200336 rootUrlFile = urls 050102 200336 threads = 4050102 200336 depth = 2050102 200336 Created webdb at crawl.demo/db......050102 200336 loading file:/u01/nutch/conf/nutch-site.xml050102 200336 crawl started in: crawl.demo050102 200336 rootUrlFile = urls050102 200336 threads = 4050102 200336 depth = 2050102 200336 Created webdb at crawl.demo/db050102 200336 Starting URL processing050102 200336 Using URL filter: net.nutch.net.RegexURLFilter......                               050102 200337 Plugins: looking in: /u01/nutch/plugins                  050102 200337 parsing: /u01/nutch/plugins/parse-html/plugin.xml        050102 200337 parsing: /u01/nutch/plugins/parse-pdf/plugin.xml         050102 200337 parsing: /u01/nutch/plugins/parse-ext/plugin.xml         050102 200337 parsing: /u01/nutch/plugins/parse-msword/plugin.xml      050102 200337 parsing: /u01/nutch/plugins/query-site/plugin.xml        050102 200337 parsing: /u01/nutch/plugins/protocol-http/plugin.xml     050102 200337 parsing: /u01/nutch/plugins/creativecommons/plugin.xml050102 200337 parsing: /u01/nutch/plugins/language-identifier/plugin.xml050102 200337 parsing: /u01/nutch/plugins/query-basic/plugin.xml       050102 200337 logging at INFO                                          050102 200337 fetching http://www.dbanotes.net/                        050102 200337 http.proxy.host = null                                   050102 200337 http.proxy.port = 8080                                   050102 200337 http.timeout = 10000                                     050102 200337 http.content.limit = 65536                               050102 200337 http.agent = NutchCVS/0.05 (Nutch; http://www.nutch.org/docs/en/bot.html; nutch-agent@lists.sourceforge.net)050102 200337 fetcher.server.delay = 1000                              050102 200337 http.max.delays = 100                                    050102 200338 http://www.dbanotes.net/: setting encoding to GB18030    050102 200338 CC: found http://creativecommons.org/licenses/by-nc-sa/2.0/ in rdf of http://www.dbanotes.net/050102 200338 CC: found text in http://www.dbanotes.net/               050102 200338 status: 1 pages, 0 errors, 12445 bytes, 1067 ms          050102 200338 status: 0.9372071 pages/s, 91.12142 kb/s, 12445.0 bytes/page050102 200339 Updating crawl.demo/db                                   050102 200339 Updating for crawl.demo/segments/20050102200336          050102 200339 Finishing update                                                                                                                64,1           7%050102 200337 parsing: /u01/nutch/plugins/query-basic/plugin.xml050102 200337 logging at INFO050102 200337 fetching http://www.dbanotes.net/050102 200337 http.proxy.host = null050102 200337 http.proxy.port = 8080050102 200337 http.timeout = 10000050102 200337 http.content.limit = 65536050102 200337 http.agent = NutchCVS/0.05 (Nutch; http://www.nutch.org/docs/en/bot.html; nutch-agent@lists.sourceforge.net)050102 200337 fetcher.server.delay = 1000050102 200337 http.max.delays = 100......

之后配置 Tomcat (我的 tomcat 安装在 /opt/Tomcat) ,

[root@fc3 nutch]# rm -rf /opt/Tomcat/webapps/ROOT*[root@fc3 nutch]# cp nutch*.war /opt/Tomcat/webapps/ROOT.war[root@fc3 webapps]# cd /opt/Tomcat/webapps/[root@fc3 webapps]# jar xvf ROOT.war[root@fc3 webapps]# ../bin/catalina.sh start

浏览器中输入 http://localhost:8080/ 查看结果(远程查看需要将 localhost 换成相应的IP):

nutch web search interface.png

搜索测试:

nutch web search result.png

可以看到,Nutch 亦提供快照功能。下面进行中文搜索测试:

nutch web Chinese search result.png

注意结果中的那个“评分详解”,是个很有意思的功能(Nutch 具有一个链接分析模块),通过这些数据可以进一步理解该算法。

考虑到带宽的限制,暂时不对整个Web爬行的方式进行了测试了。值得一提的是,在测试的过程中,nutch 的爬行速度还是不错的(相对我的糟糕带宽)。

Nutch 目前还不支持 PDF(开发中,不够完善) 与 图片 等对象的搜索。中文分词技术还不够好,通过“评分详解”可看出,对中文,比如“数据库管理员”,是分成单独的字进行处理的。但作为一个开源搜索引擎软件,功能是可圈可点的。毕竟,主要开发者 < color="#0000cc">Doug Cutting 就是开发 http://hedong.3322.org/archives/000247.html">试用Nutch

  • 车东的 http://www.chedong.com/tech/lucene.html">Lucene:基于Java的全文检索引擎简介




    • 2007年2月26日星期一

      Eclipse 开发环境的调试功能总结(ZZ)

      转载自http://www.javaeye.com/article/799
      本文概述了怎样使用 Eclipse 平台的内置调试功能来调试您的软件项目。调试是程序员无法回避的工作。调试方法有许多种,但归根结底,就是找到引发错误的代码。举例来说,在 Linux 应用程序中,分段故障被认为是最常见的错误之一。当程序尝试访问未分配给它的内存并因为分段违例而终止时,将产生这种错误。要修正这种错误,您需要找到引发该行为的那行代码。一旦找到有问题的代码行,这对于知道引发错误的上下文及其相关的值、变量和方法也是有所帮助的。使用调试器将使查找这些信息变得相当简单。Eclipse 调试器及 Debug 视图Eclipse 平台的特色在于内置了 Java 调试器,该调试器提供所有标准调试功能,包括进行单步执行、设置断点和值、检查变量和值以及暂挂和恢复线程的能力。此外,您还可以调试在远程机器上运行的应用程序。Eclipse 平台主要是一个 Java 开发环境,但其体系结构同时也向其它编程语言开放。如以下您将看到的,同一个 Eclipse 的 Debug 视图也可用于 C 和 C++ 编程语言。Eclipse 平台工作台(Eclipse Platform Workbench)及其工具是基于 Java 开发工具(JDT)组件所构建的。这些组件向 Eclipse 提供以下功能:* 项目管理工具* 透视图和视图* 构建器、编辑器、搜索和构建功能* 调试器Eclipse 调试器本身是作为 Eclipse 二进制文件中包含的标准插件而存在的。Eclipse 还有一个特别的 Debug 视图,允许您在工作台中管理程序的调试和运行。它为调试中的每个目标显示其暂挂线程的堆栈帧。程序中的各个线程以作为树的节点出现,而 Debug 视图则显示运行中的各目标的进程。如果暂挂一个线程,则其堆栈帧显示为子元素。在您开始使用 Eclipse 调试器之前,假定您已经安装了适当的 Java SDK/JRE(我推荐您使用 Java VM 1.4)和 Eclipse 平台 SDK 2.0/2.1,且两者都工作正常。一般来说,先使用 Eclipse 样本来测试一下调试选项是一个好主意。如果您要开发和调试 C/C++ 项目,您还需要得到并安装 C/C++ 开发工具(C/C++ Development Tool,CDT)。有关 Java SDK/JRE、Eclipse 平台和样本以及 CDT 的链接,请参阅本文后面的参考资料。图 1 显示了 Debug 用户界面的常规视图。图 1. Eclipse Debug 视图用户界面的常规视图[myimg]upload/debug.png[/myimg]调试 Java在您能调试您的项目之前,需要先完整地编译和运行代码。您首先需要为您的应用程序创建运行配置并确认其正常启动。之后,您需要使用 Run > Debug... 菜单,以同样的方式设置调试配置。您还需要选择作为主 Java 类的由调试器使用的类(也请参阅图 2)。对一个项目,您希望有几种调试配置就可以有几种。当调试器启动后(通过 Run > Debug...),会在一个新窗口打开它,您可以准备开始调试。图 2. 在调试配置中设置项目的主 Java 类[myimg]upload/debug-config.png[/myimg]以下是最常见的 Eclipse 调试操作的示例指示信息:设置断点当您启动应用程序以进行调试时,Eclipse 自动切换到 Debug 透视图。无庸置疑,最常见的调试过程就是设置断点,以允许检查在条件语句和循环中的变量和值。要在 Java 透视图的 Package Explorer 视图中设置断点,双击所选的源代码文件,在编辑器中打开它。遍历全部代码,将光标放置在含有可疑代码的那一行的标记栏上(在编辑器区域的左侧)。双击以设置断点(也请参阅图 3)。图 3. 在编辑器左侧边缘可看到两个断点标记[myimg]upload/breakpoints2.png[/myimg]现在通过 Run > Debug... 菜单启动调试会话。有一点很重要,不要把数条语句放在同一行,因为您不能在同一行的多条语句上单步跳过或设置行断点(也请参阅图 4)。图 4. 视图通过左侧边缘的箭头指出当前正在执行的行[myimg]upload/pointer.png[/myimg]条件断点一旦您找到出错的地方,您会想要了解在崩溃前程序在干些什么。完成该工作的一种方法是单步执行程序中的每条语句,一次一句,直到到达出问题的地方。有时候更好的方法是仅运行某段代码并在出问题的地方终止其执行,这样就可以检查该位置上的数据。要实现这一点,可能要声明每当表达式的值更改时就被触发的条件断点(请参阅图 5)。此外,在输入条件表达式时还可以使用代码辅助。图 5. 设置条件断点触发器[myimg]upload/conditional.png[/myimg]对表达式求值要在 Debug 透视图的编辑器中求表达式的值,选中设置有断点的一整行,并在上下文菜单中选择 Inspect 选项(请参阅图 6)。表达式是在当前堆栈帧的上下文中求值的,其结果显示在 Display 窗口的 Expressions 视图中。图 6. 用 Inspect 选项求表达式的值[myimg]upload/debug-menu1.png[/myimg]查看变量Variables 视图(在 Display 窗口中)显示了选中的堆栈帧中的变量值(请参阅图 7)。要查看所请求的变量,只需展开 Variables 视图中的树直到您看到所请求的元素为止。您也可以在 Debug 视图中单步执行代码的同时,在 Variables 视图中查看变量。图 7. 在 Display 窗口中查看变量[myimg]upload/variables.png[/myimg]当调试器在断点上停止时,您可以通过在 Run > Debug... 菜单上选择 Step Over 选项来继续调试器会话(请参阅图 8)。这将单步跳过突出显示的代码行并执行同一个方法中的下一行(或者它在调用当前方法的方法中继续)。作为最后一步的结果而发生更改的变量用颜色突出显示(缺省值是红色),所用颜色可在“Changed Variable Value Color”首选项(由 Debug Variable Views 指定)中指定。图 8. Run... 菜单中的调试器命令[myimg]upload/debug-menu.png[/myimg]要在 Debug 视图中暂挂线程的执行,选择一个运行中的线程并单击 Debug 视图工具栏中的 Suspend 按钮。会显示该线程的当前调用堆栈,并且在 Debug 透视图的编辑器中突出显示当前执行的行。暂挂线程时,将光标放置到 Java 编辑器中的变量上,该变量的值显示在一个小悬浮窗口中。同样,该线程的顶部堆栈帧被自动选中,该堆栈帧中的可视变量显示在 Variables 视图中。可以通过在 Variables 视图中单击变量名来检查相应的变量。热交换错误修正:实时代码修正如果您运行的是 JVM 1.4(Java 虚拟机,Java Virtual Machine),Eclipse 2.0.2 和 2.1 提供一个叫做热交换错误修正(Hotswap Bug Fixing)的新功能(无法用于 JVM 1.3 或更低版本 - 也请参阅图 9)。它允许在调试器会话过程中更改源代码,这要比“退出应用程序,更改源代码,重新编译,再启动另一个调试会话”的一系列步骤好多了。要使用该功能,只需在编辑器中更改代码并恢复调试。由于 JVM 1.4 与 Java 平台调试器体系结构(Java Platform Debugger Architecture,JPDA)兼容,所以能使用该功能。JPDA 实现了在运行中的应用程序中用经过修改的代码进行替换的能力。当然,当启动您的应用程序或找到出错点需要花费很长时间的时候,该功能极其有用。图 9. 热交换错误修正功能不能在 JVM 1.3 及更低版本中使用[myimg]upload/hot-code.png[/myimg]如果您完成调试时程序还没有被完整地执行过,请在 Debug 视图中的上下文菜单中选择 Terminate 选项。一个常见的错误是您在调试器会话中使用了 Debug 或 Run 而不是 Resume。这样将会启动另一个调试器会话,而不是继续当前的会话。远程调试Eclipse 调试器提供了一个有趣的选项,可用于调试远程应用程序。它可以连接到一个运行 Java 应用程序的远程 VM 上,并将其连接到内部调试器上。处理远程调试会话非常类似于本地调试。不过,远程调试配置要求对 Run > Debug... 窗口进行不同的设置。您首先要选择左侧视图中的 Remote Java Application 项,单击 New 按钮。这样就创建了一个新的远程启动配置,并显示三个选项卡:Connect、Source 和 Common。在 Connect 选项卡的 Project 域中,选择用作启动首选项的项目(用于查找源代码)。在 Connect 选项卡的 Host 域中,输入运行 Java 程序的远程主机的 IP 地址或域名。在 Connect 选项卡的 Port 域中,输入远程 VM 接受连接的端口。一般来说,该端口是在远程 VM 启动时指定的。当您想让调试器确定 Terminate 命令在远程会话中是否可用,可以选择 Allow termination of remote VM 选项。如果您希望能终止所连接的 VM,则选择该选项。现在当您选择 Debug 选项时,调试器将尝试按指定的地址和端口连接远程 VM,并在 Debug 视图中显示结果。如果启动器无法连接至指定位置上的 VM,将显示错误消息。一般来说,远程调试功能的可用性完全取决于远程主机上运行的 Java VM(虚拟机,Virtual Machine)。图 10 显示了远程调试会话的连接属性的设置。图 10. 设置远程调试会话的连接属性[myimg]upload/remote-debug.png[/myimg]调试其它语言Java 是 Eclipse 平台的主语言。然而,Eclipse 平台同时也是一个可支持许多其它语言的可扩展平台,而其中最重要的就是支持 C/C++(因为其流行性)。Eclipse 通过用 C/C++ 开发工具(CDT)支持 C/C++。请参阅参考资料以获取相关链接。CDT 通过调试 C/C++ 代码的功能扩展了标准的 Eclipse Debug 视图,同时 CDT Debug 视图允许您在工作台中管理 C/C++ 项目的调试。CDT 不包含其内部调试器,但它向必须可在本地使用的 GNU GDB 调试器提供了一个前端。下载并安装了 CDT 之后,只需切换到 Debug 视图,您就可以开始调试当前的 C/C++ 项目了(请参阅参考资料,以获取一篇介绍如何安装 CDT 的文章的链接)。这样您可以设置(并在执行过程中任何时候更改)代码中的断点,并且跟踪变量和寄存器。Eclipse 调试器显示您调试中各个目标的暂挂线程的堆栈帧。程序中的各个线程作为树的节点出现。它显示了运行中各目标的进程。请记住当 GNU GDB 调试一个带有调试符号链接的程序时,它最有效。这是在编译过程中由命令行参数 -g 来实现的。需要更多的信息请使用 -ggdb 开关,该参数包含有特定于 GNU GDB 的调试符号。如果您要调试 servlet,使用 Sysdeo Eclipse Tomcat Launcher。该插件使您能够管理 Tomcat 4.x/3.3 servlet 容器(通过创建和导入一个 Tomcat WAR 项目)。它同时在一个内部 Java Eclipse 调试器中注册一个 Tomcat 进程,这样您就能方便地调试 Tomcat 应用程序了。还有其它几个 Eclipse 插件,使我们能够对 servlet 使用内部 Eclipse 调试器,比如 Cactus 的 Eclipse 插件,Resin 插件和 X-Parrots ServletExec 插件。在下面的参考资料中可获得这些插件的链接。结束语Eclipse 平台提供了内置的 Java 调试器,该调试器具有标准调试功能,包括进行单步执行、设置断点和值、检查变量和值以及暂挂和恢复线程的能力。它还可以用于调试在远程机器上运行的应用程序。Eclipse 平台主要是一个 Java 开发环境,但是同一个 Eclipse 的 Debug 视图也可用于 C 和 C++ 编程语言。参考资料* 在 eclipse.org 加入 Eclipse 平台社区并下载该平台。Eclipse 平台源代码是通过普通公共许可证(Common Public License)进行许可的。您还能在 eclipse.org 找到 Eclipse 项目的术语词汇表及描述,以及技术文章和新闻组。Eclipse 平台白皮书详细描述了 Eclipse 的主要组件和功能。* 在 eclipse.org 下载 C/C++ 开发工具。* 下载 Sysdeo Eclipse Tomcat Launcher。* 以下是另外一些用于 servlet 的 Eclipse 插件的链接:o Cactus 的 Eclipse 插件o Resin 插件o X-Parrots ServletExec 插件* 查看 Eclipse 插件的分类注册表。* 以下文章可以帮助您开始学习用 Eclipse 进行调试:o 请参阅由 Greg Adams 和 Marc Erickson 撰写的 developerWorks 文章“Working the Eclipse Platform”,介绍了 Eclipse 平台及其工作方式。o 要着手使用 Eclipse 平台开发应用程序,请参阅由 David Gallardo 撰写的 developerWorks 文章“Getting started with the Eclipse Platform”。o 要学习 Eclipse 中的启动框架(Launching Framework),请参阅 eclipse.org 文章“We Have Lift-off: The Launching Framework in Eclipse”。o 要获取关于产品构建器和特性的更多信息,请参阅 eclipse.org 文章“Project Builders and Natures”。* 如果您是一名开发人员,且希望参与和实现不同 Eclipse 组件相关的讨论,请访问为该项目中各组件所创建的开发人员邮件列表。* 在 developerWorks 查找更多为 Eclipse 用户准备的参考资料。关于作者Paul Leszek,是 Studio B 的一名作者,同时也是专长于 Linux/Win/Mac OS 系统体系结构和管理的独立软件顾问。在许多操作系统、编程语言和网络协议,尤其是 Lotus Domino 和 DB2 方面,他都有经验。Paul 同时还是“LinuxWorld”系列文章的作者,以及波兰语版“PC World”的 Linux 专栏作家。

      2007年2月25日星期日

      Nutch=lucene+Crawler(转载)

      Nutch 是一个开源Java 实现的搜索引擎。它提供了我们运行自己的搜索引擎所需的全部工具。可以为什么我们需要建立自己的搜索引擎呢?毕竟我们已经有google可以使用。这里我列出3点原因:
      透明度:Nutch是开放源代码的,因此任何人都可以查看他的排序算法是如何工作的。商业的搜索引擎排序算法都是保密的,我们无法知道为什么搜索出来的排序结果是如何算出来的。更进一步,一些搜索引擎允许竞价排名,比如百度,这样的索引结果并不是和站点内容相关的。因此 Nutch 对学术搜索和政府类站点的搜索来说,是个好选择。因为一个公平的排序结果是非常重要的。
      对搜索引擎的理解:我们并没有google的源代码,因此学习搜索引擎Nutch是个不错的选择。了解一个大型分布式的搜索引擎如何工作是一件让人很受益的事情。在写Nutch的过程中,从学院派和工业派借鉴了很多知识:比如:Nutch的核心部分目前已经被重新用 Map Reduce 实现了。看过开复演讲的人都知道 Map Reduce 的一点知识吧。Map Reduce 是一个分布式的处理模型,最先是从 Google 实验室提出来的。你也可以从下面获得更多的消息。 http://www.domolo.com/bbs/list.asp?boardid=29http://domolo.oicp.net/bbs/list.asp?boardid=29并且 Nutch 也吸引了很多研究者,他们非常乐于尝试新的搜索算法,因为对Nutch 来说,这是非常容易实现扩展的。
      扩展性:你是不是不喜欢其他的搜索引擎展现结果的方式呢?那就用 Nutch 写你自己的搜索引擎吧。 Nutch 是非常灵活的:他可以被很好的客户订制并集成到你的应用程序中:使用Nutch 的插件机制,Nutch 可以作为一个搜索不同信息载体的搜索平台。当然,最简单的就是集成Nutch到你的站点,为你的用户提供搜索服务。
      Nutch 的安装分为3个层次:基于本地文件系统,基于局域网,或者基于 internet 。不同的安装方式具有不同的特色。比如:索引一个本地文件系统相对于其他两个来说肯定是要稳定多了,因为没有 网络错误也不同缓存文件的拷贝。基于Internet 的搜索又是另一个极端:抓取数以千计的网页有很多技术问题需要解决:我们从哪些页面开始抓取?我们如何分配抓取工作?何时需要重新抓取?我们如何解决失效的链接,没有响应的站点和重复的内容?还有如何解决对大型数据的上百个并发访问?搭建这样一个搜索引擎是一笔不小的投资呀!在 " Building Nutch: Open Source Search," 的作者 Mike Cafarella 和 Doug Cutting 总结如下::
      ... 一个具有完全功能的搜索系统:1亿页面索引量,每秒2个并发索引,需要每月800美元。10亿页面索引量,每秒50个页面请求,大概需要每月30000美元。
      这篇文章将为你演示如何在中等级别的网站上搭建Nutch。第一部分集中在抓取上。Nutch的抓取架构,如何运行一个抓取程序,理解这个抓取过程产生了什么。第二部分关注搜索。演示如何运行Nutch搜索程序。以及如何订制Nutch
      Nutch Vs. Lucene
      Nutch 是基于 Lucene的。Lucene为 Nutch 提供了文本索引和搜索的API。一个常见的问题是;我应该使用Lucene还是Nutch?最简单的回答是:如果你不需要抓取数据的话,应该使用Lucene。常见的应用场合是:你有数据源,需要为这些数据提供一个搜索页面。在这种情况下,最好的方式是直接从数据库中取出数据并用Lucene API建立索引。中文用户,可以参考 WebLucene 或者 车东 的一些列文章。如果需要中文分词帮助还可以联系作者。 http://domolo.oicp.net/bbs/list.asp?boardid=24 Erik Hatcher 和 Otis Gospodneti?'s 的 Lucene in Action 中详细讲述了这个过程。Nutch 适用于你无法直接获取数据库中的网站,或者比较分散的数据源的情况下使用。
      架构
      总体上Nutch可以分为2个部分:抓取部分和搜索部分。抓取程序抓取页面并把抓取回来的数据做成反向索引,搜索程序则对反向索引搜索回答用户的请求。抓取程序和搜索程序的接口是索引。两者都使用索引中的字段。()
      实际上搜索程序和抓取程序可以分别位于不同的机器上。()
      这里我们先看看Nutch的抓取部分。
      抓取程序:
      抓取程序是被Nutch的抓取工具驱动的。这是一组工具,用来建立和维护几个不同的数据结构: web database, a set of segments, and the index。下面我们逐个解释上面提到的3个不同的数据结构。
      The web database, 或者WebDB, 是一个特殊存储数据结构,用来映像被抓取网站数据的结构和属性的集合。WebDB 用来存储从抓取开始(包括重新抓取)的所有网站结构数据和属性。WebDB 只是被 抓取程序使用,搜索程序并不使用它。WebDB 存储2种实体:页面 和 链接。页面 表示 网络上的一个网页,这个网页的Url作为标示被索引,同时建立一个对网页内容的MD5 哈希签名。跟网页相关的其它内容也被存储,包括:页面中的链接数量(外链接),页面抓取信息(在页面被重复抓取的情况下),还有表示页面级别的分数 score 。链接 表示从一个网页的链接到其它网页的链接。因此 WebDB 可以说是一个网络图,节点是页面,链接是边。
      Segment 是 网页 的集合,并且它被索引。 Segment 的 Fetchlist 是抓取程序使用的 url 列表 , 它是从 WebDB中生成的。Fetcher 的输出数据是从 fetchlist 中抓取的网页。Fetcher 的输出数据先被反向索引,然后索引后的结果被存储在segment 中。 Segment 的生命周期是有限制的,当下一轮抓取开始后它就没有用了。默认的 重新抓取间隔是30天。因此删除超过这个时间期限的segment是可以的。而且也可以节省不少磁盘空间。Segment 的命名是 日期加时间 ,因此很直观的可以看出他们的存活周期。
      索引库 是 反向索引所有系统中被抓取的页面,他并不直接从页面反向索引产生,它是合并很多小的 segment 的索引中产生的。Nutch 使用 Lucene 来建立索引,因此所有 Lucene 相关的工具 API 都用来建立索引库。需要说明的是 Lucene 的 segment 的概念 和 Nutch 的 segment 概念是完全不同的,不要混淆哦。 可以参考 车东 的相关文章。 http://www.chedong.com/ 简单来说 Lucene 的 segment 是 Lucene 索引库的一部分,而 Nutch 的 Segment 是 WebDB 中 被 抓取和索引的一部分。

      Java I/O 学习笔记4(利用Socket进行Java网络编程) ZZ

      出自:天极网 郗旻
      Socket是网络上运行的两个程序间双向通讯的一端,它既可以接受请求,也可以发送请求,利用它可以较为方便的编写网络上数据的传递。在Java中,有专门的Socket类来处理用户的请求和响应。利用Socket类的方法,就可以实现两台计算机之间的通讯。这里就介绍一下在Java中如何利用Socket进行网络编程。   
      在Java中Socket可以理解为客户端或者服务器端的一个特殊的对象,这个对象有两个关键的方法,一个是getInputStream方法,另一个是getOutputStream方法。getInputStream方法可以得到一个输入流,客户端的Socket对象上的getInputStream方法得到的输入流其实就是从服务器端发回的数据流。GetOutputStream方法得到一个输出流,客户端Socket对象上的getOutputStream方法返回的输出流就是将要发送到服务器端的数据流,(其实是一个缓冲区,暂时存储将要发送过去的数据)。  
      程序可以对这些数据流根据需要进行进一步的封装。本文的例子就对这些数据流进行了一定的封装(关于封装可以参考Java中流的实现部分)。  
      为了更好的说明问题,这里举了一个网上对话的例子,客户端启动以后,服务器会启动一个线程来与客户进行文字交流。  
      要完成这个工作,需要完成三个部分的工作,以下依次说明:  
      一、建立服务器类  Java中有一个专门用来建立Socket服务器的类,名叫ServerSocket,可以用服务器需要使用的端口号作为参数来创建服务器对象。
      ServerSocket server = new ServerSocket(9998)   这条语句创建了一个服务器对象,这个服务器使用9998号端口。当一个客户端程序建立一个Socket连接,所连接的端口号为9998时,服务器对象server便响应这个连接,并且server.accept()方法会创建一个Socket对象。服务器端便可以利用这个Socket对象与客户进行通讯。Socket incoming = server.accept()   
      进而得到输入流和输出流,并进行封装BufferedReader in = new BufferedReader(new InputStreamReader(incoming.getInputStream()));PrintWriter out = new PrintWriter(incoming.getOutputStream(),true);   
      随后,就可以使用in.readLine()方法得到客户端的输入,也可以使用out.println()方法向客户端发送数据。从而可以根据程序的需要对客户端的不同请求进行回应。  
      在所有通讯结束以后应该关闭这两个数据流,关闭的顺序是先关闭输出流,再关闭输入流,即使用 out.close();in.close();
      二、建立客户端代码  
      相比服务器端,客户端要简单一些,客户端只需用服务器所在机器的ip以及服务器的端口作为参数创建一个Socket对象。得到这个对象后,就可以用"建立服务器"部分介绍的方法实现数据的输入和输出。Socket socket = new Socket("168.160.12.42",9998);in = new BufferedReader(new InputStreamReader(socket.getInputStream()));out = new PrintWriter(socket.getOutputStream(),true);   以上的程序代码建立了一个Socket对象,这个对象连接到ip地址为168.160.12.42的主机上、端口为9998的服务器对象。并且建立了输入流和输出流,分别对应服务器的输出和客户端的写入。  
      三、建立用户界面  
      读者可以根据自己的喜好建立自己的用户界面,这不是本文的重点。  经过以上三个步骤,就可以建立一个比较简单的对话程序。但是,为了使这个程序更加完善,应进行以下几个改进:  一、现在服务器只能服务一个客户,也就是单线程的。可以将它改进为多线程服务器。try{ file://建立服务器 ServerSocket server = new ServerSocket(9998); int i=1; for(;;) {  Socket incoming = server.accept();  new ServerThread(incoming,i).start();  i++; }}catch (IOException ex){ ex.printStackTrace(); }   循环检测是否有客户连接到服务器上,如果有,则创建一个线程来服务这个客户,这个线程的名称是ServerThread,这个类扩展了Thread类,它的编写方法与前述的服务器的写法相同。 
       二、为了可以随时得到对方传送过来的消息,可以在服务器以及客户端各建立一个独立的线程来察看输入流,如果输入流中有输入,则可以即时显示出来。代码如下:new Thread(){ public void run() {  try  {    while(true)   {    checkInput();    sleep(1000);//每1000毫秒检测一次   }  }catch (InterruptedException ex) { }catch(IOException ex) {  } }}.start();其中的checkInput()方法为private void checkInput() throws IOException{ String line; if((line=in.readLine())!=null) file://检测输入流中是否有新的数据  t.setPartner(line); file://将数据流中的消息显示出来}

      附:服务器的实现代码:
      import java.net.*;import java.io.*;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;
      public class talkServer{ public static void main(String[] args) { try  { file://建立服务器   ServerSocket server = new ServerSocket(9998);   int i=1;   for(;;)    { Socket incoming = server.accept();     new ServerThread(incoming,i).start();     i++;    }   }catch (IOException ex){   ex.printStackTrace();  } }}
      class ServerThread extends Thread implements ActionListener{ private int threadNum; private Socket socket; talkServerFrm t; BufferedReader in; PrintWriter out; private boolean talking=true; public ServerThread(Socket s,int c) { threadNum = c;  socket = s; }
      public void actionPerformed(ActionEvent e){ Object source = e.getSource(); try{  if(source==t.btnSend)   { out.println(t.getTalk());    t.clearTalk();  }else  if(source==t.btnEnd)   { out.println("谈话过程被对方终止");    out.close();    in.close();    talking = false;   } }catch(IOException ex){ }}
      public void run(){ try{  t=new talkServerFrm(new Integer(threadNum).toString(),this);  t.setSize(500,500);  t.show();  in = new BufferedReader(new       InputStreamReader(socket.getInputStream()));  out = new PrintWriter(socket.getOutputStream(),true); }catch(Exception e){} new Thread() { public void run()  { try{    while(true)    { checkInput();     sleep(1000);  } }catch (InterruptedException ex){ }catch(IOException ex){ } } }.start(); while(talking) { } t.dispose(); }
      private void checkInput() throws IOException{ String line; if((line=in.readLine())!=null)  t.setPartner(line); file://这是界面类里的方法,  file://用来将line的内容输出到用户界面 }}

      java I/O 学习笔记3 转载

      摘自http://blog.csdn.net/csusuntao/archive/2005/04/04/336354.aspx
      低级流类用于与磁盘文件的通信,高级流类用于组织那些通过低级流移动的信息,高级流类也适用于组织发送到网络或者从网络接受的信息。
      抽象上级流类:(都继承于类Object)
      InputStream-------OutputStream.:所有面向字节的输入和输出流的父类(自己本身是抽象类)
      Reader------Writer:所有面向字符的输入和输出流得的父类(自己本身是抽象类)
      低级流类:
      ByteArrayInputStream (extends InputStream): 从字节数组或字节数组中的一部分读取输入。
      ByteArrayOutputStream (extends OutputStream) :向字节数组写入。
      附:有几种方法可以将字节数组输出流变成可以访问的数据。
      一: String toString() 返回迄今为止写入流的所有字节构成的字符串。
      二: byte[] toByteArray() 返回包涵迄今为止写入流的所有字节的数组。这个数组是流内容的备份,可以修改而不会影响原始数据。
      高级流类: 高级输入流从其他输入流读取输入,而高级输出流从其他输出流写入输出。
      一: DataInputStream (extends InputStream) :
      DataInputStream类从另一流读取字节并将其翻译成Java原型,字符数组和字符串。没有面向字符的对应读取器类。
      boolean readBoolean(); byte readByte(); char readChar(); int readInt();
      long readLong(); floag readFloat(); double readDouble(); String readUTF();
      Static String readUTF(DataInput din) 静态方法。从指定的输入流读取UTF字符串。
      二:DataOutputStream (extends OutputStream) :
      DataOutputStream类支持将Java的原型数据写入输出流。也可以写入字符串和字节数组。没有相应的面向字符的写入器类。
      void writeBoolean(boolean b);
      void writeByte(int i) 写入i的低位字节。
      void writeShort(int i) 写入i的低两位字节。
      void write int(int i) 写入i的所有字节
      void writeBytes(String s) 将S写成一系列字节,只写入每个双字节Unicode代码的低位字节。
      void writeChars(String s); 将S 写成一系列Unicode代码。每个Unicode代码写成两个字节,从高位字节开始。
      void writeUTF(String s) 将S写成UTF字符串。

      java I/O 学习笔记2

      Java 输入/输出(I/O)机制提供了一套简单的,标准化的API以便从不同的数据源读取和写入字符和字节数据。在“面向对象编程:Java collection更有效管理elements”一文中,我们讨论了Java 集合类架构中的类和功能并介绍了它的排序功能。在本文中,我们将学习Java 平台提供的这些I/O类,接口和操作。让我们先从了解Java 数据流开始。
      数据流 Java所有的I/O机制都是基于数据流的,这些数据流表示了字符或者字节数据的流动序列。Java的I/O流提供了读写数据的标准方法。任何Java中表示数据源的对象都会提供以数据流的方式读写它的数据的方法。
      Java.io是大多数面向数据流的输入/输出类的主要软件包。这个软件包包含了两个抽象类,InputStream和OutputStream。所有其它面象数据流的输入/输出类都要扩展这两个基类。
      java.io软件包提供了一些类和接口,它们在由InputStream和OuputStream类提供的读写操作的顶端定义了一些有用的抽象。例如,ObjectInputStream类提供了让你把输入/输出流中的数据当成对象来读取的方法,而ObjectOutputStream类提供了让你能够把Java对象写入数据流中的方法。
      优化读写过程JDK 1.1 增加了一套读写类,它们提供了比现有数据流类更有用的抽象和更好的输入/输出性能。例如,BufferedReader和BufferedWriter 类被用来从基于字符的输入和输出流中读取和写入文本。BufferdReader 类缓存字符以更高效的读取字符串,数组和文本行。BufferedWriter类缓存字符以更高效的写入字符串,数组和文本行。BufferedReader和BufferedWriter 类可以按需求进行设置。
      Java输入/输出架构提供的读取器和写入器类包括 LineNumberReader 类,CharArrayReader类,FileReader类,FilterReader类,PushbackReader类,PipedReader类,StringReader类以及其它一些类。这些类是在InputStream和OuputStream类顶部的包裹类因此提供了与InputStream和OuputStream类相似的方法。但是,这些类为读写特定的对象,比方文件,字符数组和字符串等等提供了更高效而有用的抽象。
      读取数据当你从一个相应的数据源对象里提取输入流或者是创建一个读取器对象的时候就会自动打开一个输入流。例如,要为一个文件打开输入流,我们只需要以下面的方式把文件名传递给Java.io.FileReader对象的构造函数:
      java.io.FileReader fileReader = new java.io.FileReader("/home/me/myfile.txt");
      要按顺序读取FileReader底层的输入流中的一个字节数据,只需要使用不带参数的read方法。表A中的代码段从一个文件读取文本数据,一次一个字符,然后把它写入System.out里。
      要从输入流读取指定数目的字节数据到char数组里,只需要使用带一个char[]参数的read方法。数组的长度被用来确定应该读取的字符的个数。表B演示了这个技术。
      要关闭一个输入流以及这个流使用的所有系统资源,你只需要以下面的方式调用close方法:
      fileReader.close();
      写入数据象一个输入流一样,输出流通常在你从相应的数据源提取它或者是在你创建一个写入对象的时候被自动的打开。例如,要为一个文件打开输出流,我们把文件的名字传递给java.io.FileWriter对象的构造函数,如下所示:
      java.io.FileWriter fileWriter = new java.io.FileWriter("/home/me/out.txt");
      要将一个特定的字符写入到输出流中,可以使用带一个int参数的write方法,int参数代表要定入的字符。
      int aChar = (int)'X'; fileWriter.write(aChar);
      要在输出流给定的偏移地址写入一个char数组中特定数目的字符,你可以使用带一个char[]参数,一个int 偏移量参数和一个int长度参数的write方法,如下面的例子所示:
      fileWriter.write(buffer, 0, byteCount);
      要关闭一个输出流并释放所有与之相关的系统资源,可以使用close方法,就象这样:
      fileWriter.close();
      要强迫写出一个输出流中的所有数据,可以使用下面的flush方法:
      fileWriter.flush();
      把它们全部综合起来我们可以使用我们学习过的这些函数从一个文件中读取数据并同时写到另一个文件中去,如表C所示。
      总结Java的输入/输出机制为从不同的数据源读取和写入字符增加了一套简单而标准化的API。你对一种数据源使用Java流的经验能够让你容易的使用其它由Java提供的数据源类型。
      在我们下一篇文章中,我们将会开始学习Java平台的联网和远程通讯架构。我们将会把我们对Java流的讨论扩展到这些环境并演示如何打开远程数据源,并象操作本地数据源,比方文件一样,写入数据和读取数据

      Java IO学习笔记(1) 转载

      最近在看lucene的原代码,里面有好多Java 的I/O操作,现在顺带复习一下,网上找到一个东东很好,
      于是转载过来http://blog.csdn.net/yangxp_82/archive/2005/09/13/479306.aspx

      java IO最关键的四个父类:InputStream、OutputStream、Reader、Writer。它们都是public abstract class。
      InputSream和OutputStream对于数据的传送是以“byte”为单位的,而Reader和Writer对于数据的传送是以“character”为单位的。所以我们看到java.io包中的类大体上可以分为两大类,一类是以byte处理为主的Stream类,它们都是以XXXStream方式命名的;一类是以character处理为主的Reader/Writer类,它们都是以XXXReader或XXXWriter的方式命名的。
      InputStream类方法简介:
      InputStream是输入字节数据用的类,所以InputStream类提供了3种重载的read方法(具体方法参见API)。
      如果我们要知道这个流中还有多少个byte的数据可以读取时可以调用available方法,注意的是如果InputStream对象调用这个方法的话,它只会返回0,这个方法必须由继承InputStream类的子类对象调用才有用,另外它会抛出IOException异常,必须对此异常进行处理。
      如果我们想跳过一些字节来读取的话可以调用skip方法来跳过一些字节来读取。skip方法所需要的参数是long型的,指出要跳过的字节数。
      流的处理是单向的,如果想重新读取已经读过的数据,我们必须进行一些处理。有些InputStream是不支持重新读取的(也就是不允许重新定位读取指针),这样我们在进行重新读取前最好先调用markSupported方法来检查一下这个InputStream对象是否支持重定位指针,如果通过,那么我们就可以先使用mark方法来指定指针的位置,然后调用reset方法来让指针指到你要求得位置。(注意的是reset方法可能抛出IOException异常)。
      还有就是我们在使用完后,必须对我们打开的流进行关闭(close)。
      OutputStream方法的介绍:
      OutputStream提供了3个write方法来做数据的输出,这个是和InputStream是相对应的。
      还有一个常用的清除缓冲区的方法就是flush。
      最好就是close方法
      Reader方法的介绍
      Reader是输入字符数据用的,它所提供的方法跟InputStream基本一样。不过Reader类没有提供available方法,取而代之的是ready方法。
      Writer方法的介绍
      Writer类有5个write方法,具体可以查看API。
      学习完上面的四个最最基本的四个抽象类之后,我们就来学习Java的节点类,这个对于理解使用IO来说是至关重要的,下面就用一个表格来简单说明,若要详细了解请查看API。
      文件(File)
      FileInputStream
      FileOutputStream
      FileReader
      FileWriter

      内存(数组)
      ByteArrayInputStream
      ByteArrayOutputStream
      CharArrayReader
      CharArrayWriter

      内存(字符串)

      StringReader
      StringWriter

      管道(Pipe)
      PipeInputStream
      PipeOutputStream
      PipeReader
      PipeWriter


      下面就是一些必须学习的类(java.io.File)
      这个类对文件的操作是至关重要的,对于它的学习必须深入,它不是一仅仅是我们概念中的文件,而且包括目录。详细细节请查看API。
      RandomAccessFile对于IO系统来说也是很重要的,它与我们前面介绍的四个抽象类没有关系,它是直接继承自Object类,用于对文件进行随机读取操作。
      下面给出一个文件操作的例子:
      /**
      * 学生类
      * @author Sunny
      * @version 1.0
      */
      import java.io.*;
      public class Student implements Serializable{
      private String name;
      private String id;
      private int age;

      /**
      * 构造函数
      * @param name
      * @param id
      * @param age
      */
      public Student(String name, String id, int age) {
      setName(name);
      setId(id);
      setAge(age);
      }
      /**
      *
      * @return int
      */
      public int getAge() {
      return age;
      }
      /**
      *
      * @param age
      */
      public void setAge(int age) {
      if(age>0 && age <100)
      this.age = age;
      else
      this.age = 10;
      }
      /**
      *
      * @return String
      */
      public String getId() {
      return id;
      }
      /**
      *
      * @param id
      */
      public void setId(String id) {
      this.id = id;
      }
      /**
      *
      * @return String
      */
      public String getName() {
      return name;
      }
      /**
      *
      * @param name
      */
      public void setName(String name) {
      this.name = name;
      }
      }




      /**
      * 随即存取Student类
      * @author Sunny
      * @version 1.0
      */
      import java.io.RandomAccessFile;
      import java.io.IOException;
      public final class RandomAccessStudent extends Student {
      //name域所占的字节数
      private static final int NAME_SIZE = 30;
      //id域所占的字节数
      private static final int ID_SIZE = 30;
      //该对象所占的字节数
      public static final int SISE = NAME_SIZE + ID_SIZE + 4;
      /**
      * 构造函数
      * @param name
      * @param id
      * @param age
      */
      public RandomAccessStudent(String name, String id, int age) {
      super(name, id, age);
      }
      /**
      * 从随即文件读取一个Student类
      * @param file
      * @throws IOException
      */
      public void read(RandomAccessFile file) throws IOException {
      setName(readName(file));
      setId(readId(file));
      setAge(file.readInt());
      }
      //从随即文件中读取学生姓名
      private String readName(RandomAccessFile file) throws IOException {
      return readString(file, NAME_SIZE);
      }
      //从随即文件中读取学生ID
      private String readId(RandomAccessFile file) throws IOException {
      return readString(file, ID_SIZE);
      }
      //从随即文件中读取size字节长度的字符串
      private String readString(RandomAccessFile file, int size) throws IOException {
      char [] temp = new char[15];
      for(int i = 0; i temp[i] = file.readChar();

      return new String(temp).replace('\0', ' ');
      }
      /**
      * 把对象写入随即文件
      * @param file
      * @throws IOException
      */
      public void write(RandomAccessFile file) throws IOException {
      writeName(file);
      writeId(file);
      file.writeInt(getAge());
      }
      //把name写入随即文件
      private void writeName(RandomAccessFile file) throws IOException {
      writeString(file, getName());
      }
      //把id写入随即文件
      private void writeId(RandomAccessFile file) throws IOException {
      writeString(file, getId());
      }
      //把给定的字符串写入随即文件
      private void writeString(RandomAccessFile file, String content) throws IOException {
      StringBuffer buffer = null;

      if(content != null)
      buffer = new StringBuffer(content);
      else
      buffer = new StringBuffer(15);

      buffer.setLength(15);
      file.writeChars(buffer.toString());
      }
      }




      /**
      * 测试随即文件的读取
      * @author Sunny
      * @version 1.0
      */
      import java.io.*;
      public class RandomAccessFileTest {
      /**
      * @param args
      */
      public static void main(String[] args) throws IOException, FileNotFoundException {
      File file = new File("random.dat");
      if(file.exists())
      file.createNewFile();
      RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");

      RandomAccessStudent yang = new RandomAccessStudent("Sunny", "123456", 23);
      RandomAccessStudent jin = new RandomAccessStudent("JJJJJ", "12345678", 24);

      yang.write(randomAccessFile);
      jin.write(randomAccessFile);

      randomAccessFile.close();

      RandomAccessFile read = new RandomAccessFile(file, "rw");

      jin.read(read);
      System.out.println(jin.getAge());

      read.close();
      }
      }
      上面仅仅是基本的IO简介,更多的知识就是去查看API来完善,还有在JDK1.4中又有java.nio.*这个包,它又是一种新的机制,如果有时间将介绍它的基本机制。

      2007年2月19日星期一

      Lucene Resource

      在应用中加入全文检索功能——基于Java的全文索引引擎Lucene简介: http://www.chedong.com/tech/lucene.html
      Apache Lucene:http://lucene.apache.org/java/docs/index.html
      Java开源全文检索:http://www.open-open.com/32.htm
      水木BBS上搜索引擎的论坛: http://www.newsmth.org/bbsdoc.php?board=SearchEngineTech

      最近在看一本书叫做Ajax+Lucene构建搜索引擎,好像写的满好的,可以学到不少东东.

      2007年2月15日星期四

      新开的博客

      寒假最近无事,于是开始玩玩Blog,下学期实验室也可能有个Blog 搜索的项目.这是我的有关于专注于CS技术的博客,现在的主题是搜索引擎的设计顺带Java技术的讨论,现在刚刚起步.想在这里和大家一起讨论一下Lucene和Java技术^_^现在刚开张的博客,比较简陋,以后会慢慢填料的.之前用过MSN spaces,感觉限制太多,虽然好用但是看不到一点技术细节,这也符合MS的一贯风格.最近开始喜欢玩玩Linux和Javascript,喜欢DIY的感觉.大家多捧捧场拉,多点击广告和下载FireFox,开了2天,好像帐户上显示有10美分拉-_-