Android移动开发-使用HttpURLConnection实现多线程的下载

来源:互联网 发布:淘宝开店申请要多久 编辑:程序博客网 时间:2024/05/17 19:20

HttpURLConnection继承了URLConnection,因此也可用于向指定网站发送GET请求、POST请求。它在URLConnection的基础上提供了如下便捷的方法。
->int getResponseCode():获取服务器的响应代码;
->String getResponseMessage():获取服务器的响应消息;
->String getRequestMethod():获取发送请求的方法;
->void setRequestMethod():设置发送请求的方法。

下面通过一个实例Demo程序来示范使用HttpURLConnection实现多线程下载。

使用多线程下载文件可以更快完成文件的下载,因为客户端启动多条线程进行下载就意味着服务器也需要为该客户端提供响应的服务。假设服务器同时最多服务100个用户,在服务器中一条线程对应一个用户,100条线程在计算机内并发执行,也就是有CPU划分时间片轮流执行,如果A应用使用了99条线程下载文件,那么相当于占用了99个用户的资源,自然就拥有了较快的下载速度。

为了实现多线程下载,程序可按如下步骤进行:
Step1:创建URL对象;
Step2:获取指定URL对象所指向资源大小(由getContentLength()方法实现),此处用到了HTTPURLConnection类;
Step3:在本地磁盘上创建一个与网络资源相同大小的空文件;
Step4:计算每条线程应该下载网络资源的哪个部分(从哪个字节开始,到哪个字节结束);
Step5:依次创建、启动多条线程来下载网络资源的指定部分。

该程序Demo提供的下载工具类的代码如下:

  • DownUtil.java逻辑代码如下:
