我的Android网络框架之旅(三)

来源:互联网 发布:保险网络推广 编辑:程序博客网 时间:2024/05/17 22:51

在编写网络框架的过程,数据报文的传输是最繁琐难懂的一节,因为它涉及到了较多的网络通信协议的内容。了解其中的通信细节对我们把握整个网络通讯过程至关重要,这也是为什么要自定义网络框架的原因。使用httpUrlConnection搭建网络通讯模块,不仅可以灵活的控制网络参数格式,使用自定义的HTTPS证书,我们还可以自由的截取整个传输过程中的输入输出流,完全不需要使用第三方的网络抓包工具来进行数据联调。

在这里,我们根据业务需求把网络请求分为以下三块
1.普通的get请求
2.表单提交请求
3.上传文件的请求

1.Get请求
网络get请求大家都知道,是在我们的url后面跟上key=value这样的形式进行传统数据传递,使用?与url进行分离。一般的get请求都是向服务器请求一个状态或者索引,故不需要隐藏用户信息,这里我们的get请求也是直接拼接在url地址后面进行传递。

 /**     * @param params 参数列表     * @return     * @date 2015/12/28  13:55     * @author ZhongR     * @description 封装Get请求的参数     */    protected String packageTextParamsForGet(HashMap<String, String> params) {        if (params == null) {            throw new NullPointerException(                    "packageTextParamsForGet fail,the params 'params' is null ");        }        StringBuilder stringBuilder = new StringBuilder();        // 依次取出params里面的参数进行拼接        for (Entry<String, String> entry : params.entrySet()) {            stringBuilder.append(entry.getKey() + "=" + entry.getValue() + "&");        }        String tmp = stringBuilder.toString();        if (tmp.endsWith("&")) {            tmp = "?" + tmp;            int len = tmp.length();            tmp = tmp.substring(0, len - 1);        }        LogUtil.d(TAG, "packageTextParamsForGet:" + tmp);        return tmp;    }

这样把给packageTextParamsForGet返回的参数进行地址拼接就可以算完成啦,很轻松吧!

2.Post提交表单
直接上代码,注释已经写得很清楚了

    /**     * @param params     * @return     * @date 2015/12/28  13:55     * @author ZhongR     * @description 封装Post请求的参数,(提交表单)     */    protected byte[] PackagePostForm(HashMap<String, String> params) {        if (params == null) {            throw new NullPointerException(                    "packageTextParamsForPost fail,the params 'params' or 'boundary' is null ");        }        StringBuffer stringParams = new StringBuffer();        // 表单参数与get形式一样        Iterator it = params.entrySet().iterator();        while (it.hasNext()) {            Entry element = (Entry) it.next();            stringParams.append(element.getKey());            stringParams.append("=");            stringParams.append(element.getValue());            stringParams.append("&");        }        return stringParams.toString().getBytes();    }

上面的Post请求是单纯的表单提交,比如我们最常见的登陆模块,提交的数据是以键值对的形式传输的比如 username=xxx&password=xxx这样的格式,看起来和get请求很像,只不过是存放在了post数据包中。这里别忘了在报头指定你的消息类型

connection.setRequestProperty("Content-Type","application/x-www-form-urlencoded");

3.Post上传文件
重头戏来啦!post请求是支持文件上传,表单提交,文件+表单一起提交这样的形式的。所以在这里,我们的请求头中一定要设置connection.setRequestProperty("ContentType","multipart/form-data"+ ";boundary=" + boundary);
boundary学名叫做“分割边界”,我们在ContentType中指定了随机生成的字符串为boundary,在后面的拼接中,boundary会在中间起到分隔数据的作用。

下面是表单上传

  /**     * @param params 参数列表     * @param boundary 分隔符     * @return     * @date 2015/12/28  13:55     * @author ZhongR     * @description 封装Post请求的参数     */    protected byte[] packageTextParamsForPost(HashMap<String, String> params,            String boundary) {        if (params == null || TextUtils.isEmpty(boundary)) {            throw new NullPointerException(                    "packageTextParamsForPost fail,the params 'params' or 'boundary' is null ");        }        StringBuilder stringBuilder = new StringBuilder();        // 依次取出params里面的参数进行拼接        for (Entry<String, String> entry : params.entrySet()) {            stringBuilder.append(PREFIX);            stringBuilder.append(boundary);            stringBuilder.append(LINE_END);            stringBuilder.append("Content-Disposition: form-data; name=\""                    + entry.getKey() + "\"" + LINE_END);            stringBuilder.append(                    "Content-Type: text/plain; charset=" + CHARSET + LINE_END);            stringBuilder.append("Content-Transfer-Encoding: 8bit" + LINE_END);            stringBuilder.append(LINE_END);            stringBuilder.append(entry.getValue());            stringBuilder.append(LINE_END);        }        LogUtil.d(TAG, "packageTextParamsForPost:" + stringBuilder.toString());        return stringBuilder.toString().getBytes();    }

上面那段代码看晕了?没关系,总结下来,我们传递了一个hashMap,也就是一个表单,输出的body如下:

Content-Type:multipart/form-data; boundary=Op0M2780150KrHh_G0XUkrBL9r8OPsRPmJ7Content-Disposition: form-data;name="userAccount"Content-Type: text/plain; charset=UTF-8Content-Transfer-Encoding: 8bitxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxOp0M2780150KrHh_G0XUkrBL9r8OPsRPmJ7

xxxxx就是你要传递的键值对,数据格式在上一节已经描述过了,boundary是我们在Content-Type中指定的Op0M2780150KrHh_G0XUkrBL9r8OPsRPmJ7,到这里我们就完成了表单数据,下面就是上传文件的

  /**     * @param dos      输出流     * @param file    文件对象     * @param boundary     边界     * @param key       附件key值     * @throws IOException     * @date 2015/12/28  13:55     * @author ZhongR     * @description 封装一条带附件的请求参数     */    private void packageFileParam(DataOutputStream dos, File file,            String boundary, String key, AbstractLoader loader)                    throws IOException {        if (dos == null || file == null || TextUtils.isEmpty(boundary)                || TextUtils.isEmpty(key) || loader == null) {            LogUtil.e(TAG,                    " the params 'dos' 、'file'、'boundary' or 'key' has exist null ");            return;        }        if (!file.exists()) {            LogUtil.e(TAG, "packageFileParam fail, the file isn`t exist");            return;        }        // 增加文件开始结束标识        StringBuffer stringBuilder = new StringBuffer();        stringBuilder.append(PREFIX);        stringBuilder.append(boundary);        stringBuilder.append(LINE_END);        // 这里重点注意: name里面的值为服务器端需要key 只有这个key 才可以得到对应的文件 filename是文件的名字,包含后缀名        stringBuilder.append("Content-Disposition: form-data; name=\"" + key                + "\"; filename=\"" + file.getName() + "\"" + LINE_END);        stringBuilder.append("Content-Type: application/octet-stream; charset="                + CHARSET + LINE_END);        stringBuilder.append(LINE_END);        dos.write(stringBuilder.toString().getBytes());        InputStream is = new FileInputStream(file);        byte[] bytes = new byte[1024]; // 每次上传1024Byte        int len = 0;        float total = 0.0f;        float length = file.length();        while ((len = is.read(bytes)) != -1) {            // 循环写入数据            dos.write(bytes, 0, len);            total += len;            float tempProgress=total / length;            if(tempProgress<0.01)                tempProgress=0.01f;            // 向UI回调当前上传进度            loader.doProgress(tempProgress);        }        is.close();        dos.write(LINE_END.getBytes());    }

这就是我们上传文件时候的封装,注意在上传前指定Content-Disposition和Content-Type,分别用于描述我们的上传文件信息和数据类型,结合上面的表单提交,完整输出如下:

User-Agent: Mozilla/5.0 Accept-Language: zh-cn,zh;q=0.5Accept-Charset: GBK,utf-8;q=0.7,*;q=0.7Connection: keep-aliveContent-Length: 51409Content-Type:multipart/form-data; boundary=Op0M2780150KrHh_G0XUkrBL9r8OPsRPmJ7Host: xxxxxxxx--Op0M2780150KrHh_G0XUkrBL9r8OPsRPmJ7Content-Disposition: form-data;name="userAccount"Content-Type: text/plain; charset=UTF-8Content-Transfer-Encoding: 8bitxxxxxxx--Op0M2780150KrHh_G0XUkrBL9r8OPsRPmJ7Content-Disposition: form-data;name="userAvatar"; filename="photo.jpg"Content-Type: application/octet-streamContent-Transfer-Encoding: binaryxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--Op0M2780150KrHh_G0XUkrBL9r8OPsRPmJ7--

这样就是一次标准的post请求输出啦!对应不同的业务需求,我们还会在报头添加不同的参数,比如X-Requested-With,App-Agent,User-Agent,Cookie等等,具体的参数要求,你得根据你们的后台API文档来定义。

好不容易写完了网络请求的组成,最后肯定有人会问,不是说好了不用网络抓包了吗,对呀,干货在这里我推荐大家看看faceBook团队出品的调试神器stetho,进行网络抓包so esay!妈妈再也不用担心我联调接口的时候找不到问题啦!

0 0
原创粉丝点击