HTTP学习(3)--解析报文

上一篇博客介绍了,如何简单的组装报文,现在来处理比较麻烦的另一部分了–解析报文。组装报文实际上是偷了一个懒,把解析工作交给了浏览器。但是,如果直接解析一下实际的报文的话,还是会对报文的结构认识更加深刻一些,下面就来开始吧!

HTTP学习(2)–组装报文

解析报文

模拟报文

我们根据前面博客知道了报文的具体结构,下面就来解析报文,可是哪里来的报文呢?相信,虽然每天都在经历各种报文的传递,但是真正见到过报文的人还是不多吧。让我们来想象一下,报文从产生到传递给客户(或者服务器)被消费掉,其实时间是很短的。所以说,一个报文存在的时间极短,可以报文说就是朝生夕死。这里我们先来提供一个简单的模拟报文,存放在一个文件里面(把流动的报文固定住),这样处理起来比较方便。如果解析成功了,我们再去处理实际的报文。相当于,开发过程中,先使用模拟的数据,等开发完成了,再使用真实的数据。

模拟请求报文

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了。

HTTP学习(4)–违反协议的错误

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值