HTTP文件上传请求格式详解,利用HttpURLConnection上传文件

关于文件上传功能,现在的移动开发中有很多好的网络框架提供这个功能。早期开发Android时网络框架少,只能靠自己手写。虽然现在不用自己手写,但是弄明白原理还是很有必要的,出了问题也好排查。

要弄明白原理就得看看上传的请求都传了什么参数,下面是请求的抓包:

请求头内容:

POST /day1701/upload2 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 394
Cache-Control: max-age=0
Origin: http://localhost:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary6TAB8KxvuJTZYfUn
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://localhost:8080/day1701/form2.jsp
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8
Cookie: JSESSIONID=B0F1C54AB51D4453AA1E7A9D48B607A5

请求体内容:

------WebKitFormBoundary6TAB8KxvuJTZYfUn
Content-Disposition: form-data; name="username"

sdafdsa
------WebKitFormBoundary6TAB8KxvuJTZYfUn
Content-Disposition: form-data; name="f"; filename="default_launcher.conf"
Content-Type: application/octet-stream

packageName=com.cy.chineseonline
className=com.cy.chineseonline.activity.MainActivity

------WebKitFormBoundary6TAB8KxvuJTZYfUn--

下面分析一下请求的内容,

首先是请求头:

最重要的内容是Content-Type,他的内容中是:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary6TAB8KxvuJTZYfUn

用分号隔开了不同的参数:
第一个参数multipart/form-data是必须的,是指提交的表单中有附件
第二个参数boundary表示分割线,请求体中会用来分割不同的请求参数
至于其他参数,在自己写请求的时候直接模仿传过去即可。

接下来是请求体

为了方便讲解,再把请求体复制过来:
以//开头的是我添加的注释,原来是没有的

------WebKitFormBoundary6TAB8KxvuJTZYfUn   // 第一行,是“--”字符串(不包括引号)和请求头中的boundary拼成的字符串
Content-Disposition: form-data; name="username" // 第二行是固定的Content-Disposition: form-data;和name="请求的参数名称"  二者拼接的字符串
// 这里是空行
sdafdsa //这是请求的参数,就是username的具体值
------WebKitFormBoundary6TAB8KxvuJTZYfUn  // 和第一行一样,是“--”字符串(不包括引号)和请求头中的boundary拼成的字符串
Content-Disposition: form-data; name="f"; filename="default_launcher.conf" // 和第二行类似,但是因为是文件,所以多了一个filename参数,是指你上传的文件名
Content-Type: application/octet-stream // 文件的minetype
// 这里有个空行,下面是要上传的文件内容
packageName=com.cy.chineseonline
className=com.cy.chineseonline.activity.MainActivity
// 这里有个空行,上面是要上传的文件内容
------WebKitFormBoundary6TAB8KxvuJTZYfUn-- // 最后一行 --和boundary和--拼接的字符串

