4.1 内容类型Content-Type
4.1.1 什么是Content-Type
Content-Type是HTTP协议头中的一个字段,它用于描述HTTP请求或响应中所传输的实体数据的媒体类型(MIME类型)。Content-Type告诉客户端和服务器端所传输的数据的实际内容类型,使得客户端和服务器端可以正确地解析和处理数据。
Content-Type一般由两部分组成:媒体类型和字符集。其中,媒体类型指的是数据的格式类型,常见的媒体类型有text、image、audio、video等,每个媒体类型都有一个唯一的标识符,例如text/plain表示纯文本格式,image/jpeg表示JPEG格式的图片。字符集则是指所传输的数据所采用的字符编码格式,常见的字符集有UTF-8、GBK等。
Content-Type在Web开发中非常重要,它能够指示浏览器和服务器如何解析和处理数据。如果Content-Type指定的格式不正确,浏览器或服务器可能无法正确地解析和处理数据,从而导致各种问题。因此,在Web开发中,需要正确设置Content-Type来保证应用程序的正常运行。
4.1.2 错误的Content-Type示例
举个例子,各位试试错误的Content-Type会出现什么问题?比如将HttpServletResponse中的Content-Type设置为如下:
Content-Type: text/hmtl; charset=utf-8
使用浏览器访问:http://loclahost:8088/index.html,显示了如下结果:
显然结果并不理想,浏览器没有将HTML解析为网页内容显示Hello World,而是将网页的HTML脚本内容直接显示了出来。这就是错误的设置Content-Type出现的问题,浏览器无法正确解析和渲染这些内容,从而导致显示异常。如果Content-Type指定的字符集与实际使用的字符集不一致,也可能会导致乱码等问题。
只有正确设置HTTP响应Content-Type值,Content-Type头才能告诉浏览器内容的格式和字符编码,浏览器需要根据这个头来解析和渲染内容。如果Content-Type头设置正确,浏览器就可以正确地处理内容;否则,可能会导致内容无法正常显示或处理,从而影响用户体验。因此,在开发Web应用程序时,需要正确设置Content-Type头,以确保数据能够被正确地解析和处理。
4.1.3 媒体类型
Content-Type一般由两部分组成:媒体类型和字符集。
媒体类型(Media Type),也称为MIME类型(Multipurpose Internet Mail Extensions),是一种用于标识HTTP请求和响应中传输的数据类型的机制。它是由MIME标准定义的一种格式,用于描述一个文件的类型和格式,常见的媒体类型有text、image、audio、video等。
媒体类型以一种“type/subtype”的形式来定义,其中type表示媒体类型,subtype表示具体的子类型。例如,text/plain表示纯文本格式,image/jpeg表示JPEG格式的图片。除此之外,媒体类型还可以指定一些可选的参数,比如charset参数用于指定所传输的数据所采用的字符编码格式。
在HTTP请求和响应中,媒体类型通过Content-Type头部字段来指定。在HTTP请求中,Content-Type头部字段告诉服务器所发送的实体数据的格式类型和编码方式;在HTTP响应中,Content-Type头部字段则告诉客户端所返回的实体数据的格式类型和编码方式。由于媒体类型的正确设置对于HTTP协议的正常交互非常重要,因此,正确设置Content-Type头部字段是Web开发中必须要注意的一个方面。
4.1.4 错误的媒体类型示例
目前的原生Web框架Content-Type设置为固定的"text/html",所以只能处理html类型的文件,不能处理其他多种媒体类型的文件。如果把一个图片放到static文件夹,请求后会出现意想不到的结果:
需要将Content-Type设置为正确的媒体类型,我们必须了解有哪些媒体类型,如下是常用的媒体类型:
- text/plain:纯文本格式,不包含任何格式化信息,只包含基本的文本字符
- text/html:HTML格式的文本,可以用于在Web浏览器中显示Web页面
- application/json:JSON格式的数据,常用于Web应用程序之间的数据传输
- application/xml:XML格式的数据,常用于Web服务之间的数据传输
- image/jpeg:JPEG格式的图像,常用于Web页面中的图像显示
- image/png:PNG格式的图像,常用于Web页面中的图像显示
- audio/mpeg:MPEG格式的音频,常用于在线音乐播放等应用场景
- video/mp4:MP4格式的视频,常用于在线视频播放等应用场景
当然,这里只是一些常见的媒体类型,实际上还有很多其他的媒体类型。
要确定文件的媒体类型,可以使用文件的扩展名,文件的扩展名是文件名的最后一部分,通常由点号(.)和几个字符组成,例如,.txt或.jpg等。扩展名是根据文件的类型或格式而分配的。通过查看文件的扩展名,可以大致了解文件的类型。例如,.txt表示文本文件,.jpg表示图像文件,.mp3表示音频文件等等。
这样就可以重构代码,更新HttpServletResponse根据文件的扩展名找到对应的媒体类型作为Content-Type的值:
if (contentFile.getName().endsWith(".html")){println("Content-Type: text/html; charset=utf-8");
}else if (contentFile.getName().endsWith(".png")){println("Content-Type: image/png");
}
这样就可以正确显示图片了,这里使用if语句处理媒体类型的方式显然不可取,首先无法处理很多的文件扩展名转化为媒体类型,再有也不方便处理扩展名的大小写问题。Java提供了处理多种媒体类型的API。
4.1.5 Java中识别文件MIME的方法
在Java中识别文件的MIME类型有以下几种方法:
1. 使用 java.nio.file.Files.probeContentType(Path path):
如上述示例代码所示,使用Files.probeContentType(Path path)方法可以根据文件内容探测文件的MIME类型。
2. 使用 Apache Tika:
Apache Tika是一个开源的Java库,可以帮助识别文件的MIME类型以及提取文本和元数据。您可以通过添加Apache Tika依赖来使用它。
3. 使用 Java 7 的 URLConnection:
Java 7引入了URLConnection类的guessContentTypeFromStream()方法,可以通过输入流来猜测文件的MIME类型。
4. 使用 Apache Commons IO:
Apache Commons IO是Apache的一个通用IO类库,它提供了一个FileUtils类,其中包含了一个probeContentType(File file)方法,可以用于获取文件的MIME类型。
4.1.6 使用 Apache Tika识别文件MIME
在这个案例中,我们将使用 Apache Tika 库来识别文件的 MIME 类型。首先,确保您已将 Apache Tika 添加到您的项目依赖中。
<!-- 导入Tika 解决文件类型识别问题 -->
<dependency><groupId>org.apache.tika</groupId><artifactId>tika-core</artifactId><version>2.8.0</version>
</dependency>
以下是使用 Apache Tika 的示例代码,演示如何识别文件的 MIME 类型:
import org.apache.tika.Tika;public class FileMimeTypeExample {public static void main(String[] args) {String filePath = "path/to/your/file"; // 替换为实际文件路径Tika tika = new Tika();try {// 获取文件的 MIME 类型String mimeType = tika.detect(filePath);System.out.println("File MIME Type: " + mimeType);} catch (Exception e) {e.printStackTrace();}}
}
在上述示例代码中,我们首先创建了一个 Tika 实例,并使用 tika.detect(filePath) 方法来获取文件的 MIME 类型。将 "path/to/your/file" 替换为实际文件的路径。
请确保您的项目中已经包含了 Apache Tika 的相关依赖,否则该代码将无法编译和执行。
运行示例代码后,您将得到文件的 MIME 类型,例如:"text/plain"、"image/jpeg"、"application/pdf" 等。
Apache Tika 是一个功能强大的工具,可以处理多种文件类型的内容提取和元数据解析。它适用于需要处理多种文件格式的应用程序。
4.1.7 发送多个响应头
我们可以使用Tika识别文件的类型,发送文件的Content-Type。然而HTTP的响应头在实际应用中通常会包含很多信息,不仅限于Content-Type和Content-Length。处理多个响应头是Web服务框架中的重要功能,它允许服务器与浏览器之间进行更加灵活和复杂的交互,以满足各种不同的需求。为了处理多个响应头信息,我们可以对HttpServletResponse类进行进一步扩展,使其能够处理更多的响应头。
我们可以为HttpServletResponse添加了一个HashMap类型的headers字段来存储响应头信息,并添加了setHeader()方法来设置响应头。在send()方法中,我们先发送状态行,然后遍历headers字段,将所有响应头发送到客户端。在发送完所有响应头后,再发送响应正文。
这样,通过对HttpServletResponse类的扩展,我们能够处理多个响应头,使得Web服务框架能够更好地与客户端进行通信,提供更加丰富和定制化的功能。这有助于实现更高效、灵活和符合实际需求的Web应用程序。
4.1.8 重构HttpServletResponse发送多个响应头
1. 添加一个headers用于缓存多个响应头信息
在重构时候,HttpServletResponse中添加一个HashMap类型变量 headers用于缓存任意多的响应头信息,并且在发送响应头时候遍历headers,将全部响应头发送到浏览器。代码示意如下:
//响应头相关信息, key是响应头名称,value是响应头的值private HashMap<String, String> headers = new HashMap<>();/*** 添加自定义的响应头* @param name 响应头名称* @param value 响应头的值*/public void addHeader(String name, String value){headers.put(name, value);}/*** 发送响应头* @throws IOException 网络故障*/private void sendHeaders() throws IOException{//遍历Header集合,发送每个响应头for (Map.Entry<String, String> entry: headers.entrySet()){String name = entry.getKey();String value = entry.getValue();println(name + ": " + value);System.out.println("发响应头: " + name + ": " + value);}//发送空行println("");}
2. 添加响应头
在HttpServletResponse中定义 Tika类型的tika变量,用于查询文件的媒体类型。并且在添加响应文件时候,查询文件的媒体类型,添加响应头信息:
private Tika tika = new Tika();public void setContentFile(File contentFile) throws IOException {this.contentFile = contentFile;//添加响应头// 使用 Tika 来获取 Content-Type 类型addHeader("Content-Type", tika.detect(contentFile));addHeader("Content-Length", Long.toString(contentFile.length()));addHeader("File-Name", contentFile.getName());}
3. 定义发送状态行和响应正文的方法
为了代码更加整洁增加可读性,将发送状态行和发送响应正文代码抽取为方法:
/*** 发送状态行* @throws IOException 网络故障*/private void sendStatusLine() throws IOException {String statusLine = "HTTP/1.1 " + statusCode + " " + statusReason;//发送状态行println(statusLine);System.out.println("发送状态行: "+statusLine);}/*** 发送响应正文(响应体)* @throws IOException 网络故障*/private void sendContent() throws IOException{//将文件内容发送到浏览器FileInputStream in = new FileInputStream(contentFile);OutputStream out = socket.getOutputStream();byte[] buf = new byte[8*1024];int n;while ((n=in.read(buf))!=-1){out.write(buf, 0, n);}out.flush();System.out.println("发响应正文");}
4. 重构发送响应的方法
最终,发送方法重构为:
/*** 发送HTTP响应消息,发送3个部分内容,状态行、响应头和响应正文* @throws IOException 网络故障*/public void send() throws IOException {//发送状态行sendStatusLine();//发送响应头sendHeaders();//发送响应正文sendContent();}
HttpServletResponse重构完成后,使其能够处理多种媒体类型,代码更加的清晰:
其中:
- Tika类型的tika变量,用于查询文件的媒体类型
- Socket类型的socket变量,表示客户端与服务端之间的网络连接
- int类型的statusCode变量和String类型的statusReason变量,用于表示HTTP响应的状态行信息
- HashMap<String, String>类型的headers变量,用于表示HTTP响应的响应头信息
- File类型的contentFile变量,用于表示HTTP响应的正文内容
除此之外,该类还包括一些方法:
- sendStatusLine():发送HTTP响应的状态行信息
- sendHeaders():发送HTTP响应的响应头信息
- sendContent():发送HTTP响应的正文内容
- send():将上述三个方法组合起来,一次性发送完整的HTTP响应
- setContentFile(File contentFile):设置HTTP响应的正文内容,并同时设置Content-Length和Content-Type两个响应头信息
- setStatusCode(int statusCode):设置HTTP响应的状态码
- setStatusReason(String statusReason):设置HTTP响应的状态描述
- println(String line):发送一行字符串到网络流中
- addHeader(String name, String value):添加自定义的响应头信息
在该类中,通过OutputStream的write()方法将HTTP响应的状态行、响应头和正文内容发送到Socket所表示的网络连接中,从而将HTTP响应返回给客户端。
代码参考:
/*** 封装HTTP响应逻辑*/
public class HttpServletResponse {// MimetypesFileTypeMap 用于查询文件的媒体类型private MimetypesFileTypeMap fileTypeMap = new MimetypesFileTypeMap();private Socket socket;//状态行相关信息private int statusCode = 200; //状态代码private String statusReason = "OK"; //状态描述//响应头相关信息, key是响应头名称,value是响应头的值private HashMap<String, String> headers = new HashMap<>();//响应正文相关信息private File contentFile; //响应正文对应的实体文件public HttpServletResponse(Socket socket){this.socket = socket;}/*** 发送状态行* @throws IOException 网络故障*/private void sendStatusLine() throws IOException {String statusLine = "HTTP/1.1 " + statusCode + " " + statusReason;//发送状态行println(statusLine);System.out.println("发送状态行: "+statusLine);}/*** 发送响应头* @throws IOException 网络故障*/private void sendHeaders() throws IOException{//遍历Header集合,发送每个响应头for (Map.Entry<String, String> entry: headers.entrySet()){String name = entry.getKey();String value = entry.getValue();println(name + ": " + value);System.out.println("发响应头: " + name + ": " + value);}//发送空行println("");}/*** 发送响应正文(响应体)* @throws IOException 网络故障*/private void sendContent() throws IOException{//将文件内容发送到浏览器FileInputStream in = new FileInputStream(contentFile);OutputStream out = socket.getOutputStream();byte[] buf = new byte[8*1024];int n;while ((n=in.read(buf))!=-1){out.write(buf, 0, n);}out.flush();System.out.println("发响应正文");}/*** 发送HTTP响应消息,发送3个部分内容,状态行、响应头和响应正文* @throws IOException 网络故障*/public void send() throws IOException {//发送状态行sendStatusLine();//发送响应头sendHeaders();//发送响应正文sendContent();}public void setContentFile(File contentFile){this.contentFile = contentFile;//设置响应头:addHeader("Content-Lentgh", Long.toString(contentFile.length()));addHeader("Content-Type", fileTypeMap.getContentType(contentFile));}/*** 添加自定义的响应头* @param name 响应头名称* @param value 响应头的值*/public void addHeader(String name, String value){headers.put(name, value);}public void setStatusCode(int statusCode) {this.statusCode = statusCode;}public void setStatusReason(String statusReason){this.statusReason = statusReason;}/*** 发送一行到网络流* @param line 一行* @throws IOException 网络故障*/private void println(String line) throws IOException {OutputStream out = socket.getOutputStream();byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);out.write(data);out.write('\r');//发送回车符out.write('\n');//发送换行符}
}