HTTP首部——Content-Type的作用

前言
对于我们习以为常的东西,却没有仔细思考它的作用。 HTTP的首部都各有个的价值,最近看到这个Content-Type,忽然让我想起来以前自己的一个小小的失误,而产生了一个bug。但是当时却搞得我焦头烂额,我在网络上查找了一圈也没有发现什么解决方案。最后,还是自己发现了写错的地方,而这个错误就是由于Content-Type所引起的。作为一名应用软件程序员,从计算机网络的角度来看,我们是工作在应用层的。所以对于应用层使用广泛的HTTP协议,还是要多了解一些。这样也有助于我们更好的理解应用,当出现我上面的这个问题时,可以很快的解决!

:虽然基本上不会遇到这个问题,因为Content-Type并不需要我们去管理,当时了解它的作用本身仍然是一件很有趣的事情!

请求报文的结构
HTTP首部——Content-Type的作用_第1张图片

github地址: https://github.com/crazy-dragon/simple_server
注:第一次在github里面创建仓库,感觉挺麻烦的,主要是网络实在是难以忍受,太慢了。感兴趣的就看看吧,不怎么会使用github,不过现在也在了解。

Content-Type 首部

先来看一下网络上对于MIME的介绍:

MIME(Multipurpose Internet Mail Extensions) 多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。它是一个互联网标准,扩展了电子邮件标准,使其能够支持:
非ASCII字符文本;非文本格式附件(二进制、声音、图像等);由多部分(multiple parts)组成的消息体;包含非ASCII字符的头信息(Header information)。 这个标准被定义在RFC 2045、RFC 2046、RFC 2047、RFC 2048、RFC 2049等RFC中。 MIME改善了由RFC 822转变而来的RFC 2822,这些旧标准规定电子邮件标准并不允许在邮件消息中使用7位ASCII字符集以外的字符。正因如此,一些非英语字符消息和二进制文件,图像,声音等非文字消息原本都不能在电子邮件中传输(MIME可以)。MIME规定了用于表示各种各样的数据类型的符号化方法。 此外,在万维网中使用的HTTP协议中也使用了MIME的框架,标准被扩展为互联网媒体类型。

所以MIME是帮助客户端自动使用指定的应用程序来处理传输来的文件,如果你使用过socket传输过文件,你就会明白了。通过网络传输的数据,是文件本身的二进制数据,当是但我们在计算机上使用文件时,它是具有特定的扩展名的,以便于使用特定的程序来打开它。可以设想这样一个问题:我通过socket向你发送了一幅图片,当是你却以为是一个文档。那么尝试打开并阅读它的努力都是白费的!或者是,有些人喜欢修改某些文件的扩展名,以逃避别人的检查。所以当你遇到一个位置扩展名或者没有扩展名的文件时,通常会使用打开为这个选项。这样来枚举一下各种可能性(大部分人都不了解这个,而且被隐藏的文件类型也很好猜出来。),多半就能试出来了。所以,可见文件的类型是多么的重要。

正是因为MIME在邮件中的广泛应用,所以HTTP协议直接就使用了它并做了一些扩展。HTTP报文中的 Content-Type 首部就是用来说明实体主体(报文的数据体)的MIME类型。它的值就是标准化的MIME类型,MIME类型由一个主媒体类型后面跟一条斜线以及一个子类型组成,子类型用于进一步描述媒体类型。

下面介绍几个常用的媒体类型:

媒体类型 描述
text/html 实体主体是HTML文档
text/plain 实体主体是纯文本文档
image/jpeg 实体主体是JPEG格式的图像
application/json 实体主体是json
application/x-icon 实体主体是ICO格式的图像
application/octet-stream 实体是二进制数据(用于下载)

媒体的类型很多,这也构成了丰富的互联网世界。这里我只是选择了这几种来举例子,其它的只要需要使用时,再去查MIME表获取即可。

实际编程使用

Talk is cheap, show me your code!

上面都是一些理论性的介绍,下面让我们使用代码来实际使用上面介绍的几个首部的具体作用。这里提供一个小的demo用于演示,代码很简短。全部使用的原生代码,因为也就100多行,但是我觉得它还是很有趣的,用来学习HTTP的一些知识是很好的。

演示demo及介绍

目录结构

它非常简单,所以用来学习是再合适不过了,甚至不使用IDE都是可以的。
HTTP首部——Content-Type的作用_第2张图片
HTTP首部——Content-Type的作用_第3张图片
注: 这是我在resource文件夹下面存放的文件,如果想要运行的话,只要有这些同名的文件就行了。404.html、poem.html和json.txt也都一样。

demo功能

在IDE中启动项目后,打开浏览器:

