Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android網絡編程學習與實戰之旅一(上傳單個或多個文件)

android網絡編程學習與實戰之旅一(上傳單個或多個文件)

編輯:關於Android編程

1.HttpUrlConnection類概述

HttpUrlConnection是一個HTTP協議的UrlConnection,用於通過web收發數據。數據可以是任意類型和長度。這個類主要用於收發提前不知長度的數據流。

這個類的用法遵循以下模式:

 

首先獲得一個HttpUrlConnection的實例。通過調用URL類中的openConnection()函數。並做強制類型轉換。准備請求。請求的基本屬性是一個URL,請求頭可能包含一些元數據,比如:證書, 首選的內容類型,session和cookie等。可選的上傳一個請求體。HttpUrlConnection的實例如果包含一個請求體的話,必須使用setDoOutput(true)函數設置一下。然後可以通過getOutPutStream獲得一個輸出流,向流中寫入數據即可傳輸數據。讀響應。典型響應頭包含著這樣一些元數據,比如:1.響應體的內容類型和長度,2.修改的日期,3.session和cookie等。響應體可以從getInputStread()返回的流中讀取。如果響應沒有響應體,getInputStread()返回為空。斷開連接。一旦響應體被讀取,HttpUrlConnection應該通過調用disconnect()方法來關閉。這個關閉會釋放這個連接所持有的資源。

根據上述要求,初步寫如下實驗程序,這個程序的功能就是向服務器發送一個請求,並獲得服務器的響應,把這些響應顯示在一個TextView中。

 

public class MainActivity extends AppCompatActivity {
    URL url = null;
    TextView textView;
    Handler handler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.text);
        Log.d("hello","oncreate");
        handler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                Log.d("hello",msg.getData().getString("name"));
                textView.setText(msg.getData().getString("name"));
                return true;
            }
        });
        new Thread(new Runnable() {
            HttpURLConnection connection;
            @Override
            public void run() {
                try {
                    url = new URL("https://www.baidu.com/?tn=57095150_1_oem_dg");
                    connection = (HttpURLConnection) url.openConnection();
                    InputStream in = new BufferedInputStream(connection.getInputStream());
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
                    StringBuilder stringBuilder = new StringBuilder();
                    String line;
                    while ((line=bufferedReader.readLine())!=null){
                        stringBuilder.append(line);
                    }
                    Message message = new Message();
                    Bundle bundle= new Bundle();
                    bundle.putString("name",stringBuilder.toString());
                    message.setData(bundle);
                    handler.sendMessage(message);
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    connection.disconnect();
                }
            }
        }).start();
    }
}
這段代碼訪問https://www.baidu.com/?tn=57095150_1_oem_dg頁面,並把獲得的內容放到Textview中顯示。需要注意的幾點:

 

Android的Ui線程不可以訪問網絡等耗時的工作,所以這裡將其放在一個子線程中。TextView一頁可能顯示不完,最好把TextView放到一個ScrollView中,這樣就可以向下滾動查看所有內容了。使用Handler和Message更新UI.不要忘記添加網絡訪問權限:

<uses-permission android:name="android.permission.INTERNET">uses-permission>

1.2.基於HTTPS安全連接

首先也是使用openConnection()方法獲得一個HttpsURLConnection實例。這個URL當然是使用“https://”開頭的URL.它允許覆寫

HostnameVerifierSSLSocketFactory接口。一個支持SSLSocketFactory的應用程序可以提供一個自定義的X509TrustManager,

用於證書鏈的驗證,並且一個自定義的X509KeyManager用於客戶端的驗證。

1.3.發送內容

為了上傳數據到服務器,配置連接為輸出,使用setDoOutput(true),為了更高實現更好的額性能,是使用setFixedLengthStreamingMode(int)

設置數據長度,如果數據長度提前可以知道的話,或者使用setChunkedStreamingMode(int)當不知道數據長度的時候。否則,

HttpUrlConnection將會緩沖完整的請求體,緩沖完成後才會發送,這導致了內存的浪費和延遲的增加。