package com.fukaimei.multithreaddown;import java.io.IOException;import java.io.InputStream;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.URL;/** * Created by FuKaimei on 2017/10/2. */public class DownUtil {    // 定义下载资源的路径    private String path;    // 指定所下载的文件的保存位置    private String targetFile;    // 定义需要使用多少条线程下载资源    private int threadNum;    // 定义下载的线程对象    private DownThread[] threads;    // 定义下载的文件的总大小    private int fileSize;    public DownUtil(String path, String targetFile, int threadNum) {        this.path = path;        this.targetFile = targetFile;        this.threadNum = threadNum;        // 初始化数组threads数组        threads = new DownThread[threadNum];        this.targetFile = targetFile;    }    public void download() throws Exception {        URL url = new URL(path);        HttpURLConnection conn = (HttpURLConnection) url.openConnection();        conn.setConnectTimeout(5 * 1000);        conn.setRequestMethod("GET");        conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, "                + "application/x-shockwave-flash, application/xaml+xml, "                + "application/vnd.ms-xpsdocument, application/x-ms-xbap, "                + "application/x-ms-application, application/vnd.ms-excel, "                + "application/vnd.ms-powerpoint, application/msword, */*");        conn.setRequestProperty("Accept-Language", "zh-CN");        conn.setRequestProperty("Charset", "UTF-8");        conn.setRequestProperty("Connection", "Keep-Alive");        // 得到文件的大小        fileSize = conn.getContentLength();        conn.disconnect();        int currentPartSize = fileSize / threadNum + 1;        RandomAccessFile file = new RandomAccessFile(targetFile, "rw");        // 设置本地文件的大小        file.setLength(fileSize);        file.close();        for (int i = 0; i < threadNum; i++) {            // 计算每条线程下载的开始位置            int startPos = i * currentPartSize;            // 每条线程使用一个RandomAccessFile进行下载            RandomAccessFile currentPart = new RandomAccessFile(targetFile, "rw");            // 定位该线程的下载位置            currentPart.seek(startPos);            // 创建下载线程            threads[i] = new DownThread(startPos, currentPartSize, currentPart);            // 启动下载线程            threads[i].start();        }    }    // 获取下载的完成百分比    public double getCompleteRate() {        // 统计多条线程已经下载的总大小        int sumSize = 0;        for (int i = 0; i < threadNum; i++) {            sumSize += threads[i].length;        }        // 返回已经完成的百分比        return sumSize * 1.0 / fileSize;    }    private class DownThread extends Thread {        // 当前线程的下载位置        private int startPos;        // 定义当前线程负责下载的文件大小        private int currentPartSize;        // 当前线程需要下载的文件块        private RandomAccessFile currentPart;        // 定义该线程已下载的字节数        public int length;        public DownThread(int startPos, int currentPartSize, RandomAccessFile currentPart) {            this.startPos = startPos;            this.currentPartSize = currentPartSize;            this.currentPart = currentPart;        }        @Override        public void run() {            try {                URL url = new URL(path);                HttpURLConnection conn = (HttpURLConnection) url.openConnection();                conn.setConnectTimeout(5 * 1000);                conn.setRequestMethod("GET");                conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, "                        + "application/x-shockwave-flash, application/xaml+xml, "                        + "application/vnd.ms-xpsdocument, application/x-ms-xbap, "                        + "application/x-ms-application, application/vnd.ms-excel, "                        + "application/vnd.ms-powerpoint, application/msword, */*");                conn.setRequestProperty("Accept-Language", "zh-CN");                conn.setRequestProperty("Charset", "UTF-8");                InputStream inStream = conn.getInputStream();                // 跳过startPos个字节,表明该线程只下载自己负责的那部分文件                skipFully(inStream, this.startPos);                byte[] buffer = new byte[1024];                int hasRead = 0;                // 读取网络数据,并写入本地文件中                while (length < currentPartSize && (hasRead = inStream.read(buffer)) > 0) {                    currentPart.write(buffer, 0, hasRead);                    // 累计该线程下载的总大小                    length += hasRead;                }                currentPart.close();                inStream.close();            } catch (Exception e) {                e.printStackTrace();            }        }    }    // 定义一个为InputStream跳过bytes字节的方法    public static void skipFully(InputStream in, long bytes) throws IOException {        long remainning = bytes;        long len = 0;        while (remainning > 0) {            len = in.skip(remainning);            remainning -= len;        }    }}

上面的DownUtil工具类中包括一个DownloadThread内部类,该内部类的run()方法负责打开远程资源的输出流,并调用InputStream的skip(int)方法跳过指定数量的字节,这样就让该线程读取由它自己负责下载的部分了。
提供了上面的DownUtil工具类之后,接下来就可以在Activity中调用该DownUtil类来执行下载任务了。该程序界面中包含两个文本框:一个用于输入网络文件的源路径;另一个用于指定下载到本地的文件的文件名。

  • layout/activity_main.xml界面布局代码如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <TextView        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="要下载的资源的URL:" />    <EditText        android:id="@+id/url"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="http://172.xx.xx.xxx:8080/fukaimei/%E6%9D%8E%E8%8D%A3%E6%B5%A9_%E5%96%9C%E5%89%A7%E4%B9%8B%E7%8E%8B.mp3" />    <TextView        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="目标文件:" />    <EditText        android:id="@+id/target"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="/mnt/sdcard/my_music.mp3" />    <Button        android:id="@+id/down"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="下载" />    <!-- 定义一个水平进度条,用于显示下载进度 -->    <ProgressBar        android:id="@+id/bar"        style="?android:attr/progressBarStyleHorizontal"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:max="100" /></LinearLayout>
  • MainActivity.java逻辑代码如下:
package com.fukaimei.multithreaddown;import android.os.Handler;import android.os.Message;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.ProgressBar;import java.util.Timer;import java.util.TimerTask;public class MainActivity extends AppCompatActivity {    EditText url;    EditText target;    Button downBn;    ProgressBar bar;    DownUtil downUtil;    private int mDownStatus;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        // 获取程序界面的三个界面按钮        url = (EditText) findViewById(R.id.url);        target = (EditText) findViewById(R.id.target);        downBn = (Button) findViewById(R.id.down);        bar = (ProgressBar) findViewById(R.id.bar);        // 创建一个Handler对象        final Handler handler = new Handler() {            @Override            public void handleMessage(Message msg) {                if (msg.what == 0x123) {                    bar.setProgress(mDownStatus);                }            }        };        downBn.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                // 初始化DownUtil对象(最后一个参数指定线程数)                downUtil = new DownUtil(url.getText().toString(), target.getText().toString(), 5);                new Thread() {                    @Override                    public void run() {                        try {                            // 开始下载                            downUtil.download();                        } catch (Exception e) {                            e.printStackTrace();                        }                        // 定义每秒调度获取一次系统的完成度                        final Timer timer = new Timer();                        timer.schedule(new TimerTask() {                            @Override                            public void run() {                                // 获取下载任务的完成比例                                double completeRate = downUtil.getCompleteRate();                                mDownStatus = (int) (completeRate * 100);                                // 发送消息通知界面更新进度条                                handler.sendEmptyMessage(0x123);                                // 下载完成后取消任务调度                                if (mDownStatus >= 100) {                                    timer.cancel();                                }                            }                        }, 0, 100);                    }                }.start();            }        });    }}

上面的Activity不仅使用了DownUtil来控制程序下载,而且程序还启动了一个定时器,该定时器控制每隔0.1秒查询一次下载进度,并通过程序中的进度条来显示任务的下载进度。

  • 注意:由于该程序不仅需要访问网络,还需要访问系统SD卡,在SD卡中创建文件,因此还需要在清单文件AndroidManifest.xml文件中授权该程序访问网络、访问SD卡文件的权限:
<!-- 在SD卡中创建与删除文件权限 -->    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>    <!-- 向SD卡写入数据权限 -->    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>    <!-- 授权访问网络 -->    <uses-permission android:name="android.permission.INTERNET"/>
  • Demo程序运行效果界面截图如下:

这里写图片描述


Demo程序源码下载地址