访问路径 功能
localhost:8888/ 返回主页
localhost:8888/poem.html 返回主页
localhost:8888/no_poem.html 返回不解析的html
localhost:8888/json 返回json数据
localhost:8888/favicon 返回网页的图标,多次访问,结果不同
localhost:8888/any.jpg 以指定的格式下载二进制数据
other 返回404页

代码

package dragon.httpserver;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Random;

public class DragonServer {
     
	public static final String CRLF = "\r\n";
	public static final String BLANK = " ";
	private static final int BOUND = 7;
	private int port;
	
	public DragonServer(int port) {
     
		this.port = port;
	}
	
	public static void main(String[] args) throws IOException {
     
		System.out.println("The server has started...");
		new DragonServer(8888).start();
	}
	
	public void start() {
     
		try (ServerSocket server = new ServerSocket(port)) {
     
			while (true) {
     
				Socket client = server.accept();
				new Thread(()-> {
     
					try {
     
						// 获取输入流
						InputStream in = new BufferedInputStream(client.getInputStream());
						// 获取输出流
						OutputStream out = new BufferedOutputStream(client.getOutputStream());
						// 读取报文第一行,即请求行
						StringBuilder requestLine = new StringBuilder();
						// 我这里只读一行,不读取全部报文,程序没有报错!这是为什么?
						while (true) {
     
							int c = in.read();
							if (c == '\r' || c == '\r' || c == -1) break;
							// 不要直接使用 char 去读取,因为读取到结束的 -1 会转成 65535,导致后序的判断失败!
							requestLine.append((char)c);
						}
						
						// 这里有一个奇怪的问题,线程没有读取到任何数据,我这几就直接返回它了!
						if (requestLine.length() == 0) {
     
							return ;
						}
						String line = requestLine.toString();
						String[] lines = line.split(" ");
						System.out.println("request line: --> " + line);
						
						String method = lines[0];
						String path = lines[1];
						String protocol = lines[2];
						
						System.out.println("request method: " + method);
						System.out.println("request path: " + path);
						System.out.println("request protocol: " + protocol);
						
						// 设置一个标志变量,如果为 1,就默认为 Content-Type: plain/html,否则为其它的
						Path filepath = null;
						String contentType = null;
						String statusLine = "200 OK";
						// 路由分发
						switch (path) {
     
						case "/":
						case "/poem.html": 
							filepath = Paths.get("./resource", "poem.html");
							contentType = "text/html;charset=UTF-8";
							break;
						case "/no_poem.html":
							filepath = Paths.get("./resource",  "poem.html");
							contentType = "text/plain;charset=UTF-8";         // 区别在这里!浏览器不会解析该 html !
							break;
						case "/json":
							filepath = Paths.get("./resource", "json.txt");
							contentType = "application/json;charset=UTF-8";   // 虽然它的功能和 text/plain 相似,但是表示的范围更小!
							break;
						case "/favicon.ico": 
							Random rand = new Random();
							String name = "favicon" + rand.nextInt(BOUND) + ".ico";
							filepath = Paths.get("./resource", name);
							contentType = "image/x-icon";      // 写错成了 image/x-ico 变成自动下载了!
							break;
						case "/any":
							filepath = Paths.get("./resource", "404.html");  
							contentType = "application/octet-stream";    // 这个首部非常有趣,当是对于它的介绍很少。
							break;
						default: 
							filepath = Paths.get("./resource",  "404.html");
							statusLine = "404 Not Found";
							contentType = "text/html;charset=UTF-8";         // plain/html 会自动下载了,奇怪!
							break;
						}
						
						StringBuilder headerBuilder = new StringBuilder();
						byte[] entity = Files.readAllBytes(filepath);		
						// 构造响应头
						headerBuilder.append("HTTP/1.0").append(BLANK).append(statusLine).append(CRLF)
							         .append("Server:").append(BLANK).append("dragon 1.0").append(CRLF)
						             .append("Content-Length:").append(BLANK).append(entity.length).append(CRLF)
						             .append("Content-Type:").append(BLANK).append(contentType).append(CRLF)
						             .append(CRLF);
						
						// 输出响应头
						System.out.println(headerBuilder);
						byte[] header = headerBuilder.toString().getBytes(StandardCharsets.UTF_8);
						out.write(header);
						out.write(entity);
						out.flush();   // 一定要显示刷新流,防止出错!
					} catch (IOException e) {
     
						e.printStackTrace();
					} finally {
     
						try {
     
							if (client != null) {
     
								client.close();
							}
						} catch (IOException e) {
     
							e.printStackTrace();
						}
					}
				}).start();
			}
		} catch (IOException e) {
     
			e.printStackTrace();
		}
	}
}

演示

