上一篇博客介绍了,如何简单的组装报文,现在来处理比较麻烦的另一部分了–解析报文。组装报文实际上是偷了一个懒,把解析工作交给了浏览器。但是,如果直接解析一下实际的报文的话,还是会对报文的结构认识更加深刻一些,下面就来开始吧!
解析报文
模拟报文
我们根据前面博客知道了报文的具体结构,下面就来解析报文,可是哪里来的报文呢?相信,虽然每天都在经历各种报文的传递,但是真正见到过报文的人还是不多吧。让我们来想象一下,报文从产生到传递给客户(或者服务器)被消费掉,其实时间是很短的。所以说,一个报文存在的时间极短,可以报文说就是朝生夕死。这里我们先来提供一个简单的模拟报文,存放在一个文件里面(把流动的报文固定住),这样处理起来比较方便。如果解析成功了,我们再去处理实际的报文。相当于,开发过程中,先使用模拟的数据,等开发完成了,再使用真实的数据。
模拟请求报文
Host: localhost:8080
Content-Type: image/jpeg
Content-Length: 112008
如果只是这样看的话,其实是缺少了很多必要的信息的,比如不可见的回车换行符 CRLF。所以我们换一个方式来查看模拟的报文:
这样是不是就非常清晰了,模拟的报文头部每一行包括一个首部字段信息和CRLF,最后还有一个CRLF(这里把它作为结束读取请求头的标识),后面的就是报文体的部分了,这里只考虑报文头部分。因为只要获取了头部,那么剩下的也就见到那了。(对于GET方式,是没有报文体的!)
首先是构造一个模拟的报文头部
这里就是向文件写入头部的信息,构造一个包含简单报文头的文件。
//构造一个简易的 报文头部,比较简陋,但是应该是可以满足学习条件了。
static void createMessage(File file) {
try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {
out.write("Host: localhost:8080\r\n".getBytes(Charset.forName("UTF-8")));
out.write("Content-Type: image/jpeg\r\n".getBytes(Charset.forName("UTF-8")));
out.write("Content-Length: 112008\r\n".getBytes(Charset.forName("UTF-8")));
out.write("\r\n".getBytes(Charset.forName("UTF-8")));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
解析报文头部
static void resolve(File file) {
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) {
/**
* 当取出来的行为空时,意味着已经取出了所有的头部。
* 下面就是数据部分了(这里考虑的是简单的报文,非Multip-part类型的报文)
* */
String header = null;
do {
header = getHeader(in); //每次取一行,即一个首部字段
if ("".equals(header)) break;
System.out.println("header:-----> " + header);
} while (true);
System.out.println("报文解析完成!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
获取报文首部的方法
这个方法参考了 getLine 方法,但是它是读取的字节而非字符。每次取一个字节的数据,直到遇到 \n 符号,就表示取出一个完整的首部了,然后去除两端的空白字符(主要是 \r 符号)。
//封装一个 getHeader 方法,每次取一个头部行,参考 getLine 方法。
static String getHeader(InputStream in) throws IOException {
StringBuilder header = new StringBuilder();
while (true) {
int c = in.read();
if (c == '\n' || c == -1) break;
header.append((char)c);
}
return header.toString().trim(); //取出空格字符
}
完整的代码
package com.dragon.resolve;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
public class HttpMessageResolver {
public static void main(String... args) {
File file = new File("./src/main/java/com/dragon/resolve/httpmessage.txt");
createMessage(file); //创建报文
resolve(file); //解析报文
}
//构造一个简易的 报文头部,比较简陋,但是应该是可以满足学习条件了。
static void createMessage(File file) {
try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {
out.write("Host: localhost:8080\r\n".getBytes(Charset.forName("UTF-8")));
out.write("Content-Type: image/jpeg\r\n".getBytes(Charset.forName("UTF-8")));
out.write("Content-Length: 112008\r\n".getBytes(Charset.forName("UTF-8")));
out.write("\r\n".getBytes(Charset.forName("UTF-8")));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
static void resolve(File file) {
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) {
/**
* 当取出来的行为空时,意味着已经取出了所有的头部。
* 下面就是数据部分了(这里考虑的是简单的报文,非Multip-part类型的报文)
* */
String header = null;
do {
header = getHeader(in);
if ("".equals(header)) break;
System.out.println("header:-----> " + header);
} while (true);
System.out.println("报文解析完成!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//封装一个 getHeader 方法,每次取一个头部行,参考 getLine 方法。
static String getHeader(InputStream in) throws IOException {
StringBuilder header = new StringBuilder();
while (true) {
int c = in.read();
if (c == '\n' || c == -1) break;
header.append((char)c);
}
return header.toString().trim(); //取出空格字符
}
}
运行结果
可见首部已经成功获取出来了,然后应该使用一个 Map 来存储这些首部字段。当然了,这里主要是演示如何取出这些首部字段。这里是我思考的一种解析的方式,非常简陋,但是可以工作就行了。
真实的报文
获取真实的报文
这里还是使用上篇博客组装报文的那个程序,使用一个简单的请求信息来请求(虽然这个请求没有用处),注意网络流的获取和文件流的获取是有区别的。通常使用本地文件可以使用读取字节的返回值为-1来结束读取,但是读取网络流的话,如果读取为-1,那么就表示当前流已经关闭了。只有几个方法可以做到:Socket 的 close()
或者是 Socket 的 shutInputStream()
和 shutOutputStream()
方法。所以,如果对方不关闭连接或者关闭对应的输入输出流,我们是无法根据-1来结束读取的。最好的办法,就是知道需要读取的长度。这也就是 Content-Length 的作用了。这个首部还是很必要的,虽然也有不使用指定长度的分块传输模式,但是这里只考虑使用 Content-Length的简单情况。
通过刚才的那个程序,我们其实已经可以获取到Content-Length的大小了,也就知道报文体部分需要获取多少字节了。但是这里我还是使用提前设置好的报文大小来读取了,这样更方便一点。不然这里我还是需要解析报文头部,才能获取到报文体的大小。注意这里的重点,只要是获取完整的报文头部就行了。
注意: 这里我使用的其实是响应报文,并不是请求报文,因为我当时获取响应报文比较方便,使用响应报文也是可以的,主要是因为请求报文和响应报文的区别不是很大(在我使用的例子里,我这里只是针对简单情况处理)。
完整的代码
package httpmsg;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.Charset;
//这个类用于获取一个完整的可被浏览器解析的Http报文。
public class HttpMessageSpider {
public static void main(String[] args) {
//构造一个简单的不含请求体的请求报文
String requestMsg = "GET / HTTP/1.0\r\n"
+ "User-Agent: CrazyDragon\r\n"
+ "Host: localhost: 10000\r\n"
+ "\r\n";
try (Socket client = new Socket("localhost", 10000)) { //监听本地的10000端口
//发送请求报文
OutputStream out = new BufferedOutputStream(client.getOutputStream());
out.write(requestMsg.getBytes(Charset.forName("UTF-8")));
out.flush();
//接收响应报文
InputStream in = new BufferedInputStream(client.getInputStream()); //获取输入流
int len = 0;
byte[] data = new byte[1024]; //报文的总大小,这里一次性读取
int size = 1222990; //总共需要读取的字节大小
int count = 0; //读取字节计数器
File file = new File("D:/DragonFile/httpmessage");
try (OutputStream output = new BufferedOutputStream(new FileOutputStream(file))) {
while (count < size && (len = in.read(data)) != -1) {
count += len;
//将报文写入文件
output.write(data, 0, len);
}
}
System.out.println(count);
System.out.println(file.length());
System.out.println("报文获取完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果
在指定的目录下可以找到完整的报文文件,我们把它打开,就可以看到完整的报文信息了。
完整的报文信息
说明: 前面是报文头部,后面是图片的信息。图片的信息也包括了图片的一些详细信息了,后面的是二进制数据,这里太长了,就不截取了。
原来这些报文都是流动的,从服务端到客户端,当你看到的时候,已经是解析好的了。所以说一个报文存在的时间是很短的,但是现在我把它固定下来了,哈哈。
解析真实的报文
package com.dragon.resolve;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
public class HttpMessageResolver {
public static void main(String... args) {
//真实报文的位置
File file = new File("D:/DragonFile/httpmessage");
resolve(file); //解析报文
}
static void resolve(File file) {
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) {
/**
* 当取出来的行为空时,意味着已经取出了所有的头部。
* 下面就是数据部分了(这里考虑的是简单的报文,非Multip-part类型的报文)
* */
String header = null;
do {
header = getHeader(in);
if ("".equals(header)) break;
System.out.println("header:-----> " + header);
} while (true);
//报文头部取出来,剩下的就是报文体了,取出报文体,写入文件,应该就是一张图片了。
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(new File("D:/DragonFile/newImg.jpg")))) {
int len = 0;
byte[] data = new byte[1024];
while ((len = in.read(data)) != -1) {
out.write(data, 0, len);
}
}
System.out.println("报文解析完成!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//封装一个 getHeader 方法,每次取一个头部行,参考 getLine 方法。
static String getHeader(InputStream in) throws IOException {
StringBuilder header = new StringBuilder();
while (true) {
int c = in.read();
if (c == '\n' || c == -1) break;
header.append((char)c);
}
return header.toString().trim(); //取出空格字符
}
}
说明:
在控制台可以看到报文头部的输出:
在指定目录下面,可以找到名为 newImg.jpg 的图片:
说明
其实上面这个解析还是不够真实,因为真实的报文是流动的网络流,并不是固定的文件流。所以有两点需要解决:
1.在从网络流中获取报文的时候,获取该报文的数据部分的长度。
注意:长度其实是很重要的,因为必须要知道报文什么时候结束,否则可能因为 1 字节,而造成读取阻塞。
2.在从网络流中获取报文的时候,获取该报文的数据部分的类型–Content-Type
但是这已经不是问题了,前面的那个读取首部功能已经解决了这两个问题了。下一步就是一个完整的整合过程了,下一篇博客介绍一个很有趣的错误。第5篇会使用前面博客的知识来编写一个完整的demo了。