用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:

Paste_Image.png
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请求):

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"

<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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • apache下的httpclient工具可大大简化开发过程中的点对点通信,本人将以微信多媒体接口为例,展示http...
    划破的天空阅读 5,248评论 0 32
  • 整体Retrofit内容如下: 1、Retrofit解析1之前哨站——理解RESTful2、Retrofit解析2...
    隔壁老李头阅读 14,987评论 4 39
  • iOS开发系列--网络开发 概览 大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博、微信等,这些应用本身可...
    lichengjin阅读 3,617评论 2 7
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,490评论 18 139
  • 最近有一个项目需要重构网络部分代码,由于之前的网络部分都已经封装好,直接调用接口就行,重构的时候才发现,好多东西已...
    mymdeep阅读 10,562评论 5 21