HTTP学习(5)--demo编写(2)

上一篇博客介绍了一些和具体功能无关的类,还记得那个使用switch来处理的路由吗?每一个case都会对应一个具体的类,用来返回具体的响应。下面会逐一介绍每一个类的功能。

HTTP学习(5)–demo编写(1)

ServDragon类

因为每一个处理具体功能的类,都会有requestresponse属性和doGet方法,所以定义一个抽象基类 ServDragon,这样每个类直接继承它会更加方便了,可以简化一些代码,并且结构更有层次。

package com.dragon.server;

public abstract class ServDragon {
	protected Request request;
	protected Response response;
	
	public ServDragon(Request request, Response response) {
		this.request = request;
		this.response = response;
	}
	
	public abstract void doGet();
}

Index类

处理用户请求:/ 和 /index.mmp

package com.dragon.server;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;

public class Index extends ServDragon {
	
	public Index(Request request, Response response) {
		super(request, response);
	}

	public void doGet() {
		StringBuilder msg = new StringBuilder();
		msg.append("<!DOCTYPE html><html><head><meta charset=\"utf-8\"/><title>HomePage 龙先生</title></head><body>");
		for (String filename : DragonServer.picMap.keySet()) {
			msg.append("<img src=\"/pictures/").append(filename).append("\" width=\"300\"/>"); //这里使用相对路径,不使用绝对路径,方便调试部署(虽然不需要部署)
		}
		msg.append("</body></html>");
		byte[] content = msg.toString().getBytes(Charset.forName("UTF-8"));
		byte[] header = response.getHeader("text/html", content.length, 200, "OK");
		
		OutputStream out = new BufferedOutputStream(response.getOutputStream());
		try {
			out.write(header);
			out.write(content);
			out.flush();          //刷新流
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			response.close();
		}
	} 
}

说明: 这里上面是一个拼接的html片段,可以看出来是很简单的一个html。我对应前端其实不太了解,没有系统学习过。
它的结构大致如下,这里 img 标签上面的算第一个部分,img 标签算第二部分,下面是第三部分。
然后使用循环生成多个 img 图片,把图片的地址填充进去。最后将这三部分拼接起来,就组成了向用户返回的请求报文体(html页面)。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8"/>
		<title>HomePage 龙先生</title>
	</head>
	<body>
	<img src="/pictures/xxx.jpg"/>
	</body>
</html>

疑问:为什么需要这样做?
答: 这是因为请求报文头需要获取到报文体的大小,也就是说头部包含了报文体的信息,而且头部必须先发送,所以如果不提前准备好报文体,我这里是无法发送报文的。因为报文体还没准备好,我怎么可能知道报文的大小呢!(注意:大小指的是字节大小!)这里也是有解决方式的,可以使用一种称为 chunked 传输的方式(分块传输),当时这个我也只是听过,并不是很了解。所以,我这里使用 Content-Length 的方式,是必须提前准备好报文体的。

注:实际生成的页面,这个东西是我们前面拼接出来的。如果具有JSP和Servlet的学习经验,应该会了解到在没有JSP这项技术之前,Java Web程序员就是通过这种方式来编写web应用的。并且,JSP本质上也是 Servlet 类,只不过编写的时候方便多了。
在这里插入图片描述

Favicon类

当使用浏览器访问某个web页面时,浏览器会自动发送一个请求,请求该网站的图标。(当然了应该也是会缓存的,而且通常只发送一次。)

package com.dragon.server;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Favicon extends ServDragon {
	
	public Favicon(Request request, Response response) {
		super(request, response);
	}
	
	public void doGet() {
		Path path = Paths.get("D:", "DragonFile", "ServerImg", "favicon.ico");
		//响应体数据
		File file = path.toFile();
		byte[] header = null;
		byte[] content = null;
		try {
			content = Files.readAllBytes(path);           //使用 Files 工具类,一次性读取文件
			String contentType = Files.probeContentType(file.toPath());   //获取文件的 MIME 类型
			long length = file.length();                           //获取文件字节长度
			header = response.getHeader(contentType, length, 200, "OK");          // 填充响应头
 		} catch (IOException e) {
			e.printStackTrace();
		}

		OutputStream out = new BufferedOutputStream(response.getOutputStream());
		try {
			out.write(header);
			out.write(content);
			out.flush();  //刷新输出流
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			response.close();
		}
	}
}

