简单的Tomcat实现--1.4HTTP协议
Posted xsliu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简单的Tomcat实现--1.4HTTP协议相关的知识,希望对你有一定的参考价值。
Http协议
什么是Http协议?
- HTTP超文本传输协议Hyper Text Transfer Protocol
- 当我们在浏览器中输入一个地址,就能够访问服务器的某个页面,这个过程实际上是服务器与浏览器之间的交互,协议的本质是约定好的通信方式,有了协议的双方才能顺利理解双方的意思。浏览器和服务器之间使用的就是Http协议
请求协议
- 请求文本是从浏览器发给服务器的文本,Http协议是纯文本协议,发送的都是字符串。
# 第一行表示请求,这是一个GET请求, /表示访问的地址以及参数,HTTP/1.1表示使用的协议以及版本
GET /?name=gareen HTTP/1.1
Host: 127.0.0.1:18080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/80.0.3987.149 Safari/537.36
Sec-Fetch-Dest: document
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: CNZZDATA1258013751=1277252084-1540108380-%7C1573522796; user.uuid=""; isLogin=false; name=Gareen(cookie)
Request对象
- 目前我们从浏览器获取的信息是所有的都放在一个String当中,伴随着服务器功能的开发,需要从这个字符串里解析出更加丰富的信息,为了方便后续的解析工作,引入Request对象,用来代表浏览器发过来的请求信息。伴随着服务器功能的完善,这个Request对象将会不断改进和重构。
MiniBrowser的改动
- 以前的MiniBrowser对于处理从服务器端获取的数据,使用的一个固定长度(1024)的buffer来读取clientScoket的输入流从而得到从服务器端返回的信息。存在的问题是,如果返回的信息长度超过1024,那就造成一部分信息被丢弃,如果小于1024,就会有一部分空间冗余。为了处理这个问题,使用循环来实现从输入字符流中读取信息并转成字符数组。
public static byte[] readBytes(InputStream inputStream) throws IOException {
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while (true) {
int length = inputStream.read(buffer);
if (-1 == length) {
// read方法返回-1说明已经读到头了,否则返回读到的字符的数量
break;
}
byteArrayOutputStream.write(buffer, 0, length);
if (length != bufferSize){
break;
}
}
return byteArrayOutputStream.toByteArray();
}
创建Request类来解析requestString和uri
package http;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.StrUtil;
import util.MiniBrowser;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
* @author :xiaosong
* @description:TODO
* @date :2020/8/3 20:44
*/
public class Request {
private String requestString;
private String uri;
private Socket socket;
/**
* 构造方法
*/
public Request(Socket socket) throws IOException {
this.socket = socket;
}
private void parseHttpRequest() throws IOException {
// 解析Request,服务器端获取浏览器端传过来的请求
InputStream inputStream = this.socket.getInputStream();
byte[] bytes = MiniBrowser.readBytes(inputStream);
this.requestString = new String(bytes, "utf-8");
}
private void parseUri() {
// 解析uri,定位服务器上的文件
String temp;
/*
StrUtil.subBetween方法返回before和after之间的子串,不包含before和after
此处就是获取两个空格之间的内容,如果地址是 http://127.0.0.1:18080/index.html?name=gareen
那么http请求就会是
GET /index.html?name=gareen HTTP/1.1
Host: 127.0.0.1:18080
Connection: keep-alive
。。。。
只需要获取两个空格之间的部分就可以获得请求的uri
*/
temp = StrUtil.subBetween(requestString, " ", " ");
// StrUtil.subBefore()用于获取标识符之前的子字符串
this.uri = StrUtil.subBefore(temp, "?", false);
}
public String getUri(){
return uri;
}
public String getRequestString() {
return requestString;
}
}
修改bootstrap
- 下面通过更改
bootStrap.java
让服务器通过Request类来获取浏览器的输入请求
import cn.hutool.core.util.NetUtil;
import cn.hutool.log.LogFactory;
import cn.hutool.system.SystemUtil;
import com.sun.org.apache.xpath.internal.objects.XString;
import http.Request;
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.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* @author :xiaosong
* @description:项目的启动类
* @date :2020/7/28 20:41
*/
public class Bootstrap {
/**
定义服务器的端口号
*/
final static int PORT = 10086;
public static void main(String[] args){
logJvm();
try {
// if (!NetUtil.isUsableLocalPort(PORT)){
// //查看当前定义的端口是否已经被占用,如果NetUtil.isUsableLocalPort方法返回true表示port定义的端口号可用
// System.out.println(PORT + "端口已经被占用, 排查关闭本端口的方法请用
https:baidu.com");
// return;
// }
// 在port端口上新建serverSocket
ServerSocket serverSocket = new ServerSocket(PORT);
// 外部使用一个while循环,当处理完一个Socket的链接请求之后,再处理下一个链接请求
while (true) {
Socket socket = serverSocket.accept();
// 获取输入流,这个输入流表示的是收到一个浏览器客户端的请求
Request request = new Request(socket);
System.out.println("浏览器的输入信息:
" + request.getRequestString());
// 打开输出流,准备给客户端输出信息
OutputStream outputStream = socket.getOutputStream();
String responseHead = "HTTP/1.1 200 OK
" + "Content-Type:text/html
";
String responseString = "Hello JerryMice";
responseString = responseHead + responseString;
// 以字节数组的形式包装从服务器端给用户端的数据
outputStream.write(responseString.getBytes());
outputStream.flush();
// 关闭socket
socket.close();
}
}catch (IOException e) {
LogFactory.get().error(e);
}
}
private static void logJvm(){
// 创建一个Map用于保存各种信息
Map<String, String> infoMap = new LinkedHashMap<>();
infoMap.put("Server version", "JerryMice 1.0.0");
infoMap.put("Server build", "2020-08-03");
infoMap.put("OS: ", SystemUtil.get("os.name"));
infoMap.put("OS version", SystemUtil.get("os.version"));
infoMap.put("Architecture", SystemUtil.get("os.arch"));
infoMap.put("Java Home", SystemUtil.get("java.home"));
infoMap.put("JSM Version",SystemUtil.get("java.runtime.version"));
infoMap.put("JVM Vendor", SystemUtil.get("java.vm.specification.vendor"));
Set<String> keys = infoMap.keySet();
for (String key: keys){
// 调用hutool的LogFactory工厂函数获取logger,logger会自动根据log4j.properties来对Log4j的Logger进行配置
LogFactory.get().info(key + ": " + infoMap.get(key));
}
}
}
- 运行上述的代码可以在控制台观察到相同的输出:
- 进行上面的改动后也可以通过jnunit单元测试。
Response对象
- 上面已经有了request对象来封装请求,下面使用建立一个Response类来封装服务器返回的应答。
package http;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
/**
* @author :xiaosong
* @description:TODO
* @date :2020/8/3 21:43
*/
public class Response {
/**
* 用于存放返回的 html 文本
*/
private StringWriter stringWriter;
/**
* writer可以直接调用write方法向页面中写html内容
*/
private PrintWriter writer;
/**
* Content-type ,默认是 "text/html"
*/
private String contentType;
public Response(){
this.stringWriter = new StringWriter();
this.writer = new PrintWriter(stringWriter);
this.contentType = "text/html";
}
public String getContentType() {
return contentType;
}
public PrintWriter getWriter(){
return writer;
}
/**
* 返回html的字符数组
* @return
* @throws UnsupportedEncodingException
*/
public byte[] getBody() throws UnsupportedEncodingException {
String content = stringWriter.toString();
return content.getBytes();
}
public void setContentType(String type){
this.contentType = type;
}
}
重构bootstrap.java
- 首先使用刚才创建的Response类来对服务器响应进行封装
- 然后将整个的响应部分重构到一起
/**
* @param socket:
* @param response:Response对象,服务器对浏览器请求的响应,可以通过response的getBody()获取存储在其中的html文本
* @throws IOException
*/
private static void handle200(Socket socket, Response response) throws IOException{
// 获取类型
String contentType = response.getContentType();
String headText = Constant.responseHead200;
headText = StrUtil.format(headText, contentType);
byte[] head = headText.getBytes();
// 获取response中的html文本,这个html文本是通过writer写到stringWriter字符流上的
byte[] body = response.getBody();
byte[] responseBytes = new byte[head.length + body.length];
ArrayUtil.copy(head, 0, responseBytes, 0, head.length);
ArrayUtil.copy(body, 0, responseBytes, head.length, body.length);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(responseBytes);
socket.close();
}
}
- 运行重构后的代码可以得到和之前一样的输出
- 使用junit对重构后的代码进行单元测试
以上是关于简单的Tomcat实现--1.4HTTP协议的主要内容,如果未能解决你的问题,请参考以下文章