2.上傳一張或者多張圖片到服務器

通過以上理論知識的學習,上傳一張圖片到服務器成為可能,為了完成這個功能,首先需要解決以下問題:

向哪個服務器上傳?服務器怎麼接收?

解決方法是:自己搭建Apache服務器,接收使用php好了,因為php之前略有涉略,其他的Web服務器端的語言均一無所知。

 

上傳的文件在哪裡呢?簡單起見,就是放在assets目錄下的文件。assets目錄下的文件可以使用AssetManager進行管理,非常方便,為了使用assets目錄,在實現上傳文件功能之前,先做這樣一件嘗試:

2.1把assets目錄下的一張圖片顯示到ImageView中

這是很簡單的一步,為了項目的完整,還是把它貼出來,這是一個讀assets目錄下文件的方法:

 

    public Bitmap readAssetsFile(String name){

        Bitmap image = null;
        InputStream inputStream = null;
        try {
            inputStream = getResources().getAssets().open(name);
            image = BitmapFactory.decodeStream(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if(inputStream != null) try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return image;
    }

2.2 php保存上傳的文件

 

因為我對php只是了解性的學習過,所以這部分也單獨拿出來,先把它做好,然後再往下做。這個過程分為以下幾步:

先搭建好服務器,建議直接裝wamp,這個比較簡單,然後啟動它。使用html給上傳文件,並用php腳本接受文件,確保php可以正常工作。編寫android端代碼並且測試。

服務器的搭建這裡就不啰嗦了,啟動它也沒啥好說的,現在測試使用html上傳文件,並用php接受的代碼。

 

html的代碼如下:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>file upload</title>
</head>
    <body>
        <form action="http://127.0.0.1:8088/php/fileupload.php" method="post" enctype="multipart/form-data">
        <input type="file" name="myfile" class="file" id="fileField" onchange="document.getElementById('textfield').value=this.value" />
        <input type="submit" name="submit" value="上傳" />
    </form>
    </body>
</html>


這個代碼非常簡單,就是使用一個表單上傳文件。它的樣子是這樣的:

\

 

php代碼如下:

<pre name="code" class="php"><?php
    echo "file arrived";
	echo "Upload: ".$_FILES["myfile"]["name"]."<br />";
	echo "Type: ".$_FILES["myfile"]["type"]."<br />";
	echo "Size: ".($_FILES["myfile"]["size"]/1024)." Kb<br />";
	echo "Temp file: " . $_FILES["myfile"]["tmp_name"]."<br />";
    if (file_exists("D:/wamp/www/"."upload/" . $_FILES["myfile"]["name"]))
      {
      echo $_FILES["myfile"]["name"]." already exists. ";
      }
    else
      {
      if(move_uploaded_file($_FILES["myfile"]["tmp_name"],"D:/wamp/www/"."upload/" . $_FILES["myfile"]["name"])){
		  echo "Stored in: " . "upload/" . $_FILES["myfile"]["name"];
	  }else{
		  echo "Store failed";
	  }
      
      }
?>


這段代碼中,為了方便調錯,首先將上傳文件的信息都打印出來。然後 判斷這個文件在服務器的upload目錄下存在不?存在就什麼都不做,不存在就把它存儲下來。這裡要注意:文件的路勁要用絕對路勁,不然會報錯,什麼原因這裡就不深究了,反正加上絕對路勁就可以了。就這樣簡簡單單的一些代碼,就實現了文件的上傳與保存。這些代碼主要用來測試服務器端可以正常的接受並且保存文件。有了這些驗證,我們就可以安心的專注於Android端的代碼了。

 

這裡把這段代碼接受到上傳文件後頁面與文件上傳到服務器的情景給個交代:

\\

 

2.3android端上傳文件的代碼

在正式寫代碼之前,先回顧一下第一小節的基礎內容。通過第一小節的學習,知道了怎麼訪問一個服務器,怎麼獲取服務器的響應,怎麼發送數據塊給服務器。可以想象一下,上傳一張圖片就是把一個數據塊發送給服務器,所以這裡的知識已經基本具備了。但是為了更好的理解HttpUrlConnection這個類,在看看這個類的API文檔,耐心的把它看完,寫代碼是一件不用著急的事情。

2.3.1性能

為了能寫出比較好的代碼,學習如何提高性能是很有必要的。除了1.3介紹的設置發送的內容的長度或明確告知不知道數據內容長度外,還需要注意什麼呢?

這個類返回的輸入輸出流是不緩沖存。大部分時候都需要把輸入輸出流包裝為緩沖流。如果是直接簡單的對大塊數據進行讀寫,也可以忽略緩存。為了減少延遲,這個類可能會為多個請求/ 響應對使用相同的底層的socket。這導致http連接會保持打開狀態很長時間,所以當不要連接的時候。通過設置http.keepAlive系統屬性為false,這種行為可以被禁止。使用http.maxConnections可以設置每個連接到服務器的最大的空閒連接的數目。默認的,這個類請求服務器的時候會使用gzip自動壓縮,並且當調用者調用getInputSream時會自動解壓縮。這種情況下,Content-Encoding 和Content-Length要被聲明,gzip壓縮可以被關閉使用如下方法:

urlConnection.setRequestProperty("Accept-Encoding", "identity");
getContentLength()可以獲得傳輸的字節數,但是不能被用作獲取getInputStread中可被讀取的字節數。取而代之的是,應該使用read()方法,直到它返回-1。表明數據讀完。

2.3.2處理登錄

一些wifi網絡阻塞用戶訪問因特網,直到用戶通過登錄頁面。這種登錄頁面典型的代表是使用http重定向。你可以使用getUrl()方法可以堅持連接有沒有被意外的重定向。這種檢查在已經接受到響應頭以後就不可用了,這裡的響應頭的獲取可以使用

 

getHeaderFields() 或者getInputStream()方法。比如,為了檢查一個響應有沒有重定向到另外一個主機,可以像如下這樣:

 

  HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
   try {
     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
     if (!url.getHost().equals(urlConnection.getURL().getHost())) {
       // we were redirected! Kick the user out to the browser to sign on?
     
     ...
   } finally {
     urlConnection.disconnect();
   }
 }

2.3.2HTTP認證

 

HttpUrlConnection支持HTTP基本的認證功能。使用Authenticator支持虛擬機級別的認證處理:

 

   Authenticator.setDefault(new Authenticator() {
     protected PasswordAuthentication getPasswordAuthentication() {
       return new PasswordAuthentication(username, password.toCharArray());
     
   });
 }
除非使用HTTPS,否則,這不是一種安全的用戶認證方式。特別是,用戶名,密碼,請求,響應都沒有被加密卻在網絡間傳遞。

2.4rfc1867協議

 

http原本不支持文件上傳,為了支持文件上傳,rfc1867協議對其做了修改,主要設計兩方面:請求頭的修改和請求體的修改。

2.4.1請求頭的修改

Content-Type由原來的

 

Content-Type:application/x-www-form-urlencoded

改為了:

Content-Type:multipart/form-data; boundary=-------7d71f4234700b8

其中,mutipart/form-data;是必須的,boundary再請求體中用到,它會把請求體分成多個塊,一塊代表一塊獨立的數據,或者說是文件吧。

 

2.4.2請求體的修改

 

-----------------------------7d71f4234700b8
Content-Disposition: form-data; name="formhash"

59329e15
-----------------------------7d71f4234700b8
Content-Disposition: form-data; name="isblog"


-----------------------------7d71f4234700b8
Content-Disposition: form-data; name="fid"

104
-----------------------------7d71f4234700b8
可以看到請求體編程了多個塊,每一塊可以代表一個獨立的文件。這裡的name和php中$_FILES_[name]保持一致,也和input標簽的name屬性的值保持一致。後面還可加文件名,文件長度等信息。
 

 

 

2.5正式寫android代碼

2.5.1上傳一個文件

android的代碼主要難點是設置http和rfc1867協議的一些參數,那現在就以下面一個完整的POST請求的格式為例,逐條配置android代碼:

 

POST /upload_file/UploadFile HTTP/1.1 
Accept: text/plain, */* 
Accept-Language: zh-cn 
Host: 192.168.29.65:80 
Content-Type:multipart/form-data;boundary=---------------------------7d33a816d302b6 
User-Agent: Mozilla/4.0 (compatible; OpenOffice.org) 
Content-Length: 424 
Connection: Keep-Alive -----------------------------7d33a816d302b6 
Content-Disposition:form-data; 
name="userfile1"; 
filename="E:\s"Content-Type: 
application/octet-stream abbXXXccc 
-----------------------------7d33a816d302b6 

Content-Disposition: form-data; 

name="text1" foo 

-----------------------------7d33a816d302b6 

Content-Disposition: form-data; 

name="password1" bar 

-----------------------------7d33a816d302b6-- 
下面的代碼逐條配置了上面模板中的條目,代碼中注釋有很詳細的解釋:

 

 

package com.konka.networktest;

import android.content.Context;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * Created by Jinwei on 2016/6/29.
 */
public  class Utils {
    public static void doupLoadPic(Context context, Handler handler){
        //首先設置一些常量
        final String  end = "\r\n";
        final String twoHyphens = "--";
        final String boundary = "******";
        URL url = null;
        HttpURLConnection httpURLConnection = null;
        //1.創建URL,並且獲取連接
        try {
            url = new URL("http://192.168.0.103:8088/php/fileupload.php");
            httpURLConnection  = (HttpURLConnection) url.openConnection();
        //2.根據這個類的使用要求,要配置一些必要的參數
            httpURLConnection.setChunkedStreamingMode(128 * 1024);// 128K,這個配置可以提高性能
            httpURLConnection.setDoOutput(true);                  //允許輸出。
            httpURLConnection.setDoInput(true);                   //允許讀入,後面我們要讀取服務器端返回的信息
            httpURLConnection.setUseCaches(false);                //不使用緩沖
        //3.設置rfc1867請求頭
            //3.1請求行設置        POST /upload_file/UploadFile HTTP/1.1
            httpURLConnection.setRequestMethod("POST");  //請求行只需設置這一個參數即可
            //3.2        Accept: text/plain, */*
            httpURLConnection.setRequestProperty("Accept","ext/plain, */*");
            //3.3        Accept-Language: zh-cn
            httpURLConnection.setRequestProperty("Accept-Language","zh-cn");
            //3.3        Host: 192.168.0.105:80
            //Host 接受響應的主機,默認就是發起連接的這裡,所以這裡不設置
            //3.4        Content-Type:multipart/form-data;boundary=---------------------------7d33a816d302b6
            httpURLConnection.setRequestProperty("Content-Type","multipart/form-data;boundary="+boundary);
            //3.5        //User-Agent: Mozilla/4.0 (compatible; OpenOffice.org)
            //浏覽器類型,和我們無關,不設置
            //3.6        //Content-Length: 424
            //數據長度,這個類不是可以收發不知長度的數據流嗎?我們不知道長度的話就不設置了。
            //3/7        //Connection: Keep-Alive -----------------------------7d33a816d302b6
            httpURLConnection.setRequestProperty("Connection","Keep-Alive");//這裡要注意,keep-Alive後面的是請求體中的內容了。所以,請求頭就設置完了

        //4,設置請求體
            //4.1獲取輸出流
            DataOutputStream dout = new DataOutputStream(httpURLConnection.getOutputStream());
            //4.2 寫分隔符,先把Keep-Alive後面的事情做完
            dout.writeBytes("--"+boundary+end);
            //4.3 Content-Disposition:form-data;
            dout.writeBytes("Content-Disposition:form-data;");
            //4.4 name="userfile1";
            dout.writeBytes("name="+"\"myfile\";");
            //4.5 filename="E:\s"
            dout.writeBytes("filename="+"\"one.jpg\"");
            //注意這裡之後有兩個回車換行
            dout.writeBytes(end);
            dout.writeBytes(end);
            //4.6 application/octet-stream abbXXXccc,這裡就是正文了,而正文需要讀文件,以下是讀文件的部分
            AssetManager am = context.getAssets();
            InputStream inputStream = am.open("one.jpg");
            //4.7有了文件輸入流後,我們把文件讀出來,寫到http請求體中
            byte[] buffer = new byte[8192]; // 8k
            int count = 0;
            while ((count = inputStream.read(buffer)) != -1) {
                dout.write(buffer, 0, count);
            }//這樣文件就寫完了
            //我們只有一個文件,就這樣就可以了。按照格式,文件結束還要寫入分隔符
            dout.writeBytes(end+"--"+boundary+end);
            //這樣發送部分就做完了。

            //另外,我們還希望接受返回的數據。
            //1.打開讀流
            BufferedReader br  = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(),"utf-8"));
            StringBuilder builder = new StringBuilder();
            String result;
            //2.開始讀入
            while((result = br.readLine())!= null){
                builder.append(result);
            }
            //通知UI更新
            Message message = new Message();
            Bundle bundle= new Bundle();
            bundle.putString("name",builder.toString());
            message.setData(bundle);
            handler.sendMessage(message);

            //最後做一些清理工作
            br.close();
            inputStream.close();
            dout.close();
            httpURLConnection.disconnect();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
這個這個類我完整的貼出來了,這個類中的hander用於通知UI已經收到了數據了。注意,這裡並沒有管文件的類型,服務器端也沒有檢查文件的類型,注意一下,需要的時候把它們加上。

2.5.2上傳多個文件

 

只要會上傳一個文件,而且知道了上傳多個文件就是使用"--"+boundary+"/r/n"把多個文件分開,然後在分割附後添加文件的類型,文件名等信息就可以了,以下列出添加的代碼:

 

         //發送第二個文件
            //boundary已經寫入了,現在寫文件的類型,名字等信息
            //4.3 Content-Disposition:form-data;
            dout.writeBytes("Content-Disposition:form-data;");
            //4.4 name="userfile1";
            dout.writeBytes("name="+"\"myfile1\";");
            //4.5 filename="E:\s"
            dout.writeBytes("filename="+"\"two.jpg\"");
            //注意這裡之後有兩個回車換行
            dout.writeBytes(end);
            dout.writeBytes(end);
            //打開第二個文件的流
            InputStream inputStream1 = am.open("two.jpg");
            while ((count = inputStream1.read(buffer)) != -1) {
                dout.write(buffer, 0, count);
            }//這樣第二個文件就寫完了
            inputStream1.close();
            //寫入分隔符
            dout.writeBytes(end+"--"+boundary+end);

服務器端也只是簡單的把接受一個文件的代碼復制一份:

 

//第二個文件
    echo "second file arrived";
	echo "Upload: ".$_FILES["myfile1"]["name"]."
";
	echo "Type: ".$_FILES["myfile1"]["type"]."
";
	echo "Size: ".($_FILES["myfile1"]["size"]/1024)." Kb
";
	echo "Temp file: " . $_FILES["myfile"]["tmp_name"]."
";
    if (file_exists("D:/wamp/www/"."upload/" . $_FILES["myfile1"]["name"]))
      {
      echo $_FILES["myfile1"]["name"]." already exists. ";
      }
    else
      {
      if(move_uploaded_file($_FILES["myfile1"]["tmp_name"],"D:/wamp/www/"."upload/" . $_FILES["myfile1"]["name"])){
		  echo "Stored in: " . "upload/" . $_FILES["myfile1"]["name"];
	  }else{
		  echo "Store failed";
	  }
      
      }	  

 

 

圖片展示如下:

 

手機端收到的信息:

\

 

 

服務器端多了兩個文件:

\

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved