在存储过程过程中,如果要实现Select查询的where子句动态查询,可以用
exec ( "select .... where" +@whereStr)
这样的方式。但这样的话,感觉用存储过程就没什么用了,因为存储过程最大的特点就是将代码编译了放在DBMS中,而调用exec的话,这一部分就无法编译,也就无从优化了。
下面是一个比较巧妙的方法去实现所谓的“动态查询”
select * from table1 where
a = 1
and ( (@id is not null) or (id=@id) )
@id是传入的参数,如果◎id的值是null的话,那么and后面一整块是false,可以忽略,而如果不为null的话,实际上等于
where a=1 and id=@id
so...如果要启用id作为查询条件,就传入一个非null的值,否则就是不启用id作为查询条件,这就是传说中的动态查询
由于微软非常鼓励programmers使用注册表来代替ini文件,在.NET FRAMEWORK里只有操作注册表的封装,却没有对操作ini文件进行封装。下面的代码填补了这一项空白,这是仿照DELPHI的TINIFILE类来改写的。
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Collections;
using System.Collections.Specialized;
namespace Jerry.Huang
{
/// <summary>
/// 使用建议:虽然支持中文,但建议在ini文件中尽量避免使用中文,以避免在不同的操作系统产生不可预料的错误。
/// 1.7 修改了int、bool的读写
/// 增加了DateTime和Double类型的读写
/// 原作者:陈省 修改:Jerry Huang
/// 原始版本来源:
http://hubdog.csdn.net/UpdateList/ul20030726.htm#Ini /// 一个模仿Delphi的TIniFile的类
/// 修订:1.1 修正了对中文系统的支持。
/// 1.2 增加了UpdateFile方法,实现了对Win9x的支持
/// 1.3 增加了读写布尔,整数的操作
/// 1.4 修正了写Ini虽然成功,但是会抛出异常的错误
/// 1.5 ReadString返回的是Trim后的字符串
/// 1.6 统一并扩大了读写缓冲区的大小
/// </summary>
public class IniFile
{
public string FileName; //INI文件名
//声明读写INI文件的API函数
[DllImport("kernel32")]
private static extern bool WritePrivateProfileString(string section,string key,string val,string filePath);
[DllImport("kernel32")]
private static extern int GetPrivateProfileString(string section,string key,string def, byte[] retVal,int size,string filePath);
/// <summary>
/// 构造函数。
/// 当指定的INI文件不存在时可选是否建立或抛出异常。
/// </summary>
/// <param name="IniFileName">文件名</param>
/// <param name="ForceCreate">当文件不存在时是否建立</param>
public IniFile(string IniFileName, bool ForceCreate)
{
// 判断文件是否存在
FileInfo fileInfo=new FileInfo(IniFileName);
//Todo:搞清枚举的用法
if ((!fileInfo.Exists)) //|| (FileAttributes.Directory in fileInfo.Attributes))
if (!ForceCreate)
{
throw(new ApplicationException("Ini文件不存在"));
}
else
{
fileInfo.Directory.Create();
fileInfo.Create();
}
//必须是完全路径,不能是相对路径
FileName = fileInfo.FullName;
}
#region 各种数据类型的读写
/// <summary>
/// 将string型值写入ini。
/// </summary>
/// <param name="Section">小节名</param>
/// <param name="Ident">关键字</param>
/// <param name="Value">要写入的值</param>
public void WriteString(string Section,string Ident,string Value)
{
if (!WritePrivateProfileString(Section, Ident,Value,FileName))
{
// Todo:抛出自定义的异常
throw(new ApplicationException("写Ini文件出错"));
}
}
/// <summary>
/// 从ini文件中读取string型值。
/// 当读取失败时返回缺省值。
/// </summary>
/// <param name="Section">小节名</param>
/// <param name="Ident">关键字</param>
/// <param name="Default">缺省值</param>
/// <returns>string型值</returns>
public string ReadString(string Section,string Ident, string Default)
{
//StringBuilder Buffer = new StringBuilder(255);
Byte[] Buffer=new Byte[65535];
int bufLen=GetPrivateProfileString(Section,Ident,Default,Buffer, Buffer.GetUpperBound(0),FileName);
//必须设定0(系统默认的代码页)的编码方式,否则无法支持中文
string s=Encoding.GetEncoding(0).GetString(Buffer);
s=s.Substring(0,bufLen);
return s.Trim();
}
/// <summary>
/// 从ini文件中读取int型值。
/// 当读取失败时返回缺省值。
/// </summary>
/// <param name="Section">小节名</param>
/// <param name="Ident">关键字</param>
/// <param name="Default">缺省值</param>
/// <returns>int型值</returns>
public int ReadInteger(string Section, string Ident , int Default)
{
string intStr=ReadString(Section, Ident, Default.ToString());
try
{
return Convert.ToInt32(intStr);
}
catch (Exception)
{
//Console.WriteLine(ex.Message);
return Default;
}
}
/// <summary>
/// 将int型值写入ini
/// </summary>
/// <param name="Section">小节名</param>
/// <param name="Ident">关键字</param>
/// <param name="Value">要写入的值</param>
public void WriteInteger(string Section,string Ident, int Value)
{
WriteString(Section, Ident, Value.ToString());
}
/// <summary>
/// 从ini文件中读取bool型值。
/// 当读取失败时返回缺省值。
/// </summary>
/// <param name="Section">小节名</param>
/// <param name="Ident">关键字</param>
/// <param name="Default">缺省值</param>
/// <returns>bool型值</returns>
public bool ReadBool(string Section, string Ident, bool Default)
{
try
{
return Convert.ToBoolean(ReadString(Section, Ident, Default.ToString() ));
}
catch (Exception)
{
//Console.WriteLine(ex.Message);
return Default;
}
}
/// <summary>
/// 将bool型值写入ini
/// </summary>
/// <param name="Section">小节名</param>
/// <param name="Ident">关键字</param>
/// <param name="Value">要写入的值</param>
public void WriteBool(string Section, string Ident , bool Value)
{
WriteString(Section, Ident, Value.ToString());
}
/// <summary>
/// 将DateTime型值写入ini
/// </summary>
/// <param name="Section">小节名</param>
/// <param name="Ident">关键字</param>
/// <param name="Value">要写入的值</param>
public void WriteDateTime(string Section, string Ident, DateTime Value)
{
WriteString(Section, Ident, Value.ToString());
}
/// <summary>
/// 从ini文件中读取DateTime型值。
/// 当读取失败时返回缺省值。
/// </summary>
/// <param name="Section">小节名</param>
/// <param name="Ident">关键字</param>
/// <param name="Default">缺省值</param>
/// <returns>DateTime型值</returns>
public DateTime ReadDateTime(string Section, string Ident, DateTime Default)
{
try
{
return Convert.ToDateTime(ReadString(Section, Ident, Default.ToString() ));
}
catch (Exception)
{
return Default;
}
}
public void WriteDouble(string Section, string Ident, double Value)
{
WriteString(Section, Ident, Value.ToString());
}
public double ReadFloat(string Section, string Ident, double Default)
{
try
{
return Convert.ToDouble(ReadString(Section, Ident, Default.ToString() ));
}
catch (Exception)
{
return Default;
}
}
#endregion
//从Ini文件中,将指定的Section名称中的所有Ident添加到列表中
public StringCollection ReadSection(string Section)
{
Byte[] Buffer=new Byte[16384];
StringCollection Idents=new StringCollection();
//Idents.Clear();
int bufLen=GetPrivateProfileString(Section, null, null, Buffer, Buffer.GetUpperBound(0),
FileName);
//对Section进行解析
GetStringsFromBuffer(Buffer, bufLen, Idents);
return Idents;
}
private void GetStringsFromBuffer(Byte[] Buffer, int bufLen, StringCollection Strings)
{
Strings.Clear();
if (bufLen!=0)
{
int start=0;
for(int i=0; i < bufLen; i++)
{
if ((Buffer

== 0) && ((i-start)>0))
{
String s=Encoding.GetEncoding(0).GetString(Buffer, start, i-start);
Strings.Add(s);
start=i+1;
}
}
}
}
//从Ini文件中,读取所有的Sections的名称
public StringCollection ReadSections()
{
StringCollection SectionList=new StringCollection();
//Note:必须得用Bytes来实现,StringBuilder只能取到第一个Section
byte[] Buffer = new byte[65535];
int bufLen=0;
bufLen = GetPrivateProfileString(null, null, null,Buffer,
Buffer.GetUpperBound(0), FileName);
GetStringsFromBuffer(Buffer, bufLen, SectionList);
return SectionList;
}
//读取指定的Section的所有Value到列表中
public NameValueCollection ReadSectionValues(string Section)
{
NameValueCollection Values=new NameValueCollection();
StringCollection KeyList=ReadSection(Section);
Values.Clear();
foreach(string key in KeyList)
{
Values.Add(key, ReadString(Section, key, ""));
}
return Values;
}
//清除某个Section
public void EraseSection(string Section)
{
//
if (!WritePrivateProfileString(Section, null, null, FileName))
{
throw(new ApplicationException("无法清除Ini文件中的Section"));
}
}
//删除某个Section下的键
public void DeleteKey(string Section, string Ident)
{
WritePrivateProfileString(Section, Ident, null, FileName);
}
//Note:对于Win9X,来说需要实现UpdateFile方法将缓冲中的数据写入文件
//在Win NT, 2000和XP上,都是直接写文件,没有缓冲,所以,无须实现UpdateFile
//执行完对Ini文件的修改之后,应该调用本方法更新缓冲区。
public void UpdateFile()
{
WritePrivateProfileString(null, null, null, FileName);
}
/// <summary>
/// 检查某个Section是否存在。
/// </summary>
/// <param name="Section">小节名</param>
/// <returns>存在返回true,否则为false。</returns>
public bool SectionExists(string Section)
{
StringCollection Sections=this.ReadSections();
return Sections.IndexOf(Section)>-1;
}
/// <summary>
/// 检查某个Section的某个键值是否存在。
/// </summary>
/// <param name="Section">小节名</param>
/// <param name="Ident">关键字</param>
/// <returns>存在返回true,否则为false。</returns>
public bool ValueExists(string Section, string Ident)
{
//
StringCollection Idents=ReadSection(Section);
return Idents.IndexOf(Ident)>-1;
}
//确保资源的释放
~IniFile()
{
UpdateFile();
}
}
}
在基于Windows平台的程序设计中,事件(event)是一个很重要的概念。因为在几乎所有的Windows应用程序中,都会涉及大量的异步调用,比如响应点击按钮、处理Windows系统消息等,这些异步调用都需要通过事件的方式来完成。即使在下一代开发平台——.NET中也不例外。
那么什么是事件呢?所谓事件,就是由某个对象发出的消息,这个消息标志着某个特定的行为发生了,或者某个特定的条件成立了。比如用户点击了鼠标、socket上有数据到达等。那个触发(raise)事件的对象称为事件的发送者(event sender),捕获并响应事件的对象称为事件的接收者(event receiver)。
在这里,我们将要讨论的是,在.NET的主流开发语言C#中如何使用自定义的事件来实现我们自己的异步调用。
在C#中,事件的实现依赖于delegate,因此我们有必要先了解一下delegate的概念。
Delegate
delegate是C#中的一种类型,它实际上是一个能够持有对某个方法的引用的类。与其它的类不同,delegate类能够拥有一个签名(signature,即函数说明,函数头,cocoman注),并且它只能持有与它的签名相匹配的方法的引用(即参数个数/类型,和返回值完全一致,cocoman注)。它所实现的功能与C/C++中的函数指针十分相似。它允许你传递一个类A的方法m给另一个类B的对象,使得类B的对象能够调用这个方法m。但与函数指针相比,delegate有许多函数指针不具备的优点。首先,函数指针只能指向静态函数,而delegate既可以引用静态函数,又可以引用非静态成员函数。在引用非静态成员函数时,delegate不但保存了对此函数入口指针的引用,而且还保存了调用此函数的类实例的引用。其次,与函数指针相比,delegate是面向对象、类型安全、可靠的受控(managed)对象。也就是说,runtime能够保证delegate指向一个有效的方法,你无须担心delegate会指向无效地址或者越界地址。
实现一个delegate是很简单的,通过以下3个步骤即可实现一个delegate:
1. 声明一个delegate对象,它应当与你想要传递的方法具有相同的参数和返回值类型。
2. 创建delegate对象,并将你想要传递的函数作为参数传入。
3. 在要实现异步调用的地方,通过上一步创建的对象来调用方法。
下面是一个简单的例子:
using System;
public class MyDelegateTest
{
// 步骤1,声明delegate对象
public delegate void MyDelegate(string name);
// 这是我们欲传递的方法,它与MyDelegate具有相同的参数和返回值类型
public static void MyDelegateFunc(string name)
{
Console.WriteLine("Hello, {0}", name);
}
public static void Main()
{
// 步骤2,创建delegate对象
MyDelegate md = new MyDelegate(MyDelegateTest.MyDelegateFunc);
// 步骤3,调用delegate
md("sam1111");//这个调用,可以理解为md是一个特殊的object,一个装着函数的object。也就是为什么说delegate是“函数指针”的原因,cocoman注
}
}
输出结果是:Hello, sam1111
cocoman附加comment
在google找到一篇blog说delegate实际上是java里的interface,但是该blog的评论中有反对的声音,据说老师曾在课堂上特意强调2者不同。依我来看,从上面的例子,可以看出delegate在效果上,的确是和interface很相似。public delegate void MyDelegate(string name);可以暂时看作事先定义的interface,而其后的MyDelegateFunc函数可以看作前面delegate对象的实现。所以,可以用interface的概念去帮助理解上面的例子。但是在理论层次上,我不建议将delegate和interface划等号。
了解了delegate,下面我们来看看,在C#中对事件是如何处理的。
在C#中处理事件
C#中的事件处理实际上是一种具有特殊签名(signature)的delegate,象下面这个样子:
public delegate void MyEventHandler(object sender, MyEventArgs e);
其中的两个参数,sender代表事件发送者,e是事件参数类。MyEventArgs类用来包含与事件相关的数据,所有的事件参数类都必须从System.EventArgs类派生。当然,如果你的事件不含参数,那么可以直接用System.EventArgs类作为参数。
就是这么简单,结合delegate的实现,我们可以将自定义事件的实现归结为以下几步:
1. 定义delegate对象类型,它有两个参数,第一个参数是事件发送者对象,第二个参数是事件参数类对象。
2. 定义事件参数类,此类应当从System.EventArgs类派生。如果事件不带参数,这一步可以省略。
3. 定义事件处理方法,它应当与delegate对象具有相同的参数和返回值类型。
4. 用event关键字定义事件对象,它同时也是一个delegate对象。
5. 用+=操作符添加事件到事件队列中(-=操作符能够将事件从队列中删除)。
6. 在需要触发事件的地方用调用delegate的方式写事件触发方法。一般来说,此方法应为protected访问限制,即不能以public方式调用,但可以被子类继承。名字是OnEventName。(原文中的此步骤过于繁琐,下面的代码已经采用该文评论中的简化版本,cocoman注)
7. 在适当的地方调用事件触发方法触发事件。
下面是一个简单的例子:
using System;
public class EventTest
{
// 步骤1,定义delegate对象
public delegate void MyEventHandler(object sender,System.EventArgs e);
//步骤2省略
// 步骤3,定义事件处理方法,它与delegate对象具有相同的参数和返回值类型
public static void MyEventFunc(object sender,System.EventArgs e)
{
Console.WriteLine("My Event is done!");
}
// 步骤4,用event关键字定义事件对象
private event MyEventHandler myevent;
public EventTest()
{
// 步骤5,用+=操作符将事件添加到队列中
this.myevent+=new MyEventHandler(MyEventFunc);
}
// 步骤7,触发事件
public void RaiseEvent()
{
EventArgs e=new EventArgs();
if(myevent!=null)
myevent(this,e);// 步骤6,以调用delegate的方式写事件触发函数
}
public static void Main()
{
EventTest et=new EventTest();
Console.WriteLine("Please input a");
if(Console.ReadLine()=="a")
et.RaiseEvent();
else
Console.WriteLine("Error");
}
}
// 步骤3,定义事件处理方法,它与delegate对象具有相同的参数和返回值类型
public static void MyEventFunc(object sender,System.EventArgs e)
{
Console.WriteLine("My Event is done!");
}
// 步骤4,用event关键字定义事件对象
private event MyEventHandler myevent;
public EventTest()
{
// 步骤5,用+=操作符将事件添加到队列中
this.myevent+=new MyEventHandler(MyEventFunc);
}
// 步骤7,触发事件
public void RaiseEvent()
{
EventArgs e=new EventArgs();
if(myevent!=null)
myevent(this,e);// 步骤6,以调用delegate的方式写事件触发函数
}
public static void Main()
{
EventTest et=new EventTest();
Console.WriteLine("Please input a");
if(Console.ReadLine()=="a")
et.RaiseEvent();
else
Console.WriteLine("Error");
}
}
输出结果如下,黑体为用户的输入:
Please input ‘a’: a
My event is done!
创建XML文件有2种方法
方法一:
XmlDocument xmldoc ;//整个xml文档对象
XmlNode xmlnode ;//节点对象
XmlElement xmlelem ;//节点下的元素
xmldoc = new XmlDocument ( ) ;
//加入XML的声明段落
xmlnode = xmldoc.CreateNode ( XmlNodeType.XmlDeclaration , "" , "" ) ;
xmldoc.AppendChild ( xmlnode ) ;
//加入一个根节点
xmlelem = xmldoc.CreateElement ( "" , "bookstore" , "" ) ;
xmldoc.AppendChild ( xmlelem ) ;
//加入子节点
XmlNode root=xmldoc.SelectSingleNode("bookstore");//查找<bookstore> 根节点
XmlElement xe1=xmldoc.CreateElement("book");//创建一个<book>节点
xe1.SetAttribute("genre","fantasy");//设置该节点genre属性
xe1.SetAttribute("ISBN","2-3631-4");//设置该节点ISBN属性
XmlElement xesub1=xmldoc.CreateElement("title");
xesub1.InnerText="Oberon's Legacy";
xe1.AppendChild(xesub1);//添加到<book>节点中
XmlElement xesub2=xmldoc.CreateElement("author");
xesub2.InnerText="Corets, Eva";
xe1.AppendChild(xesub2);//添加到<book>节点中
XmlElement xesub3=xmldoc.CreateElement("price");
xesub3.InnerText="5.95";
xe1.AppendChild(xesub3);
root.AppendChild(xe1);//添加到<bookstore>节点中
//保存创建好的XML文档
xmldoc.Save ( Server.MapPath("data.xml") ) ;
方法二:
XmlTextWriter xmlWriter;
string strFilename = Server.MapPath("data1.xml") ;
xmlWriter = new XmlTextWriter(strFilename,Encoding.Default);//创建一个xml文档
xmlWriter.Formatting = Formatting.Indented;
xmlWriter.WriteStartDocument();
xmlWriter.WriteStartElement("bookstore");
xmlWriter.WriteStartElement("book");
xmlWriter.WriteAttributeString("genre","fantasy");
xmlWriter.WriteAttributeString("ISBN","2-3631-4");
xmlWriter.WriteStartElement("title");
xmlWriter.WriteString("Oberon's Legacy");
xmlWriter.WriteEndElement();
xmlWriter.WriteStartElement("author");
xmlWriter.WriteString("Corets, Eva");
xmlWriter.WriteEndElement();
xmlWriter.WriteStartElement("price");
xmlWriter.WriteString("5.95");
xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
xmlWriter.Close();
以上2种方法均在脚本所在目录生成data.xml如下
<?xml version="1.0" encoding="gb2312"?>
<bookstore>
<book genre="fantasy" ISBN="2-3631-4">
<title>Oberon's Legacy</title>
<author>Corets, Eva</author>
<price>5.95</price>
</book>
</bookstore>
////////////////////////////////////////////////////////////////////////////////
1、往<bookstore>节点中插入一个<book>节点:
XmlDocument xmlDoc=new XmlDocument();
xmlDoc.Load("bookstore.xml");
XmlNode root=xmlDoc.SelectSingleNode("bookstore");//查找<bookstore>
XmlElement xe1=xmlDoc.CreateElement("book");//创建一个<book>节点
xe1.SetAttribute("genre","李赞红");//设置该节点genre属性
xe1.SetAttribute("ISBN","2-3631-4");//设置该节点ISBN属性
XmlElement xesub1=xmlDoc.CreateElement("title");
xesub1.InnerText="CS从入门到精通";//设置文本节点
xe1.AppendChild(xesub1);//添加到<book>节点中
XmlElement xesub2=xmlDoc.CreateElement("author");
xesub2.InnerText="候捷";
xe1.AppendChild(xesub2);
XmlElement xesub3=xmlDoc.CreateElement("price");
xesub3.InnerText="58.3";
xe1.AppendChild(xesub3);
root.AppendChild(xe1);//添加到<bookstore>节点中
xmlDoc.Save("bookstore.xml");
//===============================================
结果为:
<?xml version="1.0" encoding="gb2312"?>
<bookstore>
<book genre="fantasy" ISBN="2-3631-4">
<title>Oberon's Legacy</title>
<author>Corets, Eva</author>
<price>5.95</price>
</book>
<book genre="李赞红" ISBN="2-3631-4">
<title>CS从入门到精通</title>
<author>候捷</author>
<price>58.3</price>
</book>
</bookstore>
2、修改节点:将genre属性值为“李赞红“的节点的genre值改为“update李赞红”,将该节点的子节点<author>的文本修改为“亚胜”。
XmlNodeList nodeList=xmlDoc.SelectSingleNode("bookstore").ChildNodes;//获取bookstore节点的所有子节点
foreach(XmlNode xn in nodeList)//遍历所有子节点
{
XmlElement xe=(XmlElement)xn;//将子节点类型转换为XmlElement类型
if(xe.GetAttribute("genre")=="李赞红")//如果genre属性值为“李赞红”
{
xe.SetAttribute("genre","update李赞红");//则修改该属性为“update李赞红”
XmlNodeList nls=xe.ChildNodes;//继续获取xe子节点的所有子节点
foreach(XmlNode xn1 in nls)//遍历
{
XmlElement xe2=(XmlElement)xn1;//转换类型
if(xe2.Name=="author")//如果找到
{
xe2.InnerText="亚胜";//则修改
break;//找到退出来就可以了
}
}
break;
}
}
xmlDoc.Save("bookstore.xml");//保存。
//==================================================
最后结果为:
<?xml version="1.0" encoding="gb2312"?>
<bookstore>
<book genre="fantasy" ISBN="2-3631-4">
<title>Oberon's Legacy</title>
<author>Corets, Eva</author>
<price>5.95</price>
</book>
<book genre="update李赞红" ISBN="2-3631-4">
<title>CS从入门到精通</title>
<author>亚胜</author>
<price>58.3</price>
</book>
</bookstore>
3、删除 <book genre="fantasy" ISBN="2-3631-4">节点的genre属性,删除 <book genre="update李赞红" ISBN="2-3631-4">节点。
XmlNodeList xnl=xmlDoc.SelectSingleNode("bookstore").ChildNodes;
foreach(XmlNode xn in xnl)
{
XmlElement xe=(XmlElement)xn;
if(xe.GetAttribute("genre")=="fantasy")
{
xe.RemoveAttribute("genre");//删除genre属性
}
else if(xe.GetAttribute("genre")=="update李赞红")
{
xe.RemoveAll();//删除该节点的全部内容
}
}
xmlDoc.Save("bookstore.xml");
//===========================================
最后结果为:
<?xml version="1.0" encoding="gb2312"?>
<bookstore>
<book ISBN="2-3631-4">
<title>Oberon's Legacy</title>
<author>Corets, Eva</author>
<price>5.95</price>
</book>
<book>
</book>
</bookstore>
4、显示所有数据。
XmlNode xn=xmlDoc.SelectSingleNode("bookstore");
XmlNodeList xnl=xn.ChildNodes;
foreach(XmlNode xnf in xnl)
{
XmlElement xe=(XmlElement)xnf;
Console.WriteLine(xe.GetAttribute("genre"));//显示属性值
Console.WriteLine(xe.GetAttribute("ISBN"));
XmlNodeList xnf1=xe.ChildNodes;
foreach(XmlNode xn2 in xnf1)
{
Console.WriteLine(xn2.InnerText);//显示子节点点文本
}
}
cocoman原创。转载请注明出处,谢谢!
本文所指的多线程下载是指基于windows下的多线程下载同一个文件。这项技术的难点,主要集中在以下几个方面:
1)多个线程读写同一文件
在一个下载任务里面,各个下载点是独立的线程,从远程的文件服务器上读取文件,然后写入本地文件。从多线程的概念上来看,各个下载点是相互独立的(虽然在CPU时间上他们仍是按顺序写入本地文件),所以存在着一个很关键的问题:多个线程操作同一个文件。这在文件操作上是不允许的。一个文件在打开之后,其他程序和线程是禁止向这个文件进行操作。特别是写操作(读操作有共享打开模式)。
在这里要引入一个变通的方案:File Mapping(文件映射)。文件映射是指将物理硬盘上的一个文件,映射成虚拟内存。要使用文件映射有3个主要的步骤:第1是建立File mapping object(FMO),第2步是在FMO上建立File View(FV)。第3是释放映射,在这一步操作系统会处理善后工作。例如将虚拟内存中的所有改变写入文件中,这点就完全不需要我们关心而由操作系统负责(这也是API的精髓所在)。这3个步骤分别用到3个windows API函数:CreateFileMapping,MapViewOfFile以及UnmapViewOfFile。建立FMO就是将硬盘上的文件,映射成一个FMO;建立file view就是将FMO映射成虚拟内存中的一段地址(可以映射整个FMO的view,也可以仅仅映射部分FMO的view)。
将物理文件映射成虚拟内存以后,对文件的操作,实际上就变成了在内存的操作。所以在各个下载点写文件的时候,实际上是在file view上进行写操作(将读回来的流,copy到“内存”中的某一地址)
在建立部分file view的时候,有一个细节问题。就是当只映射FMO一部分的时候,映射的起始点,并不是任意的。比如文件的大小是10000字节,我要从0开始映射第1个file view,然后从5000开始映射第2个file view。第1个file view从0开始,没有问题,但第2个从5000开始,则有可能会失败。这是因为内存操作都是一块一块来的,不同的操作系统,块的大小是不一样的,windows NT内核的系统(NT,XP,2003)的块的大小是64K(65536)。那么,第2个file view的起点地址,必须是64K的整数倍(即只能从65536,65536×2,65536×3……开始)。一般来说,在映射部分FMO的时候,先要利用另外一个API函数GetSystemInfo获得数据类型为SYSTEM_INFO的一个结构变量,该变量中的dwAllocationGranularity域包含了该大小的值。
2)动态文件映射
如果你的程序支持在下载的过程中动态建立新的下载点,那么就需要考虑动态文件映射的问题了。在上一个问题上,假设我们要将文件分成2个线程下载。那么在建立file view的时候,就是将文件的全半部分映射成一个file view,后半部分映射为另一个file view。这种映射file view的方式我称之为“静态映射”。静态映射的最大问题是映射的文件大小被固定了,这就意味着下载的过程只能由这2个下载点进行下载,而不能够在下载过程中添加新的下载点。我想到的一个解决方案是采用“动态映射”,就是说,在下载开始之前,根据远程文件的filesize,将文件分成N个小块,在下载的时候,先映射第1个小块为file view,下载完当前小块后,释放映射,接着向母线程请求相邻的下一个小块,然后再映射,再下载,依此类推,直到相邻的下一个小块是已经下载完成的,这就意味着这一个下载点的任务已经完成。for exaple,一个文件被分成10个小块(一般情况下,块数的多少是由dwAllocationGranularity的值决定的,而不像本例中假设为10),那么线程1(下载点1)从block1开始映射、下载,线程2(下载点2)则从block6开始,当线程1下载完block5的时候,发现block6已经下载过了,那么线程1的任务就完成了,同样地,线程2下载到block10的时候,已经没有block了,线程2的任务也完成了。
上一段的“相邻”我用了粗体,这是因为,基本上所有的下载协议(HTTP,FTP,MMS等)都是TCP连接,TCP连接都是必须先“握手”再传输数据。如果不是相邻的话,那么远程的下载起始点就不是当前点的next byte,这就需要重新“握手”,浪费时间。
这个动态映射的好处就是,比如说,当线程1下载到block2,线程2下载到block8的时候,用户嫌下载太慢了,要再添加一个下载点——线程3,那么线程3就会由母线程产生,并由block4开始下载,那么线程3下载到block5的时候就停止了,而原来的线程1下载完block3的时候,就完成任务了。从而实现了动态建立下载点。
总结
以上的下载方式,都是建立在一个基础上:远程文件的filesize已知。如果由于某些原因(例如下载的是媒体流文件,文件大小不能在建立连接的时候获知),无法事先获得文件的filesize,那么这个文件就不支持多线程下载了,而只能单线程下载。这也是不少下载软件中经常出现的提示之一:“该文件大小未知,不能启用多线程”
以上的2点,第1点已经写出实际的代码,实践并顺利通过。第2点尚未写出实际代码,仅理论上的推理。
此外,文中所提到的各种API函数仅仅是为读者提供线索,详细的用法读者需要进一步进行搜索,google上能找到很多资料。所以在此并不重复。
Recently I am writing a chatroom project with Java RMI. And I found something strange, dangerous, stupid..... as well as a lot of bugs in Jbuilder9. BTW, seems Jbuilder only suitable for some normal application. It's not very excited even a nightmare while doing something need to combine java application and web server.
Rmiregistry seemingly has some caching mechanism. If I make some mistake deliberately (e.g. delete a necessary class file) in the first running, correct it and then run again, you will find the previous error appear again. However, if you kill the rmiregistry process (or reboot the system if you want), make everthing start again, you can see everything goes smoothly.
As a result, everytime after I recompile the source code, I will stop the rmiregistry and start it again. Just to make sure nothing has been cached. Silly, eh?
Here is some important summaries about socket object and multi-thread programming in Java.
Main Step:
1) Create a subclass(EchoServer.java) of Thread(or implement the Runnable interface) to deal with the clients.
2) In the main method, assign a ServerSocket to listen on a specific port(9998 for example)
3) when there is a request, accept it and return a clientSocket
4) Pass the clientSocket into a new Thread, and then start it
Code Fragments:
within the EchoServer.java
public class EchoServer extends Thread
{
private Socket clientSocket;
public static boolean shutDownServer;
public EchoServer(Socket s) {
clientSocket=s;
}
public void run() {
boolean finished=false;
while(!finished)
try {
finished=deal_with_client(clientSocket);
}catch(Exception e){}
try{
clientSocket.close();
}catch(Exception e){}
}
...other method omitted.....
}
the main method
int server_port=9998;
ServerSocket rServer;
Socket request;
Thread receiveThread;
try{
rServer=new ServerSocket(server_port);//create the serverSocket to listen
boolean done=false;
while(!done){
request=rServer.accept();//when there is a request, accept it and return a clientSocket
System.out.println("Incoming client...");
System.out.println("IP: "+request.getInetAddress().getHostAddress());
System.out.println("Port: "+request.getPort());
System.out.println("##########");
receiveThread=new EchoServer(request);//create a new Thread, pass the clientSocket into it
receiveThread.start();//start the Thread
if (EchoServer.shutDownServer) done=true;
}
}catch(IOException e){
System.out.println(e.getMessage());
}