菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
9
0

下载

原创
05/13 14:22
阅读数 52268

C# 多线程下载

下面是一个完整的多线程下载源码,我在写代码的时候遇到点问题也放在下面,希望大家别犯相同的错误。

问题1、线程偷懒?
在程序中我设置N个线程去下载时,然而有的线程却偷懒去了,当时非常奇怪,花了很多时间在代码上。
这其实是因为服务器不支持多线程下载造成的,大部分专业的下载站都禁止多线程下载,既然是服务器的原因那就没法了,在这里我想提一下在IIS7中启用和禁止多线程的方法。
应用程序池 -》 右击属性“高级设置” -》 进程模型 -》 最大工作进程数(这里便是设置允许多少线程)
至于IIS6也在应用程序池里设置,应用程序池- 》右击属性 -》 性能 -》 最大工作进程数。好了废话不说了,看下面的源码:
使用:
JhxzThreading mt = new JhxzThreading(5, “下载地址”, "本地保存路径");
mt.FileName = "wenjian"; //保存的文件名
mt.Start();
JhxzThreading公开了一些属性方便调用,如IsComplete表示这个下载任务是否完成,还有DownloadSize这个是实时下载了多少字节,通过这两个我们可以很容易实现进度条。如果有进度控件par:
pbar.Maximum = (int)mt.FileSize;
while (!mt.IsComplete)
{
    pbar.Value = mt.DownloadSize; 
}
上面虽然实现进度条了,但是由于主线程一直在循环的工作,窗体可能会有假死现象,针对这个原因我们专门用一个线程来控制进度。于是有了下面的做法。
pbar.Maximum = (int)mt.FileSize;
Thread bar = new Thread(() => {
    while (!mt.IsComplete)
    {
        Thread.Sleep(50);
        this.SafeInvoke(() => { pbar.Value = mt.DownloadSize; });
    }
    MessageBox.Show("恭喜!文件已下载完成","提示",
        MessageBoxButtons.OK,MessageBoxIcon.Information);
});
bar.Start();
如果对this.SafeInvoke有疑问点这里
多线程下载类:
public class JhxzThreading
{
    private int _threadNum;             //线程数量
    private long _fileSize;             //文件大小
    private string _extName;            //文件扩展名
    private string _fileUrl;            //文件地址
    private string _fileName;           //文件名
    private string _savePath;           //保存路径
    private short _threadCompleteNum;   //线程完成数量
    private bool _isComplete;           //是否完成
    private volatile int _downloadSize; //当前下载大小
    private Thread[] _thread;           //线程数组
    private List<string> _tempFiles = new List<string>();
 
    public string FileName
    {
        get
        {
            return _fileName;
        }
        set
        {
            _fileName = value;
        }
    }
 
    public long FileSize
    {
        get
        {
            return _fileSize;
        }
    }
 
    public int DownloadSize
    {
        get
        {
            return _downloadSize;
        }
    }
 
    public bool IsComplete
    {
        get
        {
            return _isComplete;
        }
        set
        {
            _isComplete = value;
        }
    }
 
    public int ThreadNum
    {
        get
        {
            return _threadNum;
        }
        set
        {
            _threadNum = value;
        }
    }
 
    public string SavePath
    {
        get
        {
            return _savePath;
        }
        set
        {
            _savePath = value;
        }
    }
 
    public JhxzThreading(int threahNum, string fileUrl, string savePath)
    {
        this._threadNum = threahNum;
        this._thread = new Thread[threahNum];
        this._fileUrl = fileUrl;
        this._savePath = savePath;
    }
 
    public void Start()
    {
        HttpWebRequest request =
            (HttpWebRequest)WebRequest.Create(_fileUrl);
        HttpWebResponse response =
            (HttpWebResponse)request.GetResponse();
        //获取真实扩展名
        _extName =
            response.ResponseUri.ToString().Substring(
            response.ResponseUri.ToString().LastIndexOf('.'));
        _fileSize = response.ContentLength;
        int singelNum = (int)(_fileSize / _threadNum);      //平均分配
        int remainder = (int)(_fileSize % _threadNum);      //获取剩余的
        request.Abort();
        response.Close();
        for (int i = 0; i < _threadNum; i++)
        {
            List<int> range = new List<int>();
            range.Add(i * singelNum);
            //剩余的交给最后一个线程
            if (remainder != 0 && (_threadNum - 1) == i)   
                range.Add(i * singelNum + singelNum +
                    remainder - 1);
            else
                range.Add(i * singelNum + singelNum - 1);
            _thread[i] =
                new Thread(() => { Download(range[0], range[1]); });
            _thread[i].Name = "jhxz_{0}".Formart(i + 1);
            _thread[i].Start();
        }
    }
 
    private void Download(int from, int to)
    {
        Stream httpFileStream = null, localFileStram = null;
        try
        {
            string tmpFileBlock =
                @"{0}\{1}_{2}.dat".Formart(_savePath, _fileName,
                Thread.CurrentThread.Name);
            _tempFiles.Add(tmpFileBlock);
            HttpWebRequest httprequest =
                (HttpWebRequest)WebRequest.Create(_fileUrl);
            httprequest.AddRange(from, to);
            HttpWebResponse httpresponse =
                (HttpWebResponse)httprequest.GetResponse();
            httpFileStream = httpresponse.GetResponseStream();
            localFileStram = new FileStream(tmpFileBlock, FileMode.Create);
            byte[] by = new byte[5000];
            //Read方法将返回读入by变量中的总字节数
            int getByteSize =
                httpFileStream.Read(by, 0, (int)by.Length);          
            while (getByteSize > 0)
            {
                Thread.Sleep(20);
                _downloadSize += getByteSize;
                localFileStram.Write(by, 0, getByteSize);
                getByteSize = httpFileStream.Read(by, 0, (int)by.Length);
            }
            _threadCompleteNum++;
        }
        catch (Exception ex)
        {
            throw new Exception(ex.Message.ToString());
        }
        finally
        {
            if (httpFileStream != null) httpFileStream.Dispose();
            if (localFileStram != null) localFileStram.Dispose();
        }
        if (_threadCompleteNum == _threadNum)
        {
            _isComplete = true;
            Complete();
        }
    }
 
    private void Complete()
    {
        Stream mergeFile =
            new FileStream(@"{0}\{1}{2}".Formart(_savePath,
                _fileName, _extName), FileMode.Create);
        BinaryWriter AddWriter = new BinaryWriter(mergeFile);
        foreach (string file in _tempFiles)
        {
            using (FileStream fs = new FileStream(file, FileMode.Open))
            {
                BinaryReader TempReader = new BinaryReader(fs);
                AddWriter.Write(TempReader.ReadBytes((int)fs.Length));
                TempReader.Close();
            }
            File.Delete(file);
        }
        AddWriter.Close();
    }
}
 
View Code

C# 下载图片修改后显示

C#下载图片修改后显示
using System.Net;

using System.Drawing;



//1、将原始图片下载下来:

string url = "http://www.mozilla.org/images/feature-back-cnet.png";

WebClient myWebClient = new WebClient();

myWebClient.DownloadFile(url,"C:\\temp\\feature-back-cnet.png");



System.Drawing.Image image =

   System.Drawing.Image.FromFile("C:\\temp\\feature-back-cnet.png");

Bitmap bmp = new Bitmap(image,new Size(100,100));



//2.将新图片保存到 C:\temp\newPic.png

bmp.Save("C:\\temp\\newPic.png",

   System.Drawing.Imaging.ImageFormat.Png);

Image1.ImageUrl = "C:\\temp\\newPic.png";






using System.Net;

using System.Drawing;



//1、将原始图片下载下来:

string url = "http://www.mozilla.org/images/feature-back-cnet.png";

WebClient myWebClient = new WebClient();

myWebClient.DownloadFile(url,"C:\\temp\\feature-back-cnet.png");



System.Drawing.Image image =

   System.Drawing.Image.FromFile("C:\\temp\\feature-back-cnet.png");

Bitmap bmp = new Bitmap(image,new Size(100,100));



//2.将新图片保存到 C:\temp\newPic.png

bmp.Save("C:\\temp\\newPic.png",

   System.Drawing.Imaging.ImageFormat.Png);

Image1.ImageUrl = "C:\\temp\\newPic.png";
 
View Code

C#实现HTTP协议下的多线程文件传输

很多人都有过使用网络蚂蚁或网络快车软件下载互联网文件的经历,这些软件的使用可以大大加速互联网上文件的传输速度,减少文件传输的时间。这些软件为什么有如此大的魔力呢?其主要原因是这些软件都采用了多线程下载和断点续传技术。如果我们自己来编写一个类似这样的程序,也能够快速的在互联网上下载文件,那一定是非常愉快的事情。下面我就讲一讲如何利用C#语言编写一个支持多线程下载文件的程序,你会看到利用C#语言编写网络应程序是多么的容易,从中也能体会到C#语言中强大的网络功能。

  首先介绍一下HTTP协议,HTTP亦即Hpyer Text Transfer Protocal的缩写,它是现代互联网上最重要的一种网络协议,超文本传输协议位于TCP/IP协议的应用层,是一个面向无连接、简单、快速的C/S结构的协议。HTTP的工作过程大体上分连接、请求、响应和断开连接四个步骤。C#语言对HTTP协议提供了良好的支持,在.NET类库中提供了WebRequest和WebResponse类,这两个类都包含在System.Net命名空间中,利用这两个类可以实现很多高级的网络功能,本文中多线程文件下载就是利用这两个类实现的。 WebRequest和WebResponse都是抽象基类,因此在程序中不能直接作为对象使用,必须被继承,实际使用中,可根据URI参数中的URI前缀选用它们合适的子类,对于HTTP这类URI,HttpWebRequest和HttpWebResponse类可以用于处理客户程序同WEB服务器之间的HTTP通讯。

  HttpWebRequest类实现了很多通过HTTP访问WEB服务器上文件的高级功能。HttpWebRequest类对WebRequest中定义的属性和方法提供支持,HttpWebRequest将发送到Internet资源的公共HTTP标头的值公开为属性,由方法或系统设置,常用的由属性或方法设置的HTTP标头为:接受, 由Accept属性设置, 连接, 由Connection属性和KeepAlive属性设置, Content-Length, 由ContentLength属性设置, Content-Type, 由ContentType属性设置, 范围, 由AddRange方法设置. 实际使用中是将标头信息正确设置后,传递到WEB服务器,WEB服务器根据要求作出回应。

  HttpWebResponse类继承自WebResponse类,专门处理从WEB服务器返回的HTTP响应,这个类实现了很多方法,具有很多属性,可以全面处理接收到的互联网信息。在HttpWebResponse类中,对于大多数通用的HTTP标头字段,都有独立的属性与其对应,程序员可以通过这些属性方便的访问位于HTTP接收报文标头字段中的信息,本例中用到的HttpWebResponse类属性为:ContentLength 既接收内容的长度。

  有了以上的了解后,下面看看这两个类的用法,要创建HttpWebRequest对象,不要直接使用HttpWebRequest的构造函数,而要使用WebRequest.Create方法初始化一个HttpWebRequest实例,如: 

HttpWebRequest hwr=(HttpWebRequest)WebRequest.Create(http://www.163.com/); 
  创建了这个对象后,就可以通过HttpWebRequest属性,设置很多HTTP标头字段的内容,如hwr.AddRange(100,1000);设置接收对象的范围为100-1000字节。

  HttpWebReques对象使用GetResponse()方法时,会返回一个HttpWebResponse对象,为提出HTTP返回报文信息,需要使用HttpWebResponse的GetResponseStream()方法,该方法返回一个Stream对象,可以读取HTTP返回的报文,如:首先定义一个Strean 对象 public System.IO.Stream ns; 然后 ns=hwr.GetResponse ().GetResponseStream ();即可创建Stream对象。有了以上的准备知识后下面开始设计我们的多线程互联网文件的下载程序,首先打开Visual Studio.Net集成开发环境,选择“文件”、“新建”、“项目”,然后选择“Visual C#项目”,在向导右边列表框中选中“Windows应用程序”,输入项目名称,如本例为:httpftp,然后选择“确定”按钮,向导自动生成了一个Windows应用程序项目。首先打开窗口设计器设计应用程序窗口,增加如下控件:

  一个列表框 listBox1 三个文本标签 label1-label3 三个文本框 textBox1-textBox3 一个开始接收按钮 button1 设计好的窗口如下图:



  控件定义代码是: 

public System.Windows.Forms.ListBox listBox1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox textBox1
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.TextBox textBox2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.TextBox textBox3;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.TextBox textBox4;
  打开Form1的代码编辑器,增加如下的命名空间:

using System.Net;//网络功能 
using System.IO;//流支持
using System.Threading ;//线程支持
  增加如下的程序变量:

public bool[] threadw; //每个线程结束标志
public string[] filenamew;//每个线程接收文件的文件名
public int[] filestartw;//每个线程接收文件的起始位置
public int[] filesizew;//每个线程接收文件的大小
public string strurl;//接受文件的URL
public bool hb;//文件合并标志
public int thread;//进程数
 
 
 
很多人都有过使用网络蚂蚁或网络快车软件下载互联网文件的经历,这些软件的使用可以大大加速互联网上文件的传输速度,减少文件传输的时间。这些软件为什么有如此大的魔力呢?其主要原因是这些软件都采用了多线程下载和断点续传技术。如果我们自己来编写一个类似这样的程序,也能够快速的在互联网上下载文件,那一定是非常愉快的事情。下面我就讲一讲如何利用C#语言编写一个支持多线程下载文件的程序,你会看到利用C#语言编写网络应程序是多么的容易,从中也能体会到C#语言中强大的网络功能。

  首先介绍一下HTTP协议,HTTP亦即Hpyer Text Transfer Protocal的缩写,它是现代互联网上最重要的一种网络协议,超文本传输协议位于TCP/IP协议的应用层,是一个面向无连接、简单、快速的C/S结构的协议。HTTP的工作过程大体上分连接、请求、响应和断开连接四个步骤。C#语言对HTTP协议提供了良好的支持,在.NET类库中提供了WebRequest和WebResponse类,这两个类都包含在System.Net命名空间中,利用这两个类可以实现很多高级的网络功能,本文中多线程文件下载就是利用这两个类实现的。 WebRequest和WebResponse都是抽象基类,因此在程序中不能直接作为对象使用,必须被继承,实际使用中,可根据URI参数中的URI前缀选用它们合适的子类,对于HTTP这类URI,HttpWebRequest和HttpWebResponse类可以用于处理客户程序同WEB服务器之间的HTTP通讯。

  HttpWebRequest类实现了很多通过HTTP访问WEB服务器上文件的高级功能。HttpWebRequest类对WebRequest中定义的属性和方法提供支持,HttpWebRequest将发送到Internet资源的公共HTTP标头的值公开为属性,由方法或系统设置,常用的由属性或方法设置的HTTP标头为:接受, 由Accept属性设置, 连接, 由Connection属性和KeepAlive属性设置, Content-Length, 由ContentLength属性设置, Content-Type, 由ContentType属性设置, 范围, 由AddRange方法设置. 实际使用中是将标头信息正确设置后,传递到WEB服务器,WEB服务器根据要求作出回应。

  HttpWebResponse类继承自WebResponse类,专门处理从WEB服务器返回的HTTP响应,这个类实现了很多方法,具有很多属性,可以全面处理接收到的互联网信息。在HttpWebResponse类中,对于大多数通用的HTTP标头字段,都有独立的属性与其对应,程序员可以通过这些属性方便的访问位于HTTP接收报文标头字段中的信息,本例中用到的HttpWebResponse类属性为:ContentLength 既接收内容的长度。

  有了以上的了解后,下面看看这两个类的用法,要创建HttpWebRequest对象,不要直接使用HttpWebRequest的构造函数,而要使用WebRequest.Create方法初始化一个HttpWebRequest实例,如: 

HttpWebRequest hwr=(HttpWebRequest)WebRequest.Create(http://www.163.com/); 
  创建了这个对象后,就可以通过HttpWebRequest属性,设置很多HTTP标头字段的内容,如hwr.AddRange(100,1000);设置接收对象的范围为100-1000字节。

  HttpWebReques对象使用GetResponse()方法时,会返回一个HttpWebResponse对象,为提出HTTP返回报文信息,需要使用HttpWebResponse的GetResponseStream()方法,该方法返回一个Stream对象,可以读取HTTP返回的报文,如:首先定义一个Strean 对象 public System.IO.Stream ns; 然后 ns=hwr.GetResponse ().GetResponseStream ();即可创建Stream对象。有了以上的准备知识后下面开始设计我们的多线程互联网文件的下载程序,首先打开Visual Studio.Net集成开发环境,选择“文件”、“新建”、“项目”,然后选择“Visual C#项目”,在向导右边列表框中选中“Windows应用程序”,输入项目名称,如本例为:httpftp,然后选择“确定”按钮,向导自动生成了一个Windows应用程序项目。首先打开窗口设计器设计应用程序窗口,增加如下控件:

  一个列表框 listBox1 三个文本标签 label1-label3 三个文本框 textBox1-textBox3 一个开始接收按钮 button1 设计好的窗口如下图:



  控件定义代码是: 

public System.Windows.Forms.ListBox listBox1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox textBox1
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.TextBox textBox2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.TextBox textBox3;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.TextBox textBox4;
  打开Form1的代码编辑器,增加如下的命名空间:

using System.Net;//网络功能 
using System.IO;//流支持
using System.Threading ;//线程支持
  增加如下的程序变量:

public bool[] threadw; //每个线程结束标志
public string[] filenamew;//每个线程接收文件的文件名
public int[] filestartw;//每个线程接收文件的起始位置
public int[] filesizew;//每个线程接收文件的大小
public string strurl;//接受文件的URL
public bool hb;//文件合并标志
public int thread;//进程数
 
View Code

C#实现http多线程文件传输

网络蚂蚁或网络快车这些软件的使用可以大大加速互联网上文件的传输速度,减少文件传输的时间。主要原因是这些软件都采用了多线程下载和断点续传技术。下面我就讲一讲如何利用C#语言编写一个支持多线程下载文件的程序。

  首先介绍一下HTTP协议,HTTP亦即Hpyer Text Transfer Protocal的缩写,是现代互联网上最重要的一种网络协议,超文本传输协议位于TCP/IP协议的应用层,是一个面向无连接、简单、快速的C/S结构的协议。HTTP的工作过程大体上分连接、请求、响应和断开连接四个步骤。C#语言对HTTP协议提供了良好的支持,在.NET类库中提供了WebRequest和WebResponse类,这两个类都包含在System.Net命名空间中,利用这两个类可以实现很多高级的网络功能,本文中多线程文件下载就是利用这两个类实现的。
 
        WebRequest和WebResponse都是抽象基类,因此在程序中不能直接作为对象使用,必须被继承,实际使用中,可根据URI参数中的URI前缀选用它们合适的子类,对于HTTP这类URI,HttpWebRequest和HttpWebResponse类可以用于处理客户程序同WEB服务器之间的HTTP通讯。

  HttpWebRequest类实现了很多通过HTTP访问WEB服务器上文件的高级功能。HttpWebRequest类对WebRequest中定义的属性和方法提供支持,HttpWebRequest将发送到Internet资源的公共HTTP标头的值公开为属性,由方法或系统设置,常用的由属性或方法设置的HTTP标头为:接受, 由Accept属性设置, 连接, 由Connection属性和KeepAlive属性设置, Content-Length, 由ContentLength属性设置, Content-Type, 由ContentType属性设置, 范围, 由AddRange方法设置. 实际使用中是将标头信息正确设置后,传递到WEB服务器,WEB服务器根据要求作出回应。

  HttpWebResponse类继承自WebResponse类,专门处理从WEB服务器返回的HTTP响应,这个类实现了很多方法,具有很多属性,可以全面处理接收到的互联网信息。在HttpWebResponse类中,对于大多数通用的HTTP标头字段,都有独立的属性与其对应,程序员可以通过这些属性方便的访问位于HTTP接收报文标头字段中的信息,本例中用到的HttpWebResponse类属性为:ContentLength 既接收内容的长度。
  有了以上的了解后,下面看看这两个类的用法,要创建HttpWebRequest对象,不要直接使用HttpWebRequest的构造函数,而要使用WebRequest.Create方法初始化一个HttpWebRequest实例,如:
HttpWebRequest hwr=(HttpWebRequest)WebRequest.Create(http://www.163.com/);
创建了这个对象后,就可以通过HttpWebRequest属性,设置很多HTTP标头字段的内容,如hwr.AddRange(100,1000);设置接收对象的范围为100-1000字节。
  HttpWebReques对象使用GetResponse()方法时,会返回一个HttpWebResponse对象,为提出HTTP返回报文信息,需要使用HttpWebResponse的GetResponseStream()方法,该方法返回一个Stream对象,可以读取HTTP返回的报文,如:首先定义一个Strean 对象 public System.IO.Stream ns; 然后 ns=hwr.GetResponse ().GetResponseStream ();即可创建Stream对象。有了以上的准备知识后下面开始设计我们的多线程互联网文件的下载程序,首先打开Visual Studio.Net集成开发环境,选择“文件”、“新建”、“项目”,然后选择“Visual C#项目”,在向导右边列表框中选中“Windows 应用程序”,输入项目名称,如本例为:httpftp,然后选择“确定”按钮,向导自动生成了一个Windows应用程序项目。首先打开窗口设计器设计应用程序窗口,增加如下控件:
  一个列表框 listBox1 三个文本标签 label1-label3 三个文本框 textBox1-textBox3 一个开始接收按钮 button1 设计好的窗口如下图:
 

控件定义代码是:
public  System.Windows.Forms.ListBox listBox1;

private System.Windows.Forms.Label label1;

private System.Windows.Forms.TextBox textBox1

private System.Windows.Forms.Button button1;

private System.Windows.Forms.Label label2;

private System.Windows.Forms.TextBox textBox2;

private System.Windows.Forms.Label label3;

private System.Windows.Forms.TextBox textBox3;

private System.Windows.Forms.Label label4;

private System.Windows.Forms.TextBox textBox4;
 
 
打开Form1的代码编辑器,增加如下的命名空间:
using System.Net;//网络功能

using System.IO;//流支持

using System.Threading ;//线程支持
 
 
增加如下的程序变量:
public bool[] threadw; //每个线程结束标志

public string[] filenamew;//每个线程接收文件的文件名

public int[] filestartw;//每个线程接收文件的起始位置

public int[] filesizew;//每个线程接收文件的大小

public string strurl;//接受文件的URL

public bool hb;//文件合并标志

public int thread;//进程数
 
 
定义一个HttpFile类,用于管理接收线程,其代码如下: 
    public class HttpFile

    {

        public Form1 formm;

        public int threadh;//线程代号

        public string filename;//文件名

        public string strUrl;//接收文件的URL

        public FileStream fs;

        public HttpWebRequest request;

        public System.IO.Stream ns;

        public byte[] nbytes;//接收缓冲区

        public int nreadsize;//接收字节数


        public HttpFile(Form1 form, int thread)//构造方法

        {

            formm = form;

            threadh = thread;

        }


        ~HttpFile()//析构方法

        {

            formm.Dispose();

        }


        public void receive()//接收线程

        {

            filename = formm.filenamew[threadh];

            strUrl = formm.strurl;

            ns = null;

            nbytes = new byte[512];

            nreadsize = 0;


            formm.listBox1.Items.Add("线程" + threadh.ToString() + "开始接收");

            fs = new FileStream(filename, System.IO.FileMode.Create);


            try

            {

                request = (HttpWebRequest)HttpWebRequest.Create(strUrl);

                //接收的起始位置及接收的长度

                request.AddRange(formm.filestartw[threadh],

                        formm.filestartw[threadh] + formm.filesizew[threadh]);

                ns = request.GetResponse().GetResponseStream();//获得接收流

                nreadsize = ns.Read(nbytes, 0, 512);


                while (nreadsize > 0)

                {

                    fs.Write(nbytes, 0, nreadsize);

                    nreadsize = ns.Read(nbytes, 0, 512);

                    formm.listBox1.Items.Add("线程" + threadh.ToString() + "正在接收");

                }


                fs.Close();

                ns.Close();

            }

            catch (Exception er)

            {

                MessageBox.Show(er.Message);

                fs.Close();

            }


            formm.listBox1.Items.Add("进程" + threadh.ToString() + "接收完毕!");

            formm.threadw[threadh] = true;

        }

    }





该类和Form1类处于统一命名空间,但不包含在Form1类中。下面定义“开始接收”按钮控件的事件响应函数: 

    private void button1_Click(object sender, System.EventArgs e)

    {

        DateTime dt = DateTime.Now;//开始接收时间

        textBox1.Text = dt.ToString();

        strurl = textBox2.Text.Trim().ToString();

        HttpWebRequest request;

        long filesize = 0;

        try

        {

            request = (HttpWebRequest)HttpWebRequest.Create(strurl);

            filesize = request.GetResponse().ContentLength;//取得目标文件的长度

            request.Abort();

        }

        catch (Exception er)

        {

            MessageBox.Show(er.Message);

        }

        // 接收线程数

        thread = Convert.ToInt32(textBox4.Text.Trim().ToString(), 10);

        //根据线程数初始化数组

        threadw = new bool[thread];

        filenamew = new string[thread];

        filestartw = new int[thread];

        filesizew = new int[thread];


        //计算每个线程应该接收文件的大小

        int filethread = (int)filesize / thread;//平均分配

        int filethreade = filethread + (int)filesize % thread;//剩余部分由最后一个线程完成

        //为数组赋值

        for (int i = 0; i < thread; i++)

        {

            threadw[i] = false;//每个线程状态的初始值为假

            filenamew[i] = i.ToString() + ".dat";//每个线程接收文件的临时文件名

            if (i < thread - 1)

            {

                filestartw[i] = filethread * i;//每个线程接收文件的起始点

                filesizew[i] = filethread - 1;//每个线程接收文件的长度


            }

            else

            {

                filestartw[i] = filethread * i;

                filesizew[i] = filethreade - 1;

            }

        }

        //定义线程数组,启动接收线程

        Thread[] threadk = new Thread[thread];

        HttpFile[] httpfile = new HttpFile[thread];

        for (int j = 0; j < thread; j++)

        {

            httpfile[j] = new HttpFile(this, j);

            threadk[j] = new Thread(new ThreadStart(httpfile[j].receive));

            threadk[j].Start();

        }

        //启动合并各线程接收的文件线程

        Thread hbth = new Thread(new ThreadStart(hbfile));

        hbth.Start();


    }




合并文件的线程hbfile定义在Form1类中,定义如下:
    public void hbfile()

    {

        while (true)//等待

        {

            hb = true;

            for (int i = 0; i < thread; i++)

            {

                if (threadw[i] == false)//有未结束线程,等待

                {

                    hb = false;

                    Thread.Sleep(100);

                    break;

                }

            }

            if (hb == true)//所有线程均已结束,停止等待,

            {

                break;

            }

        }

        FileStream fs;//开始合并

         FileStream fstemp;


        int readfile;

        byte[] bytes = new byte[512];

        fs =

            new FileStream(textBox3.Text.Trim().ToString(),

                System.IO.FileMode.Create);


        for (int k = 0; k < thread; k++)

        {

            fstemp = new FileStream(filenamew[k], System.IO.FileMode.Open);

            while (true)

            {

                readfile = fstemp.Read(bytes, 0, 512);

                if (readfile > 0)

                {

                    fs.Write(bytes, 0, readfile);

                }

                else

                {

                    break;

                }

            }

            fstemp.Close();

        }

        fs.Close();

        DateTime dt = DateTime.Now;

        textBox1.Text = dt.ToString();//结束时间

          MessageBox.Show("接收完毕!!!");

    }





  至此,一个多线程下载文件的程序就大功告成了,注意在输入本地文件名时,应按如下格式输入:
“c:\\test\\httpftp\\bin\\d.htm”,
因”\”后的字符在C#中是转义字符,线程数并非越大越好,一般5个线程就可以了,该程序在Visual Studio.Net 2002开发环境及Windows xp 操作系统上通过。
 
View Code
网络蚂蚁或网络快车这些软件的使用可以大大加速互联网上文件的传输速度,减少文件传输的时间。主要原因是这些软件都采用了多线程下载和断点续传技术。下面我就讲一讲如何利用C#语言编写一个支持多线程下载文件的程序。

  首先介绍一下HTTP协议,HTTP亦即Hpyer Text Transfer Protocal的缩写,是现代互联网上最重要的一种网络协议,超文本传输协议位于TCP/IP协议的应用层,是一个面向无连接、简单、快速的C/S结构的协议。HTTP的工作过程大体上分连接、请求、响应和断开连接四个步骤。C#语言对HTTP协议提供了良好的支持,在.NET类库中提供了WebRequest和WebResponse类,这两个类都包含在System.Net命名空间中,利用这两个类可以实现很多高级的网络功能,本文中多线程文件下载就是利用这两个类实现的。
 
        WebRequest和WebResponse都是抽象基类,因此在程序中不能直接作为对象使用,必须被继承,实际使用中,可根据URI参数中的URI前缀选用它们合适的子类,对于HTTP这类URI,HttpWebRequest和HttpWebResponse类可以用于处理客户程序同WEB服务器之间的HTTP通讯。

  HttpWebRequest类实现了很多通过HTTP访问WEB服务器上文件的高级功能。HttpWebRequest类对WebRequest中定义的属性和方法提供支持,HttpWebRequest将发送到Internet资源的公共HTTP标头的值公开为属性,由方法或系统设置,常用的由属性或方法设置的HTTP标头为:接受, 由Accept属性设置, 连接, 由Connection属性和KeepAlive属性设置, Content-Length, 由ContentLength属性设置, Content-Type, 由ContentType属性设置, 范围, 由AddRange方法设置. 实际使用中是将标头信息正确设置后,传递到WEB服务器,WEB服务器根据要求作出回应。

  HttpWebResponse类继承自WebResponse类,专门处理从WEB服务器返回的HTTP响应,这个类实现了很多方法,具有很多属性,可以全面处理接收到的互联网信息。在HttpWebResponse类中,对于大多数通用的HTTP标头字段,都有独立的属性与其对应,程序员可以通过这些属性方便的访问位于HTTP接收报文标头字段中的信息,本例中用到的HttpWebResponse类属性为:ContentLength 既接收内容的长度。
  有了以上的了解后,下面看看这两个类的用法,要创建HttpWebRequest对象,不要直接使用HttpWebRequest的构造函数,而要使用WebRequest.Create方法初始化一个HttpWebRequest实例,如:
HttpWebRequest hwr=(HttpWebRequest)WebRequest.Create(http://www.163.com/);
创建了这个对象后,就可以通过HttpWebRequest属性,设置很多HTTP标头字段的内容,如hwr.AddRange(100,1000);设置接收对象的范围为100-1000字节。
  HttpWebReques对象使用GetResponse()方法时,会返回一个HttpWebResponse对象,为提出HTTP返回报文信息,需要使用HttpWebResponse的GetResponseStream()方法,该方法返回一个Stream对象,可以读取HTTP返回的报文,如:首先定义一个Strean 对象 public System.IO.Stream ns; 然后 ns=hwr.GetResponse ().GetResponseStream ();即可创建Stream对象。有了以上的准备知识后下面开始设计我们的多线程互联网文件的下载程序,首先打开Visual Studio.Net集成开发环境,选择“文件”、“新建”、“项目”,然后选择“Visual C#项目”,在向导右边列表框中选中“Windows 应用程序”,输入项目名称,如本例为:httpftp,然后选择“确定”按钮,向导自动生成了一个Windows应用程序项目。首先打开窗口设计器设计应用程序窗口,增加如下控件:
  一个列表框 listBox1 三个文本标签 label1-label3 三个文本框 textBox1-textBox3 一个开始接收按钮 button1 设计好的窗口如下图:
 

控件定义代码是:
public  System.Windows.Forms.ListBox listBox1;

private System.Windows.Forms.Label label1;

private System.Windows.Forms.TextBox textBox1

private System.Windows.Forms.Button button1;

private System.Windows.Forms.Label label2;

private System.Windows.Forms.TextBox textBox2;

private System.Windows.Forms.Label label3;

private System.Windows.Forms.TextBox textBox3;

private System.Windows.Forms.Label label4;

private System.Windows.Forms.TextBox textBox4;
 
 
打开Form1的代码编辑器,增加如下的命名空间:
using System.Net;//网络功能

using System.IO;//流支持

using System.Threading ;//线程支持
 
 
增加如下的程序变量:
public bool[] threadw; //每个线程结束标志

public string[] filenamew;//每个线程接收文件的文件名

public int[] filestartw;//每个线程接收文件的起始位置

public int[] filesizew;//每个线程接收文件的大小

public string strurl;//接受文件的URL

public bool hb;//文件合并标志

public int thread;//进程数
 
 
定义一个HttpFile类,用于管理接收线程,其代码如下: 
    public class HttpFile

    {

        public Form1 formm;

        public int threadh;//线程代号

        public string filename;//文件名

        public string strUrl;//接收文件的URL

        public FileStream fs;

        public HttpWebRequest request;

        public System.IO.Stream ns;

        public byte[] nbytes;//接收缓冲区

        public int nreadsize;//接收字节数


        public HttpFile(Form1 form, int thread)//构造方法

        {

            formm = form;

            threadh = thread;

        }


        ~HttpFile()//析构方法

        {

            formm.Dispose();

        }


        public void receive()//接收线程

        {

            filename = formm.filenamew[threadh];

            strUrl = formm.strurl;

            ns = null;

            nbytes = new byte[512];

            nreadsize = 0;


            formm.listBox1.Items.Add("线程" + threadh.ToString() + "开始接收");

            fs = new FileStream(filename, System.IO.FileMode.Create);


            try

            {

                request = (HttpWebRequest)HttpWebRequest.Create(strUrl);

                //接收的起始位置及接收的长度

                request.AddRange(formm.filestartw[threadh],

                        formm.filestartw[threadh] + formm.filesizew[threadh]);

                ns = request.GetResponse().GetResponseStream();//获得接收流

                nreadsize = ns.Read(nbytes, 0, 512);


                while (nreadsize > 0)

                {

                    fs.Write(nbytes, 0, nreadsize);

                    nreadsize = ns.Read(nbytes, 0, 512);

                    formm.listBox1.Items.Add("线程" + threadh.ToString() + "正在接收");

                }


                fs.Close();

                ns.Close();

            }

            catch (Exception er)

            {

                MessageBox.Show(er.Message);

                fs.Close();

            }


            formm.listBox1.Items.Add("进程" + threadh.ToString() + "接收完毕!");

            formm.threadw[threadh] = true;

        }

    }





该类和Form1类处于统一命名空间,但不包含在Form1类中。下面定义“开始接收”按钮控件的事件响应函数: 

    private void button1_Click(object sender, System.EventArgs e)

    {

        DateTime dt = DateTime.Now;//开始接收时间

        textBox1.Text = dt.ToString();

        strurl = textBox2.Text.Trim().ToString();

        HttpWebRequest request;

        long filesize = 0;

        try

        {

            request = (HttpWebRequest)HttpWebRequest.Create(strurl);

            filesize = request.GetResponse().ContentLength;//取得目标文件的长度

            request.Abort();

        }

        catch (Exception er)

        {

            MessageBox.Show(er.Message);

        }

        // 接收线程数

        thread = Convert.ToInt32(textBox4.Text.Trim().ToString(), 10);

        //根据线程数初始化数组

        threadw = new bool[thread];

        filenamew = new string[thread];

        filestartw = new int[thread];

        filesizew = new int[thread];


        //计算每个线程应该接收文件的大小

        int filethread = (int)filesize / thread;//平均分配

        int filethreade = filethread + (int)filesize % thread;//剩余部分由最后一个线程完成

        //为数组赋值

        for (int i = 0; i < thread; i++)

        {

            threadw[i] = false;//每个线程状态的初始值为假

            filenamew[i] = i.ToString() + ".dat";//每个线程接收文件的临时文件名

            if (i < thread - 1)

            {

                filestartw[i] = filethread * i;//每个线程接收文件的起始点

                filesizew[i] = filethread - 1;//每个线程接收文件的长度


            }

            else

            {

                filestartw[i] = filethread * i;

                filesizew[i] = filethreade - 1;

            }

        }

        //定义线程数组,启动接收线程

        Thread[] threadk = new Thread[thread];

        HttpFile[] httpfile = new HttpFile[thread];

        for (int j = 0; j < thread; j++)

        {

            httpfile[j] = new HttpFile(this, j);

            threadk[j] = new Thread(new ThreadStart(httpfile[j].receive));

            threadk[j].Start();

        }

        //启动合并各线程接收的文件线程

        Thread hbth = new Thread(new ThreadStart(hbfile));

        hbth.Start();


    }




合并文件的线程hbfile定义在Form1类中,定义如下:
    public void hbfile()

    {

        while (true)//等待

        {

            hb = true;

            for (int i = 0; i < thread; i++)

            {

                if (threadw[i] == false)//有未结束线程,等待

                {

                    hb = false;

                    Thread.Sleep(100);

                    break;

                }

            }

            if (hb == true)//所有线程均已结束,停止等待,

            {

                break;

            }

        }

        FileStream fs;//开始合并

         FileStream fstemp;


        int readfile;

        byte[] bytes = new byte[512];

        fs =

            new FileStream(textBox3.Text.Trim().ToString(),

                System.IO.FileMode.Create);


        for (int k = 0; k < thread; k++)

        {

            fstemp = new FileStream(filenamew[k], System.IO.FileMode.Open);

            while (true)

            {

                readfile = fstemp.Read(bytes, 0, 512);

                if (readfile > 0)

                {

                    fs.Write(bytes, 0, readfile);

                }

                else

                {

                    break;

                }

            }

            fstemp.Close();

        }

        fs.Close();

        DateTime dt = DateTime.Now;

        textBox1.Text = dt.ToString();//结束时间

          MessageBox.Show("接收完毕!!!");

    }





  至此,一个多线程下载文件的程序就大功告成了,注意在输入本地文件名时,应按如下格式输入:
“c:\\test\\httpftp\\bin\\d.htm”,
因”\”后的字符在C#中是转义字符,线程数并非越大越好,一般5个线程就可以了,该程序在Visual Studio.Net 2002开发环境及Windows xp 操作系统上通过。
 
View Code

C#实现文件下载

一.概述:
本文通过一个实例向大家介绍用Visual C#进行Internet通讯编程的一些基本知识。我们知道.Net类包含了请求/响应层、应用协议层、传输层等层次。在本程序中,我们运用了位于请求/响应层的WebRequest类以及WebClient类等来实现高抽象程度的Internet通讯服务。本程序的功能是完成网络文件的下载。
二.实现原理:
程序实现的原理比较简单,主要用到了WebClient类和FileStream类。其中WebClient类处于System.Net名字空间中,该类的主要功能是提供向URI标识的资源发送数据和从URI标识的资源接收数据的公共方法。我们利用其中的DownloadFile()方法将网络文件下载到本地。然后用FileStream类的实例对象以数据流的方式将文件数据写入本地文件。这样就完成了网络文件的下载。
三.实现步骤:
首先,打开Visual Studio.Net,新建一个Visual C# Windows应用程序的工程,不妨命名为"MyGetCar"。
接着,布置主界面。我们先往主窗体上添加如下控件:两个标签控件、两个文本框控件、一个按钮控件以及一个状态栏控件。
完成主窗体的设计,我们接着完成代码的编写。
在理解了基本原理的基础上去完成代码的编写是相当容易。程序中我们主要用到的是WebClient类,不过在我们调用WebClient类的实例对象前,我们需要用WebRequest类的对象发出对统一资源标识符(URI)的请求。
 
            try

            {

                WebRequest myre = WebRequest.Create(URLAddress);

            }

            catch (WebException exp)

            {

                MessageBox.Show(exp.Message, "Error");

            }
这是一个try-catch语句,try块完成向URI的请求,catch块则捕捉可能的异常并显示异常信息。其中的URLAddress为被请求的网络主机名。
在请求成功后,我们就可以运用WebClient类的实例对象中的DownloadFile()方法实现文件的下载了。其函数原型如下:
public void DownloadFile( string address, string fileName);
其中,参数address为从中下载数据的 URI,fileName为要接收数据的本地文件的名称。
之后我们用OpenRead()方法来打开一个可读的流,该流完成从具有指定URI的资源下载数据的功能。其函数原型如下:
public Stream OpenRead(string address);
其中,参数address同上。
最后就是新建一个StreamReader对象从中读取文件的数据,并运用一个while循环体不断读取数据,只到读完所有的数据。
还有在使用以上方法时,你将可能需要处理以下几种异常:
WebException:下载数据时发生错误。
UriFormatException:通过组合 BaseAddress、address 和 QueryString 所构成的 URI 无效。
这部分的代码如下:(client为WebClient对象,在本类的开头处声明即可)
 
            statusBar.Text = "开始下载文件...";

            client.DownloadFile(URLAddress, fileName);


            Stream str = client.OpenRead(URLAddress);

            StreamReader reader = new StreamReader(str);


            byte[] mbyte = new byte[100000];

            int allmybyte = (int)mbyte.Length;

            int startmbyte = 0;


            statusBar.Text = "正在接收数据...";


            while (allmybyte > 0)

            {

                int m = str.Read(mbyte, startmbyte, allmybyte);

                if (m == 0)

                    break;


                startmbyte += m;

                allmybyte -= m;

            }
完成了文件数据的读取工作后,我们运用FileStream类的实例对象将这些数据写入本地文件中:
FileStream fstr = new FileStream(Path,FileMode.OpenOrCreate,FileAccess.Write);
fstr.Write(mbyte,0,startmbyte);
这样,程序主体部分的代码已经完成了,不过要完成全部程序还需要一些工作。由于在程序接收网络文件数据的时候运用到了while循环体,这样会很占程序资源,表现的形式就是主窗体不能自由移动。为了解决这个问题,我们在程序中用到了多线程机制。我们在响应按钮的事件中新建一个线程,该线程就是用来实现网络文件下载功能的。如此,文件下载的线程和程序主线程并存,共享进程资源,使得程序顺畅运行。这样,我们在按钮控件的消息响应函数里添加如下代码:
Thread th = new Thread(new ThreadStart(StartDownload));
th.Start();
该线程的实现函数就是StartDownload(),而上面介绍的那些代码就是这个函数的主体部分。
最后,因为程序中运用到了WebRequest、WebClient、FileStream、Thread等类,所以最重要的就是在程序的开始处添加如下名字空间:
using System.Net;
using System.IO;
using System.Threading;
下面就是程序的源代码: 
 
using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

using System.Net;

using System.IO;

using System.Threading;


namespace MyGetCar

{


    /// 

    /// Form1 的摘要说明。

    /// 


    public class Form1 : System.Windows.Forms.Form

    {


        private System.Windows.Forms.Label label1;

        private System.Windows.Forms.Label label2;

        private System.Windows.Forms.TextBox srcAddress;

        private System.Windows.Forms.TextBox tarAddress;

        private System.Windows.Forms.StatusBar statusBar;

        private System.Windows.Forms.Button Start;


        private WebClient client = new WebClient();


        /// 

        /// 必需的设计器变量。

        /// 


        private System.ComponentModel.Container components = null;


        public Form1()

        {


            //

            // Windows 窗体设计器支持所必需的

            //


            InitializeComponent();


            //

            // TODO: 在 InitializeComponent 调用后添加任何构造函数代码

            //

        }


        /// 

        /// 清理所有正在使用的资源。

        /// 

        protected override void Dispose(bool disposing)

        {

            if (disposing)

            {

                if (components != null)

                {

                    components.Dispose();

                }

            }


            base.Dispose(disposing);

        }


        #region Windows Form Designer generated code


        /// 

        /// 设计器支持所需的方法 - 不要使用代码编辑器修改

        /// 此方法的内容。

        /// 

        private void InitializeComponent()

        {

            this.label1 = new System.Windows.Forms.Label();

            this.label2 = new System.Windows.Forms.Label();

            this.srcAddress = new System.Windows.Forms.TextBox();

            this.tarAddress = new System.Windows.Forms.TextBox();

            this.statusBar = new System.Windows.Forms.StatusBar();

            this.Start = new System.Windows.Forms.Button();

            this.SuspendLayout();


            //

            // label1

            //


            this.label1.Location = new System.Drawing.Point(8, 32);

            this.label1.Name = "label1";

            this.label1.Size = new System.Drawing.Size(72, 23);

            this.label1.TabIndex = 0;

            this.label1.Text = "文件地址:";

            this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleRight;


            //

            // label2

            //


            this.label2.Location = new System.Drawing.Point(8, 72);

            this.label2.Name = "label2";

            this.label2.Size = new System.Drawing.Size(72, 23);

            this.label2.TabIndex = 1;

            this.label2.Text = "另存到:";

            this.label2.TextAlign = System.Drawing.ContentAlignment.MiddleRight;


            //

            // srcAddress

            //


            this.srcAddress.Location = new System.Drawing.Point(80, 32);

            this.srcAddress.Name = "srcAddress";

            this.srcAddress.Size = new System.Drawing.Size(216, 21);

            this.srcAddress.TabIndex = 2;

            this.srcAddress.Text = "";


            //

            // tarAddress

            //


            this.tarAddress.Location = new System.Drawing.Point(80, 72);

            this.tarAddress.Name = "tarAddress";

            this.tarAddress.Size = new System.Drawing.Size(216, 21);

            this.tarAddress.TabIndex = 3;

            this.tarAddress.Text = "";


            //

            // statusBar

            //


            this.statusBar.Location = new System.Drawing.Point(0, 151);

            this.statusBar.Name = "statusBar";

            this.statusBar.Size = new System.Drawing.Size(312, 22);

            this.statusBar.TabIndex = 4;


            //

            // Start

            //


            this.Start.FlatStyle = System.Windows.Forms.FlatStyle.Flat;

            this.Start.Location = new System.Drawing.Point(216, 112);

            this.Start.Name = "Start";

            this.Start.Size = new System.Drawing.Size(75, 24);

            this.Start.TabIndex = 5;

            this.Start.Text = "开始下载";

            this.Start.Click += new System.EventHandler(this.Start_Click);


            //

            // Form1

            //


            this.AutoScaleBaseSize = new System.Drawing.Size(6, 14);

            this.ClientSize = new System.Drawing.Size(312, 173);


            this.Controls.AddRange(new System.Windows.Forms.Control[]

            {

                this.Start,

                this.statusBar,

                this.tarAddress,

                this.srcAddress,

                this.label2,

                this.label1});


            this.MaximizeBox = false;

            this.Name = "Form1";

            this.Text = "文件下载器";


            this.ResumeLayout(false);

        }


        #endregion


        /// 

        /// 应用程序的主入口点。

        /// 

        [STAThread]

        static void Main()

        {

            Application.Run(new Form1());

        }


        private void StartDownload()

        {

            Start.Enabled = false;


            string URL = srcAddress.Text;

            int n = URL.LastIndexOf("/");

            string URLAddress = URL.Substring(0, n);

            string fileName = URL.Substring(n + 1, URL.Length - n - 1);

            string Dir = tarAddress.Text;

            string Path = Dir + "\\" + fileName;


            try

            {

                WebRequest myre = WebRequest.Create(URLAddress);

            }

            catch (WebException exp)

            {

                MessageBox.Show(exp.Message, "Error");

            }


            try

            {

                statusBar.Text = "开始下载文件...";

                client.DownloadFile(URLAddress, fileName);


                Stream str = client.OpenRead(URLAddress);

                StreamReader reader = new StreamReader(str);


                byte[] mbyte = new byte[100000];

                int allmybyte = (int)mbyte.Length;

                int startmbyte = 0;


                statusBar.Text = "正在接收数据...";


                while (allmybyte > 0)

                {

                    int m = str.Read(mbyte, startmbyte, allmybyte);


                    if (m == 0)

                        break;


                    startmbyte += m;


                    allmybyte -= m;

                }


                FileStream fstr =

                    new FileStream(Path, FileMode.OpenOrCreate, FileAccess.Write);


                fstr.Write(mbyte, 0, startmbyte);

                str.Close();

                fstr.Close();


                statusBar.Text = "下载完毕!";

            }

            catch (WebException exp)

            {

                MessageBox.Show(exp.Message, "Error");


                statusBar.Text = "";

            }


            Start.Enabled = true;

        }


        private void Start_Click(object sender, System.EventArgs e)

        {

            Thread th = new Thread(new ThreadStart(StartDownload));


            th.Start();

        }

    }

}

 
四.总结:
以上我通过一个实例向大家展示了如何用Visual C#实现网络文件的下载,我们不难发现用Visual C#进行Internet通讯编程是非常方便的。在上面的程序中,我们仅仅用到了WebClient类的一些方法,而WebClient类不光提供了网络文件下载的方法,还提供了文件上传的方法,有兴趣的读者不妨一试――用之实现一个文件上传器。同时这个程序只是一个非常简单的例子,程序下载完一个网页后,它所获得的仅仅是主页面的内容,并不能获得其中的图片、CSS等文件,所以要做出一个比较好的文件下载器还需读者进一步改进之。
 
View Code

C#使用TcpIp实现下载文件介绍

使用tcp/ip传送文件。简单设计思路是在服务器端启动TcpListener监听客户端的Socket连接。
当Client发送连接请求,TcpListener捕获当前请求的Socket,并获取收到的数据(字符串,称为命令)。
然后由命令处理程序分析字符串,如果字符串头部包含GET_FILE则为下载文件请求。

例:如客户机向服务器程序发送请求:"GET_FILE|D:\PUBLISH\TEST.DLL"。首先TcpListener捕获
当前请求的Socket,收到字符串,如果是下载文件请求就通过socket.SendFile(file="D:\PUBLISH\
TEST.DLL")将文件传送给当前Socket。在客户端由NetworkStream.Read()方法接收来自服务器发送的文件。

同理,如果实现网上象棋对战游戏,通过Socket.Send(命令)->分析->执行->移动棋子,不就实现了吗?

Socket应用往往不止这些,学会Socket组件是网络编程基础。
 
服务器界面运行图。



客户机界面截图:


输入要下载的文件名及保存目录。
 
思考:
1.如果提供一个下载文件清单,是不是可以批量下载文件?
2.如果服务器上的文件与本机的文件最后修改日期相比较,是不是可以改装为版本升级程序?
3.如果客户端向服务器上传文件,是不是可以改装成mini版的ftp程序?
4.如果收到的byte[]是某对象的序列化数据,而后将byte[]还原成对象,是不是实现tcp/ip传送对象?
5.如果对象是一个电脑向另一个电脑传送的某个命令,是不是可以实现网上对战游戏?
6.你思想有多远socket就能走多远。
 
部分原代码:
UpgraderClient.cs
 
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.IO;

namespace VJSDN.Tech.TcpIpDownloader
{
   /// <summary> 
   /// Tcp/ip下载器客户端 
   /// </summary> 
   public class UpgraderClient
   {
      private OutputInvoke _writeOutput = null;
      
      public UpgraderClient(OutputInvoke writeOutput)
      {
         _writeOutput = writeOutput;
      }
      
      /// <summary> 
      /// 下载文件 
      /// </summary> 
      public bool DownloadFile(string file, string savePath)
      {
         string hostIp = "8.8.8.2"; //主机ip 
         int hostPort = 12346; //主机端口 
         
         try
         {
            TcpClient client = new TcpClient();
            client.ReceiveTimeout = 1000 * 60;
            
            if (client == null || file.Trim() == "") return false;
            
            TryConnect(client, hostIp, hostPort); //连接服务器 
            if (!client.Connected) return false;//连线线失败,退出 
            
            byte[] bs = Encoding.Unicode.GetBytes("GET_FILE|" + file);
            client.Client.Send(bs); //发送请求 
            
            //开始接受数据.... 
            NetworkStream ns = client.GetStream();
            MemoryStream ms = new System.IO.MemoryStream();
            
            byte[] resBytes = new byte[256];
            int resSize;
            do
            {
               resSize = ns.Read(resBytes, 0, resBytes.Length);
               string msg = Byte2Str(resBytes);
               if (msg.Trim().ToUpper() == "FILE_NOT_FOUND")
               {
                  if (_writeOutput != null) _writeOutput("找不到文件:" + file);
                  break;
               }
               if (resSize == 0) break;
               
               ms.Write(resBytes, 0, resSize);
               } while (ns.DataAvailable);
               
               if (ms.Length > 0)
               {
                  FileStream fs = File.Open(savePath, FileMode.OpenOrCreate, FileAccess.Write);
                  fs.Write(ms.ToArray(), 0, (int)ms.Length);
                  fs.Flush();
                  fs.Close(); //关闭文件流 
                  if (_writeOutput != null) _writeOutput("文件已下载:" + file);
               }
               
               ms.Close(); //关闭内存流 
               client.Close();
               return true;
            }
            catch
            {
               return false;
            }
         }
         
         private string Byte2Str(byte[] buffer)
         {
            string msg = Encoding.Unicode.GetString(buffer).Replace("\0", "");
            return msg.Trim();
         }
         
         private void TryConnect(TcpClient client, string hostIP, int hostPort)
         {
            try
            {
               client.Connect(IPAddress.Parse(hostIP), hostPort); //连接服务器 
            }
            catch
            {
               throw new Exception("主机已关闭或网络存在问题,不能建立连线!");
            }
         }
      }
   }
   
   
UpgraderServer.cs
 
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Net;
using System.Windows.Forms;
using System.IO;

namespace VJSDN.Tech.TcpIpDownloader
{
   public delegate void OutputInvoke(string msg);
   
   public class UpgraderServer
   {
      private TcpListener _ServerListener = null; //本地监 听 器 
      private Thread _ServerListenerThread = null; //监 听 器使用的线程 
      private bool _ServerRunning = false;
      
      private Form _owner = null;
      private OutputInvoke _writeOutput = null;
      
      public UpgraderServer(Form owner, OutputInvoke method)
      {
         _owner = owner;
         _writeOutput = method;
      }
      
      //启动服务器程序. 参数说明 ip:本地消息服务器IP地址; port:端口 
      public void StartListening(string ip, int port)
      {
         if (_ServerRunning) return;
         
         //构建监 听 器 
         _ServerListener = new TcpListener(IPAddress.Parse(ip), port);
         _ServerListener.Start(255);
         _ServerRunning = true;
         
         //启动线程 
         _ServerListenerThread = new Thread(new ThreadStart(DoStartServerListener));
         _ServerListenerThread.IsBackground = true;
         _ServerListenerThread.Start();
      }
      
      //关闭服务器程序. 
      public void Stop()
      {
         _ServerRunning = false;
         _ServerListenerThread.Abort(101);
         _ServerListenerThread = null;
         _ServerListener.Stop();
      }
      
      /// <summary> 
      ///启动服务器程序. 
      /// </summary> 
      private void DoStartServerListener()
      {
         //监听客户连线请求 
         while (_ServerRunning)
         {
            try
            {
               if (_ServerListener == null) return; //防止其它地方关闭监 听 器 
               Application.DoEvents();
               Socket socket = _ServerListener.AcceptSocket(); //有客户请求连接 
               if (socket == null) continue;
               
               byte[] buffer = new Byte[socket.ReceiveBufferSize];
               int i = socket.Receive(buffer); //接收请求数据. 
               if (i <= 0) continue;
               
               string msg = Encoding.Unicode.GetString(buffer).Replace("\0", "");
               msg = msg.Trim();
               if (msg.ToUpper().StartsWith("GET_FILE")) //命令字符串:GET_FILE|.\a.dll
               {
                  string[] sps = msg.Split(new char[] { char.Parse("|") });
                  string file = sps[1]; //取出文件a.dll 
                  this.SendFile(socket, file);
               }
               socket.Close();// 
            }
            catch (Exception ex)
            {
               if (ex is ThreadAbortException)
               {
                  if ((ex as ThreadAbortException).ExceptionState.ToString() == "101")
                  {
                     _ServerRunning = false;
                     ShowMessage("用户关闭服务器.");
                  }
                  else
                  ShowMessage(ex.Message);
               }
               else
               {
                  ShowMessage(ex.Message);
               }
            }
         }
      }
      
      private void ShowMessage(string msg)
      {
         _owner.Invoke(this._writeOutput,msg);
      }
      
      private void SendFile(Socket socket, string file)
      {
         if (File.Exists(file))
         {
            ShowMessage("获取文件[" + file + "]....");
            socket.SendFile(file);
            ShowMessage("[" + file + "]已发送.");
         }
         else
         {
            socket.Send(Encoding.Unicode.GetBytes("FILE_NOT_FOUND")); //通知客户程序 
            ShowMessage("[" + file + "]不存在.");
         }
      }
   }
}

服务器界面代码:
 
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Net.Sockets;
using VJSDN.Tech.TcpIpDownloader;

namespace VJSDN.Tech.TcpIpDownloaderServer
{
   public partial class frmUpgraderServer : Form
   {
      private UpgraderServer _UpgradeListener = null;
      
      public frmUpgraderServer()
      {
         InitializeComponent();
      }
      
      private void frmUpgraderServer_Load(object sender, EventArgs e)
      {
         // 
      }
      
      private void btnStart_Click(object sender, EventArgs e)
      {
         string hostIp = "8.8.8.2";
         int hostPort = 12346;
         
         _UpgradeListener = new UpgraderServer(this, this.Output);
         _UpgradeListener.StartListening(hostIp, hostPort);
         listBox1.Items.Add("已启动服务.");
      }
      
      private void btnStop_Click(object sender, EventArgs e)
      {
         if (_UpgradeListener != null)
         {
            _UpgradeListener.Stop();
            _UpgradeListener = null;
            listBox1.Items.Add("已关闭服务.");
         }
      }
      
      public void Output(string msg)
      {
         this.listBox1.Items.Add(msg);
      }
      
      private void frmUpgraderServer_FormClosing(object sender, FormClosingEventArgs e)
      {
         if (_UpgradeListener != null) _UpgradeListener.Stop();
      }
   }
}

 
下载窗体代码:
 
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using VJSDN.Tech.TcpIpDownloader;
using System.IO;

namespace TestProject
{
   public partial class frmDownloader : Form
   {
      public frmDownloader()
      {
         InitializeComponent();
      }
      
      private void button1_Click(object sender, EventArgs e)
      {
         string filename = Path.GetFileName(textBox1.Text);
         string local = textBox2.Text + @"\" + filename;
         bool ret = new UpgraderClient(this.ShowOutput).DownloadFile(textBox1.Text, local);
         if (ret)
         MessageBox.Show("下载完成!");
         else
         MessageBox.Show("下载失败!");
      }
      
      private void ShowOutput(string msg)
      {
         MessageBox.Show(msg);
      }
   }
}

完整源代码:www.soaspx.com/uploads/soft/200910/Tech.TcpipDownloader.rar。解压后加载客户测试窗体到项目工程。
 
View Code

C#通过url下载图片介绍

实现代码如下:
public partial class DownLoadFile : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            string picName = Request.QueryString["InternalSysURL"];
            if (!String.IsNullOrEmpty(picName))
            {
                byte[] content = this.GetImageContent(picName);
                this.WriteResponse(picName, content);
            }
        }
 
        #region
        private byte[] GetImageContent(string picName)
        {
            string fileURL = GetImgUrlPrefix() + picName;
 
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(fileURL);
            request.AllowAutoRedirect = true;
 
            WebProxy proxy = new WebProxy();
            proxy.BypassProxyOnLocal = true;
            proxy.UseDefaultCredentials = true;
 
            request.Proxy = proxy;
 
            WebResponse response = request.GetResponse();
 
            using (Stream stream = response.GetResponseStream())
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    Byte[] buffer = new Byte[1024];
                    int current = 0;
                    while ((current = stream.Read(buffer, 0, buffer.Length)) != 0)
                    {
                        ms.Write(buffer, 0, current);
                    }
                    return ms.ToArray();
                }
            }
        }
 
        private void WriteResponse(string picName, byte[] content)
        {
            Response.Clear();
            Response.ClearHeaders();
            Response.Buffer = false;
            Response.ContentType = "application/octet-stream";
            Response.AppendHeader("Content-Disposition", "attachment;filename=" + HttpUtility.UrlEncode(picName, Encoding.Default));
            Response.AppendHeader("Content-Length", content.Length.ToString());
            Response.BinaryWrite(content);
            Response.Flush();
            Response.End();
        }
 
        private static string GetImgUrlPrefix()
        {
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(AppDomain.CurrentDomain.BaseDirectory + "//Pages//ItemMaintain//ImageDownLoad.xml");
            XmlNodeList nodes = xmlDoc.GetElementsByTagName("ProductImageOriginal");
            if (nodes.Count > 0)
            {
                return nodes[0].ChildNodes[0].Value;
            }
            else { return ""; }
        }
 
        #endregion
    } 
 
