前言
对于我们习以为常的东西,却没有仔细思考它的作用。 HTTP的首部都各有个的价值,最近看到这个Content-Type,忽然让我想起来以前自己的一个小小的失误,而产生了一个bug。但是当时却搞得我焦头烂额,我在网络上查找了一圈也没有发现什么解决方案。最后,还是自己发现了写错的地方,而这个错误就是由于Content-Type所引起的。作为一名应用软件程序员,从计算机网络的角度来看,我们是工作在应用层的。所以对于应用层使用广泛的HTTP协议,还是要多了解一些。这样也有助于我们更好的理解应用,当出现我上面的这个问题时,可以很快的解决!
注:虽然基本上不会遇到这个问题,因为Content-Type并不需要我们去管理,当时了解它的作用本身仍然是一件很有趣的事情!
github地址: https://github.com/crazy-dragon/simple_server
注:第一次在github里面创建仓库,感觉挺麻烦的,主要是网络实在是难以忍受,太慢了。感兴趣的就看看吧,不怎么会使用github,不过现在也在了解。
先来看一下网络上对于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的一些知识是很好的。
它非常简单,所以用来学习是再合适不过了,甚至不使用IDE都是可以的。
注: 这是我在resource文件夹下面存放的文件,如果想要运行的话,只要有这些同名的文件就行了。404.html、poem.html和json.txt也都一样。
在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
说明
这里返回类型是 Content-Type: text/html,它告诉浏览器响应为一个html文档,所以浏览器会自动解析它。所以,你不要以为只要返回的是html页面,浏览器就会解析它。因为浏览器其实没有那么智能!
2.访问 /poem.html
这个结果和访问 / 是一样的,不做过多介绍,看上面即可!
3.访问 /no_poem.html
说明:
这个返回的类型是:Content-Type: text/plain,它表示的是纯文本文件。所以浏览器只是会展示它,而不是去解析它。因为html文档本身也是纯文本,所以浏览器只是单纯的显示它的内容而已。这里就引出了我开头所述的问题,我将返回html页面的首部写错了,本来是 text/html,结果写成了 text/plain。导致我的页面浏览器不解析,我遇到这个问题真是一脸懵逼。网络上也没有找到答案,因为大家都是使用现有的Web服务器,所以几乎不会遇到这种问题。后来,找了很久才发现是这里出错了。不过倒也是因此对Content-Type有了一些认识。
4.访问 /json
说明:
返回一个简单的json数据,这个是应用很广泛的了,现在流行使用json来传递数据(xml在数据传输上基本上很少使用了)。最近看到一句王阳明的话,觉得非常有趣。于是就写进去了和大家分享一下:此心光明,亦复何言。 它的 Content-Type 是 application/json,但是从效果上来看似乎和 text/plain 没有什么区别。确实也是,即使换成 text/plain 也是没有问题的。但是它表示的更加精确,对于应用程序来说更加友好。
5.访问 /favicon.ico
这个路径基本上任何一个网站都有的,所以你可以到任何一个网站的根目录后面加上该路径进行访问,查看它们的图标。还有一个路径是 /robots.txt,该路径基本上正规的网站也都有,它是一个爬虫的道德约束文件。
这里举一个例子,所以网站要有一个简单好认的图标,提高识别度。
我自己demo的图标:
我特意玩了一个小技巧,弄了一个随机访问的图标(总共有7个图标)。但是由于浏览器会缓存,导致它没有多少用处了。但是你还是可以通过多次访问路径,查看不同的图标。
注意:
最好不要使用黑色风格的图标,因为整个背景是黑色的,黑+黑就会遮盖了很多东西,我有一个图标是一个黑色的龙,结果只剩下一个全黑的背景了。哈哈!
说明:
这个返回的类型是:Content-Type: application/octet-stream,它表示某种二进制数据,浏览器会立刻进行下载,而下载的文件的名字,就是最后的路径名。并且,如果你写错了 Content-Type,或者不写 Content-Type,浏览器都会进行下载操作。似乎,它就是默认的Content-Type。并且,在postman中,发送请求时,如果是文件的话,可以选择body的类型为 binary。它就是表示使用该首部。
注意:当我编写这个程序时,我一开始没有看MIME表,直接凭借着感觉写Content-Type,但是写错了。访问某个路径直接变成下载了。真是一个大坑!不过现在反而也更加明白了它的作用。
如果看了这篇博客,并且实际运行我的代码,相信你会对 Content-Type 有一个全新的理解。它真的挺有趣的,学习这些协议的知识,对于我们以后的开发也是很重要的。特别是很多需要使用到协议的工作,例如网络数据采集(爬虫),对于请求头的分析其实是必不可少的。所以,基础知识的掌握是很重要的!