使用HttpClient发送请求、接收响应很简单,一般需要如下几步即可。
1. 创建HttpClient对象。
2. 创建请求方法的实例,并指定请求URL。如果需要发送GET请求,创建HttpGet对象;如果需要发送POST请求,创建HttpPost对象。
3. 如果需要发送请求参数,可调用HttpGet、HttpPost共同的setParams(HetpParams params)方法来添加请求参数;对于HttpPost对象而言,也可调用setEntity(HttpEntity entity)方法来设置请求参数。
4. 调用HttpClient对象的execute(HttpUriRequest request)发送请求,该方法返回一个HttpResponse。
5. 调用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器的响应内容。程序可通过该对象获取服务器的响应内容。
6. 释放连接。无论执行方法是否成功,都必须释放连接
try {
// 创建一个默认的HttpClient
HttpClient httpclient =new DefaultHttpClient();
// 创建一个GET请求
HttpGet request =new HttpGet("www.google.com");
// 发送GET请求,并将响应内容转换成字符串
String response = httpclient.execute(request, new BasicResponseHandler());
Log.v("response text", response);
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
在实际项目中,我们很可能在多处需要进行HTTP通信,这时候我们不需要为每个请求都创建一个新的HttpClient。现在我们的应用程序使用同一个HttpClient来管理所有的Http请求,一旦出现并发请求,那么一定会出现多线程的问题。这就好像我们的浏览器只有一个标签页却有多个用户,A要上google,B要上baidu,这时浏览器就会忙不过来了。幸运的是,HttpClient提供了创建线程安全对象的API
public class CustomerHttpClient {
private static final String CHARSET = HTTP.UTF_8;
/**
* 最大连接数
*/
public final static int MAX_TOTAL_CONNECTIONS = 800;
/**
* 获取连接的最大等待时间
*/
public final static int WAIT_TIMEOUT = 60000;
/**
* 每个路由最大连接数
*/
public final static int MAX_ROUTE_CONNECTIONS = 400;
/**
* 连接超时时间
*/
public final static int CONNECT_TIMEOUT = 10000;
/**
* 读取超时时间
*/
public final static int READ_TIMEOUT = 10000;
private static HttpClient customerHttpClient;
private CustomerHttpClient() {
}
public static synchronized HttpClient getHttpClient() {
if (null == customerHttpClient) {
HttpParams params = new BasicHttpParams();
// 设置一些基本参数
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params,
CHARSET);
HttpProtocolParams.setUseExpectContinue(params, true);
HttpProtocolParams
.setUserAgent(
params,
"Mozilla/5.0(Linux;U;Android 2.2.1;en-us;Nexus One Build.FRG83) "
+ "AppleWebKit/553.1(KHTML,like Gecko) Version/4.0 Mobile Safari/533.1");
// 超时设置
/* 从连接池中取连接的超时时间 */
ConnManagerParams.setTimeout(params, WAIT_TIMEOUT);
/* 连接超时 */
HttpConnectionParams.setConnectionTimeout(params, CONNECT_TIMEOUT);
/* 请求超时 */
HttpConnectionParams.setSoTimeout(params, READ_TIMEOUT);
// 设置最大连接数
ConnManagerParams.setMaxTotalConnections(params, MAX_TOTAL_CONNECTIONS);
// 设置每个路由最大连接数
ConnPerRouteBean connPerRoute = new ConnPerRouteBean(MAX_ROUTE_CONNECTIONS);
ConnManagerParams.setMaxConnectionsPerRoute(params, connPerRoute);
// 设置我们的HttpClient支持HTTP和HTTPS两种模式
SchemeRegistry schReg = new SchemeRegistry();
schReg.register(new Scheme("http", PlainSocketFactory
.getSocketFactory(), 80));
schReg.register(new Scheme("https", SSLSocketFactory
.getSocketFactory(), 443));
// 使用线程安全的连接管理来创建HttpClient
ClientConnectionManager conMgr = new ThreadSafeClientConnManager(
params, schReg);
customerHttpClient = new DefaultHttpClient(conMgr, params);
}
return customerHttpClient;
}
}
上面的代码提到了3种超时设置,比较容易搞混,HttpClient的3种超时说明
/* 从连接池中取连接的超时时间 */
ConnManagerParams.setTimeout(params, 1000);
/* 连接超时 */
HttpConnectionParams.setConnectionTimeout(params, 2000);
/* 请求超时 */
HttpConnectionParams.setSoTimeout(params, 4000);
ThreadSafeClientConnManager默认使用了连接池
//设置最大连接数
ConnManagerParams.setMaxTotalConnections(httpParams, 10);
//设置最大路由连接数
ConnPerRouteBean connPerRoute = new ConnPerRouteBean(10);
ConnManagerParams.setMaxConnectionsPerRoute(httpParams, connPerRoute);
有了单例的HttpClient对象,我们就可以把一些常用的发出GET和POST请求的代码也封装起来,写进我们的工具类中了。POST请求示例:
private static final String TAG ="CustomerHttpClient";
public static String post(String url, NameValuePair... params) {
try {
// 编码参数
List formparams = new ArrayList(); // 请求参数
for (NameValuePair p : params) {
formparams.add(p);
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams,
HTTP.UTF_8);
// 创建POST请求
HttpPost request =new HttpPost(url);
request.setEntity(entity);
// 发送请求
HttpClient client = getHttpClient();
HttpResponse response = client.execute(request);
if(response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
throw new RuntimeException("请求失败");
}
HttpEntity resEntity = response.getEntity();
return (resEntity ==null) ?null : EntityUtils.toString(resEntity, CHARSET);
} catch (UnsupportedEncodingException e) {
Log.w(TAG, e.getMessage());
return null;
} catch (ClientProtocolException e) {
Log.w(TAG, e.getMessage());
return null;
} catch (IOException e) {
throw new RuntimeException("连接失败", e);
}
}
所谓长连接是指客户端与服务器端一旦建立连接以后,可以进行多次数据传输而不需重新建立连接,而短连接则每次数据传输都需要客户端和服务器端建立一次连接。
长连接的优势在于省去了每次数据传输连接建立的时间开销,能够大幅度提高数据传输的速度,对于P2P应用十分适合。
短连接每次数据传输都需要建立连接,我们知道HTTP协议的传输层协议是TCP协议,TCP连接的建立和释放分别需要进行3次握手和4次握手,频繁的建立连接即增加了时间开销,同时频繁的创建和销毁Socket同样是对服务器端资源的浪费。
对于诸如Web网站之类的B2C应用,并发请求量大,每一个用户又不需频繁的操作的场景下,维护大量的长连接对服务器无疑是一个巨大的考验。而此时,短连接可能更加适用。
而对于需要频繁发送HTTP请求的应用,需要在客户端使用HTTP长连接。
连接池管理的对象是长连接。连接池技术作为创建和管理连接的缓冲池技术,目前已广泛用于诸如数据库连接等长连接的维护和管理中,能够有效减少系统的响应时间,节省服务器资源开销。其优势主要有两个:其一是减少创建连接的资源开销,其二是资源的访问控制。
HTTP连接是无状态的,这样很容易给我们造成HTTP连接是短连接的错觉,实际上HTTP1.1默认即是持久连接,HTTP1.0也可以通过在请求头中设置Connection:keep-alive使得连接为长连接。
没有连接池的概念,多少次请求就会建立多少个IO,在访问量巨大的情况下服务器的IO可能会耗尽。
也有连接池的东西在里头,使用MultiThreadedHttpConnectionManager,大致过程如下:
MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
HttpClient client = new HttpClient(connectionManager);...// 在某个线程中。
GetMethod get = new GetMethod("http://jakarta.apache.org/");
try {
client.executeMethod(get);// print response to stdout
System.out.println(get.getResponseBodyAsStream());
} finally {
// be sure the connection is released back to the connection
managerget.releaseConnection();
}
HTTP Client4.0的ThreadSafeClientConnManager实现了HTTP连接的池化管理,其管理连接的基本单位是Route(路由),每个路由上都会维护一定数量的HTTP连接。这里的Route的概念可以理解为客户端机器到目标机器的一条线路,例如使用HttpClient的实现来分别请求 www.163.com 的资源和 www.sina.com 的资源就会产生两个route。缺省条件下对于每个Route,HttpClient仅维护2个连接,总数不超过20个连接,显然对于大多数应用来讲,都是不够用的,可以通过设置HTTP参数进行调整。
HttpParams params = new BasicHttpParams();
//将每个路由的最大连接数增加到200
ConnManagerParams.setMaxTotalConnections(params,200);
// 将每个路由的默认连接数设置为20
ConnPerRouteBean connPerRoute = new ConnPerRouteBean(20);
// 设置某一个IP的最大连接数
HttpHost localhost = new HttpHost("locahost", 80);
connPerRoute.setMaxForRoute(new HttpRoute(localhost), 50);
ConnManagerParams.setMaxConnectionsPerRoute(params, connPerRoute);
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(
new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schemeRegistry.register(
new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
HttpClient httpClient = new DefaultHttpClient(cm, params);
连接的有效性检测是所有连接池都面临的一个通用问题,大部分HTTP服务器为了控制资源开销,并不会永久的维护一个长连接,而是一段时间就会关闭该连接。放回连接池的连接,如果在服务器端已经关闭,客户端是无法检测到这个状态变化而及时的关闭Socket的。这就造成了线程从连接池中获取的连接不一定是有效的。这个问题的一个解决方法就是在每次请求之前检查该连接是否已经存在了过长时间,可能已过期。但是这个方法会使得每次请求都增加额外的开销。HTTP Client4.0的ThreadSafeClientConnManager 提供了closeExpiredConnections()方法和closeIdleConnections()方法来解决该问题。前一个方法是清除连接池中所有过期的连接,至于连接什么时候过期可以设置,设置方法将在下面提到,而后一个方法则是关闭一定时间空闲的连接,可以使用一个单独的线程完成这个工作。
public static class IdleConnectionMonitorThread extends Thread {
private final ClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(ClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(5000);
// 关闭过期的连接
connMgr.closeExpiredConnections();
// 关闭空闲时间超过30秒的连接
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
DefaultHttpClient httpclient = new DefaultHttpClient();
httpclient.setKeepAliveStrategy(new ConnectionKeepAliveStrategy() {
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
// Honor 'keep-alive' header
HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
try {
return Long.parseLong(value) * 1000;
} catch(NumberFormatException ignore) {
}
}
}
HttpHost target = (HttpHost) context.getAttribute(
ExecutionContext.HTTP_TARGET_HOST);
if ("www.163.com".equalsIgnoreCase(target.getHostName())) {
// 对于163这个路由的连接,保持5秒
return 5 * 1000;
} else {
// 其他路由保持30秒
return 30 * 1000;
}
}
})