View Code

C#文件上传下载及列表相关代码示例

C#文件上传下载是对文件的基本操作,C#文件上传下载主要实现的是对文件的管理与应用。这里介绍的代码将实现大部分功能。
    1.文件上传
    如下要点:
    HTML部分:
1. <form id=\"form1\" runat=\"server\" method=\"post\" enctype=\"multipart/form-data\"> 
2.     <input id=\"FileUpLoad\" type=\"file\" runat=\"server\"/><br /> 
3.     后台CS部分 按钮事件  
4.     //string strFileFullName = System.IO.Path.GetFileName(this.FileUpLoad.PostedFile.FileName);  
5.     //this.FileUpLoad.PostedFile.SaveAs(Server.MapPath(\"./Xmlzip/\") + strFileFullName); 
    2.文件下载
    ListBox的SelectedIndexChanged事件 设定相关下载连接    
1. protected void lst_DownLoadFileList_SelectedIndexChanged(object sender, EventArgs e)  
2.         {  
3.             try 
4.             {  
5.                 string strJS = \"window.open(\'Xmlzip/\";  
6.                 strJS += this.lst_DownLoadFileList.SelectedItem.Text.Trim();  
7.                 strJS += \"\'); return false; \";  
8.                 this.imgbtn_DownLoadFile.Attributes.Add(\"onclick\", strJS);  
9.             }  
10.             catch (Exception ex)  
11.             {  
12.                 ex.ToString();  
13.             }  
14.         } 
    或者也可以通过 改变Label的Text值 来实现点击后实现文件下载的超级连接
t    his.Label1.Text = \"<a href=\\\"Xmlzip/a.rar\\\">a.rar</a>\"
    3.文件删除
1. string strFilePath = Server.MapPath(\"../CountryFlowMgr/Xmlzip/\"+this.lst_DownLoadFileList.SelectedItem.Text.Trim());  
2.     if (File.Exists(strFilePath))  
3.     {  
4.        File.Delete(strFilePath);  
5.        if (File.Exists(strFilePath))  
6.        {  
7.      Response.Write(\"ok\");  
8.        }  
9.        else 
10.        {  
11.             Response.Write(\"ok\");  
12.        }  
13.     } 
    4.得到文件夹下的文件列表
1. #region 得到当前可用的文件列表  
2.         /// <summary>  
3.         /// 得到当前可用的文件列表  
4.         /// </summary>  
5.         /// <param name=\"IsAlert\">是否需要弹出提示信息</param>  
6.         private void fn_getCurrFileList(bool IsAlert)  
7.         {  
8.             try 
9.             {  
10.                 //查找Xmlzip文件夹下 属于其本身UnitCoding的相关zip文件  
11.                 string strXmlZipDirectory = Server.MapPath(\"../Xmlzip/\");  
12.                 if (Directory.Exists(strXmlZipDirectory))  
13.                 {  
14.                     //DirectoryInfo di = new DirectoryInfo(Environment.CurrentDirectory);  
15.                     DirectoryInfo di = new DirectoryInfo(strXmlZipDirectory);  
16.  
17.                     FileInfo[] FI = di.GetFiles(\"*.zip\");//只查.zip文件  
18.                     if (FI.Length > 0)  
19.                     {  
20.                         lst_DownLoadFileList.Items.Clear();  
21.                         foreach (FileInfo tmpFI in FI)  
22.                         {  
23.                             ListItem tmpItem = new ListItem();  
24.                             tmpItem.Text = tmpFI.Name;  
25.                             lst_DownLoadFileList.Items.Add(tmpItem);  
26.                         }  
27.                         lst_DownLoadFileList.SelectedIndex = 0;  
28.         }  
29.                     else 
30.                     {  
31.                         if (IsAlert)  
32.                         {  
33.                             Response.write(\"查无可以下载的文件!\");  
34.                         }  
35.                     }  
36.                 }  
37.             }  
38.             catch (Exception ex)  
39.             {  
40.                 ex.ToString();  
41.             }  
42.         }  
43.         #endregion 
    C#文件上传下载及列表相关代码示例就介绍到这里。
 
View Code

C#下载大文件并实现断点续传

http://5543541.qzone.qq.com/blog/23
 System.IO.Stream iStream = null;
     byte[] buffer = new Byte[10240];
     int length;
     long dataToRead;
     string filepath = "文件存放地址";
     string filename = System.IO.Path.GetFileName(filepath);
     iStream = new System.IO.FileStream(filepath, System.IO.FileMode.Open, System.IO.FileAccess.Read,System.IO.FileShare.Read);
     Response.Clear();
     dataToRead = iStream.Length;
     long p = 0;
     if(Request.Headers["Range"]!=null)
     {
      Response.StatusCode = 206;
      p = long.Parse( Request.Headers["Range"].Replace("bytes=","").Replace("-",""));
     }
     if(p != 0)
     {
      Response.AddHeader("Content-Range","bytes " + p.ToString() + "-" + ((long)(dataToRead - 1)).ToString() + "/" + dataToRead.ToString()); 
     }
     Response.AddHeader("Content-Length",((long)(dataToRead-p)).ToString());
     Response.ContentType = "application/octet-stream";
     Response.AddHeader("Content-Disposition", "attachment; filename=" + System.Web.HttpUtility.UrlEncode("真实文件名"));
     iStream.Position = p;
     dataToRead = dataToRead - p;
     while (dataToRead > 0)
     {
      if (Response.IsClientConnected) 
      {
       length = iStream.Read(buffer, 0, 10240);
       Response.OutputStream.Write(buffer, 0, length);
       Response.Flush();
       buffer= new Byte[10240];
       dataToRead = dataToRead - length;
      }
      else
      {
       dataToRead = -1;
      }
     }
     iStream.Close();
     Response.End();  
 
 
View Code

C#线程系列(3):线程池和文件下载服务器

如果设计一个服务器程序,每当处理用户请求时,都开始一个线程,将会在一定程序上消耗服务器的资源。为此,一个最好的解决方法就是在服务器启动之前,事先创建一些线程对象,然后,当处理客户端请求时,就从这些建好的线程中获得线程对象,并处理请求。保存这些线程对象的结构就叫做线程池。
    在C#中可以通过System.Threading.ThreadPool类来实现,在默认情况下,ThreadPool最大可建立500个工作线程和1000个I/O线程(根据机器CPU个数和.net framework版本的不同,这些数据可能会有变化)。下面是一个用C#从线程池获得线程的例子:
private static void execute(object state)
{
    Console.WriteLine(state);      
}
static void Main(string[] args)
{
  
    int workerThreads;
    int completionPortThreads;
         
    ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
    Console.WriteLine(workerThreads);
    Console.WriteLine(completionPortThreads);    
    ThreadPool.QueueUserWorkItem(execute,"线程1");   // 从线程池中得到一个线程,并运行execute
    ThreadPool.QueueUserWorkItem(execute, "线程2");
    ThreadPool.QueueUserWorkItem(execute, "线程3");
    Console.ReadLine();
}
    下图为上面代码的运行结果。

    要注意的是,使用ThreadPool获得的线程都是后台线程。
    下面的程序是我设计的一个下载文件服务器的例子。这个例子从ThreadPool获得线程,并处理相应的客户端请求。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.IO;

namespace MyThread
{
    class FileServer
    {
        private String root;
        private Thread listenerThread;

        private void worker(object state)
        {
             TcpClient client = state as TcpClient;
             try
             {

                 client.ReceiveTimeout = 2000;
                 Stream stream = client.GetStream();
                 System.IO.StreamReader sr = new StreamReader(stream);
                 String line = sr.ReadLine();
                 String[] array = line.Split(' ');
                 String path = array[1].Replace('/', '\\');
                 String filename = root + path;
                 if (File.Exists(filename))  // 如果下载文件存在,开始下载这个文件
                 {
                     FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read, 
                                                           FileShare.Read);
                     byte[] buffer = new byte[8192]; // 每次下载8K
                     int count = 0;
                     String responseHeader = "HTTP/1.1 200 OK\r\n" +
                                             "Content-Type:application/octet-stream\r\n" +
                                             "Content-Disposition:attachment;filename=" +
                                                   filename.Substring(filename.LastIndexOf("\\") + 1) + "\r\n\r\n";
                     byte[] header = ASCIIEncoding.ASCII.GetBytes(responseHeader);
                     stream.Write(header, 0, header.Length);
                     while ((count = fileStream.Read(buffer, 0, buffer.Count())) > 0)
                     {
                         stream.Write(buffer, 0, count);
                     }
                     Console.WriteLine(filename + "下载完成");
                 }
                 else  // 文件不存在,输出提示信息
                 {
                     String response = "HTTP/1.1 200 OK\r\nContent-Type:text/plain;charset=utf-8\r\n\r\n文件不存在";
                     byte[] buffer = ASCIIEncoding.UTF8.GetBytes(response);
                     stream.Write(buffer, 0, buffer.Length);
                 }

             }
             catch (Exception e)
             {
                 Console.WriteLine(e.Message);
             }
             finally
             {
                 if (client != null)
                 {
                     client.Close();
                 }
             }
        }

        private void listener()
        {
            TcpListener listener = new TcpListener(1234);
            listener.Start();  // 开始监听客户端请求
            TcpClient client = null;

            while (true)
            {
                client = listener.AcceptTcpClient();
                client.ReceiveTimeout =2000;
                ThreadPool.QueueUserWorkItem(worker, client);  // 从线程池中获得一个线程来处理客户端请求
            }
        }
        public FileServer(String root)
        {
            this.root= root;         
        }
        public void start()
        {
            listenerThread = new Thread(listener);
            listenerThread.Start();  // 开始运行监听线程
        }
    }
}
 
    FileServer类的使用方法:

    FileServer fs = new FileServer(“d:\\download”);
fs.start(); // 端口为1234
如果d:"download目录中有一个叫aa.exe的文件,在浏览器中输入如下的地址可下载:
    http://localhost:1234/aa.exe
下图为下载对话框:



 
要注意的是,本程序并没有处理含有中文和其他特殊字符(如空格)的url,因为,文件名要为英文名(不能有空格等特殊字符)。
 
View Code

C#线程系列讲座(3):线程池和文件下载服务器

如果设计一个服务器程序,每当处理用户请求时,都开始一个线程,将会在一定程序上消耗服务器的资源。为此,一个最好的解决方法就是在服务器启动之前,事先创建一些线程对象,然后,当处理客户端请求时,就从这些建好的线程中获得线程对象,并处理请求。保存这些线程对象的结构就叫做线程池。
    在C#中可以通过System.Threading.ThreadPool类来实现,在默认情况下,ThreadPool最大可建立500个工作线程和1000个I/O线程(根据机器CPU个数和.net framework版本的不同,这些数据可能会有变化)。下面是一个用C#从线程池获得线程的例子:
private static void execute(object state)
{
    Console.WriteLine(state);      
}
static void Main(string[] args)
{
  
    int workerThreads;
    int completionPortThreads;
         
    ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
    Console.WriteLine(workerThreads);
    Console.WriteLine(completionPortThreads);    
    ThreadPool.QueueUserWorkItem(execute,"线程1");   // 从线程池中得到一个线程,并运行execute
    ThreadPool.QueueUserWorkItem(execute, "线程2");
    ThreadPool.QueueUserWorkItem(execute, "线程3");
    Console.ReadLine();
}
    下图为上面代码的运行结果。



    要注意的是,使用ThreadPool获得的线程都是后台线程。
    下面的程序是我设计的一个下载文件服务器的例子。这个例子从ThreadPool获得线程,并处理相应的客户端请求。
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.IO;

namespace MyThread
{
    class FileServer
    {
        private String root;
        private Thread listenerThread;

        private void worker(object state)
        {
             TcpClient client = state as TcpClient;
             try
             {

                 client.ReceiveTimeout = 2000;
                 Stream stream = client.GetStream();
                 System.IO.StreamReader sr = new StreamReader(stream);
                 String line = sr.ReadLine();
                 String[] array = line.Split(' ');
                 String path = array[1].Replace('/', '\\');
                 String filename = root + path;
                 if (File.Exists(filename))  // 如果下载文件存在,开始下载这个文件
                 {
                     FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read, 
                                                           FileShare.Read);
                     byte[] buffer = new byte[8192]; // 每次下载8K
                     int count = 0;
                     String responseHeader = "HTTP/1.1 200 OK\r\n" +
                                             "Content-Type:application/octet-stream\r\n" +
                                             "Content-Disposition:attachment;filename=" +
                                                   filename.Substring(filename.LastIndexOf("\\") + 1) + "\r\n\r\n";
                     byte[] header = ASCIIEncoding.ASCII.GetBytes(responseHeader);
                     stream.Write(header, 0, header.Length);
                     while ((count = fileStream.Read(buffer, 0, buffer.Count())) > 0)
                     {
                         stream.Write(buffer, 0, count);
                     }
                     Console.WriteLine(filename + "下载完成");
                 }
                 else  // 文件不存在,输出提示信息
                 {
                     String response = "HTTP/1.1 200 OK\r\nContent-Type:text/plain;charset=utf-8\r\n\r\n文件不存在";
                     byte[] buffer = ASCIIEncoding.UTF8.GetBytes(response);
                     stream.Write(buffer, 0, buffer.Length);
                 }

             }
             catch (Exception e)
             {
                 Console.WriteLine(e.Message);
             }
             finally
             {
                 if (client != null)
                 {
                     client.Close();
                 }
             }
        }

        private void listener()
        {
            TcpListener listener = new TcpListener(1234);
            listener.Start();  // 开始监听客户端请求
            TcpClient client = null;

            while (true)
            {
                client = listener.AcceptTcpClient();
                client.ReceiveTimeout =2000;
                ThreadPool.QueueUserWorkItem(worker, client);  // 从线程池中获得一个线程来处理客户端请求
            }
        }
        public FileServer(String root)
        {
            this.root= root;         
        }
        public void start()
        {
            listenerThread = new Thread(listener);
            listenerThread.Start();  // 开始运行监听线程
        }
    }
}
 
    FileServer类的使用方法:

    FileServer fs = new FileServer(“d:\\download”);
fs.start(); // 端口为1234
如果d:"download目录中有一个叫aa.exe的文件,在浏览器中输入如下的地址可下载:
    http://localhost:1234/aa.exe
下图为下载对话框:



 
要注意的是,本程序并没有处理含有中文和其他特殊字符(如空格)的url,因为,文件名要为英文名(不能有空格等特殊字符)。
 
View Code

HTTP性能调优之设置连接失效时间(翻译)

Setting Connection Timeouts
设置连接失效时间
首先讲讲为什么要设置连接失效时间。
Connection time-outs help reduce the loss of processing resources consumed by idle connections. When you enable connection time-outs, IIS enforces the time-outs at the connection level.
设置失效连接时间可以减少处理空闲连接浪费系统资源的消耗。当启用了连接失效,iis将会把失效时间应用于连接级别。
 
设置连接失效时间:
Iis中-网站--属性--Connection timeout,键入一个时间,这个时间表明iis处理空闲线程的最大忍受时间,时间一过,连接将会被回收。
 
个人认为这个连接时间的设置的长短取决于网站的综合因素。如果你的网站速度很慢的话那么这个失效时间需要设置得长点,不然客户端还没有完整地下载完页面连接就关闭了。如果要是网站性能比较好,那么连接就没有必要设置得那么长,那样有些空闲的连接就会占用比较多的系统资源,及早释放资源,使得网站整体性能提升,是设置连接失效时间的最终目的。
 
View Code

NET实现断点续传

在了解HTTP断点续传的原理之前,先来说说HTTP协议,HTTP协议是一种基于tcp的简单协议,分为请求和回复两种。请求协议是由客户机(浏览器)向服务器(WEB SERVER)提交请求时发送报文的协议。回复协议是由服务器(web server),向客户机(浏览器)回复报文时的协议。请求和回复协议都由头和体组成。头和体之间以一行空行为分隔。
 
以下是一个请求报文与相应的回复报文的例子:
GET /image/index_r4_c1.jpg HTTP/1.1
Accept: */*
Referer: http://192.168.3.120:8080
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)
Host: 192.168.3.120:8080
Connection: Keep-Alive
 
 
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Tue, 24 Jun 2003 05:39:40 GMT
Content-Type: image/jpeg
Accept-Ranges: bytes
Last-Modified: Thu, 23 May 2002 03:05:40 GMT
ETag: "bec48eb862c21:934"
Content-Length: 2827
 
JFIF H H   nbsp;C [1]
….
 
下面我们就来说说“断点续传”。
顾名思义,断点续传就是在上一次下载时断开的位置开始继续下载。在HTTP协议中,可以在请求报文头中加入Range段,来表示客户机希望从何处继续下载。
比如说从第1024字节开始下载,请求报文如下:
 
GET /image/index_r4_c1.jpg HTTP/1.1
Accept: */*
Referer: http://192.168.3.120:8080
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)
Host: 192.168.3.120:8080
Range:bytes=1024-
Connection: Keep-Alive
 
.NET中的相关类
明白了上面的原理,那么,我们来看看.NET FRAMEWORK中为我们提供了哪些类可以来做这些事。
完成HTTP请求
System.Net.HttpWebRequest
HttpWebRequest 类对 WebRequest 中定义的属性和方法提供支持,也对使用户能够直接与使用 HTTP 的服务器交互的附加属性和方法提供支持。
HttpWebRequest 将发送到 Internet 资源的公共 HTTP 标头值公开为属性,由方法或系统设置。下表包含完整列表。可以将 Headers 属性中的其他标头设置为名称/值对。但是注意,某些公共标头被视为受限制的,它们或者直接由 API公开,或者受到系统保护,不能被更改。Range也属于被保护之列,不过,.NET为开发者提供了更方便的操作,就是 AddRange方法,向请求添加从请求数据的开始处或结束处的特定范围的字节范围标头
完成文件访问
System.IO.FileStream
FileStream 对象支持使用Seek方法对文件进行随机访问, Seek 允许将读取/写入位置移动到文件中的任意位置。这是通过字节偏移参考点参数完成的。字节偏移量是相对于查找参考点而言的,该参考点可以是基础文件的开始、当前位置或结尾,分别由SeekOrigin类的三个属性表示。
 
代码实现
了解了.NET提供的相关的类,那么,我们就可以方便的实现了。
代码如下:
 
static void Main(string[] args)
              {
                     
                     string StrFileName="c:\\aa.zip";      //根据实际情况设置
                     string StrUrl="http://www.xxxx.cn/xxxxx.zip";   //根据实际情况设置
 
                     //打开上次下载的文件或新建文件
                     long lStartPos =0;
                     System.IO.FileStream fs;
                     if (System.IO.File.Exists(StrFileName))
                     {
                            fs= System.IO.File.OpenWrite(StrFileName);
                            lStartPos=fs.Length;
                            fs.Seek(lStartPos,System.IO.SeekOrigin.Current);   //移动文件流中的当前指针
                     }
                     else
                     {
                            fs = new System.IO.FileStream(StrFileName,System.IO.FileMode.Create);
                            lStartPos =0;
                     }
                     
                     //打开网络连接
                     try
                     {
                            System.Net.HttpWebRequest request =(System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(StrUrl);
                            if ( lStartPos>0)
                                   request.AddRange((int)lStartPos);    //设置Range值
                            
                            //向服务器请求,获得服务器回应数据流
                            System.IO.Stream ns= request.GetResponse().GetResponseStream();
 
                            byte[] nbytes = new byte[512];
                            int nReadSize=0;
                            nReadSize=ns.Read(nbytes,0,512);
                            while( nReadSize >0)
                            {
                                   fs.Write(nbytes,0,nReadSize);
                                   nReadSize=ns.Read(nbytes,0,512);
                            }
                            fs.Close();
                            ns.Close();
                            Console.WriteLine("下载完成");
                     }
                     catch(Exception ex)
                     {
                            fs.Close();
                            Console.WriteLine("下载过程中出现错误:"+ex.ToString());
                     }
              }
 
以上是本人在开发中的一点小小体验,希望能与大家分享! :)
 
View Code

NET下的FTP上传,下载文件(支持中文名)

基本原理就是先建立Socket连接,然后发请求命令和取回应答码。编码方式采用Encoding ASCII = Encoding.Default;来支持中文文件名。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.IO;
namespace Kostech.Net
{
 /// <summary>
 /// FTP 的摘要说明。
 /// </summary>
 public class FTP
 {
  private string strRemoteHost;
  private int strRemotePort;
  private string strRemotePath;
  private string strRemoteUser;
  private string strRemotePass;
  private Boolean bConnected;
  #region 内部变量
  /// <summary>
  /// 服务器返回的应答信息(包含应答码)
  /// </summary>
  private string strMsg;
  /// <summary>
  /// 服务器返回的应答信息(包含应答码)
  /// </summary>
  private string strReply;
  /// <summary>
  /// 服务器返回的应答码
  /// </summary>
  private int iReplyCode;
  /// <summary>
  /// 进行控制连接的socket
  /// </summary>
  private Socket socketControl;
  /// <summary>
  /// 传输模式
  /// </summary>
  private TransferType trType;
  /// <summary>
  /// 传输模式:二进制类型、ASCII类型
  /// </summary>
  public enum TransferType
  {
   /// <summary>
   /// Binary
   /// </summary>
   Binary,
   /// <summary>
   /// ASCII
   /// </summary>
   ASCII
  };
  /// <summary>
  /// 接收和发送数据的缓冲区
  /// </summary>
  private static int BLOCK_SIZE = 512;
  Byte[] buffer = new Byte[ BLOCK_SIZE];
  /// <summary>
  /// 编码方式
  /// </summary>
  Encoding ASCII = Encoding.Default;
  #endregion
  #region 内部函数

  #region 构造函数
  /// <summary>
  /// 缺省构造函数
  /// </summary>
  public FTP()
  {
   strRemoteHost  = "";
   strRemotePath  = "";
   strRemoteUser  = "";
   strRemotePass  = "";
   strRemotePort  = 21;
   bConnected     = false;
  }
  /// <summary>
  /// 构造函数
  /// </summary>
  /// <param name="remoteHost"></param>
  /// <param name="remotePath"></param>
  /// <param name="remoteUser"></param>
  /// <param name="remotePass"></param>
  /// <param name="remotePort"></param>
  public FTP( string remoteHost, string remotePath, string remoteUser, string remotePass, int remotePort )
  {
   strRemoteHost  = remoteHost;
   strRemotePath  = remotePath;
   strRemoteUser  = remoteUser;
   strRemotePass  = remotePass;
   strRemotePort  = remotePort;
   Connect();
  }
  #endregion
  #region 登陆
  /// <summary>
  /// FTP服务器IP地址
  /// </summary>
  
  public string RemoteHost
  {
   get
   {
    return strRemoteHost;
   }
   set
   {
    strRemoteHost = value;
   }
  }
  /// <summary>
  /// FTP服务器端口
  /// </summary>
  public int RemotePort
  {
   get
   {
    return strRemotePort;
   }
   set
   {
    strRemotePort = value;
   }
  }
  /// <summary>
  /// 当前服务器目录
  /// </summary>
  public string RemotePath
  {
   get
   {
    return strRemotePath;
   }
   set
   {
    strRemotePath = value;
   }
  }
  /// <summary>
  /// 登录用户账号
  /// </summary>
  public string RemoteUser
  {
   set
   {
    strRemoteUser = value;
   }
  }
  /// <summary>
  /// 用户登录密码
  /// </summary>
  public string RemotePass
  {
   set
   {
    strRemotePass = value;
   }
  }
  /// <summary>
  /// 是否登录
  /// </summary>
  public bool Connected
  {
   get
   {
    return bConnected;
   }
  }
  #endregion
  #region 链接
  /// <summary>
  /// 建立连接 
  /// </summary>
  public void Connect()
  {
   socketControl = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
   IPEndPoint ep = new IPEndPoint(IPAddress.Parse(RemoteHost), strRemotePort);
   // 链接
   try
   {
    socketControl.Connect(ep);
   }
   catch(Exception)
   {
    throw new IOException("Couldn't connect to remote server");
   }
   // 获取应答码
   ReadReply();
   if(iReplyCode != 220)
   {
    DisConnect();
    throw new IOException(strReply.Substring(4));
   }
   // 登陆
   SendCommand("USER "+strRemoteUser);
   if( !(iReplyCode == 331 || iReplyCode == 230) )
   {
    CloseSocketConnect();//关闭连接
    throw new IOException(strReply.Substring(4));
   }
   if( iReplyCode != 230 )
   {
    SendCommand("PASS "+strRemotePass);
    if( !(iReplyCode == 230 || iReplyCode == 202) )
    {
     CloseSocketConnect();//关闭连接
     throw new IOException(strReply.Substring(4));
    }
   }
   bConnected = true;
   // 切换到目录
   ChDir(strRemotePath);
  }

  /// <summary>
  /// 关闭连接
  /// </summary>
  public void DisConnect()
  {
   if( socketControl != null )
   {
    SendCommand("QUIT");
   }
   CloseSocketConnect();
  }
  #endregion
  #region 传输模式
  /// <summary>
  /// 设置传输模式
  /// </summary>
  /// <param name="ttType">传输模式</param>
  public void SetTransferType(TransferType ttType)
  {
   if(ttType == TransferType.Binary)
   {
    SendCommand("TYPE I");//binary类型传输
   }
   else
   {
    SendCommand("TYPE A");//ASCII类型传输
   }
   if (iReplyCode != 200)
   {
    throw new IOException(strReply.Substring(4));
   }
   else
   {
    trType = ttType;
   }
  }

  /// <summary>
  /// 获得传输模式
  /// </summary>
  /// <returns>传输模式</returns>
  public TransferType GetTransferType()
  {
   return trType;
  }
  #endregion
  #region 文件操作
  /// <summary>
  /// 获得文件列表
  /// </summary>
  /// <param name="strMask">文件名的匹配字符串</param>
  /// <returns></returns>
  public string[] Dir(string strMask)
  {
   // 建立链接
   if(!bConnected)
   {
    Connect();
   }
   //建立进行数据连接的socket
   Socket socketData = CreateDataSocket();
   //传送命令
   SendCommand("NLST " + strMask);
   //分析应答代码
   if(!(iReplyCode == 150 || iReplyCode == 125 || iReplyCode == 226))
   {
    throw new IOException(strReply.Substring(4));
   }
   //获得结果
   strMsg = "";
   while(true)
   {
    int iBytes = socketData.Receive(buffer, buffer.Length, 0);
    strMsg += ASCII.GetString(buffer, 0, iBytes);
    if(iBytes < buffer.Length)
    {
     break;
    }
   }
   char[] seperator = {'\n'};
   string[] strsFileList = strMsg.Split(seperator);
   socketData.Close();//数据socket关闭时也会有返回码
   if(iReplyCode != 226)
   {
    ReadReply();
    if(iReplyCode != 226)
    {
     throw new IOException(strReply.Substring(4));
    }
   }
   return strsFileList;
  }

  /// <summary>
  /// 获取文件大小
  /// </summary>
  /// <param name="strFileName">文件名</param>
  /// <returns>文件大小</returns>
  private long GetFileSize(string strFileName)
  {
   if(!bConnected)
   {
    Connect();
   }
   SendCommand("SIZE " + Path.GetFileName(strFileName));
   long lSize=0;
   if(iReplyCode == 213)
   {
    lSize = Int64.Parse(strReply.Substring(4));
   }
   else
   {
    throw new IOException(strReply.Substring(4));
   }
   return lSize;
  }

  /// <summary>
  /// 删除
  /// </summary>
  /// <param name="strFileName">待删除文件名</param>
  public void Delete(string strFileName)
  {
   if(!bConnected)
   {
    Connect();
   }
   SendCommand("DELE "+strFileName);
   if(iReplyCode != 250)
   {
    throw new IOException(strReply.Substring(4));
   }
  }

  /// <summary>
  /// 重命名(如果新文件名与已有文件重名,将覆盖已有文件)
  /// </summary>
  /// <param name="strOldFileName">旧文件名</param>
  /// <param name="strNewFileName">新文件名</param>
  public void Rename(string strOldFileName,string strNewFileName)
  {
   if(!bConnected)
   {
    Connect();
   }
   SendCommand("RNFR "+strOldFileName);
   if(iReplyCode != 350)
   {
    throw new IOException(strReply.Substring(4));
   }
   //  如果新文件名与原有文件重名,将覆盖原有文件
   SendCommand("RNTO "+strNewFileName);
   if(iReplyCode != 250)
   {
    throw new IOException(strReply.Substring(4));
   }
  }
  #endregion
  #region 上传和下载
  /// <summary>
  /// 下载一批文件
  /// </summary>
  /// <param name="strFileNameMask">文件名的匹配字符串</param>
  /// <param name="strFolder">本地目录(不得以\结束)</param>
  public void Get(string strFileNameMask,string strFolder)
  {
   if(!bConnected)
   {
    Connect();
   }
   string[] strFiles = Dir(strFileNameMask);
   foreach(string strFile in strFiles)
   {
    if(!strFile.Equals(""))//一般来说strFiles的最后一个元素可能是空字符串
    {
     Get(strFile,strFolder,strFile);
    }
   }
  }

  /// <summary>
  /// 下载一个文件
  /// </summary>
  /// <param name="strRemoteFileName">要下载的文件名</param>
  /// <param name="strFolder">本地目录(不得以\结束)</param>
  /// <param name="strLocalFileName">保存在本地时的文件名</param>
  public void Get(string strRemoteFileName,string strFolder,string strLocalFileName)
  {
   if(!bConnected)
   {
    Connect();
   }
   SetTransferType(TransferType.Binary);
   if (strLocalFileName.Equals(""))
   {
    strLocalFileName = strRemoteFileName;
   }
   if(!File.Exists(strLocalFileName))
   {
    Stream st = File.Create(strLocalFileName);
    st.Close();
   }
   FileStream output = new 
    FileStream(strFolder + "\\" + strLocalFileName,FileMode.Create);
   Socket socketData = CreateDataSocket();
   SendCommand("RETR " + strRemoteFileName);
   if(!(iReplyCode == 150 || iReplyCode == 125
    || iReplyCode == 226 || iReplyCode == 250))
   {
    throw new IOException(strReply.Substring(4));
   }
   while(true)
   {
    int iBytes = socketData.Receive(buffer, buffer.Length, 0);
    output.Write(buffer,0,iBytes);
    if(iBytes <= 0)
    {
     break;
    }
   }
   output.Close();
   if (socketData.Connected)
   {
    socketData.Close();
   }
   if(!(iReplyCode == 226 || iReplyCode == 250))
   {
    ReadReply();
    if(!(iReplyCode == 226 || iReplyCode == 250))
    {
     throw new IOException(strReply.Substring(4));
    }
   }
  }

  /// <summary>
  /// 上传一批文件
  /// </summary>
  /// <param name="strFolder">本地目录(不得以\结束)</param>
  /// <param name="strFileNameMask">文件名匹配字符(可以包含*和?)</param>
  public void Put(string strFolder,string strFileNameMask)
  {
   string[] strFiles = Directory.GetFiles(strFolder,strFileNameMask);
   foreach(string strFile in strFiles)
   {
    //strFile是完整的文件名(包含路径)
    Put(strFile);
   }
  }

  /// <summary>
  /// 上传一个文件
  /// </summary>
  /// <param name="strFileName">本地文件名</param>
  public void Put(string strFileName)
  {
   if(!bConnected)
   {
    Connect();
   }
   Socket socketData = CreateDataSocket();
   SendCommand("STOR "+Path.GetFileName(strFileName));
   if( !(iReplyCode == 125 || iReplyCode == 150) )
   {
    throw new IOException(strReply.Substring(4));
   }
   FileStream input = new 
    FileStream(strFileName,FileMode.Open);
   int iBytes = 0;
   while ((iBytes = input.Read(buffer,0,buffer.Length)) > 0)
   {
    socketData.Send(buffer, iBytes, 0);
   }
   input.Close();
   if (socketData.Connected)
   {
    socketData.Close();
   }
   if(!(iReplyCode == 226 || iReplyCode == 250))
   {
    ReadReply();
    if(!(iReplyCode == 226 || iReplyCode == 250))
    {
     throw new IOException(strReply.Substring(4));
    }
   }
  }
  #endregion
  #region 目录操作
  /// <summary>
  /// 创建目录
  /// </summary>
  /// <param name="strDirName">目录名</param>
  public void MkDir(string strDirName)
  {
   if(!bConnected)
   {
    Connect();
   }
   SendCommand("MKD "+strDirName);
   if(iReplyCode != 257)
   {
    throw new IOException(strReply.Substring(4));
   }
  }

  /// <summary>
  /// 删除目录
  /// </summary>
  /// <param name="strDirName">目录名</param>
  public void RmDir(string strDirName)
  {
   if(!bConnected)
   {
    Connect();
   }
   SendCommand("RMD "+strDirName);
   if(iReplyCode != 250)
   {
    throw new IOException(strReply.Substring(4));
   }
  }

  /// <summary>
  /// 改变目录
  /// </summary>
  /// <param name="strDirName">新的工作目录名</param>
  public void ChDir(string strDirName)
  {
   if(strDirName.Equals(".") || strDirName.Equals(""))
   {
    return;
   }
   if(!bConnected)
   {
    Connect();
   }
   SendCommand("CWD "+strDirName);
   if(iReplyCode != 250)
   {
    throw new IOException(strReply.Substring(4));
   }
   this.strRemotePath = strDirName;
  }
  #endregion

  /// <summary>
  /// 将一行应答字符串记录在strReply和strMsg
  /// 应答码记录在iReplyCode
  /// </summary>
  private void ReadReply()
  {
   strMsg = "";
   strReply = ReadLine();
   iReplyCode = Int32.Parse(strReply.Substring(0,3));
  }
  /// <summary>
  /// 建立进行数据连接的socket
  /// </summary>
  /// <returns>数据连接socket</returns>
  private Socket CreateDataSocket()
  {
   SendCommand("PASV");
   if(iReplyCode != 227)
   {
    throw new IOException(strReply.Substring(4));
   }
   int index1 = strReply.IndexOf('(');
   int index2 = strReply.IndexOf(')');
   string ipData = 
    strReply.Substring(index1+1,index2-index1-1);
   int[] parts = new int[6];
   int len = ipData.Length;
   int partCount = 0;
   string buf="";
   for (int i = 0; i < len && partCount <= 6; i++)
   {
    char ch = Char.Parse(ipData.Substring(i,1));
    if (Char.IsDigit(ch))
     buf+=ch;
    else if (ch != ',')
    {
     throw new IOException("Malformed PASV strReply: " + 
      strReply);
    }
    if (ch == ',' || i+1 == len)
    {
     try
     {
      parts[partCount++] = Int32.Parse(buf);
      buf="";
     }
     catch (Exception)
     {
      throw new IOException("Malformed PASV strReply: " + 
       strReply);
     }
    }
   }
   string ipAddress = parts[0] + "."+ parts[1]+ "." +
    parts[2] + "." + parts[3];
   int port = (parts[4] << 8) + parts[5];
   Socket s = new 
    Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
   IPEndPoint ep = new 
    IPEndPoint(IPAddress.Parse(ipAddress), port);
   try
   {
    s.Connect(ep);
   }
   catch(Exception)
   {
    throw new IOException("Can't connect to remote server");
   }
   return s;
  }

  /// <summary>
  /// 关闭socket连接(用于登录以前)
  /// </summary>
  private void CloseSocketConnect()
  {
   if(socketControl!=null)
   {
    socketControl.Close();
    socketControl = null;
   }
   bConnected = false;
  }
  /// <summary>
  /// 读取Socket返回的所有字符串
  /// </summary>
  /// <returns>包含应答码的字符串行</returns>
  private string ReadLine()
  {
   while(true)
   {
    int iBytes = socketControl.Receive(buffer, buffer.Length, 0);
    strMsg += ASCII.GetString(buffer, 0, iBytes);
    if(iBytes < buffer.Length)
    {
     break;
    }
   }
   char[] seperator = {'\n'};
   string[] mess = strMsg.Split(seperator);
   if(strMsg.Length > 2)
   {
    strMsg = mess[mess.Length-2];
    //seperator[0]是10,换行符是由13和0组成的,分隔后10后面虽没有字符串,
    //但也会分配为空字符串给后面(也是最后一个)字符串数组,
    //所以最后一个mess是没用的空字符串
    //但为什么不直接取mess[0],因为只有最后一行字符串应答码与信息之间有空格
   }
   else
   {
    strMsg = mess[0];
   }
   if(!strMsg.Substring(3,1).Equals(" "))//返回字符串正确的是以应答码(如220开头,后面接一空格,再接问候字符串)
   {
    return ReadLine();
   }
   return strMsg;
  }

  /// <summary>
  /// 发送命令并获取应答码和最后一行应答字符串
  /// </summary>
  /// <param name="strCommand">命令</param>
  private void SendCommand(string strCommand)
  {
   Byte[] cmdBytes = ASCII.GetBytes((strCommand + "\r\n").ToCharArray());
   socketControl.Send(cmdBytes, cmdBytes.Length, 0);
   ReadReply();
  }
  #endregion
 }
}
很多人都找我要调用的方法,其实有了类库就很简单了,我大概写一下吧
FTP myftp = new FTP([ftp主机], [主机目录], [ftp用户名], [ftp用户密码], [ftp端口]);
myftp.Put( [本地文件全路径]); // 上传到服务器中
myftp.Get([ftp文件名称(可以是通配符表示)], [本地存放路径]); //下载到本地
其它方法的使用可以参考我代码中的注释
出处:http://blog.csdn.net/veryhappy/archive/2006/01/16/581072.aspx
 
View Code

P2P下载原理及实例

1. 介绍
 今天的Internet的"middleboxes"已经普遍存在, 比如象网络地址转换(NAT),主要是因为IPv4的地址空间消耗危机中产生的一个解决方案。然而,由这些"middleboxes"建立的不对称寻址和连接,已经成为点对点 (P2P)应用和协议中独特的问题, 这些应用和协议包括例如网络电话和多人在线游戏。这些话题甚至可能在使用 IPv6 协议后继续存在, 比如说在 NAT 常被当作兼容 IPv4 机制[NAT-PT]的地方,还有当NAT 不再需要之后,防火墙将会依然存在(这些问题)。
 当前发展的"middleboxes"最初计划用在C/S结构中,即在那些相关的匿名客户端主动去连接有着固定IP地址和DNS域名的可连接主机。大多数的"middleboxes"实现一个不对称的沟通模型,即那些私有的内部网络上的主机可以和公网上的主机连接通讯,但是公网上的外部主机不能够和内网上的主机通讯除了被 middlebox''s 的管理者明确地配置之外。 在 NAPT 的通常情形中,在内部网络上的一位客户机在公网上并没有一个唯一独特的IP地址,但是可以在同一私网上的其他客户机一样,分享一个公网IP地址,并有NAPT管理。 这些在一台"middlebox"后的不知道名称和不易访问的内部主机对客户端软件比如网页浏览器并不是一个问题,它们之需要向外连接。而且这种不易访问的特性有时候被视为对保护隐私有利。
 但是,在点对点的应用中,英特网上的主机通常会考虑要和"客户"建立直接和彼此访问的通话连接。呼叫者和被叫者可能会在不同的"middleboxes" 后面,两者都可能没有任何的固定IP地址或者其他的公网存在表现。举例来说,一个通常的在线游戏架构,是让参加游戏的主人连接到一个大家都知道的服务器上设定一些初识值,以及连接后的使用目的。然后,为了在游戏期间有更加快速和有效的游戏速度,需要建立彼此直接的连接。同样地,一个可共享的文件可能可以让一个大家都知道的资源搜索引擎发现或者查找到,但如果需要文件数据传输,就需要和那台共享文件的主机建立直接的连接了。在点对点连接时,"middlebox"就生成了一个问题。因为在"middlebox"后面的那些需要用TCP或者UDP和其他机器连接的主机通常没有固定可用的公网端口可以进行连接。 RFC 3235[ NAT-APPL]简短地说明了这个问题,但是没有提供任何的通常解决方案。
 在这一份文档中,我们就 P2P/ middlebox 问题有2点说明。 首先,我们总结那些在middleboxes存在时P2P应用程序可以工作的已知方法。其次,我们提供基于这些实践的一套应用程序设计指导方针使P2P在middleboxes下应用的更健康。更进一步,我们提供的设计指导方针可以让将来的 middleboxes 更有效率的支持支援 P2P 应用。 我们的重点是要能够穿透 middleboxes,以提供更广阔和更直接的P2P 应用。
 2. 术语
 在本章中我们首先简要描述一下一些"middlebox"术语,我们这里的重点是两种常导致P2P应用出现问题的middlebox.
 防火墙
 一个防火墙限制私人内网和公众英特网之间的通讯,典型地防火墙就是丢弃那些它认为未经许可的数据包。在数据包穿越一个防火墙时,它检查但是不修改包里的 IP地址和TCP/ UDP 端口信息。
   
 网络地址转换(NAT)
 当数据包穿过NAT时,NAT不仅检查同时也修改数据的包头信息,并且允许更多的在NAT后的主机分享少数公网IP地址(通常只有1个)。
 NAT通常有2种主要类型:
 Basic Nat
 一个Basic NAT映射一个内在的私有IP地址到一个公网IP地址,但当数据包穿过NAT时,不更换它的TCP/UDP端口号。Basic Nat通常是只用在一些具备公共IP地址池的NAT上,通过它可以地址绑定,即代表一台内部主机。
 Network Address/Port Translator (NAPT)
 但是最通常的,当数据包穿过NAT时,一个NAPT检查并修改它的TCP/UDP端口,那么就可以允许多台内网主机同时共享一个单独的公网IP地址了。
 关于 NAT 的分类和术语,[NAT-TRAD] 和 [NAT-TERM]中有更多的信息。那些将来分类的NAPT的附加术语在较近的工作[STUN]中被定义。当一个内网的主机经过一个NAT和外部进行TCP或者UDP连接的期间,NAPT分配一个公网IP 住址和端口,以便来自外部终端响应的数据包能被NAPT接收,解释,并转发给内网的主机。这个结果是由 NAPT 建立一个(私有IP地址,私有端口)和(公网IP地址,公网端口)之间的端口绑定实现的。在这个期间NAPT将为绑定的端口执行地址翻译。一个关于P2P应用的问题是,当一个内部主机从一个私有IP,私有端口同时与外网上的多台不同的主机建立多个连接时,NAT是如何运作的。
 Cone NAT
 建立一个端口,把一个(私有IP,私有端口)和(公用IP,公用端口)绑定后,对于以后来自同一私有IP和端口号的应用连接,cone NAT将重复使用这个绑定的端口,只要有一个连接会话,这个绑定端口就会保持激活状态。
 例如,下面图表中,推想一下客户端A通过一个cone NAT同时建立2个外部会话,从同样的内部网络终端(10.0.0.1:1234)到2个不同的外部服务器,S1和S2。cone NAT只会分配一个公用的终端,155.99.25.11:62000,都会到两个会话,并在地址转换期间确保客户端端口的一致。Basic NAT和防火墙不修改通过middlebox的数据包中的端口号,这些类型的middlebox可以被认为是一种特殊的Cone Nat。
  Server S1                                     Server S2
         18.181.0.31:1235                              138.76.29.7:1235
                |                                             |
                |                                             |
                +----------------------+----------------------+
                                       |
           ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
           |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
           v 155.99.25.11:62000 v      |      v 155.99.25.11:62000 v
                                       |
                                    Cone NAT
                                  155.99.25.11
                                       |
           ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
           |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
           v   10.0.0.1:1234    v      |      v   10.0.0.1:1234    v
                                       |
                                    Client A
                                 10.0.0.1:1234
 Symmetric NAT
 对称的NAT(Symmetric NAT),与Cone NAT有明显差别,在所有会话期间中不会保持绑定(私有IP,私有端口)和(公共IP,公共端口)的端口不变。相反,它会为每个新对话重新分配一个新的公共端口。
 举例来说,设想客户A从同样端口上要发起两个外部对话,一个和S1连接,另一个和S2连接。Symmetric NAT可能会为第一个会话分配一个公共的端点 155.99.25.11:62000,而为第二个会话分配一个不同的公共端点155.99.25.11:62001。为了地址转换,NAT可以区分这两个会话传输的目的,因为和这些会话有关的外部端点(就是S1、S2)是不同的,甚至在通过地址转换时丢失了客户端的目的标示。(即丢了S1、S2的IP地址NAT也知道如何区分,我是这么理解的,可能有误。)
            Server S1                                     Server S2
         18.181.0.31:1235                              138.76.29.7:1235
                |                                             |
                |                                             |
                +----------------------+----------------------+
                                       |
           ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
           |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
           v 155.99.25.11:62000 v      |      v 155.99.25.11:62001 v
                                       |
                                  Symmetric NAT
                                  155.99.25.11
                                       |
           ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
           |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
           v   10.0.0.1:1234    v      |      v   10.0.0.1:1234    v
                                       |
                                    Client A
                                 10.0.0.1:1234
 Cone NAT和Symmetric NAT之间的比较与TCP/UDP之间的比较有些类似。(TCP需要绑定,UDP不需要,Cone NAT需要绑定,Symmetric NAT不需要)
 按照NAT从已知的公共IP,公共端口接收的数据限制,Cone NAT可以更进一步的进行分类。这种分类通常都是UDP连接的,因为NAT和防火墙会拒绝任何无条件的TCP连接,除非明确地以别的方式配置。
 Full Cone NAT
 在给一个新的外部会话建立了一个公共/私有的端口绑定后,一个full cone NAT就可以通过这个公共端口从公网上的任何外部端点接收数据通讯了。Full cone NAT也常常叫做"混合"NAT。
 Restricted Cone NAT
 当网络主机发一个或者几个数据包给外部主机后,一个受限的cone NAT(Restricted Cone NAT)才会接受来自这个外部(源)IP地址的数据包。受限的cone NAT有效的运用了防火墙的原理,拒绝没有要求的数据,只接收已知道的外部IP地址传来的数据。(偏开原文,大意就是网络主机需要发一个请求给外部IP地址要求要数据,NAT才会接收这个外部IP地址传来的数据,不然不接收。)
 Port-Restricted Cone NAT
 一个端口受限的cone NAT(Port-Restricted Cone NAT),也是这样,只接收那些网络主机曾经发给一个外部IP地址和端口相匹配的数据。一个Port-Restricted cone NAT可以和对称 NAT一样(symmetric NAT),保护内部节点不接收未被请求的数据包,但是保持一个私有端口在连接过程中不变。
 (偏开原文,即不仅请求的IP和外部发来数据的IP是一样的,PORT也要是一样,这样才接收数据。对称NAT也有这个效果,但这种cone NAT保持端口不变,而对称NAT要变端口号的)。
 最后,在这篇文档中我们定义一些新的术语来给middleboxes中有关P2P的行为进行分类:
 P2P-Application
 在这篇文档中P2P-Application被当做一个应用程序,在那些参与P2P连接的并在一个公共的登录服务器注册的终端运行,可以是一个私有端点,也可以是一个公共端点,或者两者都是,并建立一个初步的会话。
 P2P-Middlebox
 P2P- Middlebox就是允许P2P应用程序交换数据的的middlebox。
 P2P-firewall
 一个P2P-防火墙就是提供防火墙功能的P2P- Middlebox,但不具备地址映射功能。
 P2P-NAT
 P2P-NAT就是提供NAT功能,也提供防火墙功能的P2P- Middlebox。一台P2P- Middlebox必须为UDP传输至少提供Cone NAT功能,允许应用程序使用UDP建立完整的P2P连接。
    
 loopback translation(自环映射)
 当一台位于私有域的主机,它的NAT试图用此主机在公网的映射地址连接其他位于同样NAT后的其他主机,相当于NAT装置在数据包上做了两次NAT转换。在数据报到达目标主机之前,源主机的私有端点被转换成NAT分配的公共端点,然后把目标主机的公共端点转换成目标主机的私有端点。我们在上面提到的地址转换用一台NAT装置完成就称为"Loopback translation"3. 用middleboxes进行P2P通讯的技术
 这段文章详细的回顾一下使用现有的middlebox实现P2P通讯的技术,主要从应用程序或协议设计上来述说。
 3.1 Relaying(传输)
 最可靠的,但是效率最低的,实现P2P通讯的方法就是在传输过程中,把P2P通讯看成向C/S通讯的网络一样。例如,推想2个客户主机,A和B,各自发起一个TCP或UDP的连接,连接到一个大家都知道的拥有固定IP地址的服务器S上。客户主机都在各自的私有网络中,但是它们各自的middlebox不允许自己的客户端可以直接和其它主机连接。
                                 Server S
                                    |
                                    |
             +----------------------+----------------------+
             |                                             |
           NAT A                                         NAT B
             |                                             |
             |                                             |
          Client A                                      Client B
 不能直接连接,两个客户端就使用S服务器进行消息的传递。例如,要发送一条信息到客户端B,客户端A以C/S连接方式简单的发送一条信息到S服务器,然后S服务器使用已经和客户端B建立的C/S连接发送这条信息到客户端B。
 这种方法的优势在于只要两个客户端都连在服务器上,它就是有效的。它的明显缺点是它需要了服务器的处理并占用了带宽,而且即使服务器的网络状况良好,也有一定的通讯滞后问题。TRUN协议[TURN]定义了这种P2P应用的相关方法。
3.2 逆向连接 (Connection reversal)
 第二种技术是在只有一个客户位于middlebox后的条件下运用的。举例来说, 推想客户A在 NAT 后面但是客户 B 有一个全球有效的 IP 地址, 参照下面图表:
                                 Server S
                             18.181.0.31:1235
                                    |
                                    |
             +----------------------+----------------------+
             |                                             |
           NAT A                                           |
     155.99.25.11:62000                                    |
             |                                             |
             |                                             |
          Client A                                      Client B
       10.0.0.1:1234                               138.76.29.7:1234
 客户A有一个私有IP地址10.0.0.1,并且一个应用程序使用TCP端口1234。这个客户端和服务器S的公网IP地址18.181.0.31和端口1235建立了一个连接。NAT A为了客户端A和服务器S的会话,临时分配了一个终端地址,其TCP端口62000,它自己的IP地址是155.99.25.11:因此,服务器S认为客户端A用的是IP地址155.99.25.11,端口是62000。然而,客户端B有着自己的固定IP地址,138.76.29.7,并且在它上面的P2P应用程序可以在端口1234接收TCP连接。
 现在推想客户端B想要与客户端A建立一个P2P连接会话。B可能用客户端A本身的地址,即10.0.0.1:1234,也可能用在服务器S上得到到的地址,155,99.25.11:62000,去尝试连接。然而无论在哪一种情况下,连接都会失败。第一种情况下,指向IP地址10.0.0.1的通讯包会被丢弃,因为10.0.0.1不是一个公网固定IP地址。第二种情况下,来自客户端B的TCP SYN请求包将会到达NAT A的62000端口,但NAT A将会拒绝这个请求,因为NAT A只允许向外发送数据。
 在尝试和客户端A建立直接连接失败后,客户端B会利用服务器S传递一个请求,让客户端A去主动连接客户。客户端A在通过服务器S接收到传递的请求后,会使用客户端B的公共IP地址和端口建立一个TCP连接。 因为这个连接是在防火墙内部发起的,所以NAT A允许这个连接建立,而客户端B也能接收这个连接,因为它并不处于middlebox后面。当前实现P2P系统的一种技术,它有一个主要的局限性,就是它只能允许P2P中一方在NAT后面:而两方都在NAT后面的情况是很常见的,这种方法就会失败。因为这种逆向连接并不是解决问题的普遍方法,通常不推荐这个方法。应用程序可以选择试一试逆向连接,但当"向前""逆向"都不能建立连接时,应用程序应该能够自动的可以选择另外的连接机制,比如relaying(即3.1说的)。
 3.3 UDP hole punching
 第三种技术,也是这篇文章的一个重要点之一,就是被称为"UDP Hole Punching"的技术。当两个需要通讯的主机可能都在middlebox后面的时候,UDP hole punching依赖于cone NAT和普通防火墙的一些特性,允许合适的P2P应用程序以"punch holes"方式通过middlebox并且建立彼此之间直接的连接。这种技术在RFC 3027[NAT- PORT]的5.1节中简要的提及,并且在英特网[KEGEL]非证实的提到,也在最近的一些协议[TEREDO, ICE]中用到。正如名字中的所提到的,这种技术只能用于UDP连接。
 我们将会考虑两个特别情况,并且考虑应用程序如何完善的处理两者之间的握手连接。第一种情况下,也是较为普通的情况,两个在不通的NAT后面的客户端要求直接的进行P2P连接。第二种情况,两台客户端位于同一个NAT后面,但不能肯定(两台客户端位于同一个NAT后面)。
 3.3.1 位于不同NAT后面(Peers behind different NATs)
 假设客户端A和B都有自己的私有IP地址,也都位于不同的NAT后面。P2P应用程序在A、B和服务器S上运行,用的都是UDP端口1234。A和B各自和服务器S建立UDP通讯连接,使NAT A为A的连接分配一个自己的公共端口62000,而NAT B为B的连接分配的是31000端口。
                                 Server S
                             18.181.0.31:1234
                                    |
                                    |
             +----------------------+----------------------+
             |                                             |
           NAT A                                         NAT B
     155.99.25.11:62000                            138.76.29.7:31000
             |                                             |
             |                                             |
          Client A                                      Client B
       10.0.0.1:1234                                 10.1.1.3:1234
 现在推想一下,客户端A想要直接和B建立一个UDP通讯会话。假设A简单的发一个UDP信息包到B的公共地址138.76.29.7:31000,然而NAT B将会丢弃这些进入的数据信息(除非它是一个FULL cone NAT),原因是NAT B和S已经建立的外部会话,而A发送的信息中的源地址和端口号是和S不匹配的(可以参照一下上面的内容,匹配才能接受)。同样,假如B发送一个条UDP数据包给A的公网地址,NAT A也会丢弃。
 但是,假设A发出一个UDP数据信息给B的公网IP地址,同时也通过服务器S传递一个请求给B,要求B也发一个UDP信息给A的公网IP地址。A直接向B的公共IP地址(138.76.29.7:31000)发送的数据包会让NAT A在A的私有地址和B的公网地址之间建立了一个新的连接会话。同时,B到A的公网地址(155.99.25.11:62000)的信息会导致NAT B在B的私有地址和A的公共地址之间建立一个新的连接会话。一旦这种新的UDP连接在两者之间建立起来,客户端A和B就不需要服务器S的"介绍"就能彼此直接通讯了。
 UDP hole punching技术有几个很有用的特点。一旦在两个位于middlebox后面的客户端建立了一个直接的P2P连接,在连接中的任何一方都可以扮演一个"介绍人"的角色,依次继续和另一个客户端建立连接,减少了最初的服务器S的负担。如果说有[STUN]的话,假如两个中的任意一个或两个都碰巧不在middlebox后面,上述应用程序将同样可以建立P2P通讯通道,应用程序不需要尝试明确middlebox的类型。Hole punching技术甚至可以自动的运用在多级NAT下面,多重NAT就是那些客户端需要经历多级地址转换才能进入公网。
 3.3.2 位于同一NAT后(Peers behind the same NAT)
 现在考虑两台客户端(但并不确定)都在同一个NAT后面的情况,因此会有私有IP地址空间。客户端A与服务器S建立一个UDP会话,NAT会分配一个公共端口62000。客户端B与服务器S也建立一个简单的连接,NAT为此分配一个公共端口62001。
                                 Server S
                             18.181.0.31:1234
                                    |
                                    |
                                   NAT
                          A-S 155.99.25.11:62000
                          B-S 155.99.25.11:62001
                                    |
             +----------------------+----------------------+
             |                                             |
          Client A                                      Client B
       10.0.0.1:1234                                 10.1.1.3:1234
 假想A和B使用UDP hole punching技术与服务器S的建立一个外部的通讯路线做为中间介绍。然后A和B将可以通过服务器S得到各自公共IP地址和端口号,然后使用这些地址各自向对方发送数据。两个客户能够以这种方式彼此通讯,只要NAT不仅仅允许外网上的主机可以和内网上的主机进行UDP传输会话,也可以允许内网上的主机可以和其他内网的主机进行UDP会话。我们在"loopback translation"中设计到这种情况,因为来自私有网络的数据包到达NAT后,会"looped back"到私有网络上就象从公网来的一样。例如,当A向B的公共IP地址发送一个UDP包,这个包的包头有一个源IP地址和端口,是10.0.0.1:1234,而目的地址是155.99.25.11.62001。NAT接受到这个包,会把源地址转换(映射)为155.99.25.11:62000(就是A的公网地址),把目的地址转换为10.1.1.3:1234,然后发给B。即使NAT支持回环映射,NAT的转换和发送步骤看上去是多余的,在A和B通讯时似乎为NAT添加了潜在的负担。
 这个问题的解决方法是直接的。当A和B一开始在服务器S上交换地址信息时,它们就可以包含他们自己的IP地址和端口号,并且是可见的,对服务器S也是可见的。客户端根据它们得到的地址同时开始向对方发数据包,并建立成功的通讯。假如这两个客户端都在同一NAT后面,数据包象通讯一开始就能直接到达,而不需要通过NAT就能建立直接连接。假如这两个客户端位于不同的NAT后,到达彼此私有地址的数据包会被丢弃,但是客户端可以通过各自的公共地址来建立连接。重要的是这些数据包需要通过一些方法去鉴别,然而,在这种情况下,A发到B的私有地址的数据包完全有可能到达A私网内其他无关的终端,B发到A的包也是这样。
 3.3.3 Peers separated by multiple NATs(多级NAT)
 在有多重NAT设备的拓扑结构中,如果没有一些拓扑的知识,在两个客户端之间建立理想的P2P链路是不可能的。看看下面的举的例子。
                                 Server S
                             18.181.0.31:1234
                                    |
                                    |
                                  NAT X
                          A-S 155.99.25.11:62000
                          B-S 155.99.25.11:62001
                                    |
                                    |
             +----------------------+----------------------+
             |                                             |
           NAT A                                         NAT B
     192.168.1.1:30000                             192.168.1.2:31000
             |                                             |
             |                                             |
          Client A                                      Client B
       10.0.0.1:1234                                 10.1.1.3:1234
 假设NAT X是由一个英特网服务提供者(ISP)设置的一个大型NAT,在一些公网IP地址上拥有许多用户,NAT A和B是小用户群的NAT网关,由ISP的用户自己独自配置,有各自的私有网络和用户群,使用的是ISP提供的IP地址。只有SERVER S和NAT X有自己全球固定的IP地址,而NAT A和B用的"公共"IP地址实际上是ISP地址域中私有地址,而客户端A和B的地址对NAT A和B来说也是私有的地址。每当客户端需要和服务器S建立一个外部的连接,都会导致NAT A和B和客户端建立一个单独的公共/私有连接,然后让NAT X为每个连接会话建立一个公共/私有连接。
 现在推想客户A和B尝试建立一个直接的P2P UDP连接。对客户端A来说,最佳的方法是发送一个数据信息到客户端B在NAT B上,属于ISP的地址域的公共IP地址192.168.1.2:31000,对客户端B来说就是发信息到A在NAT A的公共IP地址192.168.1.1:30000(原文是NAT B,是不是笔误,还是我理解有问题?)。不幸的是,A和B并没有知道这些地址的方法,因为服务器S只能看到客户端"全局"的公共IP地址,就是155.99.25.11:62000和155.99.25.11:62001。甚至当A和B有某些方法可以得到这些地址,但他们依然不能保证这些地址是有用的,因为这些由ISP的私有地址域分配的地址可能与客户自己分配的私有地址由冲突。客户端因此没有选择只能使用由服务器S知道的公共IP地址来通讯,并且依赖NAT X来提供loopback translation。
3.3.4 Consistent prot binddings(保持端口绑定)
 hole punching 技术有一个需要注意的地方:它只能工作在两台NAT都是cone NAT(或没有NAT 防火墙)的情况下,只要UDP端口还在使用,它就需要保持一个固定的端口把一个给定(私有IP,私有UDP端口)和一个(公共IP,公共UDP端口)绑定。象对称NAT一样,为每个新的连接会话分配一个新的公共端口,对一个UDP应用程序来说,为了和不同的外部通讯重用一个已经存在的地址转换是不可以的。(这边稍微有点糊涂,再多看看。) 既然cone NAT运用是相当普遍的,UDP hole punching技术应用的也相当广泛,但是还有一小部分是对等NAT配置,因此不能支持这种技术。

 3.4 UDP port number prediction
 有关UDP hole punching技术在上面已经被讨论过,它可以允许在一些对等NAT存在的地方也能建立P2P UDP连接会话。这种方法有时被称为"N+1"技术 [BIDIR ]并且由Takeda[SYM-STUN]详细介绍。这种方法分析NAT的工作方式并且试图预测它为将来的连接会话分配的公共端口。再次考虑那两个客户的状态,A和B,在各自分开的NAT后面,已经与一台拥有永久地址的服务器S建立了UDP连接。
                                  Server S
                               18.181.0.31:1234
                                      |
                                      |
               +----------------------+----------------------+
               |                                             |
        Symmetric NAT A                               Symmetric NAT B
    A-S 155.99.25.11:62000                        B-S 138.76.29.7:31000
               |                                             |
               |                                             |
            Client A                                      Client B
         10.0.0.1:1234                                 10.1.1.3:1234
 NAT A分配一个属于自己的UDP端口62000以在A和S之间建立通讯连接,而NAT B分配一个31000端口用于在B和S之间建立连接。通过与服务器的通讯,A和B可以从服务器S上得到对方的公共IP地址和端口号。客户端A现在发送一个UDP数据包到地址138.76.29.7,端口31001(注意端口数目的增加),而客户端B同时发送一个数据包到地址的155,99.25.11,端口62001上。如果NAT A和B依次为新的连接分配端口,如果从A-S和B-S连接建立后没过多少时间,那在A和B之间的一个双向通讯通道就可以工作起来。A到B的数据包让NAT A建立一个新的连接,NAT A(所期望的)分配一个公共端口62001,因为之前A和S的连接会话用的62000端口,接下来就是62001。同样的,B到A的数据包将让NAT B打开一个新连接,并将(也是所期望的)分配一个端口31001。如果客户端可以正确的预测到NAT为新的连接分配的端口,一条双向的UDP通讯通道就会象如下图所示一样建立起来。
                                  Server S
                               18.181.0.31:1234
                                      |
                                      |
               +----------------------+----------------------+
               |                                             |
             NAT A                                         NAT B
    A-S 155.99.25.11:62000                        B-S 138.76.29.7:31000
    A-B 155.99.25.11:62001                        B-A 138.76.29.7:31001
               |                                             |
               |                                             |
            Client A                                      Client B
         10.0.0.1:1234                                 10.1.1.3:1234

 显而易见有很多情况都能导致这种方法失败。假如任意一个预测的端口碰巧已经被其他无关的连接占用,NAT将会错过正确的端口,连接尝试也将失败。假如任意一个NAT有时或者总是选择非连续的端口号,这个方法也将失败。假如在A(B)建立了它和S的连接之后,但在发送第一个数据包到B(A)之前,一个不同的客户端在NAT(也或者B)打开一个新的外部连接到任何外部主机,无关的客户端会不注意的""了(A TO B或者B TO A)所要求的端口。因此在任一NAT都包含不止一台客户端时,这种方法很少使用。
 实际上,如果那些NAT是cone NAT,或者一个是cone NAT,另一个是对称NAT,这种情况下的P2P应用程序依然需要工作,应用程序需要实现查明在任何一个上与end [STUN]有关的NAT是哪一钟,并按此来修改它的工作方式,这样增加了算法的复杂程序并让网络变的脆弱。最终,假如任何一方客户端在2级以上的NAT下并且离客户端最近的NAT是对称的,预测端口的方式是无法工作的。对所有这些原因来说,应用程序是无法实现这种方法的,在这里被提及是为了历史和信息目的(就是告诉大家有这么回事,我想)
 3.5. Simultaneous TCP open(TCP同时打开)
 在一对节点都在已存在middlebox后,有一种建立直接P2P TCP连接的方法有时候会被使用。大多数TCP连接都是从一个终端发从一个SYN包到另一个终端,另一个中断同步响应一个SYN-ACK包。无论怎样,对于两个终端来说,同时通过发送同步包到对方然后用一个ACK包应答来建立一个TCP连接是可行的。这种过程就被称为"simultaneous open"(同时打开)
 如果一个middlebox从尝试建立一个TCP连接的私有网络的外面接受一个TCP SYN包,middlebox通常以丢弃这个SYN包或者发送一个TCP RST(连接复位)包的方式来拒绝这个连接尝试。但是,如果同步包与源和目的地址端口一起到达,那么会让middlebox相信一个TCP连接已经建立起来,然后middlebox将会允许数据包通过。特别是如果middlebox刚刚得到并转换了一个从同样地址和端口来的SYN包,它将认为连接是成立的并允许进来的SYN通过。如果客户端A和B能彼此预测公共端口,它们各自的middlebox将分配下一个TCP连接端口,如果其中一个客户端和另一个客户端建立一个外部的TCP连接,可以在对方SYN到达本地middlebox之前就发送SYN包通过它本地自己的middlebox,那么P2P TCP连接就可以工作了。
 令人遗憾的是,这个方法也可能比上面说的UDP端口号预测方法更脆弱并对时效更加敏感。首先,除非在进行TCP连接时,两个middleboxes是简单的防火墙或者cone NAT,在各自尝试猜测公共端口号来让NAT分配新的连接时,和上面(UDP端口预测)说到的完全一样的事情会导致连接失败。另外,如果有一方的客户发送的同步包太迅速的到达对面的middlebox,远端middlebox可能会用一个RST包拒绝SYN包,接下来就会导致本地的middlebox关闭对话并且在将来SYN重发时使用了相同但无用的端口号。最终,对simultaneous open的支持作为一个TCP的特殊应用,没有在广泛的系统中被使用。因此,这个方法也只为历史因素在这里被同样提及;它不建议被应用程序使用。在现有NAT上想要实现P2P直接通讯的应用程序应该使用UDP。
 4. Application design guidelines(应用程序设计思路)
 4.1 What works with P2P middleboxes(如何和P2P middlebox一起工作)
 既然UDP hole punching是在两个都位于NAT后的主机之间建立P2P直接通讯方法中最有效率的一个,并且它可以在多种现有NAT上使用,如果要求建立有效的P2P通讯,通常建议这种方法,但当直接通讯不能建立时,就得依靠简单的传播。(还不怎么明白)
 4.2 Peers behind the same NAT(主机在同一个NAT后面)
 实际上有相当数量的用户不止2个IP地址,会有3个或者更多的IP地址。这样的话,告诉登录服务器究竟是哪一个地址变的就有些困难了。因此在这种情况下,应用程序应该发送它所有的IP地址。
 4.3 Peer discovery(主机发现)
 应用程序会发送一些数据包到几个地址,以发现哪一个地址是最合适的,应用程序可能变成(后面的不太明白,自己理解吧,sorry), 由于主机可能会不恰当的选择一个路由地址当做一个内部局域网(例如11.0.1.1,已经被DOD网络分配,DOD是一种网络模型)。因此应用程序应该小心的发送推测的呼叫包。
 申请把包送到几地址发现, 哪一个适宜适合一规定贵族使用可能成为一个乱扔净价的'' 空间废品''的显著的源头,
 4.4 TCP P2P applications (TCP P2P应用程序)
 被程序员们广泛使用的SOCKET API,常用于C/S结构应用设计中。在它的通常使用方式中,一个SOCKET能绑定一个TCP或UDP端口。一个应用程序不会被允许用同样的端口(TCP or UDP)和多个SOCKET绑定来和多个外部主机同时建立连接(或)用一个SOCKET在端口上监听而其他SOCKET来建立外部连接。但是上述单个SOCKET的端口绑定限制在UDP上不是问题,因为UDP是基于数据报文的协议。UDP P2P应用程序设计者可以用recvfrom()和sendto()函数来让一个SOCKET不仅发送而且可以从多个主机上接受数据报文。
 这不是TCP具有的情况。由于TCP,每个输入和输出连接都要和一个单独的SOCKET有联系。Linux Sockets API用SO_REUSEADDR选项的帮助来解决这个问题(是不是应该这么说?),这个选项好象不起作用,但可以
 (在标准Single Unix里没有这个函数)。Win32 API提供了一个相同的调用SetReuseAddress。使用任何上述的选择,应用程序可以复用一个TCP端口的多个SOCKET。就是说,可以打开两个绑定在同样端口上的TCP stream Socket,一个用与listen(),另一个用与其它主机connect()
 4.5 Use of midcom protocol()
 如果应用程序知道它们需要穿越的middlebox并且这些middlebox实现midcom 协议,应用程序能使用midcom协议更容易的穿越middlebox。
 例如,P2P应用程序需要NAT middlebox保持终端端口的绑定状态。假如middlebox可以支持midcom,P2P应用程序可以控制修改绑定端口(或者绑定地址)的参数,例如生存时间,maxidletime(?),因此应用程序不仅可以直接的连接外部主机而且也可以从外部主机接受连接;这样就不需要定期保持端口绑定的状态。当应用程序不再需要绑定,也可以使用midcom协议简单的取消绑定。
 5. NAT Design Guidelines (NAT设计指导)
 这部分讨论网络地址转换的设计,他们会影响P2P应用程序。
 5.1 Deprecat the use of symmetric NATs (不赞成使用对等NAT)
 对等NAT在那些C/S结构的应用中比如网络浏览中得到广泛应用,它们只需要建立一个向外的连接即可。但是现在,比如实时消息和语音会议等P2P应用程序被广泛的应用。对等NAT不能支持保留终端的定义并且不适用于P2P应用程序。不建议使用对等NAT来支持P2P应用程序。
 一个P2P-middlebox必须在UDP通讯时具备Cone NAT的特点,允许应用程序可以建立使用UDP hole punching技术建立稳定的P2P连接。理论上,一个P2P-middlebox应该也允许应用程序既可以经过TCP,也可以通过UDP建立P2P连接。
 5.2 Add incremental cone-NAT support to symmetric NAT devices (增加递增的cone-NAT以支持对等NAT设备)
 一种可以让对等NAT设备扩展支持P2P应用程序的是分配它的可转让的端口空间,为一到一的连接预订一个合适的端口,为一个一到多的连接预订合适的一套不同的端口。
 更进一步(未来?),一个NAT装置可以明确的被那些P2P应用程序和主机配置,因此NAT装置可以自动由正确的端口数据块来分配一个P2P端口。
 5.3 Maintain consisten port bindings for UDP ports (保持UDP端口的绑定)
 这份资料对NAT设计者最主要和最重要的建议是NAT保持一个固定的端口,绑定一个给定的(内部IP地址,内部UDP端口)和一个相应的(公共IP地址,公共UDP端口)
 只要有任何连接存在并使用这个端口绑定,这个端口绑定就要存在。通过检查每一个包的源和目的IP地址和端口号,NAT可能会过滤关于每一个连接基础的数据包(? 俺8懂)。当在一个私有网络上的节点建立了一个新的外部连接时,使用了一个现有的已经转换过的UDP连接会话的IP地址和UDP端口号,NAT应该保证新的UDP连接作为现有连接给定一个同样的公共IP地址和UDP端口。
 5.3.1 Preserving port numbers(保持端口号)  (就是客户端用啥端口,NAT分配啥端口这个意思吧)
 一些NAT,当建立一个新的UDP连接时,会尝试给一个相应的私有端口号分配一个同样的公共端口号,如果这个端口号碰巧是可用的。例如,假如地址是10.0.0.1的客户端A用一个从端口1234发送的数据包建立一个外部的UDP连接,NAT的公共端口1234碰巧是可用的,那么NAT会为连接使用NAT公共IP地址上的端口号1234作为转换客户端A的地址。由于如果内部网络的最多一个节点正在使用这个端口号,它是对一个NAT保持端口唯一可行的方法,这种方式可能对一些希望只用特别的UDP端口号的过去的UDP程序有帮助,但不建议应用程序依靠这种方式。
 另外, 一个NAT不应该在一个新连接中保持端口号,如果确实如此将与保持公共和私有终端地址绑定的目的相抵触。例如,假定客户端A在内部的1234端口上与外部服务器S建立了一个连接,NAT A已经为这个连接分配了公共端口62000,因为端口号1234在NAT上此时是不可用的。现在假想在NAT上的端口号1234后来可以使用了,而此时A和S的连接仍然存在,客户端A用同样的内部端口1234建立一个新的连接到外部节点B上。在这种情况下,因为端口绑定已经在客户端A的端口1234和NAT的公共端口62000上建立,所以这个绑定应该继续保持,新连接也应该用62000端口作为公共端口,来对应客户端A的端口1234。NAT不应该仅仅因为端口1234变的可用了就为新的连接分配公共端口1234:这种特点不可能在任何情况下都对应用程序有帮助,由于应用程序已经对一个转换的端口号进行操作,并且它会中断应用程序用UDP hole punching技术建立P2P连接的尝试。
 5.4 Maintaining consistent port bindings for TCP ports (为TCP端口保持端口绑定)
 和UDP地址转换的特点一致,cone NAT也应该对TCP连接保持私有和公共IP地址,TCP端口号的绑定不变,就如上面的关于UDP描述的方式一样。保持TCP终端绑定不变将增加NAT对P2P TCP应用程序在从同样源端口建立多个TCP连接时的兼容性。
 5.5 Large timeout for P2P applications (P2P程序的大超时?)
 我们推荐middlebox在P2P应用时使用的最小超时大约5分钟(300秒),即,在P2P应用时为端口绑定或为分配端口时,来给middlebox配置这个空闲超时时间。当middlebox惯用目前配置时,就常常尝试使用更短的时间。但是更短的超时时间是有些问题的。考虑一个有16个终端节点的P2P应用程序,他们将每10秒发给网络一个保持活动状态的数据包以避免NAT超时。这样做是因为一个可能会以middlebox超时时间的5倍发送保持活动的数据包,在这种情况下,保持活动的包将在网络上被丢弃。
 5.6 Support loopback translation(支持自环转换)
 我们强烈建议middlebox 支持自环转换,允许在一台middlebox后面的主机可以和其它位于同一middlebox后的主机通过它们的可能被转换的公共端点通讯。支持自环转换是相当重要的,特别是在那些大容量多层NAT中,作为第一级的NAT。如第3.3.3 部分的描述,位于同一台一级NAT下,但是第二级NAT不同的主机无法通过UDP hole punching彼此通讯,即使全部middlebox保持端点在NAT上的映射不变,除非第一级NAT支持自环转换。
 
View Code

URL中传递中文参数 以流形式文件上传下载 演变

最近在做的一个项目中要用到文件上传下载,初看到这个东西,感觉太简单,但是真正去做了才知道,很多东西只有去做了才知道什么叫做复杂。首先,我们先不直接切入主题,先来解决一下朋友们经常会碰到在做.NET开发时候传递中文参数时候出现乱码的问题,然后引申到我们的流形式文件上传下载。开整……
    在做ASP.NET开发时,URL如果要传递中文参数,有时候会报错,会出现乱码。比如在YeanJay.aspx?parm1=中国。在Request.QueryString时,可能会出现乱码。解决的办法如下:
 1. 通过设置Web.config指定ASP.NET应用程序默认的请求和响应编码
1

<system.web>  
2

 <globalization requestEncoding="gb2312" responseEncoding="gb2312" culture="zh-CN" fileEncoding="gb2312" />
3

</system.web>
 
 
 2. 通过设置aspx页面head指定ASP.NET页面的请求和响应编码
 
1

<head runat="server">
2

<title></title>
3

<meta http-equiv="Content-Type" content="text/html;charset=gb2312">
4

</head>
 
3.在传递中文之前,对将要传递的中文参数进行编码,在接受时在进行解码
  1)通过在HTML中的用户控件编码:HttpUtility.UrlEncode
 
1

<asp:HyperLink ID="HyperLink1" runat="server" 
2

NavigateURL='<%# "YeanJay.aspx?name=" + HttpUtility.UrlEncode("戚应杰", System.Text.Encoding.GetEncoding("GB2312")) %>' >传给Ken</asp:HyperLink>
 
  2)在服务器端的编码:Server.UrlEncode
 
1

protected void btnTrans_Click(object sender,EventArgs e)
2

{
3

string Name="戚应杰";
4

Response.Redirect("YeanJay.aspx?name="+Server.UrlEncode(Name));
5

}
 
 3)在客户端JavaScript编码传输
 

Code
 
接收上面这三种编码方式传递过来的值,并且解码:
 

Code
 
一般来说,只要设置了Web.config文件就可以了。但是如果你用JS来调用WS的话,有时候设置Web.config文件会失效。
如果还是不行,那么就让机器环境支持中文
(1)将页面用记事本,然后选中“另存为”并选择“UTF-8”作为编码方式,而不是默认的ANSI。
(2)打开注册表,确保HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Inetinfo\Parameters\FavorDBCS值设置为0。
(3)修改该数值后,必须运行IISRESET以重新启动IIS服务。
这样就可以保证传递的中文参数可以识别了。
   那么接下来就上面的那个中文参数编码的方式,让我想到最近的项目中流形式文件存储与读取。
   首先先来做文件上传:
  


 1


 2

                string send = Convert.ToBase64String(filMyFile.FileBytes);
 3

                int fileLength = filMyFile.PostedFile.ContentLength;
 4

                if (fileLength == 0)
 5

                {
 6

                    ScriptManager.RegisterStartupScript(this, typeof(Page), "opennewwindow", "alert(\"不能上传空文件!\")", true);
 7

                    return;
 8

                }
 9

                //DataService ser = new DataService();
10

                DataService.DataService service = new Hiktb.Monitor.Web.DataService.DataService();
11

                service.Url = ConfigurationManager.AppSettings["UploadUrl"];
12

                PowerGridManager pgm = new PowerGridManager();
13

                
14

                //ser.Url = "http://localhost/Sendweb/DataService.asmx";
15

                try
16

                {
17

                    service.UploadFile(filMyFile.FileName, FileType.SelectedValue.Trim(), AdminUnit.SelectedValue.Trim(), send);
18

                    ScriptManager.RegisterStartupScript(this, typeof(Page), "opennewwindow", "alert(\"上传成功!\")", true);
19

                }
20

                catch (Exception ex) 
21

                {
22

                    ScriptManager.RegisterStartupScript(this, typeof(Page), "opennewwindow", "alert(\"上传不成功!\")", true);
23

                }
24

            }