web图标:
在这里插入图片描述
我把这个和我的其它图片存放到一个文件夹,但是它不会直接显示在网站上,因为它会被程序启动时过滤掉。但是通过 /favicon.ico 是可以访问到的,它的效果很简单,就是下面这个小图标。
在这里插入图片描述

NotFound 类

这里增加一个对非法请求响应的类,还记得前面博客那个消灭404的程序吗?虽然没有什么作用,但是还是挺有趣的!这个类的功能很简单,如果它识别了要给非法的请求的话,就是把一个html响应给浏览器。(这里考虑的很不完善,例如我访问一个不存在的图片,它只会导致程序异常,并不会弹出来这个页面。我也就不改了吧!好吧,我还是加上了访问错误图片的请求,但是我需要明确指出这个程序仍然是有很多的问题的,不过拿来学习参考还是挺好的。)

package com.dragon.server;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;

public class NotFound extends ServDragon {
	
	public NotFound(Request request, Response response) {
		super(request, response);
	}

	public void doGet() {
		String msg = "<!DOCTYPE html><html><head><meta charset=\"utf-8\"/>"
				+ "<title>Not Found!</title></head><body><h1>Not Found!</h1></body></html>";
		byte[] content = msg.getBytes(Charset.forName("UTF-8"));
		byte[] header = response.getHeader("text/html;charset=utf-8", content.length, 404, "Not Found");  //这里最开始我使用了 text/plain 这是不对的
		OutputStream out = new BufferedOutputStream(response.getOutputStream());
		try {
			out.write(header);
			out.write(content);
			out.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

在这里插入图片描述

UploadPage类

用于上传图片的html页面,这个页面只有一个form表单,用于提交图片使用。它和上面的 NotFound 类似,都是返回一个html页面,除了上传表单部分要指定 enctype=“multipart/form-data” 这个属性,也就没有什么了。

package com.dragon.server;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;

/**
 * 上传图片的页面
 * */
public class UploadPage extends ServDragon {

	public UploadPage(Request request, Response response) {
		super(request, response);
	}

	@Override
	public void doGet() {
		//上传页面
		String msg = "<!DOCTYPE html><html><head><meta charset=\"utf-8\"/><title>Upload 上传</title>"
				+ "</head><body><h1>Upload Picture</h1><form action=\"/upload\"  method=\"post\" "
				+ "enctype=\"multipart/form-data\"><input type=\"file\" name=\"file\"/>"
				+ "<input type=\"submit\" value=\"submit\"/></form></body></html>";
		
		byte[] content = msg.getBytes(Charset.forName("UTF-8"));
		byte[] header = response.getHeader("text/html;charset=utf-8", content.length, 200, "OK");  //指定编码,不然中文就乱码了
		OutputStream out = new BufferedOutputStream(response.getOutputStream());
		try {
			out.write(header);
			out.write(content);
			out.flush();     //刷新输出流
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

Pictures类

这个类就是响应用户请求的图片数据了,这样比通常的web开发更能感觉到底层吧。以往在 tomcat 上的图片上传、下载的代码,感觉的可以理解。但是不如这个更加底层,并且那个也是封装过的了。或者使用 Apache的 CmomonIO jar,对于开发者来说确实是很方便,但是对于初学者往往是不够友好的,因为不是太明白它的底层实现。

这里使用输出流来处理底层的实现细节,还是挺好理解的。读取图片的字节数组,然后输出给客户端。并且这里加了一个判断,如果文件不存在的话,就返回到 404 页面。
这里使用 Files 类的这个方法可以得到文件的MIME 类型,还有一个类的方法也可以做到,它是URLConnection 类。

Files.probeContentType(file.toPath());
URLConnection.guessContentTypeFromName(filename);
package com.dragon.server;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Pictures extends ServDragon {
	
	public Pictures(Request request, Response response) {
		super(request, response);
	}

	public void doGet() {
		String fileName = request.getParam("filename");
		//判断是否存在该路径
		Path path = Paths.get("D:", "DragonFile", "ServerImg", fileName);
		//响应体数据
		File file = path.toFile();
		if (!file.exists()) {  //如果文件不存在,返回404页面!
			NotFound notFound = new NotFound(request, response);
			notFound.doGet();
			return ;  //然后结束方法的执行。
		}
		
		byte[] header = null;
		byte[] content = null;
		try {
			content = Files.readAllBytes(path);           //使用 Files 工具类,一次性读取文件
			String contentType = Files.probeContentType(file.toPath());   //获取文件的 MIME 类型  这里不能加 charset=utf-8 否则打开就变成下载了
			long length = file.length();                           //获取文件字节长度
			header = response.getHeader(contentType, length, 200, "OK");          // 填充响应头
 		} catch (IOException e) {
			e.printStackTrace();
		}

		OutputStream out = new BufferedOutputStream(response.getOutputStream());
		try {
			out.write(header);
			out.write(content);
			out.flush();  //刷新输出流
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			response.close();
		}
	}
}

PicUpload类

这里是最麻烦的一个类了,因为这里涉及到了解析报文,我前面的博客里面虽然已经解析了一个简单的响应报文,但是我没有想到这个上传文件的请求报文其实是很复杂的。因为是上传文件,所以使用了 multipart/form-data 的方式。
具体可以参考这篇博客:HTTP POST请求报文格式分析与Java实现文件上传

这里我的报文中只有一个图片,我需要接收到报文,并把图片存入我的服务目录。这其实不是一件好做的事情,我不是太清楚这个 boundary 的作用,似乎是两个 boundary 之间的就是图片的数据了。
http报文的头部已经解析过之后,得到的并不全是图片的数据,还包括了其它的部分。

如下是一个去掉了报文的头部剩下的部分。中间的乱码是图片的数据部分。这里你必须先看一下上面那个参考博客,才可以知道这个东西的结构到底是啥样的?
在这里插入图片描述
前面提到了解析http报文头部,可以得到两样有用的数据:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary9twzBmnnpT88lyKL
Content-Length: 845

这里的这个 Content-Type 已经和前面提到的很不相同了,但是这里这个 boundary 是我们需要得到的,它是区分报文头和数据体的关键。但是我这里并不会直接使用它来区分,老实说,我不会使用。 并且这个 Content-Length 的含义也变了,它就是除了头部之外的其它部分的长度。但是其它部分还是每一个部分都含有一个头部,并且每一个部分都使用了 boundary 分割开了。

从上面那个截图所示的字节流中,取出来数据体,对我来说是一个挑战了。所以我决定另辟蹊径,考虑到我这个报文的简单性,我虽然是使用了多实体的方式传输,但是也只是含有一个实体!

我们再看一下上面那个截图的具体结构(显示全部字符)吧:
在这里插入图片描述

注意报文实际上可以分为三个部分:从上面的报文头部分到空白行 \r\n,然后是下面的数据体使用 “\r\n” + “–” + boundary + “–” + “\r\n” 结尾。这里是很精确的结构,实际上我可以把从上面报文头下面的数据全部写入文件,这样图片会多加了一段字符在后面,但是不会影响图片的显示。 我以前的博客,做过测试,就算加一个视频,也不会影响图片的显示。但是这里还是要严谨一些,把它给去掉。

具体思路:
把上面的截图所示的那个每个实体单独的头部去掉,但是需要记录那部分整个的大小(包括 \r\n 的大小),所以原来的 getHeader 已经不能使用了,需要进一步处理。

使用变量 headLen 记录图片数据上面报文的大小。
使用变量 tailLen 记录下面数据体部分的大小。
变量 size 是除了头部之外的数据大小(含尾部),这里必须把所有数据读取完,不然程序会阻塞。
变量 picSize 是size去除尾部的大小(完整的图片数据,不多也不少。)

采用一个计数器 count,每次读取之后,count 会加上读取的长度,如果长度超过了图片的长度,说明多读了一部分,只写入需要的那部分就行了(len - (count-picSize)),否则就写入读取的数据。
这样处理,可能有些饶人,但是也是无奈之举,毕竟我既想实现功能,但是又无法处理,只能折中使用这种复杂的方式了。

try (OutputStream out = new BufferedOutputStream(new FileOutputStream(path.toFile()))) {
	int size = Integer.parseInt(request.getParam("Content-Length")) - headLen;  //总共需要读取的字节大小  注意无法转换会抛异常。
	System.out.println("需要读取的长度:" + size + " 头部长度为:" + headLen);    //减去头部大小
	int picSize = size - tailLen;  //图片需要读取的字节数目
	System.out.println("图片的大小:" + picSize);
	int count = 0;  //读取字节计数器
	int len = 0;
	byte[] data = new byte[1024];
	while ((len = in.read(data)) != -1) {   //上面已经把数据给取走了再次读取就是请求体了
		count += len;
		if (count <= picSize ) {  
			out.write(data, 0, len);
		} else {
			int surplus = len - (count - picSize);
			out.write(data, 0, surplus);  //将报文写入文件
			System.out.println("最后写入的长度为:" + surplus);
		}
		if (count == size) break;  //退出读取报文
	}
计算头部大小及中文乱码问题

因为我需要精确的计算上面报文头部的大小了,所以原来的getHeader方法已经不适合了,每次遇到 \n 跳出循环,会导致我少读取到一个 \n,并且原来读取一个字节,就使用 (char)c 转成字符,也是遇到了一个问题,因为图片可能会以中文名字,而整个名字是会作为参数传递进来的。汉字并不是单字节的,所以如果转成一个字节字符就会出现乱码了,如下图所示:
在这里插入图片描述
中文乱码之后,它的长度改变了,从这里就能看出来了。导致我前面依赖长度来读取的方法失败了,程序会阻塞住(或者说卡死了),停止程序之后,图片虽然已经上传成功,但是它会缺少部分字节。但是它居然可以显示,我也不知道是为什么了?可能是因为图片不需要全部数据就可以显示的原因。

所以,我使用了 ByteArrayOutputStream 来处理,我把数据读取这个字节数组输出流里面,然后再转成数组,再使用 UTF-8 转码,就没有问题了。好了,中文的问题解决了。

在这里插入图片描述

private String getHeader(InputStream in) {
	ByteArrayOutputStream headers = new ByteArrayOutputStream();
	try {
		while (true) {
			int c = in.read();
			if (c == '\n' || c == -1) {
				headers.write(c);
				break;
			}
			headers.write(c);
		}
	} catch (IOException e) {
		e.printStackTrace();
	}
	byte[] headerArr =  headers.toByteArray();
	String header = new String(headerArr, Charset.forName("UTF-8"));  //直接使用 "UTF-8" 需要处理异常。
	headLen += headerArr.length;   //这里记录一下大小
	System.out.println(header);
	return header.toString().trim();  //取出空格字符
}
PicUpload 完整的代码部分
package com.dragon.server;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class PicUpload extends ServDragon {
	private int headLen;  //用于二进制内容头部的长度
	private int tailLen;  //用于记录二进制内容尾部的长度
	private String content_type;  // 设置一个变量用来获取 content_type
	
	public PicUpload(Request request, Response response) {
		super(request, response);
	}
	
	public void doGet() {
		//Content-Type: multipart/form-data; boundary=----WebKitFormBoundary9twzBmnnpT88lyKL
		String contentType = request.getParam("Content-Type");  //这个 contentType 只能获取到 boundary了
		//注意这里我采用字符串的substring方法,从 = 符号截取尾部的字串,获取到 boundary 
		String boundary = contentType.substring(contentType.lastIndexOf("=")+1);
		//尾部的长度为:两个 "\r\n--" + boundary + "--\r\n"
		String tail = "\r\n--" + boundary + "--\r\n";
		tailLen = tail.getBytes(Charset.forName("UTF-8")).length;
		
		/**
		 * ------WebKitFormBoundary9twzBmnnpT88lyKL
		 * Content-Disposition: form-data; name="file"; filename="img.jpg"  
		 * Content-Type: image/jpeg
		 * */
		//含有文件等二进制数据的报文,结构更加复杂,虽然已经只考虑只有一个二进制文件的报文了
		InputStream in = new BufferedInputStream(request.getInputStream());  //获取输入流
		String line = null;
		do {
			line = this.getHeader(in);
			if ("".equals(line)) break;  //读取到空格行就退出,下面就是完全的数据了。
			System.out.println("打印获取的line:" + line);
			if (line.contains("Content-Type")) {
				content_type = line.split(":")[1];
			}
		} while (true);
		
		//下面就是全部的数据了,第一次解析请求头获取到了 Content-Length,上面获取到了 Content-Type
		//现在是万事俱备,只欠东风了!
		if (content_type != null) {
			// 使用 MIME 判断文件的扩展名,这里就只支持 gif jpg jpeg png
			String type = null;
			switch (contentType) {
			case "image/gif": type = ".gif"; break;
			case "image/jpeg": type = ".jpg"; break;
			case "image/png": type = ".png"; break;
			default: type = ".png"; break;
			}
			
			//报文头部取出来,剩下的就是报文体了,取出报文体,写入文件,应该就是一张图片了。
			String fileName = UUID.randomUUID().toString() + type;
			Path path = Paths.get("D:", "DragonFile", "ServerImg", fileName);  //保存到相应的目录
			
			try (OutputStream out = new BufferedOutputStream(new FileOutputStream(path.toFile()))) {
				int size = Integer.parseInt(request.getParam("Content-Length")) - headLen;  //总共需要读取的字节大小  注意无法转换会抛异常。
				System.out.println("需要读取的长度:" + size + " 头部长度为:" + headLen);    //减去头部大小
				int picSize = size - tailLen;  //图片需要读取的字节数目
				System.out.println("图片的大小:" + picSize);
				int count = 0;  //读取字节计数器
				int len = 0;
				byte[] data = new byte[1024];
				while ((len = in.read(data)) != -1) {   //上面已经把数据给取走了再次读取就是请求体了
					count += len;
					if (count <= picSize ) {  
						out.write(data, 0, len);
					} else {
						int surplus = len - (count - picSize);
						out.write(data, 0, surplus);  //将报文写入文件
						System.out.println("最后写入的长度为:" + surplus);
					}
					if (count == size) break;  //退出读取报文
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			DragonServer.picMap.put(fileName, fileName);                      //上传成功后,更新图片的记录
			
			//上面是写入文件的操作,下面的代码不能接着上面的写,否则写入文件无法完成最后的刷新功能,导致图片大小有问题
			//响应报文
			String msg = "<!DOCTYPE html><html><head><meta charset=\"utf-8\"/><title>上传成功</title>"
					+ "</head><body><h1>图片上传成功!<h1></body></html>";
			byte[] content = msg.getBytes(Charset.forName("UTF-8"));
			OutputStream output = new BufferedOutputStream(response.getOutputStream());
			byte[] header = response.getHeader("text/html;charset=utf-8", content.length, 200, "OK");
			//开始写入响应报文
			try {
				output.write(header);  //写入头
				output.write(content);     //写入体
				output.flush();        //刷新输出流
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				response.close();
			}
		}
	}
	
	private String getHeader(InputStream in) {
		ByteArrayOutputStream headers = new ByteArrayOutputStream();
		try {
			while (true) {
				int c = in.read();
				if (c == '\n' || c == -1) {
					headers.write(c);
					break;
				}
				headers.write(c);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		byte[] headerArr =  headers.toByteArray();
		String header = new String(headerArr, Charset.forName("UTF-8"));  //直接使用 "UTF-8" 需要处理异常。
		headLen += headerArr.length;   //这里记录一下大小
		System.out.println(header);
		return header.toString().trim();  //取出空格字符
	}
}

使用List集合的方式,但是还是很麻烦,不如使用流方便。

private String getHeader(InputStream in) {
	List<Byte> arr = new ArrayList<Byte>();
	try {
		while (true) {
			int c = in.read();
			if (c == '\n' || c == -1) {
				arr.add((byte) c);
				break;
			}
			arr.add((byte) c);
		}
	} catch (IOException e) {
		e.printStackTrace();
	}
	Object[] arrays = arr.toArray();
	byte[] b = new byte[arrays.length];
	for (int i = 0; i < arrays.length; i++) {
		b[i] = (Byte) arrays[i];
	}
	System.out.println("使用集合处理:" + new String(b, Charset.forName("UTF-8")));
	return header.toString().trim();  //取出空格字符
}

说明

好了,总共12个类全部完成了,程序的功能还是挺好的。虽然简单,但是收获很多,至少对于 HTTP 的理解,不再只是局限于书本上的知识了,也是经历过了一些简单的实践。我这里的很多处理方法都不是很好,但是作为一个初学者,只要坚持学习下去就行了。对于计算机网络(传输层和应用层有了更深入的理解,说实话,当时我的计算机网络课学得不怎么样,哈哈!现在也算是补课了吧!)做一个综合一些的小demo,也是可以锻炼编程水平的!

结尾来一张图片吧,挺有趣的!

在这里插入图片描述

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值