简单的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协议的主要内容,如果未能解决你的问题,请参考以下文章

Tomcat 架构概述

tomcat实现http协议中的请求方法

Java后端WebSocket的Tomcat实现

Tomcat 对 HTTP 协议的实现(上)

简单Tomcat HTTP RPC框架

一个简单的时间片轮转内核代码的分析(课程作业)