25

            else
26

            {
27

                ScriptManager.RegisterStartupScript(this, typeof(Page), "opennewwindow", "alert(\"请选择一个文件!\")", true);
28

            }
这里的文件上传没有什么问题。首先是获取filMyFile这个文件上传控件里面的文件内容,然后对文件内容进行编码。然后通过调用UpLoadFile函数来上传文件。上传文件非常顺利。
然后就是下载文件:
    一开始项目中用到了是用临时目录就是先从数据库中取出流然后生成一个文件,然后将文件放到一个临时目录中,那样的话万一文件一多,那么临时目录中的文件就非常多,然后就客户需求开始变化,说不要设置临时目录。
     设置临时目录的Code如下:
     


 1

              生成附件,并下载到客户端
 2

                string url = "GetFile.aspx?id=" + id;
 3


 4

              Response.Redirect(url);
 5


 6

              string TempPath = Request.PhysicalApplicationPath + "Temp/";
 7

              if (Directory.Exists(TempPath) == false) Directory.CreateDirectory(Request.PhysicalApplicationPath + "Temp/");
 8


 9

              string url = Request.ApplicationPath + "/Temp/" + ds.Tables[0].Rows[0]["FileName"];
10

              string name = TempPath + ds.Tables[0].Rows[0]["FileName"];
