深入理解Android網絡編程(一)
一、首先介紹一下我們可能一直以來疑惑的問題,就是TCP,IP,HTTP,SOCKET區別和聯繫?
1、網絡由下往上分爲:
物理層--
數據鏈路層--
網絡層-- IP協議
傳輸層-- TCP協議
會話層--
表現成--
應用層-- HTTP協議
2、socket則是對TCP/IP協議的封裝和應用(也可說是TCP/IP層上的一個抽象接口體統程序員使用TCP/IP協議)。socket一旦建立連接雙方都可以自由通信
TPC/IP協議是傳輸層協議,主要解決數據 如何在網絡中傳輸。
而HTTP是應用層協議,主要解決如何包裝數據。每次只能一問一答得方式去交流,不能聯繫
關於TCP/IP和HTTP協議的關係,網絡有一段比較容易理解的介紹:
“我們在傳輸數據時,可以只使用(傳輸層)TCP/IP協議,但是那樣的話,如 果沒有應用層,便無法識別數據內容,如果想要使傳輸的數據有意義,則必須使用到應用層協議,應用層協議有很多,比如HTTP、FTP、TELNET等,也 可以自己定義應用層協議。WEB使用HTTP協議作應用層協議,以封裝HTTP文本信息,然後使用TCP/IP做傳輸層協議將它發到網絡上。”
我們平時說的最多的socket是什麼呢,實際上socket是對TCP/IP協議的封裝,Socket本身並不是協議,而是一個調用接口(API),通過Socket,我們才能使用TCP/IP協議。 實際上,Socket跟TCP/IP協議沒有必然的聯繫。Socket編程接口在設計的時候,就希望也能適應其他的網絡協議。所以說,Socket的出現 只是使得程序員更方便地使用TCP/IP協議棧而已,是對TCP/IP協議的抽象,從而形成了我們知道的一些最基本的函數接口,比如create、 listen、connect、accept、send、read和write等等。網絡有一段關於socket和TCP/IP協議關係的說法
3、什麼是TCP連接的三次握手
第一次握手:客戶端發送syn包(syn=j)到服務器,並進入SYN_SEND狀態,等待服務器確認;
第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;
第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。
握手過程中傳送的包裏不包含數據,三次握手完畢後,客戶端與服務器才正式開始傳送數據。理想狀態下,TCP連接一旦建立,在通信雙方中的任何一方主動關閉 連接之前,TCP 連接都將被一直保持下去。斷開連接時服務器和客戶端均可以主動發起斷開TCP連接的請求,斷開過程需要經過“四次握手”(過程就不細寫了,就是服務器和客 戶端交互,最終確定斷開)
4、利用Socket建立網絡連接的步驟
建立Socket連接至少需要一對套接字,其中一個運行於客戶端,稱爲ClientSocket ,另一個運行於服務器端,稱爲ServerSocket 。
套接字之間的連接過程分爲三個步驟:服務器監聽,客戶端請求,連接確認。
1。服務器監聽:服務器端套接字並不定位具體的客戶端套接字,而是處於等待連接的狀態,實時監控網絡狀態,等待客戶端的連接請求。
2。客戶端請求:指客戶端的套接字提出連接請求,要連接的目標是服務器端的套接字。爲此,客戶端的套接字必須首先描述它要連接的服務器的套接字,指出服務器端套接字的地址和端口號,然後就向服務器端套接字提出連接請求。
3。連接確認:當服 務器端套接字監聽到或者說接收到客戶端套接字的連接請求時,就響應客戶端套接字的請求,建立一個新的線程,把服務器端套接字的描述發給客戶端,一旦客戶端 確認了此描述,雙方就正式建立連接。而服務器端套接字繼續處於監聽狀態,繼續接收其他客戶端套接字的連接請求。
5、HTTP鏈接的特點
HTTP協議即超文本傳送協議(Hypertext Transfer Protocol ),是Web聯網的基礎,也是手機聯網常用的協議之一,HTTP協議是建立在TCP協議之上的一種應用。
HTTP連接最顯著的特點是客戶端發送的每次請求都需要服務器回送響應,在請求結束後,會主動釋放連接。從建立連接到關閉連接的過程稱爲“一次連接”。
6、TCP和UDP的區別
1。TCP是面向鏈 接的,雖然說網絡的不安全不穩定特性決定了多少次握手都不能保證連接的可靠性,但TCP的三次握手在最低限度上(實際上也很大程度上保證了)保證了連接的 可靠性;而UDP不是面向連接的,UDP傳送數據前並不與對方建立連接,對接收到的數據也不發送確認信號,發送端不知道數據是否會正確接收,當然也不用重 發,所以說UDP是無連接的、不可靠的一種數據傳輸協議。
2。也正由於1所說的特點,使得UDP的開銷更小數據傳輸速率更高,因爲不必進行收發數據的確認,所以UDP的實時性更好。
知道了TCP和 UDP的區別,就不難理解爲何採用TCP傳輸協議的MSN比採用UDP的QQ傳輸文件慢了,但並不能說QQ的通信是不安全的,因爲程序員可以手動對UDP 的數據收發進行驗證,比如發送方對每個數據包進行編號然後由接收方進行驗證啊什麼的,即使是這樣,UDP因爲在底層協議的封裝上沒有采用類似TCP的“三次握手”而實現了TCP所無法達到的傳輸效率。
二、下面介紹我們Android中如何通過http通信的幾種方法?
1、使用java的 java.net下提供了API訪問HTTP服務。
創建URL以及URLConnection/HttpURLConnection對象,設置連接參數 ,連接到服務器 ,向服務器寫數據 ,從服務器讀取數據
使用Java.net.* 例子:
import java.net.*;
public class javaNetTest{
public static void main(String args[]){
try {
//定義地址
URL url=new URL("http://www.baidu.com");
//打開連接
HttpURLConnection http=(HttpURLConnection)url.openConnection();
//得到連接狀態
int RC=http.getResponseCode();
if(RC==HttpURLConnection.HTTP_OK)
{
//取得數據
InputStream is = http.getInputStream();
//處理數據
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}|
}
2、Android SDK附帶了Apache的HttpClient API
例子:
在XML中加入單元測試類庫
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.test"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<!-- 配置測試要使用的類庫 -->
<uses-library android:name="android.test.runner"/>
</application>
<!-- 配置測試設備的主類和目標包 -->
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.scott.http"/>
<!-- 訪問HTTP服務所需的網絡權限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-sdk android:minSdkVersion="8" />
</manifest>
在Android項目中我們就可以不用創建一個Activity,創建一個TestHttpClient.java就可以
package com.scot.http.test;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import junit.framework.Assert;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
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.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import android.test.AndroidTestCase;
public class HttpTest extends AndroidTestCase {
private static final String PATH = "http://192.168.1.57:8080/web";
public void testGet() throws Exception {
HttpClient client = new DefaultHttpClient();
HttpGet get = new HttpGet(PATH + "/TestServlet?id=1001&name=john&age=60");
HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
InputStream is = response.getEntity().getContent();
String result = inStream2String(is);
Assert.assertEquals(result, "GET_SUCCESS");
}
}
public void testPost() throws Exception {
HttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost(PATH + "/TestServlet");
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("id", "1001"));
params.add(new BasicNameValuePair("name", "john"));
params.add(new BasicNameValuePair("age", "60"));
HttpEntity formEntity = new UrlEncodedFormEntity(params);
post.setEntity(formEntity);
HttpResponse response = client.execute(post);
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
InputStream is = response.getEntity().getContent();
String result = inStream2String(is);
Assert.assertEquals(result, "POST_SUCCESS");
}
}
public void testUpload() throws Exception {
InputStream is = getContext().getAssets().open("books.xml");
HttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost(PATH + "/UploadServlet");
InputStreamBody isb = new InputStreamBody(is, "books.xml");
MultipartEntity multipartEntity = new MultipartEntity();
multipartEntity.addPart("file", isb);
multipartEntity.addPart("desc", new StringBody("this is description."));
post.setEntity(multipartEntity);
HttpResponse response = client.execute(post);
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
is = response.getEntity().getContent();
String result = inStream2String(is);
Assert.assertEquals(result, "UPLOAD_SUCCESS");
}
}
//將輸入流轉換成字符串
private String inStream2String(InputStream is) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = -1;
while ((len = is.read(buf)) != -1) {
baos.write(buf, 0, len);
}
return new String(baos.toByteArray());
}
}
服務端代碼
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("doGet method is called.");
String id = request.getParameter("id");
String name = request.getParameter("name");
String age = request.getParameter("age");
System.out.println("id:" + id + ", name:" + name + ", age:" + age);
response.getWriter().write("GET_SUCCESS");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("doPost method is called.");
String id = request.getParameter("id");
String name = request.getParameter("name");
String age = request.getParameter("age");
System.out.println("id:" + id + ", name:" + name + ", age:" + age);
response.getWriter().write("POST_SUCCESS");
}
上面兩個是最基本的GET請求和POST請求,參數都是文本數據類型,能滿足普通的需求,不過在有的場合例如我們要用到上傳文件的時候,就不能使用基本的GET請求和POST請求了,我們要使用多部件的POST請求。下面介紹一下如何使用多部件POST操作上傳一個文件到服務端。
由於Android附帶的HttpClient版本暫不支持多部件POST請求,所以我們需要用到一個HttpMime開源項目,該組件是專門處理與MIME類型有關的操作。因爲HttpMime是包含在HttpComponents 項目中的,所以我們需要去apache官方網站下載HttpComponents,然後把其中的HttpMime.jar包放到項目中去
我們用HttpMime提供的InputStreamBody處理文件流參數,用StringBody處理普通文本參數,最後把所有類型參數都加入到一個MultipartEntity的實例中,並將這個multipartEntity設置爲此次POST請求的參數實體,然後執行POST請求。服務端Servlet代碼如下:
package com.scott.web.servlet;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
@SuppressWarnings("serial")
public class UploadServlet extends HttpServlet {
@Override
@SuppressWarnings("rawtypes")
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
boolean isMultipart = ServletFileUpload.isMultipartContent(request);
if (isMultipart) {
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
try {
List items = upload.parseRequest(request);
Iterator iter = items.iterator();
while (iter.hasNext()) {
FileItem item = (FileItem) iter.next();
if (item.isFormField()) {
//普通文本信息處理
String paramName = item.getFieldName();
String paramValue = item.getString();
System.out.println(paramName + ":" + paramValue);
} else {
//上傳文件信息處理
String fileName = item.getName();
byte[] data = item.get();
String filePath = getServletContext().getRealPath("/files") + "/" + fileName;
FileOutputStream fos = new FileOutputStream(filePath);
fos.write(data);
fos.close();
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
}
response.getWriter().write("UPLOAD_SUCCESS");
}
}
服務端使用apache開源項目FileUpload進行處理,所以我們需要commons-fileupload和commons-io這兩個項目的jar包
我們需要考慮一個問題,在實際應用中,我們不能每次都新建HttpClient,而是應該只爲整個應用創建一個HttpClient,並將其用於所有HTTP通信。此外,還應該注意在通過一個HttpClient同時發出多個請求時可能發生的多線程問題。針對這兩個問題,我們需要改進一下我們的項目:
1.擴展系統默認的Application,並應用在項目中。
2.使用HttpClient類庫提供的ThreadSafeClientManager來創建和管理HttpClient。
package com.scott.http;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import android.app.Application;
public class MyApplication extends Application {
private HttpClient httpClient;
@Override
public void onCreate() {
super.onCreate();
httpClient = this.createHttpClient();
}
@Override
public void onLowMemory() {
super.onLowMemory();
this.shutdownHttpClient();
}
@Override
public void onTerminate() {
super.onTerminate();
this.shutdownHttpClient();
}
//創建HttpClient實例
private HttpClient createHttpClient() {
HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
HttpProtocolParams.setUseExpectContinue(params, true);
SchemeRegistry schReg = new SchemeRegistry();
schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schReg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);
return new DefaultHttpClient(connMgr, params);
}
//關閉連接管理器並釋放資源
private void shutdownHttpClient() {
if (httpClient != null && httpClient.getConnectionManager() != null) {
httpClient.getConnectionManager().shutdown();
}
}
//對外提供HttpClient實例
public HttpClient getHttpClient() {
return httpClient;
}
}
我們重寫了onCreate()方法,在系統啓動時就創建一個HttpClient;重寫了onLowMemory()和onTerminate()方法,在內存不足和應用結束時關閉連接,釋放資源。需要注意的是,當實例化DefaultHttpClient時,傳入一個由ThreadSafeClientConnManager創建的一個ClientConnectionManager實例,負責管理HttpClient的HTTP連接。
然後,想要讓我們這個加強版的“Application”生效,需要在AndroidManifest.xml中做如下配置:
<application android:name=".MyApplication" ...>
...
</application>
如果我們沒有配置,系統默認會使用android.app.Application,我們添加了配置,系統就會使用我們的com.scott.http.MyApplication,然後就可以在context中調用getApplication()來獲取MyApplication實例。
有了上面的配置,我們就可以在活動中應用了,HttpActivity.java代碼如下
package com.scott.http;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class HttpActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
execute();
}
});
}
private void execute() {
try {
MyApplication app = (MyApplication) this.getApplication(); //獲取MyApplication實例
HttpClient client = app.getHttpClient(); //獲取HttpClient實例
HttpGet get = new HttpGet("http://192.168.1.57:8080/web/TestServlet?id=1001&name=john&age=60");
HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
InputStream is = response.getEntity().getContent();
String result = inStream2String(is);
Toast.makeText(this, result, Toast.LENGTH_LONG).show();
}
} catch (Exception e) {
e.printStackTrace();
}
}
//將輸入流轉換成字符串
private String inStream2String(InputStream is) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = -1;
while ((len = is.read(buf)) != -1) {
baos.write(buf, 0, len);
}
return new String(baos.toByteArray());
}
}
總結一下:
Android通過http實現網絡通信有兩種方式
一 、 通過 java.net.*API
①先創建一個URL url=new URl("http://www.baidu.com");對象
②創建httpURLConnection http = (HttpURLConnection)url.openConnection();打開連接
http.getResponseCode獲取請求碼,
③ 獲取InputStream is = http.getInputStream()
二、通過Apache 的HttpClient客戶端,默認Android中SDK就附帶
①HttpClient httpclient = new DefaultHttpClient();
②
1)如何是Get方式創建HttpCet get = new HttpGet(PATH+ "/TestServlet?id=1001&name=john");
2)如果是POST方式創建HttpPost post = new HttpPost(PATH+"/TestSevlet");
POST方式要創建一個表單實體對象封裝數據,
先創建一個參數列表,用到了NameValuePair對象
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("id","1000"));
params.add(new BasicNameValuePair("age","10")),;
params.add(new BasicNameValuePair("Name","john"));
HttpEntity formEntity = new UrlEncodedFormEntity(params);
post.setEntity(formEntity);
③httpClient.execute(get)或httpClient.execute(post); 返回一個HttpResponse對象
④通過HttpResponse對象獲取服務端返回來的數據流
InputStream is =response.getEntity().getContent();
如何傳來的是字符串,我們將流轉化會字符串,
String result = inStream2String(is);
如果文件上傳我們還得藉助於Apache的第三方包 HttpMime.jar來實現文件類型上傳
而針對Android程序我只用一個HttpClient對象發送多個請求是,會出現多線程問題,這裏用ThreadSafeClientConnManager實現