DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

HTTPのリジュームダウンロード(途中からのダウンロード)を行う

補足:ここではHTTPについて多少触れていますが、私自身それほど詳しくなく、さらに.NETから内容が離れてしまうため、ごく簡単にしか説明しません。HTTPに関して詳しくはRFC2616等をご覧ください。

リジュームダウンロードとは、以前途中までダウンロードしたファイルがあるとき、その続きからダウンロードを再開することです。

WebRequest、WebResponseクラスを使ってファイルをダウンロードし保存する」に少し手を加えるだけでリジュームダウンロードができるようになります。HttpWebRequestクラスのAddRangeメソッドによりダウンロードするデータの開始位置を取得し、すでに保存してあるファイルの末端に追加して書き込みます。AddRangeメソッドはつまり、Rangeヘッダフィールドを指定するものです。

基本的にはこれだけですが、いくつかの問題が残ります。

まず、本当に指定した範囲のデータをサーバーが送ってきたかをどのように判断するかという問題です。これにはHTTP応答ステータスコードを調べ、206(Partial Content)であれば成功したと判断する方法が考えられます。また、Content-Rangeヘッダフィールドを調べれば、実際に送られてきたデータの範囲を取得できます。

また、途中までダウンロードしたデータが今サーバーにあるデータと同じものであるか確かめる必要もあるでしょう。これには、If-Rangeや、If-Match、If-Unmodified-Sinceヘッダなどを使うとよいでしょう。

以上の知識を基に、MSDNのロゴを保存するサンプルを以下に示します。ここでは保存先のファイルがある時はダウンロードが途中であると判断し、リジュームダウンロードするようにしています。

VB.NET
コードを隠すコードを選択
'ダウンロードするファイル
Dim url As String = "http://www.msj.co.jp/msdn/images/msdn_logo.jpg"
'保存先のファイル名
Dim fileName As String = "c:\logo.jpg"
'If-Rangeヘッダに渡すエンティティタグを指定するときは指定する
Dim entityTag As String = ""

'WebRequestの作成
Dim webreq As System.Net.HttpWebRequest = _
    CType(System.Net.WebRequest.Create(url), _
    System.Net.HttpWebRequest)

'ファイルがあればダウンロードが途中であると判断し、
'ファイルサイズを取得する
Dim firstPos As Long
If System.IO.File.Exists(fileName) Then
    firstPos = (New System.IO.FileInfo(fileName)).Length
Else
    firstPos = 0
End If
If firstPos > 0 Then
    'バイトレンジを指定する
    webreq.AddRange(CInt(firstPos))
    'If-Rangeヘッダを追加
    If entityTag <> "" Then
        webreq.Headers.Add("If-Range", entityTag)
    End If
End If

'そのほかのヘッダを指定する
webreq.KeepAlive = False
webreq.Headers.Add("Pragma", "no-cache")
webreq.Headers.Add("Cache-Control", "no-cache")

Dim webres As System.Net.HttpWebResponse = Nothing
Try
    'サーバーからの応答を受信するためのWebResponseを取得
    webres = CType(webreq.GetResponse(), System.Net.HttpWebResponse)
Catch e As System.Net.WebException
    'HTTPプロトコルエラーを捕捉し、内容を表示する
    If e.Status = System.Net.WebExceptionStatus.ProtocolError Then
        Dim errres As System.Net.HttpWebResponse = _
            CType(e.Response, System.Net.HttpWebResponse)
        Console.WriteLine(errres.StatusCode)
        Console.WriteLine(errres.StatusDescription)
    Else
        Console.WriteLine(e.Message)
    End If
    Return
End Try

'エンティティタグの表示
Console.WriteLine(("ETag:" + webres.GetResponseHeader("ETag")))

'応答データを受信するためのStreamを取得
Dim strm As System.IO.Stream = webres.GetResponseStream()

'ファイルに書き込むためのFileStreamを作成
Dim fs As New System.IO.FileStream(fileName, _
    System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write)

'ファイルに書き込む位置を決定する
'206Partial Contentステータスコードが返された時はContent-Rangeヘッダを調べる
'それ以外のときは、先頭から書き込む
Dim seekPos As Long = 0
If webres.StatusCode = System.Net.HttpStatusCode.PartialContent Then
    Dim contentRange As String = _
        webres.GetResponseHeader("Content-Range")
    Dim m As System.Text.RegularExpressions.Match = _
        System.Text.RegularExpressions.Regex.Match(contentRange, _
        "bytes\s+(?:(?<first>\d*)-(?<last>\d*)|\*)/(?:(?<len>\d+)|\*)")
    If m.Groups("first").Value = "" Then
        seekPos = 0
    Else
        seekPos = Integer.Parse(m.Groups("first").Value)
    End If