11


12

              FileStream outputStream = new FileStream(name, FileMode.Create);
13

              outputStream.Write((byte[])ds.Tables[0].Rows[0]["FileContent"], 0, ((byte[])ds.Tables[0].Rows[0]["FileContent"]).Length);
14

              outputStream.Close();
15

              Response.Write("<script>window.open('" + url + "')</script>");
 
    唯一不变的就是变化啊!!客户要变,我就变。然后就想,直接用流输出吧。
    一阵百度,Google代码是一堆,能用的没几个。那没几个里面能实现的就更没了。
    开整……就流呗……
    又来一段代码
   

Code
 
       这样之后然后就疯狂的报错。说不能识别。丫丫的,肯定是编码问题。
       然后就是如果是txt文件不能下载,只能直接在浏览器中看。郁闷。郁闷。
       不过有个朋友提醒,肯定是编码问题,然后也看了下代码。结果要加一句
       Response.ContentEncoding = System.Text.Encoding.UTF8;
        也就是把内容也编码了。郁闷结果就像下面的那样了。要提一下的是代码中的那个checktype(typelower)这个是一个判断ContentType的函数。
       一般如.doc文件么就是   application/msword。其他的网上找找很多的。
       就变成了下面的代码了。问题解决了。所有的文件都能下载。
 

Code
 
            总结:  其实无非就是用到了一个知识点,就是编码。其实知识之间很融汇贯通的。同样是编码,在很多地方用到。
 
View Code

WebClient 轻松实现文件下载上传、网页抓取

我们知道用 WebRequest(HttpWebRequest、FtpWebRequest) 和 WebResponse(HttpWebResponse、FtpWebResponse)可以实现文件下载上传、网页抓取,可是用 WebClient 更轻松。
用 DownloadFile 下载网页
using (System.Net.WebClient client = new System.Net.WebClient())
{
    client.DownloadFile("http://www.cftea.com/", "C:\\foo.txt");
}
就这样,http://www.cftea.com/ 首页就被保存到 C 盘下了。
用 DownloadData 或 OpenRead 抓取网页
using (System.Net.WebClient client = new System.Net.WebClient())
{
    byte[] bytes = client.DownloadData("http://www.cftea.com/");
    string str = (System.Text.Encoding.GetEncoding("gb2312").GetString(bytes);
}
我们将抓取来的网页赋给变量 str,任由我们使用。也可以用 OpenRead 方法来获取数据流。
using (System.Net.WebClient client = new System.Net.WebClient())
{
    using (System.IO.Stream stream = client.OpenRead("http://www.cftea.com/"))
    {
        using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, System.Text.Encoding.GetEncoding("gb2312")))
        {
            string str = reader.ReadToEnd();
            reader.Close();
        }
        stream.Close();
    }
}
用 UploadFile 上传文件
相对于 DownloadData、OpenRead,WebClient 也具有 UploadData、OpenWrite 方法,但最常用的恐怕还是上传文件,也就是用方法 UploadFile。
using (System.Net.WebClient client = new System.Net.WebClient())
{
    client.Credentials = new System.Net.NetworkCredential("用户名", "密码");
    client.UploadFile("ftp://www.cftea.com/foo.txt", "C:\\foo.txt");
}
注意 UploadFile 的第一个参数,要把上传后形成的文件名加上去,也就是说这里不能是:ftp://www.cftea.com/。
 
View Code

c#下载文件

下载网络资源文件到本地的方法(2种)
首先说明下Page.Response :
命名空间:  System.Web.UI
程序集:  System.Web(在 System.Web.dll 中) 
获取与该 Page 对象关联的 HttpResponse 对象。该对象使您得以将 HTTP 响应数据发送到客户端,并包
含有关该响应的信息。 
方法一(上/下载):
System.Net.WebClient:
提供向 URI 标识的资源发送数据和从 URI 标识的资源接收数据的公共方法。
WebClient 类提供向 URI 标识的任何本地、Intranet 或 Internet 资源发送数据以及从这些资源接收数
据的公共方法。 
I、 将数据上载到资源的 WebClient 方法:
OpenWrite  :检索一个用于将数据发送到资源的 Stream。 
UploadFile :将本地文件发送到资源,并返回包含任何响应的 Byte 数组。
II、从资源下载数据的 WebClient 方法: 
OpenRead   : 从资源以 Stream 的形式返回数据。 
DownloadFile : 从资源将数据下载到本地文件。 
WebClient client = new WebClient(); 
client.DownloadFile(address, fileName); 
方法二(下载)
使用HttpWebRequest下载文件
[c-sharp] view plaincopy
1.     /// <summary>          
2.     /// 下载文件          
3.     /// </summary>          
4.     /// <param name="URL">下载文件地址</param>         
5.     ///   
6.     /// <param name="Filename">下载后的存放地址</param>          
7.     /// <param name="Prog">用于显示的进度条</param>          
8.     ///   
9.     public string DownloadFile(string URL, string filename)  
10.     {  
11.         try  
12.         {  
13.             System.Net.HttpWebRequest Myrq = (System.Net.HttpWebRequest)  
14.   
15. System.Net.HttpWebRequest.Create(URL);  
16.             System.Net.HttpWebResponse myrp = (System.Net.HttpWebResponse)Myrq.GetResponse  
17.   
18. ();  
19.             long totalBytes = myrp.ContentLength;  
20.             if (myrp.ContentLength != 0)  
21.             {  
22.                 // MessageBox.Show("文件存在");  
23.                 System.IO.Stream st = myrp.GetResponseStream();  
24.                 System.IO.Stream so = new System.IO.FileStream(filename,   
25.   
26. System.IO.FileMode.Create);  
27.                 long totalDownloadedByte = 0;  
28.                 byte[] by = new byte[1024];  
29.                 int osize = st.Read(by, 0, (int)by.Length);  
30.                 while (osize > 0)  
31.                 {  
32.                     totalDownloadedByte = osize + totalDownloadedByte;  
33.                     so.Write(by, 0, osize);  
34.                     osize = st.Read(by, 0, (int)by.Length);  
35.                 }  
36.                 so.Close();  
37.                 st.Close();  
38.                 return "success";  
39.             }else  
40.             {  
41.                 MessageBox.Show("文件不存在");  
42.             }  
43.         }  
44.         catch (System.Exception e)  
45.         {  
46.             return "下载文件失败!" + e.ToString();  
47.         }  
48.     }  
从服务器端下载文件到本地的方法:
[c-sharp] view plaincopy
1. //I、    
2.     /// <summary>  
3.     /// HttpResponse.AddHeader 方法--将一个 HTTP 头添加到输出流。  
4.     /// public void AddHeader( string name, string value )  
5.     /// name                              要添加 value 的 HTTP 头名称。   
6.     /// value                             要添加到头中的字符串。  
7.     /// Response.Clear() 方法:           清除缓冲区流中的所有内容输出  
8.     /// HttpRequest.ContentType    属性: 获取或设置传入请求的 MIME 内容类型  
9.     /// HttpResponse.BinaryWrite() 方法: 将一个二进制字符串写入 HTTP 输出流。  
10.     /// HttpResponse.Flush()       方法: 向客户端发送当前所有缓冲的输出  
11.     /// HttpResponse.WriteFile()   方法: 将指定的文件直接写入 HTTP 响应输出流。  
12.     /// HttpResponse.End()         方法: 将当前所有缓冲的输出发送到客户端,停止该页的执行,  
13.     ///                                   并引发 EndRequest 事件。  
14.     /// </summary>  
15.     /// <param name="fileName"></param> 客户端保存的文件名  
16.     /// <param name="filePath"></param> 服务端文件路径  
17.     private void down_load_file(string fileName, string filePath)  
18.     {  
19.       //  string fileName = "ce2.rar";  
20.       //  string filePath = Server.MapPath("keji.rar");//路径  
21.   
22.         //以字符流的形式下载文件  
23.         FileStream fs = new FileStream(filePath, FileMode.Open);  
24.         byte[] bytes = new byte[(int)fs.Length];  
25.         fs.Read(bytes, 0, bytes.Length);  
26.         fs.Close();  
27.         Response.ContentType = "application/octet-stream";  
28.         //通知浏览器下载文件而不是打开  
29.         Response.AddHeader("Content-Disposition", "attachment;   filename=" +   
30.   
31. HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8));  
32.         Response.BinaryWrite(bytes);  
33.         Response.Flush();  
34.         Response.End();  
35.     }  
36. //II、  
37.         string filePath = @"E:/下载地址保存2/1.rar";  
38.         System.IO.FileInfo file = new System.IO.FileInfo(filePath);  
39.         if (file.Exists)  
40.         {  
41.             Response.Clear();  
42.             Response.AddHeader("Content-Disposition", "attachment; filename=" + file.Name);  
43.             Response.AddHeader("Content-Length", file.Length.ToString());  
44.             Response.ContentType = "application/octet-stream";  
45.             Response.Filter.Close();  
46.             Response.WriteFile(file.FullName, false);  
47.             Response.End();  
48.         }  
 
View Code

多线程断点续传二

上篇文章写完,由于整体思路是正确的,但是没有真正形成多线程下载,所以对本身的代码进行关键点的检查,尤其在一些操作web请求的地方,看看是否有什么问题,最后发现显示的关闭HttpWebResponse对象,能稍微有所改进。
原文参看:
http://www.csharpwin.com/csharpspace/11141r7795.shtml
 
那么修改后的类,大致代码如下:
//--------------------------- Download File class ---------------------------------------
//---------------------------------------------------------------------------------------
//---File:          clsNewDownloadFile
//---Description:   The multi-download file class file using HttpWebRequest class
//---Author:        Knight
//---Date:          Aug.4, 2006
//---------------------------------------------------------------------------------------
//---------------------------{Download File class}---------------------------------------
 
