Android网络功能开发(4)——文件下载和上传

本篇介绍使用HTTP协议实现文件下载和上传。在客户端和服务器的通信过程中,可能有些多媒体或数据文件需要下载或上传,可以通过HTTP协议实现。

首先看使用HTTP协议下载文件的原理:客户端发送一个HTTP GET请求,并且在消息中用URL指出要下载的文件。

Web服务器都实现了对文件下载请求的响应,响应的消息头中包含文件的基本信息,消息体中包含文件的具体内容,文件内容是二进制格式的。

客户端用HTTP GET实现文件下载的流程和用HTTP GET从服务器获取数据的流程是一致的,区别在于对返回的响应消息的处理。在用HTTP GET从服务器上获取数据时,对于返回的消息体中的内容是直接当作数据来处理的。用HTTP GET下载文件时,对于返回的消息体中的内容要当作文件内容来保存。具体来说,就是获取消息体输入流、读取数据保存到文件、关闭流。

对应的代码是这样的,获取输入流对应的是connection.getInputStream方法,从流中读取数据用的是输入流的read方法,写到文件用的是输出流的write方法,最后用close关闭流。代码的整体框架和用HTTP GET从服务器上获取数据是一样的。    

String download(String urlStr, File f) {
        String result = null;
        HttpURLConnection connection = null;
        try{
            URL url = new URL(urlStr);
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(8000);
            connection.setReadTimeout(5000);
            int statusCode = connection.getResponseCode();
            if(statusCode==200) {
                InputStream in = connection.getInputStream();
                if(f.exists()) f.delete();
                f.createNewFile();
                FileOutputStream out = new FileOutputStream(f);
                byte[] buf = new byte[1024];
                int j = 0;
                while( (j=in.read(buf))!=-1)   out.write(buf, 0, j);
                out.flush();
                out.close();
                in.close();
                result = "Download OK.\nFrom: " + urlStr + "\nTo: " + f.getPath();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(connection!=null) connection.disconnect();
        }
        return result;
}


文件下载也需要用AsyncTask异步执行。

在前一篇BBS的例子中,我们继续添加文件下载和上传功能。关于文件下载是这样设计的,当点击Download按钮时,就会用AsyncTask异步下载上面URL中的文件。该文件保存到外部存储根目录下,因为需要向外部存储写文件,所以需要android.permission.WRITE_EXTERNAL_STORAGE权限。Android6.0以后采用了动态权限管理,这个权限是危险权限,所以最好在执行之前先检查用户是否允许了,如果没有允许则询问,具体代码如下:

if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED){
        requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
        return;
}

此外,Android 10(API29)引入了分区存储(scoped storage)的功能,不允许应用直接读写外部存储根目录下的文件。要想让本例子在Android11上运行,可以将文件存放到应用在外部存储上的私有目录下,该目录可以用方法getExternalFilesDir(null)来获得,其实际位置为:Android/data/<package-name>/files/。


检查完权限后,就可以执行异步下载,具体代码是这样的:

异步执行的代码中先准备好本地文件,然后下载文件。这个文件必须事先放在服务器的WebContent目录下,或相应子目录中。下载完成后,会在下面的文本框中显示下载文件的信息,内容是是否下载成功,从哪个链接下载到本地哪个文件。

如果文件下载需要的时间比较长,就需要显示下载进度。下面,我们用一个ProgressDialog进度对话框来显示文件下载进度,界面就像下面这样。

下载进度等于已下载字节数 / 文件总长度,文件总长度从消息头中得到,用connection对象的getContentLength方法获得,已下载字节数在读取文件内容的循环中统计。具体代码是在基本的文件下载流程的基础上,添加统计进度的代码,也就是红色代码部分。每读取一次数据,就更新下下载进度。

下载进度要显示在进度对话框中,但文件下载代码是在后台线程中执行的,不能直接操作进度对话框。所以更新进度时要调用AsyncTask的publishProgress方法,把进度传递到在主线程执行的onProgressUpdate方法,在这个方法中更新进度对话框。

接下来看文件上传的原理。客户端发送HTTP POST请求,用URL指出服务器端接收上传文件的程序,比如JavaEE中的Servlet。另外消息头中一般要包含文件类型,以便服务器端生成文件后缀。文件的内容以二进制格式放在消息体中。