End If
'書き込み位置を変更する
fs.SetLength(seekPos)
fs.Position = seekPos

'応答データをファイルに書き込む
Dim readData(1023) As Byte
Dim readSize As Integer = 0
While True
    readSize = strm.Read(readData, 0, readData.Length)
    If readSize = 0 Then
        Exit While
    End If
    fs.Write(readData, 0, readSize)
End While

'閉じる
fs.Close()
strm.Close()
C#
コードを隠すコードを選択
//ダウンロードするファイル
string url = "http://www.msj.co.jp/msdn/images/msdn_logo.jpg";
//保存先のファイル名
string fileName = "c:\\logo.jpg";
//If-Rangeヘッダに渡すエンティティタグを指定するときは指定する
string entityTag = "";

//WebRequestの作成
System.Net.HttpWebRequest webreq =
    (System.Net.HttpWebRequest) System.Net.WebRequest.Create(url);

//ファイルがあればダウンロードが途中であると判断し、
//ファイルサイズを取得する
long firstPos;
if (System.IO.File.Exists(fileName))
    firstPos = (new System.IO.FileInfo(fileName)).Length;
else
    firstPos = 0;
if (firstPos > 0)
{
    //バイトレンジを指定する
    webreq.AddRange((int) firstPos);
    //If-Rangeヘッダを追加
    if (entityTag != "")
        webreq.Headers.Add("If-Range", entityTag);
}
//そのほかのヘッダを指定する
webreq.KeepAlive = false;
webreq.Headers.Add("Pragma", "no-cache");
webreq.Headers.Add("Cache-Control", "no-cache");

System.Net.HttpWebResponse webres = null;
try
{
    //サーバーからの応答を受信するためのWebResponseを取得
    webres = (System.Net.HttpWebResponse) webreq.GetResponse();
}
catch (System.Net.WebException e)
{
    //HTTPプロトコルエラーを捕捉し、内容を表示する
    if (e.Status == System.Net.WebExceptionStatus.ProtocolError)
    {
        System.Net.HttpWebResponse errres =
            (System.Net.HttpWebResponse) e.Response;
        Console.WriteLine(errres.StatusCode);
        Console.WriteLine(errres.StatusDescription);
    }
    else
        Console.WriteLine(e.Message);

    return;
}

//エンティティタグの表示
Console.WriteLine("ETag:" + webres.GetResponseHeader("ETag"));

//応答データを受信するためのStreamを取得
System.IO.Stream strm = webres.GetResponseStream();

//ファイルに書き込むためのFileStreamを作成
System.IO.FileStream fs = new System.IO.FileStream(fileName, 
        System.IO.FileMode.OpenOrCreate, 
        System.IO.FileAccess.Write);

//ファイルに書き込む位置を決定する
//206Partial Contentステータスコードが返された時はContent-Rangeヘッダを調べる
//それ以外のときは、先頭から書き込む
long seekPos = 0;
if (webres.StatusCode == System.Net.HttpStatusCode.PartialContent)
{
    string contentRange = webres.GetResponseHeader("Content-Range");
    System.Text.RegularExpressions.Match m =
        System.Text.RegularExpressions.Regex.Match(
            contentRange,
        @"bytes\s+(?:(?<first>\d*)-(?<last>\d*)|\*)/(?:(?<len>\d+)|\*)");
    if (m.Groups["first"].Value == "")
        seekPos = 0;
    else
        seekPos = int.Parse(m.Groups["first"].Value);
}
//書き込み位置を変更する
fs.SetLength(seekPos);
fs.Position = seekPos;

//応答データをファイルに書き込む
byte[] readData = new byte[1024];
int readSize = 0;
for (;;)
{
    readSize = strm.Read(readData, 0, readData.Length);
    if (readSize == 0)
        break;
    fs.Write(readData, 0, readSize);
}

//閉じる
fs.Close();
strm.Close();

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • このサイトで紹介されているコードの多くは、例外処理が省略されています。例外処理については、こちらをご覧ください。
  • Windows Vista以降でUACが有効になっていると、ファイルへの書き込みに失敗する可能性があります。詳しくは、こちらをご覧ください。
  • .NET Tipsをご利用いただく際は、注意事項をお守りください。