namespace DownloadFile
{
    using System;
    using System.IO;
    using System.Net;
    using System.Text;
    using System.Security;
    using System.Threading;
    using System.Collections.Specialized;
    using System.Diagnostics;
    using System.Data;
 
    /// <summary>
    /// Download file info
    /// </summary>
    public class DownloadFileInfo
    {
        private string _RequestURL;
        private string _ResponseURL;
        private string _FileName;
        private int _TotalSize;
        private int _BlockLeftSize;
 
        public string RequestURL
        {
            get{ return _RequestURL; }
        }
 
        public string ResponseURL
        {
            get{ return _ResponseURL; }
        }
        public string FileName
        {
            get{ return _FileName; }
        }
        public int TotalSize
        {
            get{ return _TotalSize;}
            set{ _TotalSize = value;}
        }
        public int BlockLeftSize
        {
            get{ return _BlockLeftSize;}
            set{ _BlockLeftSize = value;}
        }
 
        public DownloadFileInfo(
            string RequestURL,
            string ResponseURL,
            string FileName,
            int TotalSize,
            int BlockLeftSize )
        {
            this._RequestURL = RequestURL;
            this._ResponseURL = ResponseURL;
            this._FileName = FileName;
            this._TotalSize = TotalSize;
            this._BlockLeftSize = BlockLeftSize;
        }
 
        public DownloadFileInfo(
            string RequestURL,
            string ResponseURL,
            string FileName ):
            this( RequestURL, ResponseURL, FileName, 0, 0 )
        {
        }
    }
 
    /// <summary>
    /// Download event arguments
    /// </summary>
    public class DownLoadEventArgs : System.EventArgs
    {
        private DownloadFileInfo _DownloadFileInfo;
        private int _Position;
        private int _ReadCount;
        private int _ThreadNO;
 
        public DownloadFileInfo DownFileInfo
        {
            get{ return _DownloadFileInfo; }
        }
        public int StartPosition
        {
            get{ return _Position; }
        }
        public int ReadCount
        {
            get{ return _ReadCount; }
        }
        public int ThreadNO
        {
            get{ return _ThreadNO; }
        }
 
        public DownLoadEventArgs(
            DownloadFileInfo DownFileInfo,
            int nStartPostion,
            int nReadCount,
            int nThreadNO )
        {
            this._DownloadFileInfo = DownFileInfo;
            this._Position = nStartPostion;
            this._ReadCount = nReadCount;
            this._ThreadNO = nThreadNO;
        }
    }
   
    public delegate void DataReceivedEventHandler( DownLoadEventArgs e );
    public delegate void ThreadFinishedHandler();
 
    /// <summary>
    /// class for sub-download threads
    /// </summary>
    public class SubDownloadThread
    {
        private readonly DownloadFileInfo _FileInfo;
        private int _ThreadNO;
        private int _Position;
        private int _BlockSizeLeft;
        public event DataReceivedEventHandler DataReceived;
        private ThreadFinishedHandler _Finished;
        private bool _IsStopped;
 
        private Thread thdDownload;
 
        public SubDownloadThread(
            DownloadFileInfo DownFileInfo,
            int ThreadNO,
            int Position,
            int BlockSizeLeft,
            ThreadFinishedHandler Finished )
        {
            this._FileInfo = DownFileInfo;
            this._ThreadNO = ThreadNO;
            this._Position = Position;
            this._BlockSizeLeft = BlockSizeLeft;
            this._Finished = Finished;
        }
 
        /// <summary>
        /// Start to create thread to download file
        /// </summary>
        public void StartDownload()
        {
            thdDownload = new Thread( new ThreadStart( this.Download ) );
            thdDownload.Start();
        }
 
        /// <summary>
        /// Stop current download-thread
        /// </summary>
        public void Stop()
        {
            _IsStopped = true;
            if( thdDownload.ThreadState == System.Threading.ThreadState.Running )
                thdDownload.Join( 10 );
            Debug.WriteLine( string.Format( "Thread NO:{0} is stopped!" ,_ThreadNO ) );
        }
 
        /// <summary>
        /// Function for sub-thread
        /// </summary>
        private void Download()
        {
            HttpWebResponse hwrp = null;
 
            try
            {
                Debug.WriteLine( string.Format( "Thread NO:{0} begins to download!" ,_ThreadNO ) );
                // Creat request by url
                HttpWebRequest hwrq = (HttpWebRequest) WebRequest.Create(
                    new Uri( this._FileInfo.RequestURL ));
                // Go to download position
                hwrq.AddRange( _Position );
                // Get response from request
                hwrp = (HttpWebResponse) hwrq.GetResponse();
            }
            catch (Exception e)
            {
                Debug.WriteLine( e.Message );
                return;
            }
 
            // download and write data from
            Debug.WriteLine( string.Format( "Thread NO:{0} call function named DownloadData!" ,
                _ThreadNO ) );
            DownloadData( hwrp );
 
            hwrp.Close();//Close response here
        }
 
        /// <summary>
        /// Download data through web request
        /// </summary>
        /// <param name="Response"></param>
        private void DownloadData( WebResponse Response )
        {
            const int BUFFER_SIZE = 0x10000;//64k buffer size
            byte[] bBuffer = new byte[ BUFFER_SIZE ];
 
            Stream sReader = Response.GetResponseStream();
            if( sReader == null ) return;
           
            int nReadCount = 0;
 
            while( !_IsStopped && this._BlockSizeLeft > 0 )
            {
                Debug.WriteLine( string.Format( "Thread NO:{0} reads data!" ,
                    _ThreadNO ) );
                // Set read size
                nReadCount = ( BUFFER_SIZE > this._BlockSizeLeft )
                    ? this._BlockSizeLeft:BUFFER_SIZE ;//Get current read count
 
                // Read data
                int nRealReadCount = sReader.Read( bBuffer, 0, nReadCount );
                if( nRealReadCount <= 0 ) break;
 
                Debug.WriteLine( string.Format( "Thread NO:{0} writes data!" ,
                    _ThreadNO ) );
                // Write data
                using( FileStream sw = new FileStream(
                           this._FileInfo.FileName,
                           System.IO.FileMode.OpenOrCreate,
                           System.IO.FileAccess.ReadWrite, System.IO.FileShare.ReadWrite))
                {
                    sw.Position = this._Position;
                    sw.Write( bBuffer, 0, nRealReadCount );
                    Debug.WriteLine( string.Format( "Thread NO:{0} writes {1} data!" ,
                        _ThreadNO, nRealReadCount ) );
                    sw.Flush();
                    sw.Close();
                }
 
                Debug.WriteLine( string.Format( "Thread NO:{0} send callback info!" ,
                    _ThreadNO ) );
                // Call back
                DataReceived( new DownLoadEventArgs( this._FileInfo,
                    this._Position,
                    nRealReadCount,
                    this._ThreadNO ) );
 
                // Set position and block left
                this._Position += nRealReadCount;
                this._BlockSizeLeft -= nRealReadCount;
            }
 
            if( _IsStopped ) return;
 
            Debug.WriteLine( string.Format( "Thread NO:{0} sends finish-info!" ,
                _ThreadNO ) );
            _Finished();
        }
    }
 
    /// <summary>
    /// Class for download thread
    /// </summary>
    public class DownloadThread
    {
        private DownloadFileInfo _FileInfo;
        private SubDownloadThread[] subThreads;
        private int nThreadFinishedCount;
        private DataRow drFileInfo;
        private DataTable dtThreadInfo;
        public DataReceivedEventHandler DataReceived;
        public event ThreadFinishedHandler Finished;
       
        /// Class's Constructor
        public DownloadThread(
            string RequestURL,
            string ResponseURL,
            string FileName,
            DataRow NewFileInfo,
            int SubThreadNum )
        {
            // Create download file info
            _FileInfo = new DownloadFileInfo( RequestURL, ResponseURL, FileName );
 
            // Create request to get file info
            bool blnMultiDownload = GetFileDownInfo();
 
            // Create file info datarow
            drFileInfo = NewFileInfo;
            InitDataRow();
 
            // Create subthreads and set these info in threadinfo table
            if( SubThreadNum <= 0 ) SubThreadNum = 5;//Defualt value
 
            //Not support to be multi-thread download or less than 64k
            if( !blnMultiDownload || _FileInfo.TotalSize <= 0x10000 ) SubThreadNum = 1;
 
            subThreads = new SubDownloadThread[SubThreadNum];
            nThreadFinishedCount = 0;
            CreateThreadTable( SubThreadNum );
        }
 
        public DownloadThread( DataRow DownloadFileInfo, DataTable ThreadInfos )
        {
            // Create download file info
            drFileInfo = DownloadFileInfo;
            _FileInfo = new DownloadFileInfo(
                drFileInfo["RequestURL"].ToString(),
                drFileInfo["ResponseURL"].ToString(),
                drFileInfo["FileName"].ToString(),
                Convert.ToInt32( drFileInfo["TotalSize"].ToString() ),
                Convert.ToInt32( drFileInfo["BlockLeftSize"].ToString() ) );
 
            // Create sub thread class objects
            dtThreadInfo = ThreadInfos;
            subThreads = new SubDownloadThread[ dtThreadInfo.Rows.Count ];
            nThreadFinishedCount = 0;
        }
        /// {Class's Constructor}
 
        /// <summary>
        /// Start to download
        /// </summary>
        public void Start()
        {
            StartSubThreads();
        }
 
        /// <summary>
        /// Create table to save thread info
        /// </summary>
        /// <param name="SubThreadNum"></param>
        private void CreateThreadTable( int SubThreadNum )
        {
            int nThunkSize = _FileInfo.TotalSize / SubThreadNum;
            dtThreadInfo = new DataTable("DownloadFileInfo");
            dtThreadInfo.Columns.Add( "ThreadNO", typeof(int) );
            dtThreadInfo.Columns.Add( "Position", typeof(int) );
            dtThreadInfo.Columns.Add( "BlockLeftSize", typeof(int) );
 
            DataRow drNew;
            int i = 0;
            for( ; i < SubThreadNum-1; i++ )
            {
                drNew = dtThreadInfo.NewRow();
                drNew["ThreadNO"] = i;
                drNew["Position"] = i * nThunkSize;
                drNew["BlockLeftSize"] = nThunkSize;
                dtThreadInfo.Rows.Add( drNew );
            }
 
            drNew = dtThreadInfo.NewRow();
            drNew["ThreadNO"] = i;
            drNew["Position"] = i * nThunkSize;
            drNew["BlockLeftSize"] = _FileInfo.TotalSize - i * nThunkSize;
            dtThreadInfo.Rows.Add( drNew );
            dtThreadInfo.AcceptChanges();
        }
 
        /// <summary>
        /// Start sub-threads to download file
        /// </summary>
        private void StartSubThreads()
        {
            foreach( DataRow dr in dtThreadInfo.Rows )
            {
                int ThreadNO = Convert.ToInt32( dr["ThreadNO"].ToString() );
                if( Convert.ToInt32( dr["BlockLeftSize"].ToString() ) > 0 )
                {
                    subThreads[ ThreadNO ] = new SubDownloadThread(
                        _FileInfo,
                        ThreadNO,
                        Convert.ToInt32( dr["Position"].ToString() ),
                        Convert.ToInt32( dr["BlockLeftSize"].ToString() ),
                        new ThreadFinishedHandler( this.DownloadFinished ) );
 
                    subThreads[ ThreadNO ].DataReceived += this.DataReceived;
                    subThreads[ ThreadNO ].StartDownload();
                }
                else
                    DownloadFinished();
            }
        }
 
        /// <summary>
        /// Save download file info
        /// </summary>
        private void InitDataRow()
        {
            drFileInfo.BeginEdit();
            drFileInfo["RequestURL"] = _FileInfo.RequestURL;
            drFileInfo["ResponseURL"] = _FileInfo.ResponseURL;
            drFileInfo["FileName"] = _FileInfo.FileName;
            drFileInfo["TotalSize"] = _FileInfo.TotalSize;
            drFileInfo["BlockLeftSize"] = _FileInfo.BlockLeftSize;
            drFileInfo["CreatedTime"] = DateTime.Now.ToString( "yyyyMMddHHmmss" );
            drFileInfo.EndEdit();
            drFileInfo.AcceptChanges();
        }
 
        /// <summary>
        /// Check url to get download file info
        /// </summary>
        /// <returns></returns>
        private bool GetFileDownInfo()
        {
            HttpWebRequest hwrq;
            HttpWebResponse hwrp = null;
            try
            {
                //Create request
                hwrq = (HttpWebRequest) WebRequest.Create( _FileInfo.RequestURL );
                hwrp = (HttpWebResponse) hwrq.GetResponse();
 
                //Get file size info
                long L = hwrp.ContentLength;
                L = ((L == -1) || (L > 0x7fffffff)) ? ((long) 0x7fffffff) : L;
               
                _FileInfo.TotalSize = (int)L;
                _FileInfo.BlockLeftSize = _FileInfo.TotalSize;
 
                //Check whether this url is supported to be multi-threads download
                bool blnMulti = ( hwrp.Headers["Accept-Ranges"] != null
                    && hwrp.Headers["Accept-Ranges"] == "bytes");
                hwrp.Close();//Close response here
                return blnMulti;
            }
            catch (Exception e)
            {
                throw e;
            }
        }
 
        /// <summary>
        /// Update download thread info
        /// </summary>
        /// <param name="ThreadNO"></param>
        /// <param name="Position"></param>
        /// <param name="DownloadSize"></param>
        public void UpdateDownloadInfo( int ThreadNO, int Position, int DownloadSize )
        {
            if( ThreadNO >= dtThreadInfo.Rows.Count ) return;
 
            DataRow drThreadNO = dtThreadInfo.Rows[ThreadNO];
            drThreadNO["Position"] = Position + DownloadSize;
            drThreadNO["BlockLeftSize"] = Convert.ToInt32( drThreadNO["BlockLeftSize"].ToString() )
                - DownloadSize;
            drThreadNO.AcceptChanges();
 
            drFileInfo["BlockLeftSize"] = Convert.ToInt32( drFileInfo["BlockLeftSize"].ToString() )
                - DownloadSize;
        }
 
        /// <summary>
        /// Stop download threads
        /// </summary>
        public void Stop()
        {
            for( int i = 0; i < subThreads.Length; i++ )
                subThreads[i].Stop();
        }
 
        /// <summary>
        /// Thread download finished
        /// </summary>
        private void DownloadFinished()
        {
            // Some download thread finished
            nThreadFinishedCount++;
 
            if( nThreadFinishedCount == subThreads.Length )
            {
                // All download threads finished
                drFileInfo.Delete();
                //drFileInfo.AcceptChanges();
 
                Finished();
            }
        }
 
        public DataTable ThreadInfo
        {
            get{ return dtThreadInfo; }
        }
    }
 
}
 
不过对于这个类来说,我也只实现了两个线程同时下载,当初始化为3个以上线程的时候,就有等待线程出现,这一点和我用FlashGet看到现象一样,那么估计要支持多个线程,服务器需要做些设置,不过这些还需要进一步的验证。
 
View Code

多线程断点续传一

原文地址为:
http://dev.csdn.net/develop/article/64/64877.shtm
 
知道通过HttpWebRequest就可以进行多线程断点下载,是我不用考虑从Socket写起。
 
对于一个多线程断点续传程序,我大致认为只要考虑如下几点问题就行了。
1. 下载数据可以从给定位置进行;
2. 可以进行分块下载;
3. 记录下载位置,以供下次重新下载的时候使用。
 
通过对原文的阅读来看,发现以上问题的前两个已经实现。这样会使需要附加的操作会更简单些。
 
为了能记录每个线程下载的位置,我借用了最简单的方式,就是xml方式。
 
以下就是多线程下载的主文件内容:
//--------------------------- Download File class ---------------------------------------
//---------------------------------------------------------------------------------------
//---File:          clsNewDownloadFile
//---Description:   The multi-download file class file using HttpWebRequest class
//---Author:        Knight
//---Date:          Aug.4, 2006
//---------------------------------------------------------------------------------------
//---------------------------{Download File class}---------------------------------------
 
namespace DownloadFile
{
    using System;
    using System.IO;
    using System.Net;
    using System.Text;
    using System.Security;
    using System.Threading;
    using System.Collections.Specialized;
    using System.Diagnostics;
    using System.Data;
 
    /// <summary>
    /// Download file info
    /// </summary>
    public class DownloadFileInfo
    {
        private string _RequestURL;
        private string _ResponseURL;
        private string _FileName;
        private int _TotalSize;
        private int _BlockLeftSize;
 
        public string RequestURL
        {
            get{ return _RequestURL; }
        }
 
        public string ResponseURL
        {
            get{ return _ResponseURL; }
        }
        public string FileName
        {
            get{ return _FileName; }
        }
        public int TotalSize
        {
            get{ return _TotalSize;}
            set{ _TotalSize = value;}
        }
        public int BlockLeftSize
        {
            get{ return _BlockLeftSize;}
            set{ _BlockLeftSize = value;}
        }
 
        public DownloadFileInfo(
            string RequestURL,
            string ResponseURL,
            string FileName,
            int TotalSize,
            int BlockLeftSize )
        {
            this._RequestURL = RequestURL;
            this._ResponseURL = ResponseURL;
            this._FileName = FileName;
            this._TotalSize = TotalSize;
            this._BlockLeftSize = BlockLeftSize;
        }
 
        public DownloadFileInfo(
            string RequestURL,
            string ResponseURL,
            string FileName ):
            this( RequestURL, ResponseURL, FileName, 0, 0 )
        {
        }
    }
 
    /// <summary>
    /// Download event arguments
    /// </summary>
    public class DownLoadEventArgs : System.EventArgs
    {
        private DownloadFileInfo _DownloadFileInfo;
        private int _Position;
        private int _ReadCount;
        private int _ThreadNO;
 
        public DownloadFileInfo DownFileInfo
        {
            get{ return _DownloadFileInfo; }
        }
        public int StartPosition
        {
            get{ return _Position; }
        }
        public int ReadCount
        {
            get{ return _ReadCount; }
        }
        public int ThreadNO
        {
            get{ return _ThreadNO; }
        }
 
        public DownLoadEventArgs(
            DownloadFileInfo DownFileInfo,
            int nStartPostion,
            int nReadCount,
            int nThreadNO )
        {
            this._DownloadFileInfo = DownFileInfo;
            this._Position = nStartPostion;
            this._ReadCount = nReadCount;
            this._ThreadNO = nThreadNO;
        }
    }
   
    public delegate void DataReceivedEventHandler( DownLoadEventArgs e );
    public delegate void ThreadFinishedHandler();
 
    /// <summary>
    /// class for sub-download threads
    /// </summary>
    public class SubDownloadThread
    {
        private readonly DownloadFileInfo _FileInfo;
        private int _ThreadNO;
        private int _Position;
        private int _BlockSizeLeft;
        public event DataReceivedEventHandler DataReceived;
        private ThreadFinishedHandler _Finished;
        private bool _IsStopped;
 
        private Thread thdDownload;
 
        public SubDownloadThread(
            DownloadFileInfo DownFileInfo,
            int ThreadNO,
            int Position,
            int BlockSizeLeft,
            ThreadFinishedHandler Finished )
        {
            this._FileInfo = DownFileInfo;
            this._ThreadNO = ThreadNO;
            this._Position = Position;
            this._BlockSizeLeft = BlockSizeLeft;
            this._Finished = Finished;
        }
 
        /// <summary>
        /// Start to create thread to download file
        /// </summary>
        public void StartDownload()
        {
            thdDownload = new Thread( new ThreadStart( this.Download ) );
            thdDownload.Start();
        }
 
        /// <summary>
        /// Stop current download-thread
        /// </summary>
        public void Stop()
        {
            _IsStopped = true;
            if( thdDownload.ThreadState == System.Threading.ThreadState.Running )
                thdDownload.Join( 10 );
            Debug.WriteLine( string.Format( "Thread NO:{0} is stopped!" ,_ThreadNO ) );
        }
 
        /// <summary>
        /// Function for sub-thread
        /// </summary>
        private void Download()
        {
            HttpWebResponse hwrp = null;
 
            try
            {
                Debug.WriteLine( string.Format( "Thread NO:{0} begins to download!" ,_ThreadNO ) );
                // Creat request by url
                HttpWebRequest hwrq = (HttpWebRequest) WebRequest.Create(
                    new Uri( this._FileInfo.RequestURL ));
                // Go to download position
                hwrq.AddRange( _Position );
                // Get response from request
                hwrp = (HttpWebResponse) hwrq.GetResponse();
            }
            catch (Exception e)
            {
                Debug.WriteLine( e.Message );
                return;
            }
 
            // download and write data from
            Debug.WriteLine( string.Format( "Thread NO:{0} call function named DownloadData!" ,
                _ThreadNO ) );
            DownloadData( hwrp );
        }
 
        /// <summary>
        /// Download data through web request
        /// </summary>
        /// <param name="Response"></param>
        private void DownloadData( WebResponse Response )
        {
            const int BUFFER_SIZE = 0x10000;//64k buffer size
            byte[] bBuffer = new byte[ BUFFER_SIZE ];
 
            Stream sReader = Response.GetResponseStream();
            if( sReader == null ) return;
           
            int nReadCount = 0;
 
            while( !_IsStopped && this._BlockSizeLeft > 0 )
            {
                Debug.WriteLine( string.Format( "Thread NO:{0} reads data!" ,
                    _ThreadNO ) );
                // Set read size
                nReadCount = ( BUFFER_SIZE > this._BlockSizeLeft )
                    ? this._BlockSizeLeft:BUFFER_SIZE ;//Get current read count
 
                // Read data
                int nRealReadCount = sReader.Read( bBuffer, 0, nReadCount );
                if( nRealReadCount <= 0 ) break;
 
                Debug.WriteLine( string.Format( "Thread NO:{0} writes data!" ,
                    _ThreadNO ) );
                // Write data
                using( FileStream sw = new FileStream(
                           this._FileInfo.FileName,
                           System.IO.FileMode.OpenOrCreate,
                           System.IO.FileAccess.ReadWrite, System.IO.FileShare.ReadWrite))
                {
                    sw.Position = this._Position;
                    sw.Write( bBuffer, 0, nRealReadCount );
                    Debug.WriteLine( string.Format( "Thread NO:{0} writes {1} data!" ,
                        _ThreadNO, nRealReadCount ) );
                    sw.Flush();
                    sw.Close();
                }
 
                Debug.WriteLine( string.Format( "Thread NO:{0} send callback info!" ,
                    _ThreadNO ) );
                // Call back
                DataReceived( new DownLoadEventArgs( this._FileInfo,
                    this._Position,
                    nRealReadCount,
                    this._ThreadNO ) );
 
                // Set position and block left
                this._Position += nRealReadCount;
                this._BlockSizeLeft -= nRealReadCount;
            }
 
            if( _IsStopped ) return;
 
            Debug.WriteLine( string.Format( "Thread NO:{0} sends finish-info!" ,
                _ThreadNO ) );
            _Finished();
        }
    }
 
    /// <summary>
    /// Class for download thread
    /// </summary>
    public class DownloadThread
    {
        private DownloadFileInfo _FileInfo;
        private SubDownloadThread[] subThreads;
        private int nThreadFinishedCount;
        private DataRow drFileInfo;
        private DataTable dtThreadInfo;
        public DataReceivedEventHandler DataReceived;
        public event ThreadFinishedHandler Finished;
       
        /// Class's Constructor
        public DownloadThread(
            string RequestURL,
            string ResponseURL,
            string FileName,
            DataRow NewFileInfo,
            int SubThreadNum )
        {
            // Create download file info
            _FileInfo = new DownloadFileInfo( RequestURL, ResponseURL, FileName );
 
            // Create request to get file info
            bool blnMultiDownload = GetFileDownInfo();
 
            // Create file info datarow
            drFileInfo = NewFileInfo;
            InitDataRow();
 
            // Create subthreads and set these info in threadinfo table
            if( SubThreadNum <= 0 ) SubThreadNum = 5;//Defualt value
 
            //Not support to be multi-thread download or less than 64k
            if( !blnMultiDownload || _FileInfo.TotalSize <= 0x10000 ) SubThreadNum = 1;
 
            subThreads = new SubDownloadThread[SubThreadNum];
            nThreadFinishedCount = 0;
            CreateThreadTable( SubThreadNum );
        }
 
        public DownloadThread( DataRow DownloadFileInfo, DataTable ThreadInfos )
        {
            // Create download file info
            drFileInfo = DownloadFileInfo;
            _FileInfo = new DownloadFileInfo(
                drFileInfo["RequestURL"].ToString(),
                drFileInfo["ResponseURL"].ToString(),
                drFileInfo["FileName"].ToString(),
                Convert.ToInt32( drFileInfo["TotalSize"].ToString() ),
                Convert.ToInt32( drFileInfo["BlockLeftSize"].ToString() ) );
 
            // Create sub thread class objects
            dtThreadInfo = ThreadInfos;
            subThreads = new SubDownloadThread[ dtThreadInfo.Rows.Count ];
            nThreadFinishedCount = 0;
        }
        /// {Class's Constructor}
 
        /// <summary>
        /// Start to download
        /// </summary>
        public void Start()
        {
            StartSubThreads();
        }
 
        /// <summary>
        /// Create table to save thread info
        /// </summary>
        /// <param name="SubThreadNum"></param>
        private void CreateThreadTable( int SubThreadNum )
        {
            int nThunkSize = _FileInfo.TotalSize / SubThreadNum;
            dtThreadInfo = new DataTable("DownloadFileInfo");
            dtThreadInfo.Columns.Add( "ThreadNO", typeof(int) );
            dtThreadInfo.Columns.Add( "Position", typeof(int) );
            dtThreadInfo.Columns.Add( "BlockLeftSize", typeof(int) );
 
            DataRow drNew;
            int i = 0;
            for( ; i < SubThreadNum-1; i++ )
            {
                drNew = dtThreadInfo.NewRow();
                drNew["ThreadNO"] = i;
                drNew["Position"] = i * nThunkSize;
                drNew["BlockLeftSize"] = nThunkSize;
                dtThreadInfo.Rows.Add( drNew );
            }
 
            drNew = dtThreadInfo.NewRow();
            drNew["ThreadNO"] = i;
            drNew["Position"] = i * nThunkSize;
            drNew["BlockLeftSize"] = _FileInfo.TotalSize - i * nThunkSize;
            dtThreadInfo.Rows.Add( drNew );
            dtThreadInfo.AcceptChanges();
        }
 
        /// <summary>
        /// Start sub-threads to download file
        /// </summary>
        private void StartSubThreads()
        {
            foreach( DataRow dr in dtThreadInfo.Rows )
            {
                int ThreadNO = Convert.ToInt32( dr["ThreadNO"].ToString() );
                if( Convert.ToInt32( dr["BlockLeftSize"].ToString() ) > 0 )
                {
                    subThreads[ ThreadNO ] = new SubDownloadThread(
                        _FileInfo,
                        ThreadNO,
                        Convert.ToInt32( dr["Position"].ToString() ),
                        Convert.ToInt32( dr["BlockLeftSize"].ToString() ),
                        new ThreadFinishedHandler( this.DownloadFinished ) );
 
                    subThreads[ ThreadNO ].DataReceived += this.DataReceived;
                    subThreads[ ThreadNO ].StartDownload();
                }
                else
                    DownloadFinished();
            }
        }
 
        /// <summary>
        /// Save download file info
        /// </summary>
        private void InitDataRow()
        {
            drFileInfo.BeginEdit();
            drFileInfo["RequestURL"] = _FileInfo.RequestURL;
            drFileInfo["ResponseURL"] = _FileInfo.ResponseURL;
            drFileInfo["FileName"] = _FileInfo.FileName;
            drFileInfo["TotalSize"] = _FileInfo.TotalSize;
            drFileInfo["BlockLeftSize"] = _FileInfo.BlockLeftSize;
            drFileInfo["CreatedTime"] = DateTime.Now.ToString( "yyyyMMddHHmmss" );
            drFileInfo.EndEdit();
            drFileInfo.AcceptChanges();
        }
 
        /// <summary>
        /// Check url to get download file info
        /// </summary>
        /// <returns></returns>
        private bool GetFileDownInfo()
        {
            HttpWebRequest hwrq;
            HttpWebResponse hwrp = null;
            try
            {
                //Create request
                hwrq = (HttpWebRequest) WebRequest.Create( _FileInfo.RequestURL );
                hwrp = (HttpWebResponse) hwrq.GetResponse();
 
                //Get file size info
                long L = hwrp.ContentLength;
                L = ((L == -1) || (L > 0x7fffffff)) ? ((long) 0x7fffffff) : L;
               
                _FileInfo.TotalSize = (int)L;
                _FileInfo.BlockLeftSize = _FileInfo.TotalSize;
 
                //Check whether this url is supported to be multi-threads download
                return (hwrp.Headers["Accept-Ranges"] != null
                    & hwrp.Headers["Accept-Ranges"] == "bytes");
            }
            catch (Exception e)
            {
                throw e;
            }
        }
 
        /// <summary>
        /// Update download thread info
        /// </summary>
        /// <param name="ThreadNO"></param>
        /// <param name="Position"></param>
        /// <param name="DownloadSize"></param>
        public void UpdateDownloadInfo( int ThreadNO, int Position, int DownloadSize )
        {
            if( ThreadNO >= dtThreadInfo.Rows.Count ) return;
 
            DataRow drThreadNO = dtThreadInfo.Rows[ThreadNO];
            drThreadNO["Position"] = Position + DownloadSize;
            drThreadNO["BlockLeftSize"] = Convert.ToInt32( drThreadNO["BlockLeftSize"].ToString() )
                - DownloadSize;
            drThreadNO.AcceptChanges();
 
            drFileInfo["BlockLeftSize"] = Convert.ToInt32( drFileInfo["BlockLeftSize"].ToString() )
                - DownloadSize;
        }
 
        /// <summary>
        /// Stop download threads
        /// </summary>
        public void Stop()
        {
            for( int i = 0; i < subThreads.Length; i++ )
                subThreads[i].Stop();
        }
 
        /// <summary>
        /// Thread download finished
        /// </summary>
        private void DownloadFinished()
        {
            // Some download thread finished
            nThreadFinishedCount++;
 
            if( nThreadFinishedCount == subThreads.Length )
            {
                // All download threads finished
                drFileInfo.Delete();
                //drFileInfo.AcceptChanges();
 
                Finished();
            }
        }
 
        public DataTable ThreadInfo
        {
            get{ return dtThreadInfo; }
        }
    }
 
}
 
对于如上类的使用,首先需要创建DataSet以及DataTable来存下载文件的信息。
        private DataTable dtFileInfos = null;
        private DataSet dsFileInfo = null;
 
当系统第一次运行的时候,需要初始化此Table。
        private void CreateTable()
        {
            dtFileInfos = new DataTable( "DownloadFileInfo" );
            dtFileInfos.Columns.Add( "RequestURL", typeof( string ) );
            dtFileInfos.Columns.Add( "ResponseURL", typeof( string ) );
            dtFileInfos.Columns.Add( "FileName", typeof( string ) );
            dtFileInfos.Columns.Add( "TotalSize", typeof( int ) );
            dtFileInfos.Columns.Add( "BlockLeftSize", typeof( int ) );
            dtFileInfos.Columns.Add( "CreatedTime", typeof( string ) );
            dsFileInfo.Tables.Add( dtFileInfos );
        }
 
进过如上的初始化后,就可以进行下载了,不过需要为DownlodThread初始化两个事件,就是在DataReceived以及DownloadFinished。
        private void DataReceived( DownLoadEventArgs e )
        {
            myDownload.UpdateDownloadInfo( e.ThreadNO, e.StartPosition, e.ReadCount );
            Debug.WriteLine( string.Format( "Thread NO:{0} read {2} bytes from {1} postion!" ,
                e.ThreadNO,
                e.StartPosition,
                e.ReadCount ) );
        }
 
        private void Finished()
        {
            dtFileInfos.AcceptChanges();
            dsFileInfo.WriteXml( FILE_NAME ); //your log file
 
            MessageBox.Show( "Download finished" );
            btnStop.Enabled = false;
            btnRestart.Enabled = false;
            btnDownload.Enabled = true;
        }
 
开始一个新任务可以如下:
            //Create new row to save download file info
            DataRow dr = dtFileInfos.NewRow();
            dtFileInfos.Rows.Add( dr );
 
            //Create object
            myDownload = new DownloadThread(
                txtURL.Text,
                "",
                @"E:\Temp\" + DateTime.Now.ToString( "yyyyMMddHHmmss" ),
                dr,
                1 );
 
            myDownload.Finished += new ThreadFinishedHandler( Finished );
            myDownload.DataReceived = new DataReceivedEventHandler( DataReceived );
 
            myDownload.Start();
 
暂停可以如下:
        private void btnStop_Click(object sender, System.EventArgs e)
        {
            // Set stop message to all
            myDownload.Stop();
 
            // Save log file
            dtFileInfos.AcceptChanges();
 
            dsFileInfo.WriteXml( FILE_NAME );
 
            foreach( DataRow dr in dtFileInfos.Rows )
            {
                DataSet dsThreadInfo = null;
                DataTable dt = myDownload.ThreadInfo;
 
                if( dt.DataSet == null )
                {
                    dsThreadInfo = new DataSet();
                    dsThreadInfo.Tables.Add( myDownload.ThreadInfo );
                }
                else
                    dsThreadInfo = dt.DataSet;
               
                dsThreadInfo.WriteXml( dr["CreatedTime"].ToString( ) + ".xml" );
            }
        }
 
重新开始可以如下:
        private void btnRestart_Click(object sender, System.EventArgs e)
        {
            // Read thread info
            DataSet dsThreadInfo = new DataSet();
            foreach( DataRow dr in dtFileInfos.Rows )
            {
                dsThreadInfo.ReadXml( dr["CreatedTime"].ToString( ) + ".xml" );
            }
            if( dsThreadInfo.Tables.Count <= 0 ) return;
 
            // Init download object
            // Restart to download
            myDownload = new DownloadThread( dtFileInfos.Rows[0],
                dsThreadInfo.Tables[0] );
            myDownload.Finished += new ThreadFinishedHandler( Finished );
            myDownload.DataReceived = new DataReceivedEventHandler( DataReceived );
            myDownload.Start();
            btnStop.Enabled = true;
        }
 
不过当程序关闭后,需要重新Load下载日志文件,如下:
        const string FILE_NAME = "Download.Log";
        private void LoadFile()
        {
            dsFileInfo = new DataSet();
            if( !File.Exists( FILE_NAME ) )
            {
                CreateTable();
                return;
            }
 
            dsFileInfo.ReadXml( FILE_NAME );
            if( dsFileInfo.Tables.Count == 0 )
                CreateTable();
            else
                dtFileInfos = dsFileInfo.Tables[ "DownloadFileInfo" ];
        }
 
按照如上的步骤,基本上一个多线程断点续传的程序就完成。
 
不过,对于多线程来说,从方法引用文章来看,需要在服务器段进行某些设置。也就是说不光是要在下载端要做处理,还要在服务器端坐处理。但是我对此产生怀疑,这也是我迟迟没有发布这篇文章的原因。
 
目前,对于我所写的类来说,单线程断点续传已经没有问题。但是多线程进行操作的时候,第二个线程发送HttpWebRequest,无法获得请求。不过在此,我没有参照文章所提的方法对服务器端作处理,因为用FlashGet就可以多线程,所以有些怀疑是HttpWebRequest的问题。考虑到时间比较紧的原因,我没有再深究下去,毕竟我没有考虑用HttpWebRequest来实现这个多线程断点下载程序,可能从Socket去写要更好些。
 
不过通过写这段小程序,我也收获不小,有很多细节没考虑得很清楚,值得仔细考虑,大致如下。
1. 文件读取分块问题;
2. 当下载线程数增加或者减少的时候,如何继续下载一个文件;
3. 日志以及下载log地记录方式。
 
可能还有些问题,如果有人有好的建议或者想法等等,可以写信告诉我,我在综合考虑后写一个比较完善的版本。
 
View Code

概述C#实现文件下载

一.概述:
    本文通过一个实例向大家介绍用Visual C#进行Internet通讯编程的一些基本知识。我们知道.Net类包含了请求/响应层、应用协议层、传输层等层次。在本程序中,我们运用了位于请求/响应层的WebRequest类以及WebClient类等来实现高抽象程度的Internet通讯服务。本程序的功能是完成C#实现文件下载。
二.实现原理:
    程序实现的原理比较简单,主要用到了WebClient类和FileStream类。其中WebClient类处于System.Net名字空间中,该类的主要功能是提供向URI标识的资源发送数据和从URI标识的资源接收数据的公共方法。我们利用其中的DownloadFile()方法将网络文件下载到本地。然后用FileStream类的实例对象以数据流的方式将文件数据写入本地文件。这样就完成了C#实现文件下载载。
三.实现步骤:
    首先,打开Visual Studio.Net,新建一个Visual C# Windows应用程序的工程,不妨命名为\"MyGetCar\"。
    接着,布置主界面。我们先往主窗体上添加如下控件:两个标签控件、两个文本框控件、一个按钮控件以及一个状态栏控件。
    完成主窗体的设计,我们接着完成代码的编写。
    在理解了基本原理的基础上去完成代码的编写是相当容易。程序中我们主要用到的是WebClient类,不过在我们调用WebClient类的实例对象前,我们需要用WebRequest类的对象发出对统一资源标识符(URI)的请求。