Web服务器一般不会自带接收上传文件的功能,需要开发者自己编写。接收文件时,服务器端一般是按照文件上传时间生成一个文件名,不用客户端文件名是为了防止不同客户端上传同名文件互相覆盖。然后把POST消息体中的文件内容保存到该文件中。再在响应消息中把文件名或链接返回给客户端,客户端可以在其它Post中使用该文件名或链接作为参数。

客户端用HTTP上传文件的流程和用HTTP上传数据的流程是一致的,区别是在消息头中添加文件类型描述,在消息体中写入的是文件内容。服务器端流程是获取文件类型、生成文件名、获取请求的输入流,读取数据写入文件,返回文件名。

客户端上传文件的代码是这样的:    

    String upload(String urlStr, File f) {
        String result = null;

        HttpURLConnection connection = null;
        try{
            URL url = new URL(etUrl.getText().toString());
            connection = (HttpURLConnection) url.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setUseCaches(false);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Charset", "UTF-8");
            connection.setRequestProperty("Connection", "Keep-Alive");
            connection.setRequestProperty("Content-Type", "binary/" + FileUtils.getExtName(f));

            OutputStream os = connection.getOutputStream();
            FileInputStream fis = new FileInputStream(f);
            byte[] buf = new byte[1024];
            int j = 0;
            while( (j=fis.read(buf))!=-1) {
                os.write(buf, 0, j);
            }
            fis.close();
            os.flush();
            os.close();

            int statusCode = connection.getResponseCode();
            if(statusCode==200) {
                InputStream in = connection.getInputStream();
                result = readInputStream(in);
                in.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(connection!=null) connection.disconnect();
        }
        return result;
    }

添加文件类型用的是setRequestProperty方法,在消息头中添加一行Content-Type属性,值是binary/文件扩展名。然后用getOutputStream打开HttpURLConnection连接的输出流,把文件内容用write方法一块一块写进去。

服务器端是用一个Servlet实现的,具体代码在Servlet的doPost方法中。

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String contentType = request.getContentType();
		int separate = contentType.lastIndexOf("/");
		String fileType = contentType.substring(separate+1);
		String uploadPath = "/upload";
		String filePath = this.getServletContext().getRealPath(uploadPath);
		String fileName = Long.toString(System.currentTimeMillis()) + "." + fileType;
                // 必须创建upload文件夹
		File f = new File(filePath + "/" + fileName);
		if(!f.getParentFile().exists()) f.getParentFile().mkdir();
		System.out.println("File upload: " + f.getPath());
		
		InputStream in = request.getInputStream();
		FileOutputStream fos = new FileOutputStream(f);
		byte[] buffer = new byte[1024];
		int j = 0;
		while( (j=in.read(buffer))!=-1 )  fos.write(buffer, 0, j);
		fos.flush();
		fos.close();
		in.close();
		
		response.setCharacterEncoding("utf-8");
		response.getWriter().print(fileName);
}

先获取文件扩展名,再用系统当前时间生成文件名,在加上路径,从而生成上传文件的名称和位置。然后从request获得输入流,从输入流读取数据保存到该文件。最后,返回该文件名。

文件上传也需要用AsyncTask异步执行。在这个例子中,关于文件上传是这样设计的,当点击Upload按钮时,就会用AsyncTask异步上传一个本地文件到URL指定的程序,具体代码是这样的。

异步执行的代码中先准备好本地文件,这个文件就是前面下载到本地的某个文件,然后上传文件。上传完成后,会在下面的文本框中显示上传文件的信息,内容是该文件上传到服务器上后的新文件名。

如果文件上传需要的时间比较长,就需要显示上传进度。下面,我们用一个ProgressDialog进度对话框来显示文件上传进度,界面就像下面这样。

上传进度等于已上传字节数 / 文件总长度,文件总长度从文件输入流得到,已上传字节数在上传文件内容的循环中统计。具体代码是在基本的文件上传流程的基础上,添加统计进度的代码,也就是红色代码部分。每上传一块数据,就更新一下上传进度。上传进度要显示在进度对话框中,也是通过AsyncTask的publishProgress方法,把进度传递到在主线程执行的onProgressUpdate方法,在这个方法中更新进度对话框。

猜你喜欢

转载自blog.csdn.net/nanoage/article/details/127842909