`
_神谕_
  • 浏览: 3306 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

[转载]通过HTTP协议实现多线程下载

    博客分类:
  • HTTP
阅读更多
1. 基本原理,每条线程从文件不同的位置开始下载,最后合并出完整的数据。

2. 使用多线程下载的好处
    下载速度快。为什么呢?很好理解,以往我是一条线程在服务器上下载。也就是说,对应在服务器上,有一个我的下载线程存在。
    这时候肯定不只我一个人在下载,服务器上肯定同时存在多条下载线程,在下载服务器资源。对于 CPU 来说,不可能实现并发执行。
    CPU 会公平的为这些线程划分时间片,轮流执行,a线程十毫秒 , b线程十毫秒...
    假设运用了本文这种手法,意味着我的下载应用,可以同时使用服务器端的任意多条线程同时下载(理论上).
    假设这个线程数目是 50 条,本应用就将更多的得到服务器 CPU 的照顾超过 50 倍.
    但是总归会受本地网络速度的限制。

3. 每条线程要负责下载的数据长度可以用 “下载数据的总长度” 除以 “参与下载的线程总数” 来计算。但是要考虑到不能整除的情况。
    假设有 5 条线程参与下载,那么计算公式应该为 :
            int block = 数据总长度%线程数 == 0? 10/3 : 10/3+1; (不能整除,则加一)

4. 和数据库分页查询类型。每条线程需要知道自己从数据的什么位置开始下载,下载到什么位置为止。
   首先,为每一个线程配备一个 id , id 从零开始,为 0 1 2 3...
   开始位置:线程 id 乘以每条线程负责下载的数据长度.
   结束位置:下一个线程开始位置的前一个位置。
   如:
      int startPosition =  线程id * 每条线程下载的数据长度
      int endPosition = (线程id + 1) * 每条线程下载的数据长度 -1;
       
5. HTTP 协议的 Range 头可以指定从文件的什么位置开始下载,下载到什么位置结束。单位为 1byte
   Range:bytes=2097152-4194304 表示从文件的 2M 的位置开始下载,下载到 4M 处结束
   假如 Range 指定要读取到 文件的 5104389 的字节数位置,但是下载的文件本身只有 4104389 个长度。那么下载操作自动会在 4104389 处停止。
   因此不会下载到多余的无效数据.

6. 另一个难题是如何按顺序将数据写往本地文件。因为,线程是同步执行的,它们同时在往本地目标文件写入数据。
   而线程于线程之间写入的数据并没有按照下载数据本身的顺序。若按照普通的 OutputStream 的写入方式,最后的本地下载文件将失真。
   于是我们将用到下面这个类:
     java.io.RandomAccessFile
     因为此类同时实现了 DataOutput 和 DataInput 的方法。使他们同时具有写入和读取功能。
     这个类仿佛存在一个类似文件指针的东西,可以随意执行文件的任意一个位置开始读写.
     因此此类的实例支持对随机访问文件的读取和写入.
    
     例如:
      
Java代码  收藏代码
File file = new File("1.txt"); 
        RandomAccessFile accessFile = new RandomAccessFile(file,"rwd"); 
        accessFile.setLength(1024); 
   
    虽然,执行完这段代码后,我们还没有向目标文件 "1.txt" 写入任何数据。但是如果此时查看其大小,已经为 1kb 了。这是我们自己设置的大小。
    这个操作类似于向这个文件存储了一个大型的 byte 数组。这个数组将这个文件撑到指定大小。等待被填满。
    既然是这样,好处就在于,我们可以通过 “索引” 随机访问此文件系统的某个部分。
    例如,可能这个文件大小为 500
    那么,我的业务需求可能需要第一次 从 300 位置开始写数据,写到 350 为止。
    第二次,我又从 50 开始写数据,写到 100 为止。
    总之,我不是 “一次性” 的 “按顺序” 的将这个文件写完。
    那么,RandomAccessFile 可以支持这种操作。
    
     API
      void setLength(long newLength)
          Sets the length of this file. (设置文件的预计大小)
      void seek(long pos)
          Sets the file-pointer offset, measured from the beginning of this file, at which the next read or write occurs.
          假设为这个方法传入 1028 这个参数,表示,将从文件的 1028 位置开始写入。
      void write(byte[] b, int off, int len)
          Writes len bytes from the specified byte array starting at offset off to this file.
      write(byte[] b)
          Writes b.length bytes from the specified byte array to this file, starting at the current file pointer.
      void writeUTF(String str)
          Writes a string to the file using modified UTF-8 encoding in a machine-independent manner.
       String readLine()
          Reads the next line of text from this file.

      实验代码:
    

Java代码  收藏代码
public static void main(String[] args) throws Exception { 
 
File file = new File("1.txt");  
 
RandomAccessFile accessFile = new RandomAccessFile(file,"rwd"); 
 
/* 设置文件为 3 个字节大小 */ 
 
accessFile.setLength(3); 
 
/* 向第二个位置写入 '2' */ 
 
accessFile.seek(1); 
 
accessFile.write("2".getBytes()); 
 
/* 向第一个位置写入 '1' */  
 
accessFile.seek(0); accessFile.write("1".getBytes());  
 
/* 向第三个位置写入 '3' */ 
 
accessFile.seek(2);  
 
accessFile.write("3".getBytes()); accessFile.close();  
 
// 期待文件的内容为 :123 
 
}  


   
    以上实验成功,虽然我们写入字符串的顺序为 "2"、"1"、"3",但是因为设置了文件偏移量的关系,文件最终保存的数据为 : 123
    另一个疑问,写完这三个数据,文件的大小已经为 3 个字节大小了。已经撑满了写入的数据,那么我们继续往里面放数据会有什么效果?
   
    /* 向超出大小的第四个字节位置写入数据 */
    accessFile.seek(3);
    accessFile.write("400".getBytes());
   
    以上代码无论 seek 方法指定的文件指针偏移量以及存入的数据,都已经超出了最开始为文件设定的 3 个字节的大小。
    按照我的猜测,至少 “accessFile.seek(3)” 位置会抛出 "ArrayIndexOutOfBoundsException" 异常,表示下标越界。
    而,单独执行 "accessFile.write("400".getBytes())" 应该可以成功。因为这个需求属于合理的,应该有执行它的机制。
    实验结果是两句代码都是成功的。貌似是说明,文件隐含的大型的字节数组,可以自动撑大。
   
    但是要注意的问题是,必须要保证所设定的文件大小的每一个位置都具有合法的数据,至少不能为空。
    例如:
        /* 向第三个位置写入 '3'  */
        accessFile.seek(2);
        accessFile.write("3".getBytes());
       
        accessFile.seek(5);
        accessFile.write("400".getBytes());
    那么结合之前的代码,最后的结果为:
        123口口400
    在空白的两个位置处出现了乱码。这是理所应当的。
   
    另外,假设我们为文件指定了一百个长度:
        accessFile.setLength(100);
    而,实际上,我们只为其前五个位置设置了值。那么理所当然的是,文件保存的数据,最后会缀上 95 个乱码。
   
7. 准备工作应该十分充分了。接下来上代码。


  
Java代码  收藏代码
import java.io.File;   
import java.io.IOException;   
import java.io.InputStream;   
import java.io.RandomAccessFile;   
import java.net.HttpURLConnection;   
import java.net.URL;   
/** 
* 多线程方式文件下载 
*/   
public class MulThreadDownload {   
    /* 下载的URL */   
    private URL downloadUrl;   
    /* 用于保存的本地文件 */   
    private File localFile;   
    /* 没条线程下载的数据长度 */   
    private int block;   
    public static void main(String[] args) {   
        /* 可以为网络上任意合法下载地址 */   
        String downPath = "http://192.168.1.102:8080/myvideoweb/down.avi";   
        MulThreadDownload threadDownload = new MulThreadDownload();   
        /* 开 10 条线程下载下载 */   
        try {   
            threadDownload.download(downPath, 10);   
        } catch (Exception e) {   
            e.printStackTrace();   
        }   
    }   
    /** 
     * 多线程文件下载 
     *  
     * @param path 下载地址 
     * @param threadCount 线程数 
     */   
    public void download(String path, int threadCount) throws Exception {   
        downloadUrl = new URL(path);   
        HttpURLConnection conn = (HttpURLConnection) downloadUrl   
                .openConnection();   
        /* 设置 GET 请求方式 */   
        conn.setRequestMethod("GET");   
        /* 设置响应时间超时为 5 秒 */   
        conn.setConnectTimeout(5 * 1000);   
        /* 获取本地文件名 */   
        String filename = parseFilename(path);   
        /* 获取下载文件的总大小 */   
        int dataLen = conn.getContentLength();   
        if (dataLen < 0) {   
            System.out.println("获取数据失败");   
            return;   
        }   
        /* 创建本地目标文件,并设置其大小为准备下载文件的总大小 */   
        localFile = new File(filename);   
        RandomAccessFile accessFile = new RandomAccessFile(localFile, "rwd");   
        /* 这时候,其实本地目录下,已经创建好了一个大小为下载文件的总大小的文件 */   
        accessFile.setLength(dataLen);   
        accessFile.close();   
        /* 计算每条线程要下载的数据大小 */   
        block = dataLen % threadCount == 0 ? dataLen / threadCount : dataLen / threadCount + 1;   
        /* 启动线程下载文件 */   
        for (int i = 0; i < threadCount; i++) {   
            new DownloadThread(i).start();   
        }   
    }   
    /** 
     * 解析文件 
     */   
    private String parseFilename(String path) {   
        return path.substring(path.lastIndexOf("/") + 1);   
    }   
    /** 
     * 内部类: 文件下载线程类 
     */   
    private final class DownloadThread extends Thread {   
        /* 线程 id */   
        private int threadid;   
        /* 开始下载的位置 */   
        private int startPosition;   
        /* 结束下载的位置 */   
        private int endPosition;   
        /** 
         * 新建一个下载线程 
         * @param threadid 线程 id 
         */   
        public DownloadThread(int threadid) {   
            this.threadid = threadid;   
            startPosition = threadid * block;   
            endPosition = (threadid + 1) * block - 1;   
        }   
        @Override   
        public void run() {   
            System.out.println("线程 '" + threadid + "'启动下载..");   
               
            RandomAccessFile accessFile = null;   
            try {   
                /* 设置从本地文件的什么位置开始写入数据 ,"rwd" 表示对文件具有读写删权限 */   
                accessFile = new RandomAccessFile(localFile, "rwd");   
                accessFile.seek(startPosition);   
                HttpURLConnection conn = (HttpURLConnection) downloadUrl.openConnection();   
                conn.setRequestMethod("GET");   
                conn.setReadTimeout(5 * 1000);   
                /* 为 HTTP 设置 Range 属性,可以指定服务器返回数据的范围 */   
                conn.setRequestProperty("Range", "bytes=" + startPosition + "-"   
                        + endPosition);   
                /* 将数据写往本地文件 */   
                writeTo(accessFile, conn);   
                   
                System.out.println("线程 '" + threadid + "'完成下载");   
            } catch (IOException e) {   
                e.printStackTrace();   
            } finally {   
                try {   
                    if(accessFile != null) {   
                        accessFile.close();   
                    }   
                 } catch (IOException ex) {   
                     ex.printStackTrace();   
                 }   
            }   
        }   
        /** 
         * 将下载数据写往本地文件 
         */   
        private void writeTo(RandomAccessFile accessFile,   
                HttpURLConnection conn){   
            InputStream is = null;   
            try {   
                is = conn.getInputStream();   
                byte[] buffer = new byte[1024];   
                int len = -1;   
                while ((len = is.read(buffer)) != -1) {   
                    accessFile.write(buffer, 0, len);   
                }   
            } catch (IOException e) {   
                e.printStackTrace();   
            } finally {   
                try {   
                    if(is != null) {   
                        is.close();   
                    }    
                } catch (Exception ex) {   
                    ex.printStackTrace();   
                }   
            }   
        }   
    }   
}   
分享到:
评论

相关推荐

    [转载] 多线程阻塞式网络编程socket_源代码

    本文将深入探讨标题和描述中提到的“多线程阻塞式网络编程socket”相关的知识点。 首先,我们需要理解“socket”。Socket是操作系统提供的一个接口,允许应用程序进行网络通信。它就像一个通信端口,通过它可以发送...

    可扩展多线程异步Socket服务器框架EMTASS

    在多核CPU环境下,多线程可以实现更高效的资源利用,使得服务器能够同时处理大量并发连接,提高了服务响应速度和整体性能。 异步Socket编程的核心在于事件驱动模型,如I/O复用(如select、poll、epoll等)、信号...

    【转载】java实现的局域网聊天软件

    【Java 实现局域网聊天软件...综上所述,构建一个Java实现的局域网聊天软件涉及众多技术,包括Java网络编程、多线程、IO流、Spring Boot框架、WebSocket、JSON数据交换等,开发者需要对这些知识有深入理解和实践能力。

    基于Qt的多功能串口通信工具分享:实时数据收发与波形绘制

    此外,软件通过使用多线程技术确保串口通信的平稳性,避免因大量数据传输导致界面卡顿。其粘包拆解机制和波形绘制功能,帮助用户更直观地观察通信数据的变化,为硬件调试和通信测试提供了强有力的支持。 ————...

    [转载]QQ示例源码(供学习C++网络编程参考)

    5. **多线程技术**:在网络编程中,多线程常用于处理并发连接。一个线程可以处理一个连接,这样服务器就能同时处理多个客户端。C++11引入了内置的线程库 `&lt;thread&gt;`,使得创建和管理线程变得简单。 6. **异步I/O**...

    WebService+Android

    【WebService+Android】是将Web服务技术应用到Android平台上的一个重要实践,主要目的是为了实现远程数据交换和交互。...通过这些技术,我们可以使Android应用具备与服务器交互的能力,实现丰富的功能。

    小小图片爬虫

    4. **多线程**:为了提高爬取速度,项目可能采用了多线程或异步处理。每个线程可以负责下载一个或多个图片,以并发的方式提升效率。 5. **图片下载与存储**:爬虫在获取到图片URL后,需要下载图片到本地,并保存到...

    并发和并行以及他们的区别

    在多核处理器上,多个线程可以真正同时执行,而在单核处理器上,线程之间通过时间片轮转实现并发。 所以当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少?离开了单位时间其实是没有意义的。 ...

    socket_套接字_

    通过阅读和理解这个文件,你可以看到具体的编程实现,包括错误处理、多线程(或多进程)处理连接、数据编码解码等细节。这将帮助你深入理解套接字通信的全貌,并能应用于自己的项目中。 总之,套接字是网络通信的...

    java编程事项(转载收集整理版)

    5. **多线程**:Java提供了内置的多线程支持,通过Thread类和Runnable接口可以创建并管理线程。理解线程同步(如synchronized关键字和Lock接口)以及并发工具类(如ExecutorService和Future)是处理并发问题的关键。...

    Socket 传输文件代码转载

    Socket编程是网络通信中的基础,它允许两个程序通过网络交换数据。在这个例子中,我们看到...在处理大文件或高并发场景时,通常会使用更高级的协议如FTP、HTTP或自定义协议,以及多线程或异步处理来提高效率和可靠性。

    悠索科技高校教务管理系统(转载)

    4. **多线程**:在处理大量并发请求时,系统可能采用了多线程技术来提高性能和响应速度,如后台任务的异步执行。 5. **设计模式**:系统可能应用了诸如工厂模式、单例模式、观察者模式等设计模式,以实现良好的代码...

    winsocket 全双工

    学习WinSocket全双工通信,不仅需要理解上述步骤,还要熟悉错误处理、多线程或多路复用技术(如select、poll、epoll),以提高服务器处理并发连接的能力。此外,深入理解TCP的工作原理,包括滑动窗口、拥塞控制等,...

    c#的1毫秒的多媒体计时器

    在一个QQ群里,有位网友说实现1毫秒的时钟需要使用一个线程不停的判断时间,不能有sleep,但是这样就会耗费CPU。 俺跟了一句 可以用多媒体时钟 Win95 就有,然后被怼了。实际上,我很早就这么用,那时是写一个超声的...

    Learning Node.js Development

    - **单线程模型**:Node.js采用单线程模型来处理请求,这与传统的多线程模型不同,能够有效提高资源利用率。 - **模块化设计**:Node.js提供了一套丰富的内置模块,如fs(文件系统)、http(HTTP服务)等,方便...

    libInternet4E:易语言官方开源的互联网支持库,github转载

    5. **多线程处理**:库中的函数支持多线程操作,可以同时处理多个网络任务,提高程序运行效率。 6. **错误处理**:libInternet4E具有完善的错误处理机制,当网络操作出现问题时,能够返回错误代码和错误信息,帮助...

    [原创]FavChat爱聊全能隐蔽穿透型聊天平台完整源码源程序包(Hedda)

    ■ 考虑FavChat实际工作中的计算机因素和网络延迟,平台充分地利用线程并发运作和多阶段队列缓冲机制,保证事务处理的顺畅和聊天过程中最重要的全双工能力的完美实现。(参考附件流程图) ■ 语音部分则使用...

    Java 最常见 200+ 面试题全解析:面试必备.pdf

    3. 多线程:介绍线程的创建和管理,线程同步机制,如synchronized关键字,wait和notify方法,以及线程池的使用。 4. 反射:讨论Java反射机制,它允许程序在运行时访问和修改类的行为,是框架开发中的重要技术。 5....

    Java面试资料大集合

    3. **多线程** - **线程状态**:新建、运行、阻塞、等待、终止等五种状态。 - **同步机制**:synchronized关键字,wait()、notify()和notifyAll()方法,以及Lock接口。 - **并发工具类**:如CountDownLatch、...

Global site tag (gtag.js) - Google Analytics