1. try{  
2. WebRequestWebRequestmyre=WebRequest.Create(URLAddress);  
3. }  
4. catch(WebExceptionexp){  
5. MessageBox.Show(exp.Message,\"Error\");  
6. } 
    这是一个try-catch语句,try块完成向URI的请求,catch块则捕捉可能的异常并显示异常信息。其中的URLAddress为被请求的网络主机名。
    在请求成功后,我们就可以运用WebClient类的实例对象中的DownloadFile()方法实现文件的下载了。其函数原型如下:
1. public void DownloadFile( string address, string fileName); 
    其中,参数address为从中下载数据的 URI,fileName为要接收数据的本地文件的名称。
    之后我们用OpenRead()方法来打开一个可读的流,该流完成从具有指定URI的资源下载数据的功能。其函数原型如下:
1. public Stream OpenRead(string address); 
    其中,参数address同上。
    最后就是新建一个StreamReader对象从中读取文件的数据,并运用一个while循环体不断读取数据,只到读完所有的数据。以上介绍C#实现文件下载。
 
View Code

文件下载

///   <summary>    

    ///  文件下载     

    ///   </summary>    

    ///   <param   name= "FullFileName "> </param>    

    public void   FileDownload(string FullFileName)   

    {   

        string FilePath = Server.MapPath("~/" + FullFileName);   

        FileInfo DownloadFile = new FileInfo(FilePath);   

        Response.Clear();   

        Response.ClearHeaders();   

        Response.Buffer = false;   

        Response.ContentType = "application/octet-stream ";   

        Response.AppendHeader("Content-Disposition ", "attachment;filename= " + 

            HttpUtility.UrlEncode(DownloadFile.FullName, System.Text.Encoding.UTF8));   

        Response.AppendHeader("Content-Length ", DownloadFile.Length.ToString());   

        Response.WriteFile(DownloadFile.FullName);   

        Response.Flush();   

        Response.End();   

            

    }  
View Code

用HttpListener实现文件断点续传

断点续传的原理很简单,就是在Http的请求和应答的报文头上和一般的下载有所不同而已。
 
普通方式请求服务器上的一个文时,所发出的请求和接受到的服务器如下:
 
request header:
Cache-Control: no-cache
Connection: close
Pragma: no-cache
Accept: */*
Host: localhost
 
response header:
200
Content-Type: application/octet-stream
Content-Disposition: attachment;FileName=time.pdf
 
当服务器支持断点续传时,请求和应答如下:
 
request header:
Cache-Control: no-cache
Connection: close
Pragma: no-cache
Accept: */*
Host: localhost
Range: bytes=15360-
 
response header:
206
Content-Type: application/octet-stream
Content-Disposition: attachment;FileName=time.pdf
 
两个报文的不同部分已用红色部分标记出来。可以看出:
 
客户端报文头中通过Range报文头来标识客户期望的下载位置。
服务器的应答号为200时表示是从文件头开始下载,而206表示是从文件的特定位置开始传输,客户端从该应答号可以看出服务器是否支持断点续传。
也就是说,支持断点续传的时候可以从文件任一部分开始下载,而普通的方式只能从文件头开始下载。
 
要使得服务器支持断点续传,需要解决以下几个问题:
 
1。需要判断客户端是否是续传请求,如果是续传请求时,需要获取客户端所需的文件范围。
 
从上面的分析可以看到,当客户端为断点传输时,报文头里会增加Range字段,则可以通过如下方式判断是否是断点传输请求。
 
string range = request.Headers["Range"];
bool isResume = string.IsNullOrEmpty(range);
 
2。对客户端做正确的应答相应,以通知客户端服务器支持端点续传
 
当为断点传输请求时,对客户端的相应号可以通过如下方式设置:
 
response.StatusCode = 206;
 
3。传送客户端所需正确的内容
 
传送客户端所需正确的内容一般需要经过以下几个步骤
 
通过分析range来获取客户端的文件请求范围。
断点传输请求时,所需的长度比文件的长度短,故需要正确的设置response.ContentLength64属性。
正确传输所需的内容
代码示例:
 
static void ProcessHttpClient(object obj)
{
    HttpListenerContext context = obj as HttpListenerContext;
    HttpListenerRequest request = context.Request;
    HttpListenerResponse response = context.Response;
 
    FileStream fs = File.OpenRead(@"f:\123.pdf"); //待下载的文件
 
    long startPos = 0;
    string range = request.Headers["Range"];
    bool isResume = string.IsNullOrEmpty(range);
    if (isResume) //断点续传请求
    {
        //格式bytes=9216-
        startPos = long.Parse(range.Split('=')[1].Split('-')[0]);
        response.StatusCode = 206;
        response.ContentLength64 = fs.Length - startPos;
        fs.Position = startPos; //设置传送的起始位置
    }
    else
    {
        response.ContentLength64 = fs.Length;
    }
 
    Console.WriteLine("request header");
    Console.WriteLine(request.Headers.ToString());
 
    response.ContentType = "application/octet-stream";
 
    string fileName = "time.pdf";
    response.AddHeader("Content-Disposition", "attachment;FileName=" + fileName);
    Stream output = response.OutputStream;
 
    try
    {
        Console.WriteLine("response header");
        Console.WriteLine(response.Headers.ToString());
        CopyStream(fs, output); //文件传输
        output.Close();
    }
    catch (HttpListenerException e) //在未写完所有文件时,如果客户端关闭连接,会抛此异常
    {
        Console.WriteLine(e.Message);
        //output.Close(); //如果执行此函数会抛异常在写入所有字节之前不能关闭流。
    }
}
 
static void CopyStream(Stream orgStream, Stream desStream)
{
    byte[] buffer = new byte[1024];
 
    int read = 0;
    while ((read = orgStream.Read(buffer, 0, 1024)) > 0)
    {
        desStream.Write(buffer, 0, read);
 
        System.Threading.Thread.Sleep(1000); //模拟慢速设备
    }
}
 
View Code

用HttpListener实现文件下载

和asp.net中一样,如果要实现url重定向,使用response.Redirect()方法即可,在中使用如下:
string desUrl = "http://www.google.com";
response.Redirect(desUrl);
response.OutputStream.Close();
如果desUrl执行的是网络上的一个文件,一般ie就会提示文件下载。但是,许多时候,ie会打开这个文件(如目标是文本文件就会这样),这有时不是我们所需要的,如何强制ie以下载的方式获取文件呢?解决方式如下:
static void ProcessHttpClient(object obj)
{
    HttpListenerContext context = obj as HttpListenerContext;
    HttpListenerRequest request = context.Request;
    HttpListenerResponse response = context.Response;
    response.ContentType = "application/octet-stream";
    string fileName = "time.txt";
    response.AddHeader("Content-Disposition", "attachment;FileName=" + fileName);
    byte[] data = Encoding.Default.GetBytes(string.Format("当前时间: {0}", DateTime.Now));
    response.ContentLength64 = data.Length;
    System.IO.Stream output = response.OutputStream;
    output.Write(data, 0, data.Length);
    output.Close();
}
这种方式有一个小问题,那就是当文件名为中文时,会产生乱码,解决方法是用调用HttpUtility.UrlEncode 函数对文件名进行编码。
 
View Code

在.NET中利用XMLHTTP下载文件

利用XMLHTTP下载文件,和以前的方法一样,先添加引用-COM-Microsoft Xml 3.0,然后在代码开始处写:
using MSXML2;
下面就是主要的代码:
private void Page_Load(object sender, System.EventArgs e)
{
string Url = "http://dotnet.aspx.cc/Images/logoSite.gif";
string StringFileName = Url.Substring(Url.LastIndexOf("/") + 1);
string StringFilePath = Request.PhysicalApplicationPath;
if(!StringFilePath.EndsWith("/")) StringFilePath += "/";
MSXML2.XMLHTTP _xmlhttp = new MSXML2.XMLHTTPClass();
_xmlhttp.open("GET",Url,false,null,null);
_xmlhttp.send("");
if( _xmlhttp.readyState == 4 )
{
if(System.IO.File.Exists(StringFilePath + StringFileName))
System.IO.File.Delete(StringFilePath + StringFileName);
System.IO.FileStream fs = new System.IO.FileStream(StringFilePath + StringFileName, System.IO.FileMode.CreateNew);
System.IO.BinaryWriter w = new System.IO.BinaryWriter(fs);
w.Write((byte[])_xmlhttp.responseBody);
w.Close();
fs.Close();
Response.Write ("文件已经得到。<br><a href='" + Request.ApplicationPath + StringFileName +"' target='_blank'>");
Response.Write ("查看" + StringFileName + "</a>");
}
else
Response.Write (_xmlhttp.statusText);
Response.End();
}
 
View Code

C# 多线程下载文件单元(Helpr)

using System; 
using System.Collections.Generic; 
using System.Text; 
using System.IO; 
using System.Net; 
using System.Threading; 
 
namespace Update 
{ 
    class MultiThreadDownLoad 
    { 
        #region 变量 
        private int _threadNum;             //线程数量 
        private long _fileSize;             //文件大小 
        private string _extName;            //文件扩展名 
        private string _fileUrl;            //文件地址 
        private string _fileName;           //文件名 
        private string _savePath;           //保存路径 
        private short _threadCompleteNum;   //线程完成数量 
        private bool _isComplete;           //是否完成 
        private volatile int _downloadSize; //当前下载大小(实时的) 
        public Thread[] _thread;           //线程数组 
        private List<string> _tempFiles = new List<string>(); 
        private List<List<int>> readft = new List<List<int>>();//存放每个线程读取的起始和结束位置 
        private object locker = new object(); 
        #endregion 
 
        #region 属性 
        public string FileName 
        { 
            get 
            { 
                return _fileName; 
            } 
            set 
            { 
                _fileName = value; 
            } 
        } 
 
        public long FileSize 
        { 
            get 
            { 
                return _fileSize; 
            } 
        } 
 
        public int DownloadSize 
        { 
            get 
            { 
                return _downloadSize; 
            } 
        } 
 
        public bool IsComplete 
        { 
            get 
            { 
                return _isComplete; 
            } 
            set 
            { 
                _isComplete = value; 
            } 
        } 
 
        public int ThreadNum 
        { 
            get 
            { 
                return _threadNum; 
            } 
        } 
 
        public string SavePath 
        { 
            get 
            { 
                return _savePath; 
            } 
            set 
            { 
                _savePath = value; 
            } 
        } 
        #endregion 
 
        /// <summary> 
        /// 构造函数 
        /// </summary> 
        /// <param name="threahNum">线程数量</param> 
        /// <param name="fileUrl">文件Url路径</param> 
        /// <param name="savePath">本地保存路径</param> 
        public MultiThreadDownLoad(int threahNum, string fileUrl, string savePath) 
        { 
            this._threadNum = threahNum; 
            this._thread = new Thread[threahNum]; 
            this._fileUrl = fileUrl; 
            this._savePath = savePath; 
        } 
 
        //_serverUrl为http://xxx.xx/xx.dll形式 
        //_localFile为\\Program File\xx.dll形式 PS:终端路径 
         
        public void Start() 
        { 
             
            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(_fileUrl); 
            HttpWebResponse response = (HttpWebResponse)request.GetResponse(); 
            _extName = response.ResponseUri.ToString().Substring(response.ResponseUri.ToString().LastIndexOf('.'));//获取真实扩展名 
            _fileSize = response.ContentLength; 
 
            int singelNum = (int)(_fileSize / _threadNum);      //平均分配 
            int remainder = (int)(_fileSize % _threadNum);      //获取剩余的 
            request.Abort(); 
            response.Close(); 
            for (int i = 0; i < _threadNum; i++) 
            { 
                List<int> range = new List<int>(); 
                range.Add(i * singelNum); 
                if (remainder != 0 && (_threadNum - 1) == i)    //剩余的交给最后一个线程 
                    range.Add(i * singelNum + singelNum + remainder - 1); 
                else 
                    range.Add(i * singelNum + singelNum - 1); 
                readft.Add(range); 
                _thread[i] = new Thread(new ThreadStart(Download)); 
                _thread[i].Name = i.ToString(); 
                _thread[i].Start(); 
            } 
        } 
 
        private void Download() 
        { 
            Stream httpFileStream = null, localFileStram = null; 
            try 
            { 
                string tmpFileBlock = String.Format(@"{0}\{1}_{2}.dat",_savePath,FileName ,Thread.CurrentThread.Name); 
                _tempFiles.Add(tmpFileBlock); 
                HttpWebRequest httprequest = (HttpWebRequest)HttpWebRequest.Create(_fileUrl); 
                httprequest.AddRange(readft[Convert.ToInt32(Thread.CurrentThread.Name)][0], readft[Convert.ToInt32(Thread.CurrentThread.Name)][1]); 
                HttpWebResponse httpresponse = (HttpWebResponse)httprequest.GetResponse(); 
                httpFileStream = httpresponse.GetResponseStream(); 
                localFileStram = new FileStream(tmpFileBlock, FileMode.Create); 
                byte[] by = new byte[1024]; 
                int getByteSize = httpFileStream.Read(by, 0, 1024);           //Read方法将返回读入by变量中的总字节数 
                while (getByteSize > 0) 
                { 
                    Thread.Sleep(20); 
                    lock (locker) _downloadSize += getByteSize; 
                    localFileStram.Write(by, 0, getByteSize); 
                    getByteSize = httpFileStream.Read(by, 0, 1024); 
                } 
                lock (locker) 
                    _threadCompleteNum++; 
                 
            } 
            catch (Exception ex) 
            { 
                throw new Exception(ex.Message.ToString()); 
            } 
            finally 
            { 
                if (httpFileStream != null) httpFileStream.Close(); 
                if (localFileStram != null) localFileStram.Close(); 
            } 
            if (_threadCompleteNum == _threadNum) 
            { 
                Complete(); 
                _isComplete = true; 
            } 
        } 
 
        /// <summary> 
        /// 下载完成后合并文件块 
        /// </summary> 
        private void Complete() 
        { 
            Stream mergeFile=null; 
            BinaryWriter AddWriter=null; 
            try 
            { 
                mergeFile = new FileStream(String.Format(@"{0}\{1}", _savePath, FileName), FileMode.Create); 
                AddWriter = new BinaryWriter(mergeFile); 
                foreach (string file in _tempFiles) 
                { 
                    using (FileStream fs = new FileStream(file, FileMode.Open)) 
                    { 
                        BinaryReader TempReader = new BinaryReader(fs); 
                        AddWriter.Write(TempReader.ReadBytes((int)fs.Length)); 
                        TempReader.Close(); 
                    } 
                    File.Delete(file); 
                } 
            } 
            catch (Exception ex) 
            { 
                throw new Exception(ex.Message); 
            } 
            finally 
            { 
                AddWriter.Close(); 
                mergeFile.Close(); 
            } 
 
        } 
    } 
} 

摘自 #Define
 
View Code

C#多线程

推荐资源



1.全国大学1000多门20000多课时课程在线看 (必收藏)

http://www.365xueyuan.com/index-htm-m-bbs-cateid-73.html
2. 数十G的photoshop教程在线看或下载

http://www.365xueyuan.com/thread-htm-fid-87.html

3.数十G ,word,excel办公软件教程在线看或下载

http://www.365xueyuan.com

    本文由jvdlvcjpyf贡献
    本文由wangchunqiang8贡献
    doc文档可能在WAP端浏览体验不佳。建议您优先选择TXT,或下载源文件到本机查看。
    C#多线程学习( C#多线程学习(一) 多线程的相关概念 多线程学习
    什么是进程? 什么是进程? 当一个程序开始运行时, 它就是一个进程, 进程包括运行中的程序和程序所使用到的内存和 系统资源。 而一个进程又是由多个线程所组成的。
    什么是线程? 什么是线程? 线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但 代码区是共享的,即不同的线程可以执行同样的函数。
    什么是多线程? 什么是多线程? 多线程是指程序中包含多个执行流, 即在一个程序中可以同时运行多个不同的线程来执行不 同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
    多线程的好处: 多线程的好处: 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它 的线程而不是等待,这样就大大提高了程序的效率。
    多线程的不利方面: 多线程的不利方面: 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
    线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题; 线程太多会导致控制太复杂,最终可能造成很多 Bug; 接下来将对 C#编程中的多线程机制进行探讨。为了省去创建 GUI 那些繁琐的步骤,更清 晰地逼近线程的本质, 接下来的所有程序都是控制台程序, 程序最后的 Console.ReadLine() 是为了使程序中途停下来,以便看清楚执行过程中的输出。
    任何程序在执行时,至少有一个主线程。
    一个直观印象的线程示例:
    //SystemThread.cs using System; using System.Threading;
    namespace ThreadTest { class RunIt { [STAThread] static void Main(string[] args) { Thread.CurrentThread.Name="System Thread";//给当前线程起名为"Sys tem Thread"
    Console.WriteLine(Thread.CurrentThread.Name+"'Status:"+Thread.Curre ntThread.ThreadState); Console.ReadLine(); } } }
    输出如下: System Thread's Status:Running
    在这里,我们通过 Thread 类的静态属性 CurrentThread 获取了当前执行的线程,对其 Name 属性赋值"System Thread",最后还输出了它的当前状态(ThreadState)。
    所谓静态属性,就是这个类所有对象所公有的属性,不管你创建了多少个这个类的实例,但 是类的静态属性在内存中只有一个。 很容易理解 CurrentThread 为什么是静态的--虽然 有多个线程同时存在,但是在某一个时刻,CPU 只能执行其中一个。
    在程序的头部,我们使用了如下命名空间: using System; using System.Threading;
    在.net framework class library 中,所有与多线程机制应用相关的类都是放在 System.Threading 命名空间中的。如果你想在你的应用程序中使用多线程,就必须包含 这个类。
    我们通过其中提供的 Thread 类来创建和控制线程,ThreadPool 类用于管理线程池等。 (此外还提供解决了线程执行安排,死锁,线程间通讯等实际问题的机制。)
    Thread 类有几个至关重要的方法,描述如下: Start():启动线程; Sleep(int):静态方法,暂停当前线程指定的毫秒数; Abort():通常使用该方法来终止一个线程; Suspend():该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复; Resume():恢复被 Suspend()方法挂起的线程的执行;
    C#多线程学习( C#多线程学习(二) 如何操纵一个线程 多线程学习 下面我们就动手来创建一个线程, 使用 Thread 类创建线程时, 只需提供线程入口即可。 (线 程入口使程序知道该让这个线程干什么事) 在 C#中, 线程入口是通过 ThreadStart 代理 (delegate) 来提供的, 你可以把 ThreadStart 理解为一个函数指针,指向线程要执行的函数,当调用 Thread.Start()方法后,线程就开 始执行 ThreadStart 所代表或者说指向的函数。
    打开你的 VS.net,新建一个控制台应用程序(Console Application),编写完全控制一 个线程的代码示例:
    //ThreadTest.cs
    using System; using System.Threading;
    namespace ThreadTest { public class Alpha { public void Beta() { while (true) { Console.WriteLine("Alpha.Beta is running in its own thread."); } } };
    public class Simple { public static int Main() { Console.WriteLine("Thread Start/Stop/Join Sample");
    Alpha oAlpha = new Alpha(); file://这里创建一个线程,使之执行 Alpha 类的 Beta()方法 Thread oThread = new Thread(new ThreadStart(oAlpha.Beta)); oThread.Start(); while (!oThread.IsAlive) Thread.Sleep(1); oThread.Abort(); oThread.Join(); Console.WriteLine(); Console.WriteLine("Alpha.Beta has finished"); try { Console.WriteLine("Try to restart the Alpha.Beta thread"); oThread.Start(); } catch (ThreadStateException)
    { Console.Write("ThreadStateException trying to restart Alpha.B eta. "); Console.WriteLine("Expected since aborted threads cannot b e restarted."); Console.ReadLine(); } return 0; } } }
    这段程序包含两个类 Alpha 和 Simple, 在创建线程 oThread 时我们用指向 Alpha.Beta() 方法的初始化了 ThreadStart 代理(delegate)对象,当我们创建的线程 oThread 调用 oThread.Start()方法启动时,实际上程序运行的是 Alpha.Beta()方法:   Alpha oAlpha = new Alpha(); Thread oThread = new Thread(new ThreadStart(oAlpha.Beta)); oThread.Start();
    然后在 Main()函数的 while 循环中,我们使用静态方法 Thread.Sleep()让主线程停了 1ms,这段时间 CPU 转向执行线程 oThread。然后我们试图用 Thread.Abort()方法终止
    线程 oThread,注意后面的 oThread.Join(),Thread.Join()方法使主线程等待,直到 oThread 线程结束。你可以给 Thread.Join()方法指定一个 int 型的参数作为等待的最长 时间。之后,我们试图用 Thread.Start()方法重新启动线程 oThread,但是显然 Abort() 方法带来的后果是不可恢复的终止线程,所以最后程序会抛出 ThreadStateException 异 常。
    主线程 Main()函数 函数 所有线程都是依附于 Main()函数所在的线程的,Main()函数是 C#程序的入口,起始线程 可以称之为主线程。 如果所有的前台线程都停止了,那么主线程可以终止,而所有的后台线程都将无条件终止。 所有的线程虽然在微观上是串行执行的,但是在宏观上你完全可以认为它们在并行执行。
    Thread.ThreadState 属性 这个属性代表了线程运行时状态, 在不同的情况下有不同的值, 我们有时候可以通过对该值 的判断来设计程序流程。 ThreadState 属性的取值如下: Aborted:线程已停止; AbortRequested:线程的 Thread.Abort()方法已被调用,但是线程还未停止; Background:线程在后台执行,与属性 Thread.IsBackground 有关; Running:线程正在正常运行; Stopped:线程已经被停止;
    StopRequested:线程正在被要求停止; Suspended:线程已经被挂起(此状态下,可以通过调用 Resume()方法重新运行); SuspendRequested:线程正在要求被挂起,但是未来得及响应; Unstarted:未调用 Thread.Start()开始线程的运行; WaitSleepJoin:线程因为调用了 Wait(),Sleep()或 Join()等方法处于封锁状态; 上面提到了 Background 状态表示该线程在后台运行,那么后台运行的线程有什么特别的 地方呢?其实后台线程跟前台线程只有一个区别, 那就是后台线程不妨碍程序的终止。 一旦 一个进程所有的前台线程都终止后,CLR(通用语言运行环境)将通过调用任意一个存活中 的后台进程的 Abort()方法来彻底终止进程。
    线程的优先级 当线程之间争夺 CPU 时间时,CPU 是按照线程的优先级给予服务的。在 C#应用程序中, 用户可以设定 5 个不同的优先级,由高到低分别是 Highest,AboveNormal,Normal, BelowNormal,Lowest,在创建线程时如果不指定优先级,那么系统默认为 ThreadPriority.Normal。 给一个线程指定优先级,我们可以使用如下代码: //设定优先级为最低 myThread.Priority=ThreadPriority.Lowest; 通过设定线程的优先级, 我们可以安排一些相对重要的线程优先执行, 例如对用户的响应等 等。
    C#多线程学习( C#多线程学习(三) 生产者和消费者 多线程学习
    前面说过,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的 函数。这可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料 的结果,因此我们必须避免这种情况的发生。
    C#提供了一个关键字 lock,它可以把一段代码定义为互斥段(critical section),互斥段 在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在 C#中,关键字 lock 定 义如下: lock(expression) statement_block
    expression 代表你希望跟踪的对象,通常是对象引用。 如果你想保护一个类的实例,一般地,你可以使用 this; 如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可 以了。 而 statement_block 就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。
    下面是一个使用 lock 关键字的典型例子,在注释里说明了 lock 关键字的用法和用途。 示例如下:
    using System; using System.Threading;
    namespace ThreadSimple {
    internal class Account { int balance; Random r = new Random();
    internal Account(int initial) { balance = initial; }
    internal int Withdraw(int amount) { if (balance < 0) { //如果 balance 小于 0 则抛出异常 throw new Exception("Negative Balance"); } //下面的代码保证在当前线程修改 balance 的值完成之前 //不会有其他线程也执行这段代码来修改 balance 的值 //因此,balance 的值是不可能小于 0 的 lock (this) {
    Console.WriteLine("Current Thread:"+Thread.CurrentThread.Name); //如果没有 lock 关键字的保护,那么可能在执行完 if 的条件判断之后 //另外一个线程却执行了 balance=balance-amount 修改了 balance 的值 //而这个修改对这个线程是不可见的,所以可能导致这时 if 的条件已经不成立了 //但是,这个线程却继续执行 balance=balance-amount, 所以导致 balance 可 能小于 0 if (balance >= amount) { Thread.Sleep(5); balance = balance - amount; return amount; } else { return 0; // transaction rejected } } } internal void DoTransactions() { for (int i = 0; i < 100; i++) Withdraw(r.Next(-50, 100));
    } }
    internal class Test { static internal Thread[] threads = new Thread[10]; public static void Main() { Account acc = new Account (0); for (int i = 0; i < 10; i++) { Thread t = new Thread(new ThreadStart(acc.DoTransactions)); threads[i] = t; } for (int i = 0; i < 10; i++) threads[i].Name=i.ToString(); for (int i = 0; i < 10; i++) threads[i].Start(); Console.ReadLine(); } } }
    Monitor 类锁定一个对象 当多线程公用一个对象时,也会出现和公用代码类似的问题,这种问题就不应该使用 lock 关键字了,这里需要用到 System.Threading 中的一个类 Monitor,我们可以称之为监视 器,Monitor 提供了使线程共享资源的方案。 Monitor 类可以锁定一个对象, 一个线程只有得到这把锁才可以对该对象进行操作。 对 象锁机制保证了在可能引起混乱的情况下一个时刻只有一个线程可以访问这个对象。 Monitor 必须和一个具体的对象相关联, 但是由于它是一个静态的类, 所以不能使用它来定 义对象, 而且它的所有方法都是静态的, 不能使用对象来引用。 下面代码说明了使用 Monitor 锁定一个对象的情形:   ……   Queue oQueue=new Queue();   ……   Monitor.Enter(oQueue);   ……//现在 oQueue 对象只能被当前线程操纵了   Monitor.Exit(oQueue);//释放锁
    如上所示, 当一个线程调用 Monitor.Enter()方法锁定一个对象时, 这个对象就归它所有了, 其它线程想要访问这个对象,只有等待它使用 Monitor.Exit()方法释放锁。为了保证线程
    最终都能释放锁,你可以把 Monitor.Exit()方法写在 try-catch-finally 结构中的 finally 代码块里。 对于任何一个被 Monitor 锁定的对象,内存中都保存着与它相关的一些信息: 其一是现在持有锁的线程的引用; 其二是一个预备队列,队列中保存了已经准备好获取锁的线程; 其三是一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用。 当拥有对象锁的线程准备释放锁时, 它使用 Monitor.Pulse()方法通知等待队列中的第一个 线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即 获得对象锁。
    下面是一个展示如何使用 lock 关键字和 Monitor 类来实现线程的同步和通讯的例子, 也是 一个典型的生产者与消费者问题。 这个例程中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取 并且显示(注释中介绍了该程序的精要所在)。 用到的系统命名空间如下: using System; using System.Threading; 首先,定义一个被操作的对象的类 Cell,在这个类里,有两个方法:ReadFromCell()和 WriteToCell。消费者线程将调用 ReadFromCell()读取 cellContents 的内容并且显示出 来,生产者进程将调用 WriteToCell()方法向 cellContents 写入数据。
    示例如下:
    public class Cell { int cellContents; // Cell 对象里边的内容 bool readerFlag = false; // 状态标志,为 true 时可以读取,为 false 则正在写入 public int ReadFromCell( ) { lock(this) // Lock 关键字保证了什么,请大家看前面对 lock 的介绍 { if (!readerFlag)//如果现在不可读取 { try { //等待 WriteToCell 方法中调用 Monitor.Pulse()方法 Monitor.Wait(this); } catch (SynchronizationLockException e) { Console.WriteLine(e); } catch (ThreadInterruptedException e)
    { Console.WriteLine(e); } } Console.WriteLine("Consume: {0}",cellContents); readerFlag = false; //重置 readerFlag 标志,表示消费行为已经完成 Monitor.Pulse(this); //通知 WriteToCell()方法(该方法在另外一个线程中执行,等待中) } return cellContents; }
    public void WriteToCell(int n) { lock(this) { if (readerFlag) { try { Monitor.Wait(this);
    } catch (SynchronizationLockException e) { //当同步方法(指 Monitor 类除 Enter 之外的方法)在非同步的代码区 被调用 Console.WriteLine(e); } catch (ThreadInterruptedException e) { //当线程在等待状态的时候中止 Console.WriteLine(e); } } cellContents = n; Console.WriteLine("Produce: {0}",cellContents); readerFlag = true; Monitor.Pulse(this); //通知另外一个线程中正在等待的 ReadFromCell()方法 } } }
    下面定义生产者类 CellProd 和消费者类 CellCons ,它们都只有一个方法 ThreadRun(),以便在 Main()函数中提供给线程的 ThreadStart 代理对象,作为线程的 入口。
    public class CellProd { Cell cell; // 被操作的 Cell 对象 int quantity = 1; // 生产者生产次数,初始化为 1
    public CellProd(Cell box, int request) { //构造函数 cell = box; quantity = request; } public void ThreadRun( ) { for(int looper=1; looper<=quantity; looper++) cell.WriteToCell(looper); //生产者向操作对象写入信息 } }
    public class CellCons { Cell cell; int quantity = 1;
    public CellCons(Cell box, int request) { //构造函数 cell = box; quantity = request; } public void ThreadRun( ) { int valReturned; for(int looper=1; looper<=quantity; looper++) valReturned=cell.ReadFromCell( );//消费者从操作对象中读取信息 } }
    然后在下面这个类 MonitorSample 的 Main()函数中,我们要做的就是创建两个线程分别 作为生产者和消费者,使用 CellProd.ThreadRun()方法和 CellCons.ThreadRun()方法
    对同一个 Cell 对象进行操作。
    public class MonitorSample { public static void Main(String[] args) { int result = 0; //一个标志位,如果是 0 表示程序没有出错,如果是 1 表明有错误发生 Cell cell = new Cell( );
    //下面使用 cell 初始化 CellProd 和 CellCons 两个类,生产和消费次数均为 20 次 CellProd prod = new CellProd(cell, 20); CellCons cons = new CellCons(cell, 20);
    Thread producer = new Thread(new ThreadStart(prod.ThreadRun)); Thread consumer = new Thread(new ThreadStart(cons.ThreadRun)); //生产者线程和消费者线程都已经被创建,但是没有开始执行 try { producer.Start( ); consumer.Start( );
    producer.Join( ); consumer.Join( ); Console.ReadLine(); } catch (ThreadStateException e) { //当线程因为所处状态的原因而不能执行被请求的操作 Console.WriteLine(e); result = 1; } catch (ThreadInterruptedException e) { //当线程在等待状态的时候中止 Console.WriteLine(e); result = 1; } //尽管 Main()函数没有返回值,但下面这条语句可以向父进程返回执行结果 Environment.ExitCode = result; } }
    C#多线程学习( 多线程的自动管理(线程池) C#多线程学习(四) 多线程的自动管理(线程池) 多线程学习
    在多线程的程序中,经常会出现两种情况: 一种情况: 一种情况: 应用程序中,线程把大部分的时间花费在等待状态,等待某个事件发生,然
    后才能给予响应 这一般使用 ThreadPool(线程池)来解决; 另一种情况: 另一种情况:线程平时都处于休眠状态,只是周期性地被唤醒 这一般使用 Timer(定时器)来解决; ThreadPool 类提供一个由系统维护的线程池(可以看作一个线程的容器),该容器需要 Windows 2000 以上系统支持,因为其中某些方法调用了只有高版本的 Windows 才有的 API 函数。 将线程安放在线程池里,需使用 ThreadPool.QueueUserWorkItem()方法,该方法的原 型如下: //将一个线程放进线程池,该线程的 Start()方法将调用 WaitCallback 代理对象代表的函 数 public static bool QueueUserWorkItem(WaitCallback); //重载的方法如下,参数 object 将传递给 WaitCallback 所代表的方法 public static bool QueueUserWorkItem(WaitCallback, object);
    注意: 注意: ThreadPool 类是一个静态类,你不能也不必要生成它的对象。而且一旦使用该方法在线程 池中添加了一个项目,那么该项目将是无法取消的。
    在这里你无需自己建立线程,只需把你要做的工作写成函数,然后作为参数传递给 ThreadPool.QueueUserWorkItem()方法就行了,传递的方法就是依靠 WaitCallback 代理对象,而线程的建立、管理、运行等工作都是由系统自动完成的,你无须考虑那些复杂 的细节问题。
    ThreadPool 的用法: 的用法: 首先程序创建了一个 ManualResetEvent 对象,该对象就像一个信号灯,可以利用它的信 号来通知其它线程。 本例中,当线程池中所有线程工作都完成以后,ManualResetEvent 对象将被设置为有信 号,从而通知主线程继续运行。 ManualResetEvent 对象有几个重要的方法: 对象有几个重要的方法: 初始化该对象时,用户可以指定其默认的状态(有信号/无信号); 在初始化以后,该对象将保持原来的状态不变,直到它的 Reset()或者 Set()方法被调用: Reset()方法:将其设置为无信号状态; Set()方法:将其设置为有信号状态。 WaitOne()方法:使当前线程挂起,直到 ManualResetEvent 对象处于有信号状态,此时 该线程将被激活。然后,程序将向线程池中添加工作项,这些以函数形式提供的工作项被系 统用来初始化自动建立的线程。 当所有的线程都运行完了以后, ManualResetEvent.Set() 方法被调用, 因为调用了 ManualResetEvent.WaitOne()方法而处在等待状态的主线程将 接收到这个信号,于是它接着往下执行,完成后边的工作。
    ThreadPool 的用法示例:
    using System; using System.Collections; using System.Threading;
    namespace ThreadExample { //这是用来保存信息的数据结构,将作为参数被传递 public class SomeState { public int Cookie; public SomeState(int iCookie) { Cookie = iCookie; } }
    public class Alpha { public Hashtable HashCount; public ManualResetEvent eventX; public static int iCount = 0; public static int iMaxCount = 0;
    public Alpha(int MaxCount) { HashCount = new Hashtable(MaxCount); iMaxCount = MaxCount; }
    //线程池里的线程将调用 Beta()方法 public void Beta(Object state) { //输出当前线程的 hash 编码值和 Cookie 的值 Console.WriteLine(" {0} {1} :", Thread.CurrentThread.GetHashCode(),((S omeState)state).Cookie); Console.WriteLine("HashCount.Count=={0}, Thread.CurrentThread.GetHash Code()=={1}", HashCount.Count, Thread.CurrentThread.GetHashCode()); lock (HashCount) { //如果当前的 Hash 表中没有当前线程的 Hash 值,则添加之 if (!HashCount.ContainsKey(Thread.CurrentThread.GetHashCode())) HashCount.Add (Thread.CurrentThread.GetHashCode(), 0); HashCount[Thread.CurrentThread.GetHashCode()] = ((int)HashCount[Thread.CurrentThread.GetHashCode()])+1;
    } int iX = 2000; Thread.Sleep(iX); //Interlocked.Increment()操作是一个原子操作,具体请看下面说明 Interlocked.Increment(ref iCount);
    if (iCount == iMaxCount) { Console.WriteLine(); Console.WriteLine("Setting eventX "); eventX.Set(); } } }
    public class SimplePool { public static int Main(string[] args) { Console.WriteLine("Thread Pool Sample:"); bool W2K = false; int MaxCount = 10;//允许线程池中运行最多 10 个线程
    //新建 ManualResetEvent 对象并且初始化为无信号状态 ManualResetEvent eventX = new ManualResetEvent(false); Console.WriteLine("Queuing {0} items to Thread Pool", MaxCount); Alpha oAlpha = new Alpha(MaxCount); //创建工作项 //注意初始化 oAlpha 对象的 eventX 属性 oAlpha.eventX = eventX; Console.WriteLine("Queue to Thread Pool 0"); try { //将工作项装入线程池 //这里要用到 Windows 2000 以上版本才有的 API,所以可能出现 NotSupp ortException 异常 ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta), n ew SomeState(0)); W2K = true; } catch (NotSupportedException) { Console.WriteLine("These API's may fail when called on a non-Wind ows 2000 system."); W2K = false;
    } if (W2K)//如果当前系统支持 ThreadPool 的方法. { for (int iItem=1;iItem < MaxCount;iItem++) { //插入队列元素 Console.WriteLine("Queue to Thread Pool {0}", iItem); ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Bet a), new SomeState(iItem)); } Console.WriteLine("Waiting for Thread Pool to drain"); //等待事件的完成,即线程调用 ManualResetEvent.Set()方法 eventX.WaitOne(Timeout.Infinite,true); //WaitOne()方法使调用它的线程等待直到 eventX.Set()方法被调用 Console.WriteLine("Thread Pool has been drained (Event fired)"); Console.WriteLine(); Console.WriteLine("Load across threads"); foreach(object o in oAlpha.HashCount.Keys) Console.WriteLine("{0} {1}", o, oAlpha.HashCount[o]); } Console.ReadLine(); return 0;
    } } }
    }
    程序中应该引起注意的地方: 程序中应该引起注意的地方: SomeState 类是一个保存信息的数据结构,它在程序中作为参数被传递给每一个线程,因 为你需要把一些有用的信息封装起来提供给线程,而这种方式是非常有效的。 程序出现的 InterLocked 类也是专为多线程程序而存在的, 它提供了一些有用的原子操作。 原子操作:就是在多线程程序中,如果这个线程调用这个操作修改一个变量,那么其他线程 就不能修改这个变量了,这跟 lock 关键字在本质上是一样的。
    程序的输出结果:
    我们应该彻底地分析上面的程序,把握住线程池的本质,理解它存在的意义是什么,这样才 能得心应手地使用它。
    在上面的例程中,同步是通过等待 Monitor.Pulse()来完成的。首先生产者生产了一个值, 而同一时刻消费者处于等待状态,直到收到生产者的"脉冲(Pulse)"通知它生产已经完成, 此后消费者进入消费状态,而生产者开始等待消费者完成操作后将调用 Monitor.Pulese() 发出的"脉冲"。 它的执行结果很简单:   Produce: 1   Consume: 1   Produce: 2   Consume: 2   Produce: 3   Consume: 3   ……   ……   Produce: 20   Consume: 20
    事实上, 这个简单的例子已经帮助我们解决了多线程应用程序中可能出现的大问题, 只要领 悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。
    C#多线程学习( 多线程的自动管理(定时器) C#多线程学习(五) 多线程的自动管理(定时器) 多线程学习
    Timer 类:设置一个定时器,定时执行用户指定的函数。 定时器启动后,系统将自动建立一个新的线程,执行用户指定的函数。 初始化一个 Timer 对象: Timer timer = new Timer(timerDelegate, s,1000, 1000);
    // 第一个参数:指定了 TimerCallback 委托,表示要执行的方法; // 第二个参数:一个包含回调方法要使用的信息的对象,或者为空引用; // 第三个参数:延迟时间--计时开始的时刻距现在的时间,单位是毫秒,指定为"0"表示 立即启动计时器; // 第四个参数:定时器的时间间隔--计时开始以后,每隔这么长的一段时间, TimerCallback 所代表的方法将被调用一次,单位也是毫秒。指定 Timeout.Infinite 可 以禁用定期终止。
    Timer.Change()方法:修改定时器的设置。(这是一个参数类型重载的方法) 使用示例: timer.Change(1000,2000);
    Timer 类的程序示例(来源:MSDN):
    using System; using System.Threading;
    namespace ThreadExample
    { class TimerExampleState { public int counter = 0; public Timer tmr; }
    class App { public static void Main() { TimerExampleState s = new TimerExampleState();
    //创建代理对象 TimerCallback,该代理将被定时调用 TimerCallback timerDelegate = new TimerCallback(CheckStatus);
    //创建一个时间间隔为 1s 的定时器 Timer timer = new Timer(timerDelegate, s,1000, 1000); s.tmr = timer;
    //主线程停下来等待 Timer 对象的终止 while(s.tmr != null)
    Thread.Sleep(0); Console.WriteLine("Timer example done."); Console.ReadLine(); }
    //下面是被定时调用的方法 static void CheckStatus(Object state) { TimerExampleState s =(TimerExampleState)state; s.counter++; Console.WriteLine("{0} Checking Status {1}.",DateTime.Now.TimeOfDa y, s.counter);
    if(s.counter == 5) { //使用 Change 方法改变了时间间隔 (s.tmr).Change(10000,2000); Console.WriteLine("changed } ");
    if(s.counter == 10) {
    Console.WriteLine("disposing of timer s.tmr.Dispose(); s.tmr = null; } } } }
    ");
    程序首先创建了一个定时器,它将在创建 1 秒之后开始每隔 1 秒调用一次 CheckStatus()方法, 当调用 5 次以后, CheckStatus()方法中修改了时间间隔为 2 秒, 在 并且指定在 10 秒后重新开始。 当计数达到 10 次, 调用 Timer.Dispose()方法删除了 timer 对象,主线程于是跳出循环,终止程序。
    C#多线程学习(六) 互斥对象 C#多线程学习( 多线程学习 如何控制好多个线程相互之间的联系,不产生冲突和重复,这需要用到互斥对象,即: System.Threading 命名空间中的 Mutex 类。 我们可以把 Mutex 看作一个出租车,乘客看作线程。乘客首先等车,然后上车,最后下车。 当一个乘客在车上时,其他乘客就只有等他下车以后才可以上车。而线程与 Mutex 对象的 关系也正是如此,线程使用 Mutex.WaitOne()方法等待 Mutex 对象被释放,如果它等待 的 Mutex 对象被释放了,它就自动拥有这个对象,直到它调用 Mutex.ReleaseMutex() 方法释放这个对象,而在此期间,其他想要获取这个 Mutex 对象的线程都只有等待。
    下面这个例子使用了 Mutex 对象来同步四个线程,主线程等待四个线程的结束,而这四个 线程的运行又是与两个 Mutex 对象相关联的。 其中还用到 AutoResetEvent 类的对象,可以把它理解为一个信号灯。这里用它的有信号 状态来表示一个线程的结束。 // AutoResetEvent.Set()方法设置它为有信号状态 // AutoResetEvent.Reset()方法设置它为无信号状态
    Mutex 类的程序示例:
    using System; using System.Threading;
    namespace ThreadExample { public class MutexSample { static Mutex gM1; static Mutex gM2; const int ITERS = 100; static AutoResetEvent Event1 = new AutoResetEvent(false); static AutoResetEvent Event2 = new AutoResetEvent(false); static AutoResetEvent Event3 = new AutoResetEvent(false); static AutoResetEvent Event4 = new AutoResetEvent(false);
    public static void Main(String[] args) { Console.WriteLine("Mutex Sample ");
    //创建一个 Mutex 对象,并且命名为 MyMutex gM1 = new Mutex(true,"MyMutex"); //创建一个未命名的 Mutex 对象. gM2 = new Mutex(true); Console.WriteLine(" - Main Owns gM1 and gM2");
    AutoResetEvent[] evs = new AutoResetEvent[4]; evs[0] = Event1; //为后面的线程 t1,t2,t3,t4 定义 AutoResetEvent 对象 evs[1] = Event2; evs[2] = Event3; evs[3] = Event4;
    MutexSample tm = new MutexSample( ); Thread t1 = new Thread(new ThreadStart(tm.t1Start)); Thread t2 = new Thread(new ThreadStart(tm.t2Start)); Thread t3 = new Thread(new ThreadStart(tm.t3Start)); Thread t4 = new Thread(new ThreadStart(tm.t4Start)); t1.Start( );// 使用 Mutex.WaitAll()方法等待一个 Mutex 数组中的对象全部被释放
    t2.Start( );// 使用 Mutex.WaitOne()方法等待 gM1 的释放 t3.Start( );// 使用 Mutex.WaitAny()方法等待一个 Mutex 数组中任意一个对象被 释放 t4.Start( );// 使用 Mutex.WaitOne()方法等待 gM2 的释放
    Thread.Sleep(2000); Console.WriteLine(" - Main releases gM1"); gM1.ReleaseMutex( ); //线程 t2,t3 结束条件满足
    Thread.Sleep(1000); Console.WriteLine(" - Main releases gM2"); gM2.ReleaseMutex( ); //线程 t1,t4 结束条件满足
    //等待所有四个线程结束 WaitHandle.WaitAll(evs); Console.WriteLine(" Console.ReadLine(); } Mutex Sample");
    public void t1Start( ) { Console.WriteLine("t1Start started, Mutex.WaitAll(Mutex[])");
    Mutex[] gMs = new Mutex[2]; gMs[0] = gM1;//创建一个 Mutex 数组作为 Mutex.WaitAll()方法的参数 gMs[1] = gM2; Mutex.WaitAll(gMs);//等待 gM1 和 gM2 都被释放 Thread.Sleep(2000); Console.WriteLine("t1Start finished, Mutex.WaitAll(Mutex[]) satisfied"); Event1.Set( ); //线程结束,将 Event1 设置为有信号状态 } public void t2Start( ) { Console.WriteLine("t2Start started, gM1.WaitOne( )"); gM1.WaitOne( );//等待 gM1 的释放 Console.WriteLine("t2Start finished, gM1.WaitOne( ) satisfied"); Event2.Set( );//线程结束,将 Event2 设置为有信号状态 } public void t3Start( ) { Console.WriteLine("t3Start started, Mutex.WaitAny(Mutex[])"); Mutex[] gMs = new Mutex[2]; gMs[0] = gM1;//创建一个 Mutex 数组作为 Mutex.WaitAny()方法的参数 gMs[1] = gM2; Mutex.WaitAny(gMs);//等待数组中任意一个 Mutex 对象被释放
    Console.WriteLine("t3Start finished, Mutex.WaitAny(Mutex[])"); Event3.Set( );//线程结束,将 Event3 设置为有信号状态 } public void t4Start( ) { Console.WriteLine("t4Start started, gM2.WaitOne( )"); gM2.WaitOne( );//等待 gM2 被释放 Console.WriteLine("t4Start finished, gM2.WaitOne( )"); Event4.Set( );//线程结束,将 Event4 设置为有信号状态 } } }
    程序的输出结果:
    Mutex Sample - Main Owns gM1 and gM2 t1Start started, Mutex.WaitAll(Mutex[]) t2Start started, gM1.WaitOne( ) t3Start started, Mutex.WaitAny(Mutex[]) t4Start started, gM2.WaitOne( ) - Main releases gM1
    t2Start finished, gM1.WaitOne( ) satisfied t3Start finished, Mutex.WaitAny(Mutex[]) - Main releases gM2 t1Start finished, Mutex.WaitAll(Mutex[]) satisfied t4Start finished, gM2.WaitOne( ) Mutex Sample
    从执行结果可以很清楚地看到,线程 t2,t3 的运行是以 gM1 的释放为条件的,而 t4 在 gM2 释放后开始执行,t1 则在 gM1 和 gM2 都被释放了之后才执行。Main()函数最后, 使用 WaitHandle 等待所有的 AutoResetEvent 对象的信号,这些对象的信号代表相应线 程的结束。
    相关文章推荐:本文由desaltsand贡献
    doc文档可能在WAP端浏览体验不佳。建议您优先选择TXT,或下载源文件到本机查看。
    多线程:C#线程同步 lock,Monitor,Mutex,同步事件和等待句柄 多线程:C#线程同步 lock,Monitor,Mutex,同步事件和等待句柄
    本篇从 Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler 的类关系图开始, 希望通过本篇的介绍能对常见的线程同步方法有一个整体的认识,而对每种方式的使用细节,适用场 合不会过多解释。让我们来看看这几个类的关系图:
    1.lock 关键字
    lock 是 C#关键词, 它将语句块标记为临界区,确保当一个线程位于代码的临界区时,另一个线程 不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释 放。方法是获取给定对象的互斥锁,执行语句,然后释放该锁。 MSDN 上给出了使用 lock 时的注意事项通常,应避免锁定 public 类型,否则实例将超出代码 的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则。
    1)如果实例可以被公共访问,将出现 lock (this) 问题。
    2)如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题由于一个类的所有实 例都只有一个类型对象(该对象是 typeof 的返回结果),锁定它,就锁定了该对象的所有实例。微
    软现在建议不要使用 lock(typeof(MyType)),因为锁定类型对象是个很缓慢的过程,并且类中的其他 线程、甚至在同一个应用程序域中运行的其他程序都可以访问该类型对象,因此,它们就有可能代替 您锁定类型对象,完全阻止您的执行,从而导致你自己的代码的挂起。 3)由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock("myLock") 问 题。 这个问题和.NET Framework 创建字符串的机制有关系, 如果两个 string 变量值都是"myLock", 在内存中会指向同一字符串对象。 最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数 据。 我们再来通过 IL Dasm 看看 lock 关键字的本质,下面是一段简单的测试代码:
    lock (lockobject) { int i = 5; }
    用 IL Dasm 打开编译后的文件,上面的语句块生成的 IL 代码为:
    IL_0045: call IL_004a: nop .try { IL_004b: nop
    void [mscorlib]System.Threading.Monitor::Enter(object)
    IL_004c: ldc.i4.5 IL_004d: stloc.1
    IL_004e: nop IL_004f: leave.s } // end .try finally { IL_0051: ldloc.3 IL_0052: call IL_0057: nop IL_0058: endfinally } // end handler 通过上面的代码我们很清楚的看到:lock 关键字其实就是对 Monitor 类的 Enter()和 Exit()方法 的封装,并通过 try……catch……finally 语句块确保在 lock 语句块结束后执行 Monitor.Exit()方法,释 放互斥锁。 2.Monitor 类 void [mscorlib]System.Threading.Monitor::Exit(object) IL_0059
    Monitor 类通过向单个线程授予对象锁来控制对对象的访问。对象锁提供限制访问临界区的能 力。当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还可以使用 Monitor 来确保不会 允许其他任何线程访问正在由锁的所有者执行的应用程序代码节,除非另一个线程正在使用其他的锁 定对象执行该代码。 通过对 lock 关键字的分析我们知道, lock 就是对 Monitor 的 Enter 和 Exit 的一个封装, 而且使 用起来更简洁,因此 Monitor 类的 Enter()和 Exit()方法的组合使用可以用 lock 关键字替代。
    另外 Monitor 类还有几个常用的方法:
    TryEnter()能够有效的解决长期死等的问题,如果在一个并发经常发生,而且持续时间长的环境 中使用 TryEnter,可以有效防止死锁或者长时间的等待。比如我们可以设置一个等待时间 bool gotLock = Monitor.TryEnter(myobject,1000),让当前线程在等待 1000 秒后根据返回的 bool 值 来决定是否继续下面的操作。 Wait()释放对象上的锁以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程 将等待。脉冲信号用于通知等待线程有关对象状态的更改。 Pulse(),PulseAll()向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更 改, 并且锁的所有者准备释放该锁。 等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。 一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。 注意:Pulse、PulseAll 和 Wait 方法必须从同步的代码块内调用。
    我们假定一种情景:妈妈做蛋糕,小孩有点馋,妈妈每做好一块就要吃掉,妈妈做好一块后,告 诉小孩蛋糕已经做好了。下面的例子用 Monitor 类的 Wait 和 Pulse 方法模拟小孩吃蛋糕的情景。
    //仅仅是说明 Wait 和 Pulse/PulseAll 的例子 //逻辑上并不严密,使用场景也并不一定合适 class MonitorSample { private int n = 1; //生产者和消费者共同处理的数据 private int max = 10000;
    private object monitor = new object();
    public void Produce() { lock (monitor) { for (; n <= max; n++) { Console.WriteLine("妈妈:第" + n.ToString() + "块蛋糕做好了"); //Pulse 方法不用调用是因为另一个线程中用的是 Wait(object,int)方法 //该方法使被阻止线程进入了同步对象的就绪队列 //是否需要脉冲激活是 Wait 方法一个参数和两个参数的重要区别 //Monitor.Pulse(monitor); //调用 Wait 方法释放对象上的锁并阻止该线程(线程状态为 WaitSleepJoin) //该线程进入到同步对象的等待队列,直到其它线程调用 Pulse 使该线程进入到就绪 队列中 //线程进入到就绪队列中才有条件争夺同步对象的所有权 //如果没有其它线程调用 Pulse/PulseAll 方法,该线程不可能被执行 Monitor.Wait(monitor); } } }
    public void Consume()
    { lock (monitor) { while (true) { //通知等待队列中的线程锁定对象状态的更改,但不会释放锁 //接收到 Pulse 脉冲后,线程从同步对象的等待队列移动到就绪队列中 //注意:最终能获得锁的线程并不一定是得到 Pulse 脉冲的线程 Monitor.Pulse(monitor); //释放对象上的锁并阻止当前线程,直到它重新获取该锁 //如果指定的超时间隔已过,则线程进入就绪队列 Monitor.Wait(monitor,1000); Console.WriteLine("孩子:开始吃第" + n.ToString() + "块蛋糕"); } } }
    static void Main(string[] args) { MonitorSample obj = new MonitorSample(); Thread tProduce = new Thread(new ThreadStart(obj.Produce)); Thread tConsume = new Thread(new ThreadStart(obj.Consume));
    //Start threads. tProduce.Start(); tConsume.Start();
    Console.ReadLine(); } }
    这个例子的目的是要理解 Wait 和 Pulse 如何保证线程同步的,同时要注意 Wait(obeject)和 Wait(object,int)方法的区别, 理解它们的区别很关键的一点是要理解同步的对象包含若干引用, 其中 包括对当前拥有锁的线程的引用、对就绪队列(包含准备获取锁的线程)的引用和对等待队列(包含 等待对象状态更改通知的线程)的引用。
    本篇继续介绍 WaitHandler 类及其子类 Mutex, ManualResetEvent, AutoResetEvent 的用法。 .NET 中线程同步的方式多的让人看了眼花缭乱,究竟该怎么去理解呢?其实,我们抛开.NET 环境看线程 同步,无非是执行两种操作:一是互斥/加锁,目的是保证临界区代码操作的"原子性";另一种是信号 灯操作,目的是保证多个线程按照一定顺序执行,如生产者线程要先于消费者线程执行。.NET 中线 程同步的类无非是对这两种方式的封装,目的归根结底都可以归结为实现互斥/加锁或者是信号灯这 两种方式,只是它们的适用场合有所不。下面我们根据类的层次结构了解 WaitHandler 及其子类。 1.WaitHandler WaitHandle 是 Mutex,Semaphore,EventWaitHandler,AutoResetEvent, ManualResetEvent 共同的祖先,它封装 Win32 同步句柄内核对象,也就是说是这些内核对象的托 管版本。
    线程可以通过调用 WaitHandler 实例的方法 WaitOne 在单个等待句柄上阻止。此外, WaitHandler 类重载了静态方法,以等待所有指定的等待句柄都已收集到信号 WaitAll,或者等待某 一指定的等待句柄收集到信号 WaitAny。这些方法都提供了放弃等待的超时间隔、在进入等待之前退 出同步上下文的机会,并允许其它线程使用同步上下文。WaitHandler 是 C#中的抽象类,不能实例 化。 2.EventWaitHandler vs. ManualResetEvent vs. AutoResetEvent(同步事件) AutoResetEvent(同步事件)
    我们先看看两个子类 ManualResetEvent 和 AutoResetEvent 在.NET Framework 中的实现:
    //.NET Framework 中 ManualResetEvent 类的实现 [ComVisible(true), HostProtection(SecurityAction.LinkDemand, Synchronization = true, ExternalThreading = true)] public sealed class ManualResetEvent : EventWaitHandle { // Methods public ManualResetEvent(bool initialState) : base(initialState, EventResetMode.ManualReset) { } }
    //.NET Framework 中 AutoResetEvent 类的实现 [ComVisible(true), HostProtection(SecurityAction.LinkDemand, Synchronization = true,
    ExternalThreading = true)] public sealed class AutoResetEvent : EventWaitHandle { // Methods public AutoResetEvent(bool initialState) : base(initialState, EventResetMode.AutoReset) { } }
    原来 ManualResetEvent 和 AutoResetEvent 都继承自 EventWaitHandler, 它们的唯一区别就 在于父类 EventWaitHandler 的构造函数参数 EventResetMode 不同,这样我们只要弄清了参数 EventResetMode 值不同时,EventWaitHandler 类控制线程同步的行为有什么不同,两个子类也就 清楚了。为了便于描述,我们不去介绍父类的两种模式,而直接介绍子类。 ManualResetEvent 和 AutoResetEvent 的共同点: 1)Set 方法将事件状态设置为终止状态,允许一个或多个等待线程继续;Reset 方法将事件状 态设置为非终止状态,导致线程阻止;WaitOne 阻止当前线程,直到当前线程的 WaitHandler 收到 事件信号。 2) 可以通过构造函数的参数值来决定其初始状态, 若为 true 则事件为终止状态从而使线程为非 阻塞状态,为 false 则线程为阻塞状态。 3)如果某个线程调用 WaitOne 方法,则当事件状态为终止状态时,该线程会得到信号,继续 向下执行。 ManualResetEvent 和 AutoResetEvent 的不同点: 1)AutoResetEvent.WaitOne()每次只允许一个线程进入,当某个线程得到信号后, AutoResetEvent 会自动又将信号置为不发送状态,则其他调用 WaitOne 的线程只有继续等待,也
    就是说 AutoResetEvent 一次只唤醒一个线程; 2)ManualResetEvent 则可以唤醒多个线程,因为当某个线程调用了 ManualResetEvent.Set()方法后,其他调用 WaitOne 的线程获得信号得以继续执行,而 ManualResetEvent 不会自动将信号置为不发送。 3)也就是说,除非手工调用了 ManualResetEvent.Reset()方法,则 ManualResetEvent 将 一直保持有信号状态,ManualResetEvent 也就可以同时唤醒多个线程继续执行。 示例场景:张三、李四两个好朋友去餐馆吃饭,两个人点了一份宫爆鸡丁,宫爆鸡丁做好需要一 段时间,张三、李四不愿傻等,都专心致志的玩起了手机游戏,心想宫爆鸡丁做好了,服务员肯定会 叫我们的。服务员上菜之后,张三李四开始享用美味的饭菜,饭菜吃光了,他们再叫服务员过来买单。 我们可以从这个场景中抽象出来三个线程,张三线程、李四线程和服务员线程,他们之间需要同步: 服务员上菜->张三、李四开始享用宫爆鸡丁->吃好后叫服务员过来买单。这个同步用什么呢? ManualResetEvent 还是 AutoResetEvent?通过上面的分析不难看出,我们应该用 ManualResetEvent 进行同步,下面是程序代码:
    public class EventWaitTest { private string name; //顾客姓名 //private static AutoResetEvent eventWait = new AutoResetEvent(false); private static ManualResetEvent eventWait = new ManualResetEvent(false); private static ManualResetEvent eventOver = new ManualResetEvent(false);
    public EventWaitTest(string name) { this.name = name; }
    public static void Product() { Console.WriteLine("服务员:厨师在做菜呢,两位稍等 Thread.Sleep(2000); Console.WriteLine("服务员:宫爆鸡丁好了"); eventWait.Set(); while (true) { if (eventOver.WaitOne(1000, false)) { Console.WriteLine("服务员:两位请买单"); eventOver.Reset(); } } } ");
    public void Consume() { while (true) { if (eventWait.WaitOne(1000, false))
    { Console.WriteLine(this.name + ":开始吃宫爆鸡丁"); Thread.Sleep(2000); Console.WriteLine(this.name + ":宫爆鸡丁吃光了"); eventWait.Reset(); eventOver.Set(); break; } else { Console.WriteLine(this.name + ":等着上菜无聊先玩会手机游戏"); } } } }
    public class App { public static void Main(string[] args) { EventWaitTest zhangsan = new EventWaitTest("张三"); EventWaitTest lisi = new EventWaitTest("李四");
    Thread t1 = new Thread(new ThreadStart(zhangsan.Consume)); Thread t2 = new Thread(new ThreadStart(lisi.Consume)); Thread t3 = new Thread(new ThreadStart(EventWaitTest.Product));
    t1.Start(); t2.Start(); t3.Start();
    Console.Read(); } }
    编译后查看运行结果,符合我们的预期,控制台输出为: 服务员:厨师在做菜呢,两位稍等…… 张三:等着上菜无聊先玩会手机游戏 李四:等着上菜无聊先玩会手机游戏 张三:等着上菜无聊先玩会手机游戏 李四:等着上菜无聊先玩会手机游戏 服务员:宫爆鸡丁好了 张三:开始吃宫爆鸡丁 李四:开始吃宫爆鸡丁 张三:宫爆鸡丁吃光了 李四:宫爆鸡丁吃光了 服务员:两位请买单 如果改用 AutoResetEvent 进行同步呢?会出现什么样的结果?恐怕张三和李四就要打起来 了,一个享用了美味的宫爆鸡丁,另一个到要付账的时候却还在玩游戏。感兴趣的朋友可以把注释的 那行代码注释去掉,并把下面一行代码注释掉,运行程序看会出现怎样的结果。 3.Mutex(互斥体) 3.Mutex(互斥体) 互斥体
    Mutex 和 EventWaitHandler 有着共同的父类 WaitHandler 类, 它们同步的函数用法也差不多, 这里不再赘述。 Mutex 的突出特点是可以跨应用程序域边界对资源进行独占访问, 即可以用于同步不 同进程中的线程,这种功能当然这是以牺牲更多的系统资源为代价的。
    前两篇简单介绍了线程同步 lock,Monitor,同步事件 EventWaitHandler,互斥体 Mutex 的基本 用法,在此基础上,我们对它们用法进行比较,并给出什么时候需要锁什么时候不需要的几点建议。 最后,介绍几个 FCL 中线程安全的类,集合类的锁定方式等,做为对线程同步系列的完善和补充。 1.几种同步方法的区别 几种同步方法的区别 lock 和 Monitor 是.NET 用一个特殊结构实现的,Monitor 对象是完全托管的、完全可移植的, 并且在操作系统资源要求方面可能更为有效, 同步速度较快, 但不能跨进程同步。 (Monitor.Enter lock 和 Monitor.Exit 方法的封装),主要作用是锁定临界区,使临界区代码只能被获得锁的线程执行。 Monitor.Wait 和 Monitor.Pulse 用于线程同步,类似信号操作,个人感觉使用比较复杂,容易造成 死锁。 互斥体 Mutex 和事件对象 EventWaitHandler 属于内核对象,利用内核对象进行线程同步,线 程必须要在用户模式和内核模式间切换,所以一般效率很低,但利用互斥对象和事件对象这样的内核 对象,可以在多个进程中的各个线程间进行同步。 互斥体 Mutex 类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一 个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其 他所有需要接力棒运行的线程都知道能等着看热闹。 EventWaitHandle 类允许线程通过发信号互相通信。通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线 程。 2.什么时候需要锁定 什么时候需要锁定 首先要理解锁定是解决竞争条件的,也就是多个线程同时访问某个资源,造成意想不到的结果。 比如,最简单的情况是,一个计数器,两个线程同时加一,后果就是损失了一个计数,但相当频繁的 锁定又可能带来性能上的消耗,还有最可怕的情况死锁。那么什么情况下我们需要使用锁,什么情况 下不需要呢? 1)只有共享资源才需要锁定 只有可以被多线程访问的共享资源才需要考虑锁定,比如静态变量,再比如某些缓存中的值,而 属于线程内部的变量不需要锁定。
    2)多使用 lock,少用 Mutex 如果你一定要使用锁定,请尽量不要使用内核模块的锁定机制,比如.NET 的 Mutex, Semaphore,AutoResetEvent 和 ManuResetEvent,使用这样的机制涉及到了系统在用户模式 和内核模式间的切换,性能差很多,但是他们的优点是可以跨进程同步线程,所以应该清楚的了解到 他们的不同和适用范围。 3)了解你的程序是怎么运行的 实际上在 web 开发中大多数逻辑都是在单个线程中展开的,一个请求都会在一个单独的线程中 处理,其中的大部分变量都是属于这个线程的,根本没有必要考虑锁定,当然对于 ASP.NET 中的 Application 对象中的数据,我们就要考虑加锁了。 4)把锁定交给数据库 数据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保 证数据的可靠和一致性,这就为我们节省了很多的精力。保证了数据源头上的同步,我们多数的精力 就可以集中在缓存等其他一些资源的同步访问上了。通常,只有涉及到多个线程修改数据库中同一条 记录时,我们才考虑加锁。 5)业务逻辑对事务和线程安全的要求 这条是最根本的东西,开发完全线程安全的程序是件很费时费力的事情,在电子商务等涉及金融 系统的案例中,许多逻辑都必须严格的线程安全,所以我们不得不牺牲一些性能,和很多的开发时间 来做这方面的工作。 而一般的应用中, 许多情况下虽然程序有竞争的危险, 我们还是可以不使用锁定, 比如有的时候计数器少一多一,对结果无伤大雅的情况下,我们就可以不用去管它。 3.InterLocked 类 Interlocked 类提供了同步对多个线程共享的变量的访问的方法。如果该变量位于共享内存中, 则不同进程的线程就可以使用该机制。互锁操作是原子的,即整个操作是不能由相同变量上的另一个 互锁操作所中断的单元。这在抢先多线程操作系统中是很重要的,在这样的操作系统中,线程可以在 从某个内存地址加载值之后但是在有机会更改和存储该值之前被挂起。 我们来看一个 InterLock.Increment()的例子, 该方法以原子的形式递增指定变量并存储结果, 示例如下:
    class InterLockedTest { public static Int64 i = 0;
    public static void Add() {
    for (int i = 0; i < 100000000; i++) { Interlocked.Increment(ref InterLockedTest.i); //InterLockedTest.i = InterLockedTest.i + 1; } }
    public static void Main(string[] args) { Thread t1 = new Thread(new ThreadStart(InterLockedTest.Add)); Thread t2 = new Thread(new ThreadStart(InterLockedTest.Add));
    t1.Start(); t2.Start();
    t1.Join(); t2.Join();
    Console.WriteLine(InterLockedTest.i.ToString()); Console.Read(); } }
    输出结果 200000000,如果 InterLockedTest.Add()方法中用注释掉的语句代替 Interlocked.Increment()方法,结果将不可预知,每次执行结果不同。InterLockedTest.Add() 方法保证了加 1 操作的原子性,功能上相当于自动给加操作使用了 lock 锁。同时我们也注意到 InterLockedTest.Add()用时比直接用+号加 1 要耗时的多,所以说加锁资源损耗还是很明显的。 另外 InterLockedTest 类还有几个常用方法,具体用法可以参考 MSDN 上的介绍。 4.集合类的同步 集合类的同步 .NET 在一些集合类,比如 Queue、ArrayList、HashTable 和 Stack,已经提供了一个供 lock 使用的对象 SyncRoot。用 Reflector 查看了 SyncRoot 属性(Stack.SynchRoot 略有不同)的源 码如下:
    public virtual object SyncRoot { get { if (this._syncRoot == null) { //如果_syncRoot 和 null 相等,将 new object 赋值给_syncRoot //Interlocked.CompareExchange 方法保证多个线程在使用 syncRoot 时是线 程安全的 Interlocked.CompareExchange(ref this._syncRoot, new object (), null); } return this._syncRoot; } }
    这里要特别注意的是 MSDN 提到:从头到尾对一个集合进行枚举本质上并不是一个线程安全的 过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚 举过程中保证线程安全,可以在整个枚举过程中锁定集合,或者捕捉由于其他线程进行的更改而引发 的异常。应该使用下面的代码:
    Queue q = new Queue(); lock (q.SyncRoot) { foreach (object item in q) { //do something } } 还有一点需要说明的是, 集合类提供了一个是和同步相关的方法 Synchronized, 该方法返回一 个对应的集合类的 wrapper 类,该类是线程安全的,因为他的大部分方法都用 lock 关键字进行了同 步处理。如 HashTable 的 Synchronized 返回一个新的线程安全的 HashTable 实例,代码如下:
    //在多线程环境中只要我们用下面的方式实例化 HashTable 就可以了 Hashtable ht = Hashtable.Synchronized(new Hashtable());
    //以下代码是.NET Framework Class Library 实现,增加对 Synchronized 的认识 [HostProtection(SecurityAction.LinkDemand, Synchronization=true)] public static Hashtable Synchronized(Hashtable table) { if (table == null) { throw new ArgumentNullException("table"); } return new SyncHashtable(table); }
    //SyncHashtable 的几个常用方法,我们可以看到内部实现都加了 lock 关键字保证线程安 全 public override void Add(object key, object value) { lock (this._table.SyncRoot) { this._table.Add(key, value); } }
    public override void Clear() { lock (this._table.SyncRoot) { this._table.Clear(); } }
    public override void Remove(object key) { lock (this._table.SyncRoot) { this._table.Remove(key); } }
    线程同步是一个非常复杂的话题, 这里只是根据公司的一个项目把相关的知识整理出来, 作为工 作的一种总结。这些同步方法的使用场景是怎样的?究竟有哪些细微的差别?还有待于进一步的学习 和实践。

1
View Code

WinFormSilverlight多线程编程中如何更新UI控件的值

单线程的winfom程序中,设置一个控件的值是很easy的事情,直接 this.TextBox1.value = "Hello World!";就搞定了,但是如果在一个新线程中这么做,比如:
private void btnSet_Click(object sender, EventArgs e)
{    
    Thread t = new Thread(new ParameterizedThreadStart(SetTextBoxValue));
    //当然也可以用匿名委托写成Thread t = new Thread(SetTextBoxValue);
    t.Start("Hello World");
}


void SetTextBoxValue(object obj) 
{
    this.textBox1.Text = obj.ToString();
}
 运行时,会报出一个无情的错误:
线程间操作无效: 从不是创建控件“textBox1”的线程访问它。

究其原因,winform中的UI控件不是线程安全的,如果可以随意在任何线程中改变其值,你创建一个线程,我创建一个线程,大家都来抢着更改"TextBox1"的值,没有任何秩序的话,天下大乱...

解决办法:
1.掩耳盗铃法(Control.CheckForIllegalCrossThreadCalls = false;)--仅Winform有效
using System;
using System.Threading;
using System.Windows.Forms;

namespace ThreadTest
{
    public partial class Form1 : Form
    {        

        public Form1()
        {
            InitializeComponent();
            Control.CheckForIllegalCrossThreadCalls = false;//这一行是关键      
        }
       

        private void btnSet_Click(object sender, EventArgs e)
        {           
            Thread t = new Thread(new ParameterizedThreadStart(SetTextBoxValue));
            t.Start("Hello World");
        }


        void SetTextBoxValue(object obj) 
        {
            this.textBox1.Text = obj.ToString();
        }        
    }
}
设置Control.CheckForIllegalCrossThreadCalls为false,相当于不检测线程之间的冲突,允许各路线程随便乱搞,当然最终TextBox1的值到底是啥难以预料,只有天知道,不过这也是最省力的办法
2.利用委托调用--最常见的办法(仅WinForm有效)
 
using System;
using System.Threading;
using System.Windows.Forms;

namespace ThreadTest
{
    public partial class Form1 : Form
    {
        delegate void D(object obj);

        public Form1()
        {
            InitializeComponent();            
        }
       

        private void btnSet_Click(object sender, EventArgs e)
        {           
            Thread t = new Thread(new ParameterizedThreadStart(SetTextBoxValue));
            t.Start("Hello World");
        }


        void SetTextBoxValue(object obj) 
        {
            if (textBox1.InvokeRequired)
            {
                D d = new D(DelegateSetValue);
                textBox1.Invoke(d,obj);

            }
            else 
            {
                this.textBox1.Text = obj.ToString();
            }
        }


        void DelegateSetValue(object obj) 
        {
            this.textBox1.Text = obj.ToString();
        }
    }
}
3.利用SynchronizationContext上下文 -- 最神秘的方法(Winform/Silverlight能用)
之所以说它神秘,是因为msdn官方对它的解释据说也是不清不楚 
 
using System;
using System.Threading;
using System.Windows.Forms;

namespace ThreadTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();            
        }       

        private void btnSet_Click(object sender, EventArgs e)
        {
            Thread t = new Thread(new ParameterizedThreadStart(Run));
            MyPram _p = new MyPram() { context = SynchronizationContext.Current, parm = "Hello World" };
            t.Start(_p);
        }

        void Run(object obj) 
        {
            MyPram p = obj as MyPram;
            p.context.Post(SetTextValue, p.parm);
        }


        void SetTextValue(object obj) 
        {
            this.textBox1.Text = obj.ToString();
        }
    }


    public class MyPram 
    {
        public SynchronizationContext context { set; get; }
        public object parm { set; get; }
    }
}
4.利用BackgroundWorker --最偷懒的办法(Winform/Silverlight通用)
BackgroundWorker会在主线程之外,另开一个后台线程,我们可以把一些处理放在后台线程中处理,完成之后,后台线程会把结果传递给主线程,同时结束自己。
using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace ThreadTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();            
        }       

        private void btnSet_Click(object sender, EventArgs e)
        {
            //MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString());
            using (BackgroundWorker bw = new BackgroundWorker())
            {
                bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
                bw.DoWork += new DoWorkEventHandler(bw_DoWork);
                bw.RunWorkerAsync("Hello World");
            }
        }

        void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            //MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString());
            e.Result = e.Argument;//这里只是简单的把参数当做结果返回,当然您也可以在这里做复杂的处理后,再返回自己想要的结果(这里的操作是在另一个线程上完成的)
        }

        void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            //这时后台线程已经完成,并返回了主线程,所以可以直接使用UI控件了
            this.textBox1.Text = e.Result.ToString();
            //MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString());
        }       
    }    
}
5.Dispatcher.BeginInvoke--Silverlight的独门秘籍 

代码
using System.Threading;
using System.Windows.Controls;
using System.Windows.Input;

namespace ThreadTest
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
        }

        private void LayoutRoot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Thread t = new Thread(SetTextValue);
            t.Start("Hello World");
        }

        void SetTextValue(object text) 
        {
            this.Dispatcher.BeginInvoke(() => { this.txt.Text = text.ToString(); });            
        }
    }
}
 
转载请注明来自菩提树下的杨过http://www.cnblogs.com/yjmyzz/archive/2009/11/25/1610253.html
 
View Code

c# 多线程下载文件-冰火战地

类代码:
using System.Threading;
using System.IO;
using System.Web;
using System.ComponentModel;
using System.Net;
using System.Text.RegularExpressions;
using System.Collections;
    /// <summary>
    /// 多线程下载类
    /// </summary>
    public class MultiDownload
    {
        #region 变量
        private int _threadNum;             //线程数量
        private long _fileSize;             //文件大小
        private string _extName;            //文件扩展名
        private string _fileUrl;            //文件地址
        private string _fileName;           //文件名
        private string _savePath;           //保存路径
        private short _threadCompleteNum;   //线程完成数量
        private bool _isComplete;           //是否完成
        private volatile int _downloadSize; //当前下载大小(实时的)
        private Thread[] _thread;           //线程数组
        private List<string> _tempFiles = new List<string>();
        private object locker = new object();
        #endregion
        #region 属性
        /// <summary>
        /// 文件名
        /// </summary>
        public string FileName
        {
            get
            {
                return _fileName;
            }
            set
            {
                _fileName = value;
            }
        }
        /// <summary>
        /// 文件大小
        /// </summary>
        public long FileSize
        {
            get
            {
                return _fileSize;
            }
        }
        /// <summary>
        /// 当前下载大小(实时的)
        /// </summary>
        public int DownloadSize
        {
            get
            {
                return _downloadSize;
            }
        }
        /// <summary>
        /// 是否完成
        /// </summary>
        public bool IsComplete
        {
            get
            {
                return _isComplete;
            }
        }
        /// <summary>
        /// 线程数量
        /// </summary>
        public int ThreadNum
        {
            get
            {
                return _threadNum;
            }
        }
        /// <summary>
        /// 保存路径
        /// </summary>
        public string SavePath
        {
            get
            {
                return _savePath;
            }
            set
            {
                _savePath = value;
            }
        }
        #endregion
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="threahNum">线程数量</param>
        /// <param name="fileUrl">文件Url路径</param>
        /// <param name="savePath">本地保存路径</param>
        public MultiDownload(int threahNum, string fileUrl, string savePath)
        {
            this._threadNum = threahNum;
            this._thread = new Thread[threahNum];
            this._fileUrl = fileUrl;
            this._savePath = savePath;
        }
        public void Start()
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_fileUrl);
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            _extName = response.ResponseUri.ToString().Substring(response.ResponseUri.ToString().LastIndexOf('.'));//获取真实扩展名
            _fileSize = response.ContentLength;
            int singelNum = (int)(_fileSize / _threadNum);      //平均分配
            int remainder = (int)(_fileSize % _threadNum);      //获取剩余的
            request.Abort();
            response.Close();
            for (int i = 0; i < _threadNum; i++)
            {
                List<int> range = new List<int>();
                range.Add(i * singelNum);
                if (remainder != 0 && (_threadNum - 1) == i)    //剩余的交给最后一个线程
                    range.Add(i * singelNum + singelNum + remainder - 1);
                else
                    range.Add(i * singelNum + singelNum - 1);
                //_thread[i] = new Thread(new ThreadStart(Download(range[0], range[1])));
                from = range[0]; to = range[1];//下载指定位置的数据
                _thread[i] = new Thread(new ThreadStart(Download));
                _thread[i].Name = "jhxz_{0}".Replace("{0}", Convert.ToString(i + 1));
                _thread[i].Start();
            }
        }
        public int from, to;
        #region 下载网页
        private void Download()
        {
            Stream httpFileStream = null, localFileStram = null;
            try
            {
                //string tmpFileBlock = @"{0}\{1}_{2}.dat".Formats(_savePath, _fileName, Thread.CurrentThread.Name);
                string tmpFileBlock = @_savePath + "\\" + _fileName + "_" + Thread.CurrentThread.Name + ".dat";
                _tempFiles.Add(tmpFileBlock);
                HttpWebRequest httprequest = (HttpWebRequest)WebRequest.Create(_fileUrl);
                httprequest.AddRange(from, to);
                HttpWebResponse httpresponse = (HttpWebResponse)httprequest.GetResponse();
                httpFileStream = httpresponse.GetResponseStream();
                localFileStram = new FileStream(tmpFileBlock, FileMode.Create);
                byte[] by = new byte[5000];
                int getByteSize = httpFileStream.Read(by, 0, (int)by.Length); //Read方法将返回读入by变量中的总字节数
                while (getByteSize > 0)
                {
                    Thread.Sleep(20);
                    lock (locker) _downloadSize += getByteSize;
                    localFileStram.Write(by, 0, getByteSize);
                    getByteSize = httpFileStream.Read(by, 0, (int)by.Length);
                }
                lock (locker) _threadCompleteNum++;
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message.ToString());
            }
            finally
            {
                if (httpFileStream != null) httpFileStream.Dispose();
                if (localFileStram != null) localFileStram.Dispose();
            }
            if (_threadCompleteNum == _threadNum)
            {
                Complete();
                _isComplete = true;
            }
        }
        #endregion
        /// <summary>
        /// 下载完成后合并文件块
        /// </summary>
        private void Complete()
        {
            //Stream mergeFile = new FileStream(@"{0}\{1}{2}".Formats(_savePath, _fileName, _extName), FileMode.Create);
            Stream mergeFile = new FileStream(@_savePath + "\\" + _fileName + _extName, FileMode.Create);
            BinaryWriter AddWriter = new BinaryWriter(mergeFile);
            foreach (string file in _tempFiles)
            {
                using (FileStream fs = new FileStream(file, FileMode.Open))
                {
                    BinaryReader TempReader = new BinaryReader(fs);
                    AddWriter.Write(TempReader.ReadBytes((int)fs.Length));
                    TempReader.Close();
                }
                File.Delete(file);
            }
            AddWriter.Close();
        }
    }
