用HttpUrlConnection伪造成HttpClient

如今,写网络连接的时候发现 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:

用HttpUrlConnection伪造成HttpClient_第1张图片
Paste_Image.png
public abstract class HttpMethod {
    protected String url;
    protected Map 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 headers) {
        this.headers.clear();
        this.headers.putAll(headers);
    }

    public void addHeader(String name, String value) {
        this.headers.put(name, value);
    }

    public void addHeaders(Map headers) {
        this.headers.putAll(headers);
    }

    public Map 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 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 formData){
        this.params.clear();
        this.params.putAll(formData);
    }

    public void addParam(String name, String value){
        params.put(name, value);
    }

    public void addParams(Map formData){
        this.params.putAll(formData);
    }

    public Map 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  void setBody(T httpBody) {
        this.httpBody = httpBody;
    }

    public HttpBody getBody() {
        return httpBody;
    }

}

2. 光请求没有有body怎么能行(除非用GetMethod请求):

用HttpUrlConnection伪造成HttpClient_第2张图片
Paste_Image.png

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"

hello world
------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("hello world"));
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组成,同时也是构造函数必传参数,它的作用就是受委托往遍历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 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 httpParams = new ArrayList<>();

    public void addHttpParam(HttpParam httpParam) {
        httpParams.add(httpParam);
    }

    public List 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

你可能感兴趣的:(用HttpUrlConnection伪造成HttpClient)