1.访问根目录
注意:我的程序是带有图标的!但是浏览器缓存了该图标,导致了它似乎不会变化了,尽管我是加入了变化的。因为第一次访问一个新的url的时候,浏览器会发出两个请求。一个是用户请求,另一个是请求该网站的ico图标。

我这里这是简单的获取请求行的信息,其它的都丢弃了。但是这里并没有请求图标的请求信息,应该是图标被缓存了,所以就不会再次请求了。网站的图标一般都是固定不变的,它是品牌的代表。
request line: --> GET / HTTP/1.1
HTTP首部——Content-Type的作用_第4张图片
说明
这里返回类型是 Content-Type: text/html,它告诉浏览器响应为一个html文档,所以浏览器会自动解析它。所以,你不要以为只要返回的是html页面,浏览器就会解析它。因为浏览器其实没有那么智能!


2.访问 /poem.html
这个结果和访问 / 是一样的,不做过多介绍,看上面即可!


3.访问 /no_poem.html
HTTP首部——Content-Type的作用_第5张图片
HTTP首部——Content-Type的作用_第6张图片
说明:
这个返回的类型是:Content-Type: text/plain,它表示的是纯文本文件。所以浏览器只是会展示它,而不是去解析它。因为html文档本身也是纯文本,所以浏览器只是单纯的显示它的内容而已。这里就引出了我开头所述的问题,我将返回html页面的首部写错了,本来是 text/html,结果写成了 text/plain。导致我的页面浏览器不解析,我遇到这个问题真是一脸懵逼。网络上也没有找到答案,因为大家都是使用现有的Web服务器,所以几乎不会遇到这种问题。后来,找了很久才发现是这里出错了。不过倒也是因此对Content-Type有了一些认识。


4.访问 /json
HTTP首部——Content-Type的作用_第7张图片
说明:
返回一个简单的json数据,这个是应用很广泛的了,现在流行使用json来传递数据(xml在数据传输上基本上很少使用了)。最近看到一句王阳明的话,觉得非常有趣。于是就写进去了和大家分享一下:此心光明,亦复何言。 它的 Content-Type 是 application/json,但是从效果上来看似乎和 text/plain 没有什么区别。确实也是,即使换成 text/plain 也是没有问题的。但是它表示的更加精确,对于应用程序来说更加友好。


5.访问 /favicon.ico
这个路径基本上任何一个网站都有的,所以你可以到任何一个网站的根目录后面加上该路径进行访问,查看它们的图标。还有一个路径是 /robots.txt,该路径基本上正规的网站也都有,它是一个爬虫的道德约束文件。

这里举一个例子,所以网站要有一个简单好认的图标,提高识别度。
HTTP首部——Content-Type的作用_第8张图片

我自己demo的图标:
我特意玩了一个小技巧,弄了一个随机访问的图标(总共有7个图标)。但是由于浏览器会缓存,导致它没有多少用处了。但是你还是可以通过多次访问路径,查看不同的图标。
HTTP首部——Content-Type的作用_第9张图片
HTTP首部——Content-Type的作用_第10张图片

HTTP首部——Content-Type的作用_第11张图片
注意:
最好不要使用黑色风格的图标,因为整个背景是黑色的,黑+黑就会遮盖了很多东西,我有一个图标是一个黑色的龙,结果只剩下一个全黑的背景了。哈哈!


6.访问 /any.jpg
在这里插入图片描述
HTTP首部——Content-Type的作用_第12张图片

说明:
这个返回的类型是:Content-Type: application/octet-stream,它表示某种二进制数据,浏览器会立刻进行下载,而下载的文件的名字,就是最后的路径名。并且,如果你写错了 Content-Type,或者不写 Content-Type,浏览器都会进行下载操作。似乎,它就是默认的Content-Type。并且,在postman中,发送请求时,如果是文件的话,可以选择body的类型为 binary。它就是表示使用该首部。

注意:当我编写这个程序时,我一开始没有看MIME表,直接凭借着感觉写Content-Type,但是写错了。访问某个路径直接变成下载了。真是一个大坑!不过现在反而也更加明白了它的作用。
HTTP首部——Content-Type的作用_第13张图片


7.访问任何其它路径
HTTP首部——Content-Type的作用_第14张图片

总结

如果看了这篇博客,并且实际运行我的代码,相信你会对 Content-Type 有一个全新的理解。它真的挺有趣的,学习这些协议的知识,对于我们以后的开发也是很重要的。特别是很多需要使用到协议的工作,例如网络数据采集(爬虫),对于请求头的分析其实是必不可少的。所以,基础知识的掌握是很重要的!

你可能感兴趣的:(Java,Java网络编程,爬虫,java,http)