前台使用 MultiDownload dow = new MultiDownload(10, "http://www.xxxxxxx.com",Application.StartupPath);
            dow.FileName = "boyuan";
View Code

用C#实现HTTP协议下的多线程文件传输

多人都有过使用网络蚂蚁或网络快车软件下载互联网文件的经历,这些软件的使用可以大大加速互联网上文件的传输速度,减少文件传输的时间。这些软件为什么有如此大的魔力呢?其主要原因是这些软件都采用了多线程下载和断点续传技术。如果我们自己来编写一个类似这样的程序,也能够快速的在互联网上下载文件,那一定是非常愉快的事情。下面我就讲一讲如何利用C#语言编写一个支持多线程下载文件的程序,你会看到利用C#语言编写网络应程序是多么的容易,从中也能体会到C#语言中强大的网络功能。 

  首先介绍一下HTTP协议,HTTP亦即Hpyer Text Transfer Protocal的缩写,它是现代互联网上最重要的一种网络协议,超文本传输协议位于TCP/IP协议的应用层,是一个面向无连接、简单、快速的C/S结构的协议。HTTP的工作过程大体上分连接、请求、响应和断开连接四个步骤。C#语言对HTTP协议提供了良好的支持,在.NET类库中提供了WebRequest和WebResponse类,这两个类都包含在System.Net命名空间中,利用这两个类可以实现很多高级的网络功能,本文中多线程文件下载就是利用这两个类实现的。 WebRequest和WebResponse都是抽象基类,因此在程序中不能直接作为对象使用,必须被继承,实际使用中,可根据URI参数中的URI前缀选用它们合适的子类,对于HTTP这类URI,HttpWebRequest和HttpWebResponse类可以用于处理客户程序同WEB服务器之间的HTTP通讯。 

  HttpWebRequest类实现了很多通过HTTP访问WEB服务器上文件的高级功能。HttpWebRequest类对WebRequest中定义的属性和方法提供支持,HttpWebRequest将发送到Internet资源的公共HTTP标头的值公开为属性,由方法或系统设置,常用的由属性或方法设置的HTTP标头为:接受, 由Accept属性设置, 连接, 由Connection属性和KeepAlive属性设置, Content-Length, 由ContentLength属性设置, Content-Type, 由ContentType属性设置, 范围, 由AddRange方法设置. 实际使用中是将标头信息正确设置后,传递到WEB服务器,WEB服务器根据要求作出回应。 

  HttpWebResponse类继承自WebResponse类,专门处理从WEB服务器返回的HTTP响应,这个类实现了很多方法,具有很多属性,可以全面处理接收到的互联网信息。在HttpWebResponse类中,对于大多数通用的HTTP标头字段,都有独立的属性与其对应,程序员可以通过这些属性方便的访问位于HTTP接收报文标头字段中的信息,本例中用到的HttpWebResponse类属性为:ContentLength 既接收内容的长度。 

  有了以上的了解后,下面看看这两个类的用法,要创建HttpWebRequest对象,不要直接使用HttpWebRequest的构造函数,而要使用WebRequest.Create方法初始化一个HttpWebRequest实例,如:  

