[HTTP] HTTP协议之MIME类型(多媒体资源类型) / Content-Type(资源内容类型)

0 引言

  • 写了这么多年了Web开发了,是有必要总结下HTTP协议中的MIME类型和Content-Type了。
  • 越是底层的、基础东西,掌握不牢靠,在项目实践中就容易闹些不明不白的幺蛾子。

1 概述

MIME 的定义、由来

  • MIME(Multipurpose Internet Mail Extensions) 多用途互联网邮件扩展类型
  • MIME 是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开

多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。

  • 诞生的原因:最初是为了解决在不同的电子邮件系统之间搬移报文时存在的问题。
  • HTTP协议借用:正因为MIME在电子邮件系统中工作得非常出色,所以 HTTP 协议也采用了它,用它来描述、并标记万维网的多媒体内容

即:在万维网( World Wide Web)中使用的HTTP协议中也使用了MIME的框架,标准被扩展为互联网多媒体资源类型

MIME 类型的描述格式

  • MIME type := type(一种主要的对象类型) / subType(一种特定的子类型)

例如:text/plaintext/htmlimage/jpegapplication/jsonapplication/x-www-form-urlencodedmultipart/form-data

MIME 类型 的用途 = 在【HTTP请求报文】、【HTTP响应报文】中向HTTP服务端、HTTP客户端声明【资源内容类型(ContentType)】

  • Content-Type 是用于指定 HTTP请求HTTP响应中主体数据的媒体类型(Media Type)或 MIME 类型(Multipurpose Internet Mail Extensions)。它通常作为请求头(Request Header)或响应头(Response Header)的一部分,用于描述HTTP报文传输的数据类型

MIME 类型