以上是请求的内容格式分析,我们自己写上传的时候按照这个格式拼接就可以了,下面结合代码来分析,可以对照上面的内容看代码

    /**
     * @param url 请求地址
     * @param map 请求的参数
     * @param filePath 文件路径
     * @param body_data 上传的文件二进制内容
     * @param charset 字符集
     * @return
     */
    public static String doPostSubmitBody(String url, Map map,
            String filePath, byte[] body_data, String charset) {
        // 设置三个常用字符串常量:换行、前缀、分界线(NEWLINE、PREFIX、BOUNDARY);
        final String NEWLINE = "\r\n"; // 换行,或者说是回车
        final String PREFIX = "--"; // 固定的前缀
        final String BOUNDARY = "#"; // 分界线,就是上面提到的boundary,可以是任意字符串,建议写长一点,这里简单的写了一个#
        HttpURLConnection httpConn = null;
        BufferedInputStream bis = null;
        DataOutputStream dos = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            // 实例化URL对象。调用URL有参构造方法,参数是一个url地址;
            URL urlObj = new URL(url);
            // 调用URL对象的openConnection()方法,创建HttpURLConnection对象;
            httpConn = (HttpURLConnection) urlObj.openConnection();
            // 调用HttpURLConnection对象setDoOutput(true)、setDoInput(true)、setRequestMethod("POST");
            httpConn.setDoInput(true);
            httpConn.setDoOutput(true);
            httpConn.setRequestMethod("POST");
            // 设置Http请求头信息;(Accept、Connection、Accept-Encoding、Cache-Control、Content-Type、User-Agent),不重要的就不解释了,直接参考抓包的结果设置即可
            httpConn.setUseCaches(false);
            httpConn.setRequestProperty("Connection", "Keep-Alive");
            httpConn.setRequestProperty("Accept", "*/*");
            httpConn.setRequestProperty("Accept-Encoding", "gzip, deflate");
            httpConn.setRequestProperty("Cache-Control", "no-cache");
            // 这个比较重要,按照上面分析的拼装出Content-Type头的内容
            httpConn.setRequestProperty("Content-Type",
                    "multipart/form-data; boundary=" + BOUNDARY);
            // 这个参数可以参考浏览器中抓出来的内容写,用chrome或者Fiddler抓吧看看就行
            httpConn.setRequestProperty(
                    "User-Agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30)");
            // 调用HttpURLConnection对象的connect()方法,建立与服务器的真实连接;
            httpConn.connect();

            // 调用HttpURLConnection对象的getOutputStream()方法构建输出流对象;
            dos = new DataOutputStream(httpConn.getOutputStream());
            // 获取表单中上传控件之外的控件数据,写入到输出流对象(根据上面分析的抓包的内容格式拼凑字符串);
            if (map != null && !map.isEmpty()) { // 这时请求中的普通参数,键值对类型的,相当于上面分析的请求中的username,可能有多个
                for (Map.Entry entry : map.entrySet()) {
                    String key = entry.getKey(); // 键,相当于上面分析的请求中的username
                    String value = map.get(key); // 值,相当于上面分析的请求中的sdafdsa
                    dos.writeBytes(PREFIX + BOUNDARY + NEWLINE); // 像请求体中写分割线,就是前缀+分界线+换行
                    dos.writeBytes("Content-Disposition: form-data; "
                            + "name=\"" + key + "\"" + NEWLINE); // 拼接参数名,格式就是Content-Disposition: form-data; name="key" 其中key就是当前循环的键值对的键,别忘了最后的换行 
                    dos.writeBytes(NEWLINE); // 空行,一定不能少,键和值之间有一个固定的空行
                    dos.writeBytes(URLEncoder.encode(value.toString(), charset)); // 将值写入
                    // 或者写成:dos.write(value.toString().getBytes(charset));
                    dos.writeBytes(NEWLINE); // 换行
                } // 所有循环完毕,就把所有的键值对都写入了
            }

            // 获取表单中上传附件的数据,写入到输出流对象(根据上面分析的抓包的内容格式拼凑字符串);
            if (body_data != null && body_data.length > 0) {
                dos.writeBytes(PREFIX + BOUNDARY + NEWLINE);// 像请求体中写分割线,就是前缀+分界线+换行
                String fileName = filePath.substring(filePath
                        .lastIndexOf(File.separatorChar) + 1); // 通过文件路径截取出来文件的名称,也可以作文参数直接传过来
                // 格式是:Content-Disposition: form-data; name="请求参数名"; filename="文件名"
                // 我这里吧请求的参数名写成了uploadFile,是死的,实际应用要根据自己的情况修改
                // 不要忘了换行
                dos.writeBytes("Content-Disposition: form-data; " + "name=\""
                        + "uploadFile" + "\"" + "; filename=\"" + fileName
                        + "\"" + NEWLINE);
                // 换行,重要!!不要忘了
                dos.writeBytes(NEWLINE);
                dos.write(body_data); // 上传文件的内容
                dos.writeBytes(NEWLINE); // 最后换行
            }
            dos.writeBytes(PREFIX + BOUNDARY + PREFIX + NEWLINE); // 最后的分割线,与前面的有点不一样是前缀+分界线+前缀+换行,最后多了一个前缀
            dos.flush();

            // 调用HttpURLConnection对象的getInputStream()方法构建输入流对象;
            byte[] buffer = new byte[8 * 1024];
            int c = 0;
            // 调用HttpURLConnection对象的getResponseCode()获取客户端与服务器端的连接状态码。如果是200,则执行以下操作,否则返回null;
            if (httpConn.getResponseCode() == 200) {
                bis = new BufferedInputStream(httpConn.getInputStream());
                while ((c = bis.read(buffer)) != -1) {
                    baos.write(buffer, 0, c);
                    baos.flush();
                }
            }
            // 将输入流转成字节数组,返回给客户端。
            return new String(baos.toByteArray(), charset);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (dos != null)
                    dos.close();
                if (bis != null)
                    bis.close();
                if (baos != null)
                    baos.close();
                httpConn.disconnect();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

这个上传方法可以直接用,亲测可用。用的时候要注意把上传附件的参数名改一改,我直接写死了叫uploadFile,需要成你自己的
还有一个需要注意的是中文乱码问题,要和服务器沟通,我的事例代码中键值对参数用了编码,服务器拿到要解码,上传文件名那里没有对文件名处理,如果是中文要注意。

好了,就分析到这,重点就是要把上传文件时的参数格式弄明白,按照格式拼出来内容即可。

你可能感兴趣的:(android)