使用HTTP通路網絡資源
前面介紹了 URLConnection己經可以非常友善地與指定站點交換資訊,URLConnection還有一個子類:HttpURLConnection,HttpURLConnection 在 LIRLConnection的基礎上做了進一步改進,增加了一些用于操作http資源的便捷方法。
1.使用HttpURLConnection
HttpURLConnection繼承了URLConnection,是以也可用于向指定網站發送GET請求 POST請求。它在URLConnection的基礎上提供了如下便捷的方法。
1) Int getResponseCode():擷取伺服器的響應代碼。
2) String getResponseMessage():擷取伺服器的響應消息。
3) String getRequestMethod():擷取發送請求的方法。
4) void setRequestMethod(String method):設定發送請求的方法。
下面通過個實用的示例來示範使用HttpURLConnection實作多線程下載下傳。
1.1執行個體:多線程下載下傳
使用多線程下載下傳檔案可以更快地完成檔案的下載下傳,因為用戶端啟動多個線程進行下寒意味着伺服器也需要為該用戶端提供相應的服務。假設伺服器同時最多服務100個使用者,伺服器中一條線程對應一個使用者,100條線程在計算機内并發執行,也就是由CPU劃分史 片輪流執行,如果A應用使用了 99條線程下載下傳檔案,那麼相當于占用了 99個使用者的資源自然就擁有了較快的下載下傳速度。
提示:實際上并不是用戶端并發的下載下傳線程越多,程式的下載下傳速度就越快,因為當用戶端開啟太多的并發線程之後,應用程式需要維護每條線程的開銷、線程同步的開銷,這些開銷反而會導緻下載下傳速度降低.
1.2為了實作多線程下載下傳,程式可按如下步驟進行:
Ø 建立URL對象。
Ø 擷取指定URL對象所指向資源的大小(由getContentLength()方法實作),此處用了 HttpURLConnection 類。
Ø 在本地磁盤上建立一個與網絡資源相同大小的空檔案。
Ø 計算每條線程應該下載下傳網絡資源的哪個部分(從哪個位元組開始,到哪個位元組結束,依次建立、啟動多條線程來下載下傳網絡資源的指定部分。
1.2該程式提供的下載下傳工具類代碼如下。
package com.jph.net;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Description:
* 建立ServerSocket監聽的主類
* @author jph
* Date:2014.08.27
*/
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.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個位元組,表明該線程隻下載下傳自己負責哪部分檔案。
inStream.skip(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();
}
}
}
}
上而的DownUtil工具類中包括一個DownloadThread内部類,該内部類的run()方法中負責打開遠端資源的輸入流,并調用inputStream的skip(int)方法跳過指定數量的位元組,這樣就讓該線程讀取由它自己負責下載下傳的部分。
提供了上面的DownUtil工具類之後,接下來就可以在Activity中調用該DownUtil類來執行下載下傳任務,該程式界面中包含兩個文本框,一個用于輸入網絡檔案的源路徑,另一個用于指定下載下傳到本地的檔案的檔案名,該程式的界面比較簡單,故此處不再給出界面布局代碼。該程式的Activity代碼如下。
package com.jph.net;
import java.util.Timer;
import java.util.TimerTask;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;
/**
* Description:
* 多線程下載下傳
* @author jph
* Date:2014.08.27
*/
public class MultiThreadDown extends Activity
{
EditText url;
EditText target;
Button downBn;
ProgressBar bar;
DownUtil downUtil;
private int mDownStatus;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.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 OnClickListener()
{
@Override
public void onClick(View v)
{
// 初始化DownUtil對象(最後一個參數指定線程數)
downUtil = new DownUtil(url.getText().toString(),
target.getText().toString(), 6);
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)
{
showToastByRunnable(MultiThreadDown.this, "下載下傳完成", 2000);
timer.cancel();
}
}
}, 0, 100);
}
}.start();
}
});
}
/**
* 在非UI線程中使用Toast
* @param context 上下文
* @param text 用以顯示的消息内容
* @param duration 消息顯示的時間
* */
private void showToastByRunnable(final Context context, final CharSequence text, final int duration) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, text, duration).show();
}
});
}
}
上面的Activity不僅使用了 DownUtil來控制程式下載下傳,而且程式還啟動了一個定時器,該定時器控制每隔0.1秒査詢一次下載下傳進度,并通過程式中的進度條來顯示任務的下載下傳進度。
該程式不僅需要通路網絡,還需要通路系統SD卡,在SD卡中建立檔案,是以必須授予該程式通路網絡、通路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"/>
程式運作效果圖:
提示:上面的程式已經實作了多線程下載下傳的核心代碼,如果要實作斷點下載下傳,則還需要額外增加一個配置檔案(大家可以發現所有斷點下載下傳工具都會在下載下傳開始生成兩個檔案:一個是與網絡資源相同大小的空檔案,一個是配置檔案),該配置檔案分别記錄每個線程已經下載下傳到了哪個位元組,當網絡斷開後再次開始下載下傳時,每個線程根據配置檔案裡記錄的位置向後下載下傳即可。
2 使用ApacheHttpClient
在一般情況下,如果隻是需要向Web站點的某個簡單頁面送出請求并擷取伺服器響應, 完全可以使用前面所介紹的HttpURLConnection來完成。但在絕大部分情況下,Web站點的網頁可能沒這麼簡單,這些頁面并不是通過一個簡單的URL就可通路的,可能需要使用者登入而且具有相應的權限才可通路該頁面。在這種情況下,就需要涉及Session、Cookie的處理了,如果打算使用HttpURLConnection來處理這些細節,當然也是可能實作的,隻是處理起來難度就大了。
為了更好地處理向Web站點請求,包括處理Session、Cookie等細節問題,Apache開源組織提供了一個HttpClient項目,看它的名稱就知道,它是一個簡單的HTTP用戶端(并不是浏覽器),可以用于發送HTTP請求,接收HTTP響應。但不會緩存伺服器的響應,不能執行HTML頁面中嵌入的JavaScript代碼;也不會對頁面内容進行任何解析、處理。
提示:簡單來說,HttpClient就是一個增強版的HttpURLConnection ,HttpURLConnection可以做的事情 HttpClient全部可以做;HttpURLConnection沒有提供的有些功能,HttpClient也提供了,但它隻是關注于如何發送請求、接收響應,以及管理HTTP連接配接。|
Android內建了HttpClient,開發人員可以直接在Android應用中使用HttpCHent來通路送出請求、接收響應。
2.1使用HttpClient發送清求、接收響應很簡單,隻要如下幾步即可:
1) 建立HttpClient對象。
2) 如果需要發送GET請求,建立HttpGet對象;如果需要發送POST 求,建立HttpPost對象。
3) 如果需要發送請求參數,可調用HttpGet、HttpPost共同的setParams(HttpParams params)方法來添加請求參數;對于HttpPost對象而言,也可調用setEntity(HttpEntityentity)方法來設定請求參數。
4) 調用HttpClient對象的execute(HttpUriRequestrequest)發送請求,執行該方法傳回一 個 HttpResponse。
5) 調用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可擷取伺服器的響應頭;調用HttpResponse的getEntity()方法可擷取HttpEntity對象,該對象包裝了伺服器的響應内容。程式可通過該對象擷取伺服器的響應内容。
2.2執行個體:通路被保護資源
下面的Android應用需要向指定頁面發送請求,但該頁面并不是一個簡單的頁面,隻有 當使用者已經登入,而且登入使用者的使用者名是jph時才可通路該頁面。如果使用HttpUrlConnection來通路該頁面,那麼需要處理的細節就太複雜了。下面将會借助于 HttpClient來通路被保護的頁面。
通路Web應用中被保護的頁面,如果使用浏覽器則十分簡單,使用者通過系統提供的登入頁面登入系統,浏覽器會負責維護與伺服器之間的Session,如果使用者登入的使用者名、密碼符合要求,就可以通路被保護資源了。
為了通過HttpClient來通路被保護頁面,程式同樣需要使用HttpClient來登入系統,隻要應用程式使用同一個HttpClient發送請求,HttpClient會自動維護與伺服器之間的Session狀态,也就是說程式第一次使用HttpCHent登入系統後,接下來使用HttpCHent即可通路被保護頁面了。
提示:雖然此處給出的執行個體隻是通路被保護的頁面,但通路其他被保護的資源也與此類似,程式隻要第一次通過HttpClient登入系統,接下來即可通過該HttpClient通路被保護資源了。
2.3程式代碼:
package com.jph.net;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.Html;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
/**
* Description:
* 使用HttpClient通路受保護的網絡資源
* @author jph
* Date:2014.08.28
*/
public class HttpClientDemo extends Activity
{
TextView response;
HttpClient httpClient;
Handler handler = new Handler()
{
public void handleMessage(Message msg)
{
if(msg.what == 0x123)
{
// 使用response文本框顯示伺服器響應
response.append(Html.fromHtml(msg.obj.toString()) + "\n");
}
}
};
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// 建立DefaultHttpClient對象
httpClient = new DefaultHttpClient();
response = (TextView) findViewById(R.id.response);
}
/**
* 此方法用于響應“通路頁面”按鈕
* */
public void accessSecret(View v)
{
response.setText("");
new Thread()
{
@Override
public void run()
{
// 建立一個HttpGet對象
HttpGet get = new HttpGet(
"http://10.201.1.32:8080/HttpClientTest_Web/secret.jsp"); //①
try
{
// 發送GET請求
HttpResponse httpResponse = httpClient.execute(get);//②
HttpEntity entity = httpResponse.getEntity();
if (entity != null)
{
// 讀取伺服器響應
BufferedReader br = new BufferedReader(
new InputStreamReader(entity.getContent()));
String line = null;
while ((line = br.readLine()) != null)
{
Message msg = new Message();
msg.what = 0x123;
msg.obj = line;
handler.sendMessage(msg);
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}.start();
}
/**
* 此方法用于響應“登陸系統”按鈕
* */
public void showLogin(View v)
{
// 加載登入界面
final View loginDialog = getLayoutInflater().inflate(
R.layout.login, null);
// 使用對話框供使用者登入系統
new AlertDialog.Builder(HttpClientDemo.this)
.setTitle("登入系統")
.setView(loginDialog)
.setPositiveButton("登入",
new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog,
int which)
{
// 擷取使用者輸入的使用者名、密碼
final String name = ((EditText) loginDialog
.findViewById(R.id.name)).getText()
.toString();
final String pass = ((EditText) loginDialog
.findViewById(R.id.pass)).getText()
.toString();
new Thread()
{
@Override
public void run()
{
try
{
HttpPost post = new HttpPost("http://10.201.1.32:8080/" +
"HttpClientTest_Web/login.jsp");//③
// 如果傳遞參數個數比較多的話可以對傳遞的參數進行封裝
List<NameValuePair> params = new
ArrayList<NameValuePair>();
params.add(new BasicNameValuePair
("name", name));
params.add(new BasicNameValuePair
("pass", pass));
// 設定請求參數
post.setEntity(new UrlEncodedFormEntity(
params, HTTP.UTF_8));
// 發送POST請求
HttpResponse response = httpClient
.execute(post); //④
// 如果伺服器成功地傳回響應
if (response.getStatusLine()
.getStatusCode() == 200)
{
String msg = EntityUtils
.toString(response.getEntity());
Looper.prepare();
// 提示登入成功
Toast.makeText(HttpClientDemo.this,
msg, Toast.LENGTH_SHORT).show();
Looper.loop();
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}.start();
}
}).setNegativeButton("取消", null).show();
}
}
上面的程式中①、②号粗體字代碼先建立了一個HttpGet對象,接下來程式調用HttpClient的execute()方法發送GET請求;程式中③、④号粗體字代碼先建立了一個HttpPost對象,接下來程式調用了HttpClient的execute()方法發送POST請求。上面的GET請求用于擷取伺服器上的被保護頁面,POST請求用于登入系統。
運作該程式,單擊“通路頁面”按鈕将可看到如下圖所示的頁面。
從上圖可以看出,程式直接向指定Web應用的被保護頁面secret.jsp發送請求,程式将無法通路被保護頁面,于是看到下圖所示的頁面。單擊下圖所示頁面中的“登入”按鈕,系統将會顯示如下圖所示的登入對話框。
在上圖所示對話框的兩個輸入框中分别輸入“jph”、“123”,然後單擊“登入”按鈕,系統将會向Web站點的login.jsp頁面發送POST請求,并将使用者輸入的使用者名、密碼作為請求參數。如果使用者名、密碼正确,即可看到登入成功的提示。
登入成功後,HttpClient将會自動維護與伺服器之間的連接配接,并維護與伺服器之間的Session狀态,再次單擊程式中的“通路頁面”按鈕,即可看到如下圖所示的輸出。
從上圖可以看出,此時使用HttpClient發送GET請求即可正常通路被保護資源,這就是因為前面使用了HttpClient登入了系統,而且HttpClient可以維護與伺服器之間的Session連接配接。
從上面的程式設計過程不難看出,使用Apache的HttpClient更加簡單,而且它比HttpURLConnection提供了更多的功能。