总的可以划分为:

  • application/*
  • audio/*
  • chemical/*
  • image/*
  • image/jpeg:JPEG 图像
  • image/png:PNG 图像
  • message/*
  • model/*
  • multipart/*
  • text/*
  • text/css:CSS 样式表
  • video/*
  • video/mp4:MP4 视频文件
  • ...

常见的 MIME 类型:

  • application/x-www-form-urlencoded

  • multipart/form-data

  • application/json

JSON 数据的 MIME 类型。这种类型的内容通常用于 API 响应或在 Web 开发中进行数据交换。浏览器不会尝试解析 JSON,而是将其作为纯文本显示或通过 JavaScript 解析。

  • application/octet-stream

这个 MIME 类型通常用于表示不属于其他任何特定 MIME 类型的数据流。
它是一种通用的二进制文件表示方式,指示内容是一个不可知的二进制流。
这意味着浏览器不会尝试解析内容,而是将其作为原始数据下载到用户的计算机上。
在许多情况下,服务器会将不可识别的文件类型标记为 application/octet-stream,以确保浏览器以正确的方式处理它们。

  • text/html

HTML 文档的 MIME 类型。当浏览器收到带有 text/html 类型的响应时,它会将其解析为网页并显示给用户。

  • text/plain

纯文本的 MIME 类型。这种类型的内容通常不包含任何格式化或标记,而是纯文本的形式。浏览器将其显示为普通文本,而不是解释任何 HTML 或其他标记。

  • image/jpeg、image/png

JPEG 和 PNG 图像的 MIME 类型。这两种类型的内容用于显示图片,浏览器会将其解析为图像并在页面上显示。

浏览器发起接口请求的方式

HTML Form Request(表单请求)

  • http协议中规定了GET、HEAD、POST、PUT、DELETE、CONNECT 等请求方式, 其中比较常用的就是 postget ,其中post多用来向服务器提交数据,post 只规定了提交的数据必须放在请求的主体中,但是并没有规定传输数据的编码方式

  • POST请求,比较主流的编码方式:

  • enctype 属性,是htmlform元素的核心属性,其规定在发送到服务器之前浏览器应该如何对表单数据进行编码

注意:只有 method="post" 时,才使用 enctype 属性。

<form action="/xxxx/demo-api.do" method="post" enctype="multipart/form-data" id="myform">
  First name: <input type="text" name="fname"><br>
  Last name: <input type="text" name="lname"><br>
  <input type="submit" value="提交">
</form>
  • enctype 的可选属性值
描述
application/x-www-form-urlencoded 默认。在发送前对所有字符进行编码(将空格转换为 "+" 符号,特殊字符转换为 ASCII HEX 值)。
multipart/form-data 不对字符编码。当使用有文件上传控件的表单时,该值是必需的。
text/plain 空格转换为 "+" 符号,但不编码特殊字符。

前端浏览器针对CORS请求,判定是否为跨域的判断条件之一:非跨域请求的Content-Type必须只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

跨域请求的判断、判定,详见:跨域问题小结 - 博客园/千千寰宇

浏览器将【CORS请求/跨域请求】(跨来源资源共享, Cross-Source Resource Sharing, CORS)分成2类:
1) 简单请求(simple request)
2) 非简单请求(not-so-simple request)
  • application/x-www-form-urlencoded
    即:浏览器HTML网页的原生 <form> 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。
POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8

title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3
  • multipart/form-data
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
 
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"
 
title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png
 
PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

既可以上传键值对也可以上传文件
有boundary的隔离可以上传多个文件

HTML Ajax Request(AJAX请求)

  • ajax request(即:Asynchronous JavaScript and XML Request(异步的 JavaScript 和 XML 请求)

前后端分离后,基于JSON数据交互的主要方式

  • application/json

application/json 作为请求头,用来告诉服务端报文主体序列化的JSON字符串,除了低版本的IE,基本都支持。服务端有处理JSON的函数,使用不会有任何麻烦。
application/json 可以方便的提交复杂的结构化数据,特别适合 RESTFul 的 WEB 接口
Google 的 AngularJS 中的 Ajax 功能,默认就是application/json

var xml = new XMLHttpRequest();
xml.withCredentials = true; // 开启Cookie,启用会话机制
var method = 'POST';
var url = "https://10.0.8.234/bms/static/pages/css/email.css?ver=20140102";    
 
xml.open(method, url, true);
xml.setRequestHeader("Content-type","application/json;charset=UTF-8"); // 常用的其他MIME类型: application/x-www-form-urlencoded
//xml.send();
 
//xml.send(JSON.stringify( //JSON单条记录
//{ flowId: "1105", areaId: "530000", departmentId : "1350", loginName :"chenguoyong", userName :"陈国勇", userPhone: "18600000000", idCard: "", email: "" }
//);
 
xml.send('[' + JSON.stringify( //JSON数组 - 形式1
{ flowId: "1105", areaId: "530000", departmentId : "1350", loginName :"chenguoyong", userName :"陈国勇", userPhone: "18600000000", idCard: "", email: "" }
//,flowId: "1105", areaId: "530000", departmentId : "1350", loginName :"chenguoyong", userName :"陈国勇", userPhone: "18600000000", idCard: "", email: "" }
) + ']');
 
xml.send(
	JSON.stringify( 
		[ //JSON数组 - 形式2
			{ flowId: "1105", areaId: "530000", departmentId : "1350", loginName :"chenguoyong", userName :"陈国勇", userPhone: "18600000000", idCard: "", email: "" }
			,{ flowId: "1105", areaId: "530000", departmentId : "1350", loginName :"chenguoyong", userName :"陈国勇", userPhone: "18600000000", idCard: "", email: "" }
		]
	)
);
//jQuery
JSvar data = {'name':'muzidigbig', 'age' : 18};
$http.post(url, data).success(
	function(result) {
		...
	}
);

最终发送内容:
BASHPOST http://www.example.com HTTP/1.1 
Content-Type: application/json;charset=utf-8
 
{"name":"muzidigbig","age":18}
------------------------------

//axios.js
axios.defaults.baseURL = process.env.BASE_API;

const service = axios.create({
	timeout : 40000,
	headers : {
		'X-Requested-With' : 'XMLHttpRequest' ,
		'Content-Type' : 'application/json; charset=UTF-8'
	}
});
  • application/x-www-form-urlencoded

2 Web开发实践篇

application/x-www-form-urlencoded (浏览器的默认类型)

前端普通表单场景

  • HTML 表单代码
<form action="http://localhost:8888/task/" method="POST">
  First name: <input type="text" name="firstName" value="Mickey&"><br>
  Last name: <input type="text" name="lastName" value="Mouse "><br>
  <input type="submit" value="提交">
</form>
  • 通过测试发现可以正常访问接口,在Chrome开发者工具中可见,表单上传MIME格式为application/x-www-form-urlencoded(Request Headers中),参数的格式为key=value&key=value

  • 我们可以看出,服务器知道参数用符号&间隔,如果参数值中需要&,则必须对其进行编码。编码格式就是application/x-www-form-urlencoded(将键值对的参数用&连接起来,如果有空格,将空格转换为+加号;有特殊符号,将特殊符号转义为ASCII HEX值)。
  • application/x-www-form-urlencoded是浏览器默认的MIME类型

ps:可以在这个网址测试表单:http://www.runoob.com/try/try.php?filename=tryhtml_form_submit

后端接口调用场景

  • 在代码中使用application/x-www-form-urlencoded MIME 类型设置Request属性调用接口。
private static String doPost(String strUrl, String content) {
    String result = "";

    try {
        URL url = new URL(strUrl);
        //通过调用url.openConnection()来获得一个新的URLConnection对象,并且将其结果强制转换为HttpURLConnection.
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setRequestMethod("POST");
        //设置连接的超时值为30000毫秒,超时将抛出SocketTimeoutException异常
        urlConnection.setConnectTimeout(30000);
        //设置读取的超时值为30000毫秒,超时将抛出SocketTimeoutException异常
        urlConnection.setReadTimeout(30000);
        //将url连接用于输出,这样才能使用getOutputStream()。getOutputStream()返回的输出流用于传输数据
        urlConnection.setDoOutput(true);
        //设置通用请求属性为默认浏览器编码类型
        urlConnection.setRequestProperty("content-type", "application/x-www-form-urlencoded");
        //getOutputStream()返回的输出流,用于写入参数数据。

        OutputStream outputStream = urlConnection.getOutputStream();
        outputStream.write(content.getBytes());
        outputStream.flush();
        outputStream.close();
        //此时将调用接口方法。getInputStream()返回的输入流可以读取返回的数据。

        InputStream inputStream = urlConnection.getInputStream();
        byte[] data = new byte[1024];
        StringBuilder sb = new StringBuilder();
        //inputStream每次就会将读取1024个byte到data中,当inputSteam中没有数据时,inputStream.read(data)值为-1
        while (inputStream.read(data) != -1) {
            String s = new String(data, Charset.forName("utf-8"));
            sb.append(s);
        }
        result = sb.toString();
        inputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return result;
}

public static void main(String[] args) {
    String str = doPost("http://localhost:8888/task/", "firstName=Mickey%26&lastName=Mouse ");
    System.out.println(str);
}

multipart/form-data

定义

定义、使用方式

  • 定义

媒体类型multipart/form-data遵循multipart MIME数据流定义(该定义可以参考Section 5.1 - RFC2046),大概含义就是:媒体类型multipart/form-data数据体由多个部分组成,这些部分由一个固定边界值Boundary分隔

  • 使用方式
  • 媒体类型multipart/form-data常用于POST方法下的HTTP请求,至于作为HTTP响应的场景相对少见。

multipart/form-data请求体布局

# 请求头 - 这个是必须的,需要指定Content-Type为multipart/form-data,指定唯一边界值
Content-Type: multipart/form-data; boundary=${Boundary}

# 请求体
--${Boundary}
Content-Disposition: form-data; name="name of file"
Content-Type: application/octet-stream

bytes of file
--${Boundary}
Content-Disposition: form-data; name="name of pdf"; filename="pdf-file.pdf"
Content-Type: application/octet-stream

bytes of pdf file
--${Boundary}
Content-Disposition: form-data; name="key"
Content-Type: text/plain;charset=UTF-8

text encoded in UTF-8
--${Boundary}--

区别

  • 媒体类型multipart/form-data相对于其他媒体类型(如application/x-www-form-urlencoded)来说,最明显的不同点是:
  • 请求头的Content-Type属性除了指定为multipart/form-data,还需要定义boundary参数
  • 请求体中的请求行数据是由多部分组成,boundary参数的值模式--${Boundary}用于分隔每个独立的分部
  • 每个部分必须存在请求头Content-Disposition: form-data; name="${PART_NAME}"; ,这里的${PART_NAME}需要进行URL编码,另外filename字段可以使用,用于表示文件名称,但是其约束性比name属性低(因为并不确认本地文件是否可用或者是否有异议)
  • 每个部分可以单独定义Content-Type和该部分的数据体
  • 请求体以boundary参数的值模式--${Boundary}--作为结束标志

RFC7578中提到两个multipart/form-data过期的使用方式:
其一是Content/Transfer-Encoding请求头的使用,这里也不展开其使用方式;
其二是请求体中单个表单属性传输多个二进制文件的方式建议换用multipart/mixed(一个"name"对应多个二进制文件的场景)

  • 特殊地:如果某个部分的内容为文本,其Content-Typetext/plain,可指定对应的字符集,如Content-Type: text/plain;charset=UTF-8
    可以通过_charset_属性指定默认的字符集,用法如下:
Content-Disposition: form-data; name="_charset_"

UTF-8
--ABCDE--
Content-Disposition: form-data; name="field"

...text encoded in UTF-8...
ABCDE--

Boundary 参数取值规约

  • Boundary参数取值规约如下:
  • Boundary的值必须以英文中间双横杠--开头,这个--称为前导连字符
  • Boundary的值除了前导连字符以外的部分不能超过70个字符
  • Boundary的值不能包含HTTP协议或者URL禁用的特殊意义的字符,例如英文冒号:
  • 每个--${Boundary}之前默认强制必须为CRLF,如果某一个部分的文本类型请求体以CRLF结尾,那么在请求体的二级制格式上,必须显式存在2个CRLF,如果某一个部分的请求体不以CRLF结尾,可以只存在一个CRLF,这两种情况分别称为分隔符的显式类型和隐式类型。

说的比较抽象,见下面的例子:

# 请求头
Content-type: multipart/data; boundary="--abcdefg"

--abcdefg
Content-Disposition: form-data; name="x"
Content-type: text/plain; charset=ascii

It does NOT end with a linebreak # <=== 这里没有CRLF,隐式类型
--abcdefg
Content-Disposition: form-data; name="y"
Content-type: text/plain; charset=ascii

It DOES end with a linebreak # <=== 这里有CRLF,显式类型

--abcdefg

## 直观看隐式类型的CRLF
It does NOT end with a linebreak CRLF --abcdefg

## 直观看显式类型的CRLF
It DOES end with a linebreak CRLF CRLF --abcdefg

前端普通表单场景 (推荐)

CASE1 spring:MultipartFile

  • 那么当服务器使用multipart/form-data接收POST请求时,服务器怎么知道每个参数的开始位置和结束位置呢?
<form action="http://localhost:8888/task/" method="POST" enctype="multipart/form-data">
  First name: <input type="text" name="firstName" value="Mickey&"><br>
  Last name: <input type="text" name="lastName" value="Mouse "><br>
  <input type="submit" value="提交">
</form>
  • 我们在开发者工具中可以看出multipart/form-data不会对参数编码,使用的boundary(分割线),相当于&boundary的值是----Web**AJv3

CASE2 spring:MultipartHttpServletRequest

//org.springframework.boot:spring-boot-starter-web:2.6.0
@RestController
public class TestController {

    @PostMapping(path = "/test")
    public ResponseEntity<?> test(MultipartHttpServletRequest request) {
        return ResponseEntity.ok("ok");
    }
}
  • Postman的模拟请求如下:

  • 后台控制器得到的请求参数如下:

后面编写的客户端可以直接调用此接口进行调试。

前端文件上传场景

  • 上传文件时,也要指定MIME类型为multipart/form-data
<form method="POST" action="http://localhost:8888/upload" enctype="multipart/form-data">
    <input type="file" name="file" />
    <input type="submit" value="Upload" />
</form>
  • 如果是SpringMVC项目,要服务器能接受multipart/form-data类型参数,还要在spring上下文配置以下内容,SpringBoot项目则不需要。
  • 定义解析器
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="defaultEncoding" value="utf-8"></property>
    <!-- 设置最大上传文件大小 -->
    <property name="maxUploadSize" value="100000"/>
</bean>
  • controller层API
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
 
@Controller
public class YourController {
 
    @PostMapping("/upload")
    public String handleFileUpload(MultipartFile file) {
        // 处理上传的文件, 例如保存文件
        // file.transferTo(new File("your-desired-location"));
        return "success"; // 返回处理结果页面
    }
}
  • 模拟:发起请求

我们可以通过FormData对象模拟表单提交,用原始的XMLHttpRequest来发送数据,让我们可以在Chrome开发工具中查看到具体格式:

<form id="form">
    First name: <input type="text" name="firstName" value="Mickey"><br>
    Last name: <input type="text" name="lastName" value="Mouse"><br>
    <input type="file" name="file"><br>
</form>
 
<button onclick="submitForm()">提交</button>
 
<script>
    function submitForm() {
        var formElement = document.getElementById("form");
 
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "/upload");
        xhr.send(new FormData(formElement));
    }
</script>
  • Demo

后端接口调用场景

CASE1 自定义 MultipartWriter

  • 这里的边界值全用显式实现边界值直接用固定前缀加上UUID生成即可。简单实现过程中做了一些简化:
  • 只考虑提交文本表单数据和二进制(文件)表单数据
  • 基于上一点,每个部分都明确指定Content-Type这个请求头
  • 文本编码固定为UTF-8
MultipartWriter
public class MultipartWriter {

    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    private static final byte[] FIELD_SEP = ": ".getBytes(StandardCharsets.ISO_8859_1);
    private static final byte[] CR_LF = "\r\n".getBytes(StandardCharsets.ISO_8859_1);
    private static final String TWO_HYPHENS_TEXT = "--";
    private static final byte[] TWO_HYPHENS = TWO_HYPHENS_TEXT.getBytes(StandardCharsets.ISO_8859_1);
    private static final String CONTENT_DISPOSITION_KEY = "Content-Disposition";
    private static final String CONTENT_TYPE_KEY = "Content-Type";
    private static final String DEFAULT_CONTENT_TYPE = "multipart/form-data; boundary=";
    private static final String DEFAULT_BINARY_CONTENT_TYPE = "application/octet-stream";
    private static final String DEFAULT_TEXT_CONTENT_TYPE = "text/plain;charset=UTF-8";
    private static final String DEFAULT_CONTENT_DISPOSITION_VALUE = "form-data; name=\"%s\"";
    private static final String FILE_CONTENT_DISPOSITION_VALUE = "form-data; name=\"%s\"; filename=\"%s\"";

    private final Map<String, String> headers = new HashMap<>(8);
    private final List<AbstractMultipartPart> parts = new ArrayList<>();
    private final String boundary;

    private MultipartWriter(String boundary) {
        this.boundary = Objects.isNull(boundary) ? TWO_HYPHENS_TEXT +
                UUID.randomUUID().toString().replace("-", "") : boundary;
        this.headers.put(CONTENT_TYPE_KEY, DEFAULT_CONTENT_TYPE + this.boundary);
    }

    public static MultipartWriter newMultipartWriter(String boundary) {
        return new MultipartWriter(boundary);
    }

    public static MultipartWriter newMultipartWriter() {
        return new MultipartWriter(null);
    }

    public MultipartWriter addHeader(String key, String value) {
        if (!CONTENT_TYPE_KEY.equalsIgnoreCase(key)) {
            headers.put(key, value);
        }
        return this;
    }

    public MultipartWriter addTextPart(String name, String text) {
        parts.add(new TextPart(String.format(DEFAULT_CONTENT_DISPOSITION_VALUE, name), DEFAULT_TEXT_CONTENT_TYPE, this.boundary, text));
        return this;
    }

    public MultipartWriter addBinaryPart(String name, byte[] bytes) {
        parts.add(new BinaryPart(String.format(DEFAULT_CONTENT_DISPOSITION_VALUE, name), DEFAULT_BINARY_CONTENT_TYPE, this.boundary, bytes));
        return this;
    }

    public MultipartWriter addFilePart(String name, File file) {
        parts.add(new FilePart(String.format(FILE_CONTENT_DISPOSITION_VALUE, name, file.getName()), DEFAULT_BINARY_CONTENT_TYPE, this.boundary, file));
        return this;
    }

    private static void writeHeader(String key, String value, OutputStream out) throws IOException {
        writeBytes(key, out);
        writeBytes(FIELD_SEP, out);
        writeBytes(value, out);
        writeBytes(CR_LF, out);
    }

    private static void writeBytes(String text, OutputStream out) throws IOException {
        out.write(text.getBytes(DEFAULT_CHARSET));
    }

    private static void writeBytes(byte[] bytes, OutputStream out) throws IOException {
        out.write(bytes);
    }

    interface MultipartPart {

        void writeBody(OutputStream os) throws IOException;
    }

    @RequiredArgsConstructor
    public static abstract class AbstractMultipartPart implements MultipartPart {

        protected final String contentDispositionValue;
        protected final String contentTypeValue;
        protected final String boundary;

        protected String getContentDispositionValue() {
            return contentDispositionValue;
        }

        protected String getContentTypeValue() {
            return contentTypeValue;
        }

        protected String getBoundary() {
            return boundary;
        }

        public final void write(OutputStream out) throws IOException {
            writeBytes(TWO_HYPHENS, out);
            writeBytes(getBoundary(), out);
            writeBytes(CR_LF, out);
            writeHeader(CONTENT_DISPOSITION_KEY, getContentDispositionValue(), out);
            writeHeader(CONTENT_TYPE_KEY, getContentTypeValue(), out);
            writeBytes(CR_LF, out);
            writeBody(out);
            writeBytes(CR_LF, out);
        }
    }

    public static class TextPart extends AbstractMultipartPart {

        private final String text;

        public TextPart(String contentDispositionValue,
                        String contentTypeValue,
                        String boundary,
                        String text) {
            super(contentDispositionValue, contentTypeValue, boundary);
            this.text = text;
        }

        @Override
        public void writeBody(OutputStream os) throws IOException {
            os.write(text.getBytes(DEFAULT_CHARSET));
        }

        @Override
        protected String getContentDispositionValue() {
            return contentDispositionValue;
        }

        @Override
        protected String getContentTypeValue() {
            return contentTypeValue;
        }
    }

    public static class BinaryPart extends AbstractMultipartPart {

        private final byte[] content;

        public BinaryPart(String contentDispositionValue,
                          String contentTypeValue,
                          String boundary,
                          byte[] content) {
            super(contentDispositionValue, contentTypeValue, boundary);
            this.content = content;
        }

        @Override
        public void writeBody(OutputStream out) throws IOException {
            out.write(content);
        }
    }

    public static class FilePart extends AbstractMultipartPart {

        private final File file;

        public FilePart(String contentDispositionValue,
                        String contentTypeValue,
                        String boundary,
                        File file) {
            super(contentDispositionValue, contentTypeValue, boundary);
            this.file = file;
        }

        @Override
        public void writeBody(OutputStream out) throws IOException {
            try (InputStream in = new FileInputStream(file)) {
                final byte[] buffer = new byte[4096];
                int l;
                while ((l = in.read(buffer)) != -1) {
                    out.write(buffer, 0, l);
                }
                out.flush();
            }
        }
    }

    public void forEachHeader(BiConsumer<String, String> consumer) {
        headers.forEach(consumer);
    }

    public void write(OutputStream out) throws IOException {
        if (!parts.isEmpty()) {
            for (AbstractMultipartPart part : parts) {
                part.write(out);
            }
        }
        writeBytes(TWO_HYPHENS, out);
        writeBytes(this.boundary, out);
        writeBytes(TWO_HYPHENS, out);
        writeBytes(CR_LF, out);
    }
}

这个类已经封装好三种不同类型的部分请求体实现,forEachHeader()方法用于遍历请求头,而最终的write()方法用于把请求体写入到OutputStream中。

HttpURLConnection 实现

实现代码如下(只做最简实现,没有考虑容错和异常处理):

public class HttpURLConnectionTest {

    private static final String URL = "http://localhost:9099/test";

    public static void main(String[] args) throws Exception {
        MultipartWriter writer = MultipartWriter.newMultipartWriter();
        writer.addTextPart("name", "throwable")
                .addTextPart("domain", "vlts.cn")
                .addFilePart("ico", new File("I:\\doge_favicon.ico"));
        DataOutputStream requestPrinter = new DataOutputStream(System.out);
        writer.write(requestPrinter);
        HttpURLConnection connection = (HttpURLConnection) new java.net.URL(URL).openConnection();
        connection.setRequestMethod("POST");
        connection.addRequestProperty("Connection", "Keep-Alive");
        // 设置请求头
        writer.forEachHeader(connection::addRequestProperty);
        connection.setDoInput(true);
        connection.setDoOutput(true);
        connection.setConnectTimeout(10000);
        connection.setReadTimeout(10000);
        DataOutputStream out = new DataOutputStream(connection.getOutputStream());
        // 设置请求体
        writer.write(out);
        StringBuilder builder = new StringBuilder();
        BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
        String line;
        while (Objects.nonNull(line = reader.readLine())) {
            builder.append(line);
        }
        int responseCode = connection.getResponseCode();
        reader.close();
        out.close();
        connection.disconnect();
        System.out.printf("响应码:%d,响应内容:%s\n", responseCode, builder);
    }
}

执行响应结果:

响应码:200,响应内容:ok

可以尝试加入两行代码打印请求体:

MultipartWriter writer = MultipartWriter.newMultipartWriter();
writer.addTextPart("name", "throwable")
        .addTextPart("domain", "vlts.cn")
        .addFilePart("ico", new File("I:\\doge_favicon.ico"));
DataOutputStream requestPrinter = new DataOutputStream(System.out);
writer.write(requestPrinter);

控制台输出如下;

JDK内置HttpClient实现

JDK11+内置了HTTP客户端实现,具体入口是java.net.http.HttpClient,实现编码如下:

public class HttpClientTest {

    private static final String URL = "http://localhost:9099/test";

    public static void main(String[] args) throws Exception {
        HttpClient httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.of(10, ChronoUnit.SECONDS))
                .build();
        MultipartWriter writer = MultipartWriter.newMultipartWriter();
        writer.addTextPart("name", "throwable")
                .addTextPart("domain", "test.cn")
                .addFilePart("ico", new File("I:\\doge_favicon.ico"));
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        writer.write(out);
        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
        writer.forEachHeader(requestBuilder::header);
        HttpRequest request = requestBuilder.uri(URI.create(URL))
                .method("POST", HttpRequest.BodyPublishers.ofByteArray(out.toByteArray()))
                .build();
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
		
        System.out.printf("响应码:%d,响应内容:%s\n", response.statusCode(), response.body());
    }
}

内置的HTTP组件几乎都是使用Reactive编程模型,使用的API都是相对底层,灵活性比较高但是易用性不高。

CASE2 HttpURLConnection

  • 在代码中使用multipart/form-data MIME 类型设置Request属性调用接口时,其中boundary的值可以在设置Content-Type时指定,让服务器知道如何拆分它接受的参数。

通过以下代码的调用接口:

private static String doPost(String strUrl, Map<String, String> params, String boundary) {
    String result = "";
 
    try {
        URL url = new URL(strUrl);
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setRequestMethod("POST");
        urlConnection.setConnectTimeout(30000);
        urlConnection.setReadTimeout(30000);
        urlConnection.setDoOutput(true);
        //设置通用请求属性为multipart/form-data
        urlConnection.setRequestProperty("content-type", "multipart/form-data;boundary=" + boundary);
        DataOutputStream dataOutputStream = new DataOutputStream(urlConnection.getOutputStream());
 
        for (String key : params.keySet()) {
            String value = params.get(key);
            //注意!此处是\r(回车:将当前位置移到本行开头)、\n(换行:将当前位置移到下行开头)要一起使用
            dataOutputStream.writeBytes("--" + boundary + "\r\n");
            dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + encode(key) + "\"\r\n");
            dataOutputStream.writeBytes("\r\n");
            dataOutputStream.writeBytes(encode(value) + "\r\n");
        }
        //最后一个分隔符的结尾后面要跟"--"
        dataOutputStream.writeBytes("--" + boundary + "--");
        dataOutputStream.flush();
        dataOutputStream.close();
        InputStream inputStream = urlConnection.getInputStream();
        byte[] data = new byte[1024];
        StringBuilder sb = new StringBuilder();
        while (inputStream.read(data) != -1) {
            String s = new String(data, Charset.forName("utf-8"));
            sb.append(s);
        }
        result = sb.toString();
        inputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
 
    return result;
}
 
private static String encode(String value) throws UnsupportedEncodingException {
    return URLEncoder.encode(value, "UTF-8");
}
 
public static void main(String[] args) {
    Map<String, String> params = new HashMap<>();
    params.put("firstName", "Mickey");
    params.put("lastName", "Mouse");
 
    //自定义boundary,有两个要求:使用不会出现在发送到服务器的HTTP数据中的值;并在请求消息中的分割位置都使用相同的值
    String boundary = "abcdefg";
    String str = doPost("http://localhost:8888/testFile", params, boundary);
    System.out.println(str);
}

通过debug,可以看出dataOutputStream的值如下:

参考与推荐文献

application/json

浏览器 Ajax 请求 Restful 接口场景(推荐)

参见本文档:ajax请求章节

后端接口调用场景(推荐)

参见本文档:ajax请求章节

text/xml

  • 基于XML—PRC的编码方式,协议简单,功能页足够日常的使用JS也有类库使用,但是XML的格式还是过于臃肿,一般场景用JSON更为方便。典型的 XML-RPC 请求是这样的:
POST http://www.example.com HTTP/1.1 
Content-Type: text/xml
 
<?xml version="1.0"?>
<methodCall>
    <methodName>examples.getStateName</methodName>
    <params>
        <param>
            <value><i4>41</i4></value>
        </param>
    </params>
</methodCall>

XML-RPC 协议简单、功能够用,各种语言的实现都有。
它的使用也很广泛,如 WordPressXML-RPC Api,搜索引擎的 ping 服务等等。
JavaScript 中,也有现成的库支持以这种方式进行数据交互,能很好的支持已有的 XML-RPC 服务。
不过,大部分开发者都还是认为 XML 结构还是过于臃肿,一般场景用 JSON 会更灵活方便。

开源框架/工具类

org.springframework.util.MimeType/MimeTypeUtils

org.springframework.http.MediaType

public class MediaType extends MimeType implements Serializable

X 参考文献

posted @ 2024-07-31 09:33  千千寰宇  阅读(929)  评论(0)    收藏  举报