如今,写网络连接的时候发现 API 23 中居然找不到 HttpClient,官方文档似乎是这样说的:
This interface was deprecated in API level 22.
Please use openConnection() instead. Please visit this webpage for further details.
是的,没错,在Android 6.0里已经将Apache那套http client从系统里给移除了,其实在很多版本前就开始警告使用http client了。据HttpClient官网所说,在Android 1.0 发布后内置了pre-BETA snapshot版本,很明显这是一个不是很完善的版本,又由于和Google合作中断导致最新版本的HttpClient没能够集成到最新的Android系统中,Google决心只维护Java那套HttpUrlConnection,对于用习惯了HttpClient的小伙伴们估计很不理解 —— 那玩意真难用,每次发个网络请求要写一大坨代码,想要发个multi-part请求估计想死的心都有了。
面对这种情况,很多人选择了其他第三方网络库,比如:Volley,android-async-http,retrofit,http-request, Netroid ,当然还有大名鼎鼎的OKHTTP,当然框架数不胜数,随之而来的是各种对比研究,然后再确定使用某一种。其实,我们还有另外一条路可选,那就是模仿Apache HttpClient的API并用HttpUrlConnection创造一份HttpClient的仿品,其实,HttpUrlConnection足以稳定和高性能,因为从Android4.4之后OKHttp已经融入其中。
首先,从一个Get请求Demo入手:
GetMethod method = new GetMethod("http://10.1.158.59:8088/header");
method.addHeader("info", "hello world");
HttpClient httpClient = new HttpClient();
int code = httpClient.executeMethod(method);
Assert.assertEquals(code, HttpURLConnection.HTTP_OK);
String response = method.getResponseBodyAsString();
1. 所有的网络请求都new 一个method,以get和post居多,他们之间有共性也有差异性,共性的比如都有url, 都有header, 不同的是get的url可以带参数等,所以可以提取出一个抽象的HttpMethod:
public abstract class HttpMethod {
protected String url;
protected Map<String, String> headers = new HashMap<>();
private HttpURLConnection connection = null;
public HttpMethod(String url) {
this.url = url;
}
public String getUrl() {
return url;
}
public abstract String getName();
public abstract URL buildURL() throws MalformedURLException;
public void setHeader(String name, String value) {
this.headers.clear();
this.headers.put(name, value);
}
public void setHeaders(Map<String, String> headers) {
this.headers.clear();
this.headers.putAll(headers);
}
public void addHeader(String name, String value) {
this.headers.put(name, value);
}
public void addHeaders(Map<String, String> headers) {
this.headers.putAll(headers);
}
public Map<String, String> getHeaders() {
return headers;
}
public void setConnection(HttpURLConnection connection) {
this.connection = connection;
}
/**
* Release the execution of this method.
*/
public void releaseConnection() {
if (connection == null) {
return;
}
connection.disconnect();
}
/**
* Returns the response status code.
*
* @return the status code associated with the latest response.
*/
public int getStatusCode() throws IOException {
if (connection == null) {
return -1;
}
return connection.getResponseCode();
}
public InputStream getResponseBodyAsStream() throws IOException {
if (connection != null) {
return connection.getInputStream();
}
return null;
}
public byte[] getResponseBody() throws IOException {
if (connection == null) {
return null;
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int len;
InputStream inputStream = connection.getInputStream();
while ((len = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, len);
}
outputStream.close();
return outputStream.toByteArray();
}
public String getResponseBodyAsString() throws IOException {
byte[] body = getResponseBody();
if (body == null) {
return "";
}
return new String(body, Charset.forName("utf-8"));
}
}
也因此有了GetMethod和PostMethod以及别的Method(暂且没有实现,想扩展也是很容易的):
public class GetMethod extends HttpMethod {
public static final String NAME = "GET";
private Map<String, String> params = new HashMap<>();
public GetMethod(String url) {
super(url);
}
@Override
public String getName() {
return NAME;
}
@Override
public URL buildURL() throws MalformedURLException {
if (params == null || params.size() == 0) {
return new URL(url);
}
StringBuilder builder = new StringBuilder();
for (String key : params.keySet()) {
builder.append(key + "=" + params.get(key) + "&");
}
return new URL(url + "?" + builder.substring(0, builder.length() - 1));
}
public void setParam(String name, String value) {
params.clear();
params.put(name, value);
}
public void setParams(Map<String, String> formData){
this.params.clear();
this.params.putAll(formData);
}
public void addParam(String name, String value){
params.put(name, value);
}
public void addParams(Map<String, String> formData){
this.params.putAll(formData);
}
public Map<String, String> getParams(){
return params;
}
}
public class PostMethod extends HttpMethod {
public static final String NAME = "POST";
private HttpBody httpBody;
public PostMethod(String url) {
super(url);
}
@Override
public String getName() {
return NAME;
}
@Override
public URL buildURL() throws MalformedURLException {
return new URL(url);
}
public <T extends HttpBody> void setBody(T httpBody) {
this.httpBody = httpBody;
}
public HttpBody getBody() {
return httpBody;
}
}
2. 光请求没有有body怎么能行(除非用GetMethod请求):
HttpClient中最吸引人的地方就是它内置各种类型的body供选择,十分方便,关于body其实也能分析出它们之间的共性和异性,比如:他们都有content-type这个属性,但又都不一样,都要向http的OutputStream里write内容,但是写的东西又各不一样,有文件,有文本,有流,所以就产生了HttpBody这个抽象body:
public abstract class HttpBody {
/**
* MIMI-TYPE @see {@link ContentType}
*/
public abstract String getContentType();
public abstract long getContentLength();
public abstract String getContent() throws UnsupportedOperationException;
/**
* Write request body content(Text, JSON, XML or bytes of File) into
* OutputStream of HttpUrlConnection.
*/
public abstract void writeTo(final OutputStream outputStream) throws IOException;
/**
* If it was stream request like File, Byte, InputStream and so on, the
* default cache should be set disabled before write data, otherwise cannot
* know the real transmission speed.
*
* @return whether stream request or not.
*/
public abstract boolean isStreaming();
}
还有他的各种子孙body,下面呈列几个比较典型的body:
- 纯文本请求body:
PostMethod method = new PostMethod("http://10.1.158.59:8088/post/text");
method.setBody(new TextBody("hello world, this is test log"));
HttpClient httpClient = new HttpClient();
int code = httpClient.executeMethod(method);
Assert.assertEquals(code, HttpURLConnection.HTTP_OK);
String response = method.getResponseBodyAsString();
TextBody的定义:
public class TextBody extends HttpBody {
protected String text;
public TextBody(String text) {
this.text = text;
}
@Override
public String getContentType() {
return ContentType.DEFAULT_TEXT;
}
@Override
public String getContent() {
return text;
}
@Override
public long getContentLength() {
return text.getBytes().length;
}
@Override
public void writeTo(OutputStream outputStream) throws IOException {
outputStream.write(text.getBytes());
outputStream.flush();
}
@Override
public boolean isStreaming() {
return false;
}
}
- 文件上传body(因为文件上传能时时看到上传进度的体验是非常好的,所以这边可以选择性地挂监听,进度是以百分比回调的):
public class FileBody extends HttpBody {
protected final File file;
private long uploadedSize;
private OnProgressListener progressListener;
public FileBody(File file){
this.file = file;
}
public FileBody(String filePath){
this.file = new File(filePath);
}
public FileBody(File file, long uploadedSize, OnProgressListener listener){
this.file = file;
this.uploadedSize = uploadedSize;
this.progressListener = listener;
}
public FileBody(String filePath, long uploadedSize, OnProgressListener listener){
this.file = new File(filePath);
this.uploadedSize = uploadedSize;
this.progressListener = listener;
if (!file.exists()) {
throw new RuntimeException("file to upload does not exist: " + filePath);
}
}
@Override
public String getContentType() {
return ContentType.DEFAULT_BINARY;
}
@Override
public String getContent() {
throw new UnsupportedOperationException("FileBody does not implement #getContent().");
}
@Override
public long getContentLength() {
return file.length();
}
@Override
public void writeTo(OutputStream outputStream) throws IOException {
FileInputStream fin = new FileInputStream(file);
copy(fin, outputStream);
outputStream.flush();
}
@Override
public boolean isStreaming() {
return true;
}
public File getFile(){
return file;
}
public long getUploadedSize(){
return uploadedSize;
}
public OnProgressListener getProgressListener(){
return progressListener;
}
private long copy(InputStream input, OutputStream output) throws IOException {
long count = 0;
int readCount;
byte[] buffer = new byte[1024 * 4];
while ((readCount = input.read(buffer)) != -1) {
output.write(buffer, 0, readCount);
count += readCount;
}
output.flush();
return count;
}
}
-
如果不提起multipart body这也太不完美了对吧,其实multipart不是http里的协议,既然http协议本身的原始方法不支持multipart/form-data请求,那这个请求自然就是由这些原始的方法演变而来的,具体如何演变且看下文:
- multipart/form-data的基础方法是post,也就是说是由post方法来组合实现的
- multipart/form-data与post方法的不同之处:请求头,请求体。
- multipart/form-data的请求头必须包含一个特殊的头信息:Content-Type,且其值也必须规定为multipart/form-data,同时还需要规定一个内容分割符用于分割请求体中的多个post的内容,如文件内容和文本内容自然需要分割开来,不然接收方就无法正常解析和还原这个文件了。具体的头信息如下:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
注:multipart/form-data的请求体也是一个字符串,不过和post的请求体不同的是它的构造方式,post是简单的name=value值连接,而multipart/form-data则是添加了分隔符等内容的构造体。具体格式如下:
POST HTTP/1.1
Host:
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="key1"
multipart-text
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="key2"
<html><head><title>hello world</title></head></html>
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="key3"; filename="/mnt/sdcard/Download/mm.apk"
Content-Type: application/octet-stream
------WebKitFormBoundary7MA4YWxkTrZu0gW--
如果用写业务代码用HttpUrlConnection来完成此类工作真够烦琐的。先看看demo:
PostMethod method = new PostMethod("http://10.1.158.59:8088/post/multipart");
MultipartBody body = new MultipartBody();
body.addPart("key1", new TextBody("multipart-text"));
body.addPart("key2", new XmlBody("<html><head><title>hello world</title></head></html>"));
body.addPart("key3", new FileBody("/mnt/sdcard/Download/mm.apk", 0, new OnProgressListener() {
@Override
public void onError(String errorMsg) {
Log.d(TAG, "upload error: " + errorMsg);
}
@Override
public void onProgress(int percentage) {
Log.d(TAG, "upload percentage: " + percentage);
}
@Override
public void onCompleted() {
Log.d(TAG, "upload complete");
}
}));
method.setBody(body);
HttpClient httpClient = new HttpClient();
int code = httpClient.executeMethod(method);
Assert.assertEquals(code, HttpURLConnection.HTTP_OK);
String response = method.getResponseBodyAsString();
首先,描述下相关类的组成结构:
WrappedFormBody: 由fileName, httpBody组成,每一个WrappedFormBody都是multipart请求中的一个单元,其中httpBody可以是jsonbody,filebody等各种body;
MultipartBodyBuilder:类似Java的StringBuilder,只是存储的是WrappedFormBody,它的构造函数需要传入boundary,内置一个build()方法返回值是MultipartFormBody;
MultipartBody: 引入MultipartBodyBuilder作为变量,负责生成boundary, 再用生成的boundary生成contentType,它也是最终被add到PostMethod中的body;
MultipartFormBody: 由boundary和List<WrappedFormBody>组成,同时也是构造函数必传参数,它的作用就是受委托往遍历List往Http OutputStream里写body,每写一个body后再写入boundary;
详细的实现过程略微繁多,但总体思路是类似StringBuilder将各种body包起来作为一个整体一次性写入OutputStream,详细代码可参考具体实现;
3. 正如Apache的HttpClient所说的真正的主角是HttpClient,接收设置了body的http method并向server请求,通过获取的InputStream读取server返回内容:
public class HttpClient {
private static final String TAG = "httpclient";
private int timeout;
private HttpParams httpParams = new DefaultHttpParams();
public void setHttpParams(HttpParams httpParams) {
this.httpParams = httpParams;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
/**
* Called once http url connection was established.
*
* @param connection
*/
protected void onUrlConnectionEstablished(HttpURLConnection connection) {
}
public int executeMethod(HttpMethod httpMethod) throws IOException {
URL url = httpMethod.buildURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
onUrlConnectionEstablished(connection);
// only "POST" method need setDoInput(true) and setDoOutput(true)
if (PostMethod.NAME.equals(httpMethod.getName())) {
connection.setDoInput(true);
connection.setDoOutput(true);
}
connection.setRequestMethod(httpMethod.getName());
connection.setUseCaches(false);
connection.setInstanceFollowRedirects(false);
connection.setReadTimeout(timeout);
connection.setConnectTimeout(timeout);
for (HttpParam param : httpParams.getParams()) {
connection.setRequestProperty(param.getName(), param.getValue());
}
if (PostMethod.NAME.equals(httpMethod.getName())) {
// set content type for POST
PostMethod httpPost = (PostMethod) httpMethod;
HttpBody httpBody = httpPost.getBody();
connection.setRequestProperty("content-type", httpBody.getContentType());
connection.setRequestProperty("content-length", String.valueOf(httpBody.getContentLength()));
// disable cache for write output stream
if (httpBody.isStreaming()) {
connection.setChunkedStreamingMode(0);
}
}
// set extra headers
Map<String, String> headers = httpMethod.getHeaders();
if (headers != null && headers.size() > 0) {
for (String key : headers.keySet()) {
connection.setRequestProperty(key, headers.get(key));
}
}
// write data for POST
if (PostMethod.NAME.equals(httpMethod.getName())) {
// do connect
connection.connect();
// write request
PostMethod httpPost = (PostMethod) httpMethod;
HttpBody httpBody = httpPost.getBody();
httpBody.writeTo(connection.getOutputStream());
}
httpMethod.setConnection(connection);
return httpMethod.getStatusCode();
}
}
4 Http连接相关的设置:
常见的Http连接的设置如user-agent,cache-control,keep-alive等等,这类配置很多,但是都有一个固定规律:都是HttpURLConnection.setRequestProperty(String key, String value)这种方式设置,所以我在HttpClient里添加了一个API叫:
public void setHttpParams(HttpParams httpParams){
this.httpParams = httpParams;
}
至于HttpParams是啥玩意,其实很简单:
public class HttpParams {
private List<HttpParam> httpParams = new ArrayList<>();
public void addHttpParam(HttpParam httpParam) {
httpParams.add(httpParam);
}
public List<HttpParam> getParams(){
return httpParams;
}
}
HttpParam 又是什么呢?请看下面:
public abstract class HttpParam {
private String name;
private String value;
public HttpParam(String name, String value){
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue(){
return value;
}
}
当然我也定义了一些常见的配置类(后期可以扩展),下面以UserAgent.java为例:
public class UserAgent extends HttpParam {
public UserAgent(String value) {
super("user-agent", value);
}
}
完整代码量其实并不大,对于Library来说或许还不够资格,但是即便是这种小体量的封装也基本能应对项目中的各种Http使用场景,在借鉴了HttpClient它的部分API设计,通过它也足以发现Apache HttpClient API设计的精美,但愿我们在模仿中有一些自己的见解和成长,而不是一味的采用第三方,详细的实现可以参考http client