HttpWebRequest hwr=(HttpWebRequest)WebRequest.Create(http://www.163.com/);   

  创建了这个对象后,就可以通过HttpWebRequest属性,设置很多HTTP标头字段的内容,如hwr.AddRange(100,1000);设置接收对象的范围为100-1000字节。 

  HttpWebReques对象使用GetResponse()方法时,会返回一个HttpWebResponse对象,为提出HTTP返回报文信息,需要使用HttpWebResponse的GetResponseStream()方法,该方法返回一个Stream对象,可以读取HTTP返回的报文,如:首先定义一个Strean 对象 public System.IO.Stream ns; 然后 ns=hwr.GetResponse ().GetResponseStream ();即可创建Stream对象。有了以上的准备知识后下面开始设计我们的多线程互联网文件的下载程序,首先打开Visual Studio.Net集成开发环境,选择“文件”、“新建”、“项目”,然后选择“Visual C#项目”,在向导右边列表框中选中“Windows应用程序”,输入项目名称,如本例为:httpftp,然后选择“确定”按钮,向导自动生成了一个Windows应用程序项目。首先打开窗口设计器设计应用程序窗口,增加如下控件: 

  一个列表框 listBox1 三个文本标签 label1-label3 三个文本框 textBox1-textBox3 一个开始接收按钮 button1 设计好的窗口如下图: 

 
控件定义代码是:  

public System.Windows.Forms.ListBox listBox1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox textBox1
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.TextBox textBox2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.TextBox textBox3;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.TextBox textBox4; 

  打开Form1的代码编辑器,增加如下的命名空间: 

using System.Net;//网络功能
using System.IO;//流支持
using System.Threading;//线程支持

  增加如下的程序变量: 

public bool[] threadw; //每个线程结束标志
public string[] filenamew;//每个线程接收文件的文件名
public int[] filestartw;//每个线程接收文件的起始位置
public int[] filesizew;//每个线程接收文件的大小
public string strurl;//接受文件的URL
public bool hb;//文件合并标志
public int thread;//进程数

定义一个HttpFile类,用于管理接收线程,其代码如下:  

public class HttpFile
{
    public Form1 formm;
    public int threadh;//线程代号
    public string filename;//文件名
    public string strUrl;//接收文件的URL
    public FileStream fs;
    public HttpWebRequest request;
    public System.IO.Stream ns;
    public byte[] nbytes;//接收缓冲区
    public int nreadsize;//接收字节数
    public HttpFile(Form1 form, int thread)//构造方法
    {
        formm = form;
        threadh = thread;
    }
    ~HttpFile()//析构方法
    {
        formm.Dispose();
    }
    public void receive()//接收线程
    {
        filename = formm.filenamew[threadh];
        strUrl = formm.strurl;
        ns = null;
        nbytes = new byte[512];
        nreadsize = 0;
        formm.listBox1.Items.Add("线程" + threadh.ToString() + "开始接收");
        fs = new FileStream(filename, System.IO.FileMode.Create);
        try
        {
            request = (HttpWebRequest)HttpWebRequest.Create(strUrl);
            //接收的起始位置及接收的长度
            request.AddRange(formm.filestartw[threadh],
            formm.filestartw[threadh] + formm.filesizew[threadh]);
            ns = request.GetResponse().GetResponseStream();//获得接收流
            nreadsize = ns.Read(nbytes, 0, 512);
            while (nreadsize > 0)
            {
                fs.Write(nbytes, 0, nreadsize);
                nreadsize = ns.Read(nbytes, 0, 512);
                formm.listBox1.Items.Add("线程" + threadh.ToString() + "正在接收");
            }
            fs.Close();
            ns.Close();
        }
        catch (Exception er)
        {
            MessageBox.Show(er.Message);
            fs.Close();
        }
        formm.listBox1.Items.Add("进程" + threadh.ToString() + "接收完毕!");
        formm.threadw[threadh] = true;
    }
}
 

  该类和Form1类处于统一命名空间,但不包含在Form1类中。下面定义“开始接收”按钮控件的事件响应函数:  

private void button1_Click(object sender, System.EventArgs e)
{
 DateTime dt=DateTime.Now;//开始接收时间
 textBox1.Text =dt.ToString ();
 strurl=textBox2.Text .Trim ().ToString ();
 HttpWebRequest request;
 long filesize=0;
 try
 {
  request=(HttpWebRequest)HttpWebRequest.Create (strurl);
  filesize=request.GetResponse ().ContentLength;//取得目标文件的长度
  request.Abort ();
 }
 catch (Exception er)
 {
  MessageBox.Show (er.Message );
 }
 // 接收线程数
 thread=Convert.ToInt32 (textBox4.Text .Trim().ToString (),10);
 //根据线程数初始化数组
 threadw=new bool [thread];
 filenamew=new string [thread];
 filestartw=new int [thread];
 filesizew=new int[thread];
 
 //计算每个线程应该接收文件的大小
 int filethread=(int)filesize/thread;//平均分配
 int filethreade=filethread+(int)filesize%thread;//剩余部分由最后一个线程完成
 //为数组赋值
 for (int i=0;i {
  threadw[i]=false;//每个线程状态的初始值为假
  filenamew[i]=i.ToString ()+".dat";//每个线程接收文件的临时文件名
  if (i  {
   filestartw[i]=filethread*i;//每个线程接收文件的起始点
   filesizew[i]=filethread-1;//每个线程接收文件的长度
  }
  else
  {
   filestartw[i]=filethread*i;
   filesizew[i]=filethreade-1;
  }
 }
 //定义线程数组,启动接收线程
 Thread[] threadk=new Thread [thread];
 HttpFile[] httpfile=new HttpFile [thread];
 for (int j=0;j {
  httpfile[j]=new HttpFile(this,j);
  threadk[j]=new Thread(new ThreadStart (httpfile[j].receive ));
  threadk[j].Start ();
 }
 //启动合并各线程接收的文件线程
 Thread hbth=new Thread (new ThreadStart (hbfile));
 hbth.Start ();
}
 
  

  合并文件的线程hbfile定义在Form1类中,定义如下:  

public void hbfile()
{
 while (true)//等待
 {
  hb=true;
  for (int i=0;i  {
   if (threadw[i]==false)//有未结束线程,等待
   {
    hb=false;
    Thread.Sleep (100);
    break;
   }
  }
  if (hb==true)//所有线程均已结束,停止等待,
  {
   break;
  }
 }
 FileStream fs;//开始合并
 FileStream fstemp;
 int readfile;
 byte[] bytes=new byte[512];
 fs=new FileStream (textBox3.Text .Trim ().ToString (),System.IO.FileMode.Create);
 for (int k=0;k {
  fstemp=new FileStream (filenamew[k],System.IO.FileMode .Open);
  while (true)
  {
   readfile=fstemp.Read (bytes,0,512);
   if (readfile>0)
   {
    fs.Write (bytes,0,readfile);
   }
   else
   {
    break;
   }
  }
  fstemp.Close ();
 }
 fs.Close ();
 DateTime dt=DateTime.Now;
 textBox1.Text =dt.ToString ();//结束时间
 MessageBox.Show ("接收完毕!!!");
}  
 

  至此,一个多线程下载文件的程序就大功告成了,注意在输入本地文件名时,应按如下格式输入:“c:\\test\\httpftp\\bin\\d.htm”,因”\”后的字符在C#中是转义字符,线程数并非越大越好,一般5个线程就可以了,该程序在Visual Studio.Net 2002开发环境及Windows xp 操作系统上通过。
View Code

编写断点续传和多线程下载模块

概述
    在当今的网络时代,下载软件是使用最为频繁的软件之一。几年来,下载技术也在不停地发展。最原始的下载功能仅仅是个“下载”过程,即从WEB服务器上连续地读取文件。其最大的问题是,由于网络的不稳定性,一旦连接断开使得下载过程中断,就不得不全部从头再来一次。
    随后,“断点续传”的概念就出来了,顾名思义,就是如果下载中断,在重新建立连接后,跳过已经下载的部分,而只下载还没有下载的部分。
无论“多线程下载”技术是否洪以容先生的发明,洪以容使得这项技术得到前所未有的关注是不争的事实。在“网络蚂蚁”软件流行开后,许多下载软件也都纷纷效仿,是否具?quot;多线程下载"技术、甚至能支持多少个下载线程都成了人们评测下载软件的要素。"多线程下载"的基础是WEB服务器支持远程的随机读取,也即支持"断点续传"。这样,在下载时可以把文件分成若干部分,每一部分创建一个下载线程进行下载。
    现在,不要说编写专门的下载软件,在自己编写的软件中,加入下载功能有时也非常必要。如让自己的软件支持自动在线升级,或者在软件中自动下载新的数据进行数据更新,这都是很有用、而且很实用的功能。本文的主题即怎样编写一个支持"断点续传""多线程"的下载模块。当然,下载的过程非常复杂,在一篇文章中难以全部阐明,所以,与下载过程关系不直接的部分基本上都忽略了,如异常处理和网络错误处理等,敬请各位读者注意。我使用的开发环境是C++ Builder 5.0,使用其他开发环境或者编程语言的朋友请自行作适当修改。
HTTP协议简介
    下载文件是电脑与WEB服务器交互的过程,它们交互的"语言"的专业名称是协议。传送文件的协议有多种,最常用的是HTTP(超文本传输协议)和FTP(文件传送协议),我采用的是HTTP。
    HTTP协议最基本的命令只有三条:Get、Post和Head。Get从WEB服务器请求一个特定的对象,比如HTML页面或者一个文件,WEB服务器通过一个Socket连接发送此对象作为响应;Head命令使服务器给出此对象的基本描述,比如对象的类型、大小和更新时间。Post命令用于向WEB服务器发送数据,通常使把信息发送给一个单独的应用程序,经处理生成动态的结果返回给浏览器。下载即是通过Get命令实现。
基本的下载过程
    编写下载程序,可以直接使用Socket函数,但是这要求开发人员理解、熟悉TCP/IP协议。为了简化Internet客户端软件的开发,Windows提供了一套WinInet API,对常用的网络协议进行了封装,把开发Internet软件的门槛大大降低了。我们需要使用的WinInet API函数如图1所示,调用顺序基本上是从上到下,其具体的函数原型请参考MSDN。
 
图1
    在使用这些函数时,必须严格区分它们使用的句柄。这些句柄的类型是一样的,都是HINTERNET,但是作用不同,这一点非常让人迷惑。按照这些句柄的产生顺序和调用关系,可以分为三个级别,下一级的句柄由上一级的句柄得到。
    InternetOpen是最先调用的函数,它返回的HINTERNET句柄级别最高,我习惯定义为hSession,即会话句柄。
    InternetConnect使用hSession句柄,返回的是http连接句柄,我把它定义为hConnect。
    HttpOpenRequest使用hConnect句柄,返回的句柄是http请求句柄,定义为hRequest。
    HttpSendRequest、HttpQueryInfo、InternetSetFilePointer和InternetReadFile都使用HttpOpenRequest返回的句柄,即hRequest。
    当这几个句柄不再使用是,应该用函数InternetCloseHandle把它关闭,以释放其占用的资源。
    首先建立一个名为THttpGetThread、创建后自动挂起的线程模块,我希望线程在完成后自动销毁,所以在构造函数中设置:
FreeOnTerminate = True; // 自动删除
    并增加以下成员变量:
char Buffer[HTTPGET_BUFFER_MAX+4]; // 数据缓冲区
AnsiString FURL; // 下载对象的URL
AnsiString FOutFileName; // 保存的路径和名称
HINTERNET FhSession; // 会话句柄
HINTERNET FhConnect; // http连接句柄
HINTERNET FhRequest; // http请求句柄
bool FSuccess; // 下载是否成功
int iFileHandle; // 输出文件的句柄
1、建立连接
    按照功能划分,下载过程可以分为4部分,即建立连接、读取待下载文件的信息并分析、下载文件和释放占用的资源。建立连接的函数如下,其中ParseURL的作用是从下载URL地址中取得主机名称和下载的文件的WEB路径,DoOnStatusText用于输出当前的状态:
//初始化下载环境
void THttpGetThread::StartHttpGet(void)
{
   AnsiString HostName,FileName;
   ParseURL(HostName, FileName); 
   try
   {
      // 1.建立会话
      FhSession = InternetOpen("http-get-demo",
            INTERNET_OPEN_TYPE_PRECONFIG,
            NULL,NULL,
            0); // 同步方式
      if( FhSession==NULL)throw(Exception("Error:InterOpen"));
      DoOnStatusText("ok:InterOpen");
      // 2.建立连接
      FhConnect=InternetConnect(FhSession,
            HostName.c_str(),
            INTERNET_DEFAULT_HTTP_PORT,
            NULL,NULL,
            INTERNET_SERVICE_HTTP, 0, 0);
      if(FhConnect==NULL)throw(Exception("Error:InternetConnect"));
      DoOnStatusText("ok:InternetConnect");
      // 3.初始化下载请求
      const char *FAcceptTypes = "*/*";
      FhRequest = HttpOpenRequest(FhConnect,
            "GET", // 从服务器获取数据
            FileName.c_str(), // 想读取的文件的名称
            "HTTP/1.1", // 使用的协议
            NULL,
            &FAcceptTypes,
            INTERNET_FLAG_RELOAD,
            0);
      if( FhRequest==NULL)throw(Exception("Error:HttpOpenRequest"));
      DoOnStatusText("ok:HttpOpenRequest");
      // 4.发送下载请求
      HttpSendRequest(FhRequest, NULL, 0, NULL, 0);
      DoOnStatusText("ok:HttpSendRequest");
   }catch(Exception &exception)
   {
      EndHttpGet(); // 关闭连接,释放资源
      DoOnStatusText(exception.Message);
   }
}
// 从URL中提取主机名称和下载文件路径
void THttpGetThread::ParseURL(AnsiString &HostName,AnsiString &FileName)
{
   AnsiString URL=FURL;
   int i=URL.Pos("http://");
   if(i>0)
   {
      URL.Delete(1, 7);
   }
   i=URL.Pos("/");
   HostName = URL.SubString(1, i-1);
   FileName = URL.SubString(i, URL.Length());
}
    可以看到,程序按照图1中的顺序,依次调用InternetOpen、InternetConnect、HttpOpenRequest函数得到3个相关的句柄,然后通过HttpSendRequest函数把下载的请求发送给WEB服务器。
    InternetOpen的第一个参数是无关的,最后一个参数如果设置为INTERNET_FLAG_ASYNC,则将建立异步连接,这很有实际意义,考虑到本文的复杂程度,我没有采用。但是对于需要更高下载要求的读者,强烈建议采用异步方式。
    HttpOpenRequest打开一个请求句柄,命令是"GET",表示下载文件,使用的协议是"HTTP/1.1"。
    另外一个需要注意的地方是HttpOpenRequest的参数FAcceptTypes,表示可以打开的文件类型,我设置为"*/*"表示可以打开所有文件类型,可以根据实际需要改变它的值。
2、读取待下载的文件的信息并分析
    在发送请求后,可以使用HttpQueryInfo函数获取文件的有关信息,或者取得服务器的信息以及服务器支持的相关操作。对于下载程序,最常用的是传递HTTP_QUERY_CONTENT_LENGTH参数取得文件的大小,即文件包含的字节数。模块如下所示:
// 取得待下载文件的大小
int __fastcall THttpGetThread::GetWEBFileSize(void)
{
   try
   {
      DWORD BufLen=HTTPGET_BUFFER_MAX;
            DWORD dwIndex=0;
            bool RetQueryInfo=HttpQueryInfo(FhRequest,
            HTTP_QUERY_CONTENT_LENGTH,
            Buffer, &BufLen,
            &dwIndex);
      if( RetQueryInfo==false) throw(Exception("Error:HttpQueryInfo"));
      DoOnStatusText("ok:HttpQueryInfo");
      int FileSize=StrToInt(Buffer); // 文件大小
      DoOnGetFileSize(FileSize);
   }catch(Exception &exception)
   {
      DoOnStatusText(exception.Message);
   }
   return FileSize;
}
    模块中的DoOnGetFileSize是发出取得文件大小的事件。取得文件大小后,对于采用多线程的下载程序,可以按照这个值进行合适的文件分块,确定每个文件块的起点和大小。
3、下载文件的模块
    开始下载前,还应该先安排好怎样保存下载结果。方法很多,我直接采用了C++ Builder提供的文件函数打开一个文件句柄。当然,也可以采用Windows本身的API,对于小文件,全部缓冲到内存中也可以考虑。
// 打开输出文件,以保存下载的数据
DWORD THttpGetThread::OpenOutFile(void)
{
   try
   {
   if(FileExists(FOutFileName))
      DeleteFile(FOutFileName);
   iFileHandle=FileCreate(FOutFileName);
   if(iFileHandle==-1) throw(Exception("Error:FileCreate"));
   DoOnStatusText("ok:CreateFile");
   }catch(Exception &exception)
   {
      DoOnStatusText(exception.Message);
   }
   return 0;
}
// 执行下载过程
void THttpGetThread::DoHttpGet(void)
{
   DWORD dwCount=OpenOutFile();
   try
   {
      // 发出开始下载事件
      DoOnStatusText("StartGet:InternetReadFile");
      // 读取数据
      DWORD dwRequest; // 请求下载的字节数
      DWORD dwRead; // 实际读出的字节数
      dwRequest=HTTPGET_BUFFER_MAX;
      while(true)
      {
         Application->ProcessMessages();
         bool ReadReturn = InternetReadFile(FhRequest,
              (LPVOID)Buffer,
              dwRequest,
              &dwRead);
         if(!ReadReturn)break;
         if(dwRead==0)break;
         // 保存数据
         Buffer[dwRead]=\0;
         FileWrite(iFileHandle, Buffer, dwRead);
         dwCount = dwCount + dwRead;
         // 发出下载进程事件
         DoOnProgress(dwCount);
      }
      Fsuccess=true;
   }catch(Exception &exception)
   {
      Fsuccess=false;
      DoOnStatusText(exception.Message);
   }
   FileClose(iFileHandle); 
   DoOnStatusText("End:InternetReadFile");
}
    下载过程并不复杂,与读取本地文件一样,执行一个简单的循环。当然,如此方便的编程还是得益于微软对网络协议的封装。
4、释放占用的资源
    这个过程很简单,按照产生各个句柄的相反的顺序调用InternetCloseHandle函数即可。
void THttpGetThread::EndHttpGet(void)
{
   if(FConnected)
   {
      DoOnStatusText("Closing:InternetConnect");
      try
      {
         InternetCloseHandle(FhRequest);
         InternetCloseHandle(FhConnect);
         InternetCloseHandle(FhSession);
      }catch(...){}
      FhSession=NULL;
      FhConnect=NULL;
      FhRequest=NULL;
      FConnected=false;
      DoOnStatusText("Closed:InternetConnect");
   }
}
    我觉得,在释放句柄后,把变量设置为NULL是一种良好的编程习惯。在这个示例中,还出于如果下载失败,重新进行下载时需要再次利用这些句柄变量的考虑。
5、功能模块的调用
    这些模块的调用可以安排在线程对象的Execute方法中,如下所示:
void __fastcall THttpGetThread::Execute()
{
   FrepeatCount=5;
   for(int i=0;i<FRepeatCount;i++)
   {
      StartHttpGet();
      GetWEBFileSize();
      DoHttpGet();
      EndHttpGet();
      if(FSuccess)break;
   }
   // 发出下载完成事件
   if(FSuccess)DoOnComplete();
   else DoOnError();
}
    这里执行了一个循环,即如果产生了错误自动重新进行下载,实际编程中,重复次数可以作为参数自行设置。
实现断点续传功能
    在基本下载的代码上实现断点续传功能并不是很复杂,主要的问题有两点:
1、 检查本地的下载信息,确定已经下载的字节数。所以应该对打开输出文件的函数作适当修改。我们可以建立一个辅助文件保存下载的信息,如已经下载的字节数等。我处理得较为简单,先检查输出文件是否存在,如果存在,再得到其大小,并以此作为已经下载的部分。由于Windows没有直接取得文件大小的API,我编写了GetFileSize函数用于取得文件大小。注意,与前面相同的代码被省略了。
DWORD THttpGetThread::OpenOutFile(void)
{
   ……
   if(FileExists(FOutFileName))
   {
      DWORD dwCount=GetFileSize(FOutFileName);
      if(dwCount>0)
      {
         iFileHandle=FileOpen(FOutFileName,fmOpenWrite);
         FileSeek(iFileHandle,0,2); // 移动文件指针到末尾
         if(iFileHandle==-1) throw(Exception("Error:FileCreate"));
         DoOnStatusText("ok:OpenFile");
         return dwCount;
      }
      DeleteFile(FOutFileName);
   }
   ……
}
2、 在开始下载文件(即执行InternetReadFile函数)之前,先调整WEB上的文件指针。这就要求WEB服务器支持随机读取文件的操作,有些服务器对此作了限制,所以应该判断这种可能性。对DoHttpGet模块的修改如下,同样省略了相同的代码:
void THttpGetThread::DoHttpGet(void)
{
   DWORD dwCount=OpenOutFile();
   if(dwCount>0) // 调整文件指针
   {
      dwStart = dwStart + dwCount;
      if(!SetFilePointer()) // 服务器不支持操作
      {
         // 清除输出文件
         FileSeek(iFileHandle,0,0); // 移动文件指针到头部
      }
   }
   ……
}多线程下载
    要实现多线程下载,最主要的问题是下载线程的创建和管理,已经下载完成后文件的各个部分的准确合并,同时,下载线程也要作必要的修改。
1、下载线程的修改
    为了适应多线程程序,我在下载线程加入如下成员变量:
int FIndex; // 在线程数组中的索引
DWORD dwStart; // 下载开始的位置
DWORD dwTotal; // 需要下载的字节数
DWORD FGetBytes; // 下载的总字节数
    并加入如下属性值:
__property AnsiString URL = { read=FURL, write=FURL };
__property AnsiString OutFileName = { read=FOutFileName, write=FOutFileName};
__property bool Successed = { read=FSuccess};
__property int Index = { read=FIndex, write=FIndex};
__property DWORD StartPostion = { read=dwStart, write=dwStart};
__property DWORD GetBytes = { read=dwTotal, write=dwTotal};
__property TOnHttpCompelete OnComplete = { read=FOnComplete, write=FOnComplete };
    同时,在下载过程DoHttpGet中增加如下处理,
void THttpGetThread::DoHttpGet(void)
{
   ……
   try
   {
      ……
      while(true)
      {
         Application->ProcessMessages();
         // 修正需要下载的字节数,使得dwRequest + dwCount <dwTotal;
         if(dwTotal>0) // dwTotal=0表示下载到文件结束
         {
            if(dwRequest+dwCount>dwTotal)
            dwRequest=dwTotal-dwCount;
         }
         ……
         if(dwTotal>0) // dwTotal <=0表示下载到文件结束
         {
            if(dwCount>=dwTotal)break;
         }
      }
   }
   ……
   if(dwCount==dwTotal)FSuccess=true;
}
2、建立多线程下载组件
    我先建立了以TComponent为基类、名为THttpGetEx的组件模块,并增加以下成员变量:
// 内部变量
THttpGetThread **HttpThreads; // 保存建立的线程
AnsiString *OutTmpFiles; // 保存结果文件各个部分的临时文件
bool *FSuccesss; // 保存各个线程的下载结果
// 以下是属性变量
int FHttpThreadCount; // 使用的线程个数
AnsiString FURL;
AnsiString FOutFileName;
    各个变量的用途都如代码注释,其中的FSuccess的作用比较特别,下文会再加以详细解释。因为线程的运行具有不可逆性,而组件可能会连续地下载不同的文件,所以下载线程只能动态创建,使用后随即销毁。创建线程的模块如下,其中GetSystemTemp函数取得系统的临时文件夹,OnThreadComplete是线程下载完成后的事件,其代码在其后介绍:
// 分配资源
void THttpGetEx::AssignResource(void)
{
   FSuccesss=new bool[FHttpThreadCount];
   for(int i=0;i<FHttpThreadCount;i++)
      FSuccesss[i]=false;
   OutTmpFiles = new AnsiString[FHttpThreadCount];
   AnsiString ShortName=ExtractFileName(FOutFileName);
   AnsiString Path=GetSystemTemp();
   for(int i=0;i<FHttpThreadCount;i++)
      OutTmpFiles[i]=Path+ShortName+"-"+IntToStr(i)+".hpt";
   HttpThreads = new THttpGetThread *[FHttpThreadCount];
}
// 创建一个下载线程
THttpGetThread * THttpGetEx::CreateHttpThread(void)
{
   THttpGetThread *HttpThread=new THttpGetThread(this);
   HttpThread->URL=FURL;
   …… // 初始化事件
   HttpThread->OnComplete=OnThreadComplete; // 线程下载完成事件
   return HttpThread;
}
// 创建下载线程数组
void THttpGetEx::CreateHttpThreads(void)
{
   AssignResource();
   // 取得文件大小,以决定各个线程下载的起始位置
   THttpGetThread *HttpThread=CreateHttpThread();
   HttpThreads[FHttpThreadCount-1]=HttpThread;
   int FileSize=HttpThread->GetWEBFileSize();
   // 把文件分成FHttpThreadCount块
   int AvgSize=FileSize/FHttpThreadCount;
   int *Starts= new int[FHttpThreadCount];
   int *Bytes = new int[FHttpThreadCount];
   for(int i=0;i<FHttpThreadCount;i++)
   {
      Starts[i]=i*AvgSize;
      Bytes[i] =AvgSize;
   }
   // 修正最后一块的大小
   Bytes[FHttpThreadCount-1]=AvgSize+(FileSize-AvgSize*FHttpThreadCount);
   // 检查服务器是否支持断点续传
   HttpThread->StartPostion=Starts[FHttpThreadCount-1];
   HttpThread->GetBytes=Bytes[FHttpThreadCount-1];
   bool CanMulti=HttpThread->SetFilePointer();
   if(CanMulti==false) // 不支持,直接下载
   {
      FHttpThreadCount=1;
      HttpThread->StartPostion=0;
      HttpThread->GetBytes=FileSize;
      HttpThread->Index=0;
      HttpThread->OutFileName=OutTmpFiles[0];
   }else
   {
      HttpThread->OutFileName=OutTmpFiles[FHttpThreadCount-1];
      HttpThread->Index=FHttpThreadCount-1;
      // 支持断点续传,建立多个线程
      for(int i=0;i<FHttpThreadCount-1;i++)
      {
         HttpThread=CreateHttpThread();
         HttpThread->StartPostion=Starts[i];
         HttpThread->GetBytes=Bytes[i];
         HttpThread->OutFileName=OutTmpFiles[i];
         HttpThread->Index=i;
         HttpThreads[i]=HttpThread;
      }
   }
   // 删除临时变量
   delete Starts;
   delete Bytes;
}
    下载文件的下载的函数如下:
void __fastcall THttpGetEx::DownLoadFile(void)
{
   CreateHttpThreads();
   THttpGetThread *HttpThread;
   for(int i=0;i<FHttpThreadCount;i++)
   {
      HttpThread=HttpThreads[i];
      HttpThread->Resume();
   }
}
    线程下载完成后,会发出OnThreadComplete事件,在这个事件中判断是否所有下载线程都已经完成,如果是,则合并文件的各个部分。应该注意,这里有一个线程同步的问题,否则几个线程同时产生这个事件时,会互相冲突,结果也会混乱。同步的方法很多,我的方法是创建线程互斥对象。
const char *MutexToThread="http-get-thread-mutex";
void __fastcall THttpGetEx::OnThreadComplete(TObject *Sender, int Index)
{
   // 创建互斥对象
   HANDLE hMutex= CreateMutex(NULL,FALSE,MutexToThread);
   DWORD Err=GetLastError();
   if(Err==ERROR_ALREADY_EXISTS) // 已经存在,等待
   {
      WaitForSingleObject(hMutex,INFINITE);//8000L);
      hMutex= CreateMutex(NULL,FALSE,MutexToThread);
   }
   // 当一个线程结束时,检查是否全部认为完成
   FSuccesss[Index]=true;
   bool S=true;
   for(int i=0;i<FHttpThreadCount;i++)
   {
      S = S && FSuccesss[i];
   }
   ReleaseMutex(hMutex);
   if(S)// 下载完成,合并文件的各个部分
   {
      // 1. 复制第一部分
      CopyFile(OutTmpFiles[0].c_str(),FOutFileName.c_str(),false);
      // 添加其他部分
      int hD=FileOpen(FOutFileName,fmOpenWrite);
      FileSeek(hD,0,2); // 移动文件指针到末尾
      if(hD==-1)
      {
         DoOnError();
         return;
      }
      const int BufSize=1024*4;
      char Buf[BufSize+4];
      int Reads;
      for(int i=1;i<FHttpThreadCount;i++)
      {
         int hS=FileOpen(OutTmpFiles[i],fmOpenRead);
         // 复制数据
         Reads=FileRead(hS,(void *)Buf,BufSize);
         while(Reads>0)
         {
            FileWrite(hD,(void *)Buf,Reads);
            Reads=FileRead(hS,(void *)Buf,BufSize);
         }
         FileClose(hS);
      }
      FileClose(hD);
   }
}
结语
    到此,多线程下载的关键部分就介绍完了。但是在实际应用时,还有许多应该考虑的因素,如网络速度、断线等等都是必须考虑的。当然还有一些细节上的考虑,但是限于篇幅,就难以一一写明了。如果读者朋友能够参照本文编写出自己满意的下载程序,我也就非常欣慰了。我也非常希望读者能由此与我互相学习,共同进步。
    关于本文的详细示例(包括下载组件和使用程序),请到《程序员》网址下载。
 
View Code

 

发表评论

0/200
9 点赞
0 评论
收藏