HTTP Get静态资源请求功能编写

HTTP协议处理真是麻烦,早知道找个现成HTTP框架,只编写Servlet相关部分……

总体流程

先把HTTP处理流程定下来,根据前面定下来的框架,需要继承AbstractHttpEventHandler,在doHandle方法中编写接受请求内容、生成响应内容,并输出到客户端的代码,模板类:

public abstract class AbstractHttpEventHandler extends AbstractEventHandler<Connection> {
    @Override
    protected void doHandle(Connection connection) {
        //1.从输入中构造出HTTP请求对象,Body的内容是延迟读取
        HttpRequestMessage requestMessage = doParserRequestMessage(connection);
        //2.构造HTTP响应对象
        HttpResponseMessage responseMessage = doGenerateResponseMessage(requestMessage);
        try {
            //3.输出响应到客户端
            doTransferToClient(responseMessage, connection);
        } catch (IOException e) {
            throw new HandlerException(e);
        } finally {
            //4.完成响应后,关闭Socket
            if (connection instanceof SocketConnection) {
                IOUtils.closeQuietly(((SocketConnection) connection).getSocket());
            }
        }

    }

    /**
     * 通过输入构造HttpRequestMessage
     *
     * @param connection
     * @return
     */
    protected abstract HttpRequestMessage doParserRequestMessage(Connection connection);

    /**
     * 根据HttpRequestMessage生成HttpResponseMessage
     *
     * @param httpRequestMessage
     * @return
     */
    protected abstract HttpResponseMessage doGenerateResponseMessage(
            HttpRequestMessage httpRequestMessage);

    /**
     * 写入HttpResponseMessage到客户端
     *
     * @param responseMessage
     * @param connection
     * @throws IOException
     */
    protected abstract void doTransferToClient(HttpResponseMessage responseMessage,
                                               Connection connection) throws IOException;
}

构造出HTTP请求对象

还是先定下处理流程,首先构造RequestLine、然后构造QueryParameter和Headers,如果有Body,则构造Body。

整个过程,需要多个parser参与,并且有parser的输出是另外的parser的输入,所有这里通过ThreadLocal来保存这些在多个parser之间共享的变量,保存在HttpParserContext中。

这里通过copyRequestBytesBeforeBody方法确定是否具有body,同时将body之前字节都保存起来。

public abstract class AbstractHttpRequestMessageParser extends AbstractParser implements HttpRequestMessageParser {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractHttpRequestMessageParser.class);

    /**
     * 定义parse流程
     *
     * @return
     */
    @Override
    public HttpRequestMessage parse(InputStream inputStream) throws IOException {
        //1.设置上下文:设置是否有body、body之前byte数组,以及body之前byte数组长度到上下文中
        getAndSetBytesBeforeBodyToContext(inputStream);
        //2.解析构造RequestLine
        RequestLine requestLine = parseRequestLine();
        //3.解析构造QueryParameters
        HttpQueryParameters httpQueryParameters = parseHttpQueryParameters();
        //4.解析构造HTTP请求头
        IMessageHeaders messageHeaders = parseRequestHeaders();
        //5.解析构造HTTP Body,如果有个的话
        Optional<HttpBody> httpBody = parseRequestBody();

        HttpRequestMessage httpRequestMessage = new HttpRequestMessage(requestLine, messageHeaders, httpBody, httpQueryParameters);
        return httpRequestMessage;
    }

    /**
     * 读取请求发送的数据,并保存为byte数组设置到解析上下文中
     *
     * @param inputStream
     * @throws IOException
     */
    private void getAndSetBytesBeforeBodyToContext(InputStream inputStream) throws IOException {
        byte[] bytes = copyRequestBytesBeforeBody(inputStream);
        HttpParserContext.setHttpMessageBytes(bytes);
        HttpParserContext.setBytesLengthBeforeBody(bytes.length);
    }

    /**
     * 解析并构建RequestLine
     *
     * @return
     */
    protected abstract RequestLine parseRequestLine();

    /**
     * 解析并构建HTTP请求Headers集合
     *
     * @return
     */
    protected abstract IMessageHeaders parseRequestHeaders();

    /**
     * 解析并构建HTTP 请求Body
     *
     * @return
     */
    protected abstract Optional<HttpBody> parseRequestBody();

    /**
     * 解析并构建QueryParameter集合
     *
     * @return
     */
    protected abstract HttpQueryParameters parseHttpQueryParameters();

    /**
     * 构造body(如果有)之前的字节数组
     * @param inputStream
     * @return
     * @throws IOException
     */
    private byte[] copyRequestBytesBeforeBody(InputStream inputStream) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(inputStream.available());
        int i = -1;
        byte[] temp = new byte[3];
        while ((i = inputStream.read()) != -1) {
            byteArrayOutputStream.write(i);
            if ((char) i == '\r') {
                int len = inputStream.read(temp, 0, temp.length);
                byteArrayOutputStream.write(temp, 0, len);
                if ("\n\r\n".equals(new String(temp))) {
                    break;
                }
            }
        }
        return byteArrayOutputStream.toByteArray();
    }
}

RequestLine解析时会设置Http请求方法和queryString到HttpParserContext中,作为QueryParameter解析的输入。

    @Override
    protected RequestLine parseRequestLine() {
        RequestLine requestLine = this.httpRequestLineParser.parse();
        HttpParserContext.setHttpMethod(requestLine.getMethod());
        HttpParserContext.setRequestQueryString(requestLine.getRequestURI().getQuery());
        return requestLine;
    }

QueryParameter的解析很简单,直接调用HttpQueryParameterParser即可。

HttpHeader解析流程为从HttpParserContext中取出请求报文,去掉第一行,然后逐行处理,构造成key-value的形式保存起来。

同时,通过Content-Length头或者Transfer-Encoding判断请求是否包含Body。

public class DefaultHttpHeaderParser extends AbstractParser implements HttpHeaderParser {
    private static final String SPLITTER = ":";
    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHttpHeaderParser.class);

    @Override
    public HttpMessageHeaders parse() {
        try {
            String httpText = getHttpTextFromContext();
            HttpMessageHeaders httpMessageHeaders = doParseHttpMessageHeaders(httpText);
            setHasBody(httpMessageHeaders);
            return httpMessageHeaders;
        } catch (UnsupportedEncodingException e) {
            throw new ParserException("Unsupported Encoding", e);
        }
    }

    /**
     * 从上下文获取bytes并转换为String
     *
     * @return
     * @throws UnsupportedEncodingException
     */
    private String getHttpTextFromContext() throws UnsupportedEncodingException {
        byte[] bytes = HttpParserContext.getHttpMessageBytes();
        return new String(bytes, "utf-8");
    }

    /**
     * 解析Body之前的文本构建HttpHeader,并保存到HttpMessageHeaders中
     *
     * @param httpText
     * @return
     */
    private HttpMessageHeaders doParseHttpMessageHeaders(String httpText) {
        HttpMessageHeaders httpMessageHeaders = new HttpMessageHeaders();
        String[] lines = httpText.split(CRLF);
        //跳过第一行
        for (int i = 1; i < lines.length; i++) {
            String keyValue = lines[i];
            if ("".equals(keyValue)) {
                break;
            }
            String[] temp = keyValue.split(SPLITTER);
            if (temp.length == 2) {
                httpMessageHeaders.addHeader(new HttpHeader(temp[0], temp[1].trim()));
            }
        }
        return httpMessageHeaders;
    }

    /**
     * 设置报文是否包含Body到上下文中
     */
    private void setHasBody(HttpMessageHeaders httpMessageHeaders) {
        if (httpMessageHeaders.hasHeader("Content-Length")
                || (httpMessageHeaders.getFirstHeader("Transfer-Encoding") != null
                    && "chunked".equals(httpMessageHeaders.getFirstHeader("Transfer-Encoding").getValue())))
        {
            HttpParserContext.setHasBody(true);
        }
    }
}

如果请求包含了Body,就从InputStream中读取Content-Length长度的内容作为Body内容。Transfer-Encoding的暂时没处理,后续再加。

public class DefaultHttpBodyParser implements HttpBodyParser {
    @Override
    public HttpBody parse() {
        int contentLength = HttpParserContext.getBodyInfo().getContentLength();
        InputStream inputStream = HttpParserContext.getInputStream();
        try {
            byte[] body = IOUtils.readFully(inputStream, contentLength);
            String contentType = HttpParserContext.getContentType();
            String encoding = getEncoding(contentType);
            HttpBody httpBody =
                    new HttpBody(contentType, encoding, body);
            return httpBody;
        } catch (IOException e) {
            throw new ParserException(e);
        }
    }

    /**
     * 获取encoding
     * 例如:Content-type: application/json; charset=utf-8
     *
     * @param contentType
     * @return
     */
    private String getEncoding(String contentType) {
        String encoding = "utf-8";
        if (StringUtils.isNotBlank(contentType) && contentType.contains(";")) {
            encoding = contentType.split(";")[1].trim().replace("charset=", "");
        }
        return encoding;
    }
}

到此为止HTTP请求对象构造完毕。

编写静态资源Handler

在AbstractHttpEventHandler的基础上,添加静态资源返回功能。

总体流程为:从HTTP请求对象中获取到请求路径,在docBase目录下查找对应路径的资源并返回。

需要注意的是不同类型文件的Content-Type响应头需要设置正确,否则文件不会正确显示。

public class HttpStaticResourceEventHandler extends AbstractHttpEventHandler {

    private final String docBase;
    private final AbstractHttpRequestMessageParser httpRequestMessageParser;

    public HttpStaticResourceEventHandler(String docBase,
                                          AbstractHttpRequestMessageParser httpRequestMessageParser) {
        this.docBase = docBase;
        this.httpRequestMessageParser = httpRequestMessageParser;
    }

    @Override
    protected HttpRequestMessage doParserRequestMessage(Connection connection) {
        try {
            HttpRequestMessage httpRequestMessage = httpRequestMessageParser
                    .parse(connection.getInputStream());
            return httpRequestMessage;
        } catch (IOException e) {
            throw new HandlerException(e);
        }
    }

    @Override
    protected HttpResponseMessage doGenerateResponseMessage(
            HttpRequestMessage httpRequestMessage) {
        String path = httpRequestMessage.getRequestLine().getRequestURI().getPath();
        Path filePath = Paths.get(docBase, path);
        //目录、无法读取的文件都返回404
        if (Files.isDirectory(filePath) || !Files.isReadable(filePath)) {
            return HttpResponseConstants.HTTP_404;
        } else {
            ResponseLine ok = ResponseLineConstants.RES_200;
            HttpMessageHeaders headers = HttpMessageHeaders.newBuilder()
                    .addHeader("status", "200").build();
            HttpBody httpBody = null;
            try {
                setContentType(filePath, headers);
                httpBody = new HttpBody(new FileInputStream(filePath.toFile()));
            } catch (FileNotFoundException e) {
                return HttpResponseConstants.HTTP_404;
            } catch (IOException e) {
                throw new HandlerException(e);
            }
            HttpResponseMessage httpResponseMessage = new HttpResponseMessage(ok, headers,
                    Optional.ofNullable(httpBody));
            return httpResponseMessage;
        }

    }

    /**
     * 根据文件后缀设置文件Content-Type
     *
     * @param filePath
     * @param headers
     * @throws IOException
     */
    private void setContentType(Path filePath, HttpMessageHeaders headers) throws IOException {
        //使用Files.probeContentType在mac上总是返回null
        //String contentType = Files.probeContentType(filePath);
        String contentType = MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(filePath.toString());
        headers.addHeader(new HttpHeader("Content-Type", contentType));
        if (contentType.indexOf("text") == -1) {
            headers.addHeader(new HttpHeader("Content-Length",
                    String.valueOf(filePath.toFile().length())));
        }
    }

    @Override
    protected void doTransferToClient(HttpResponseMessage responseMessage,
                                      Connection connection) throws IOException {
        HttpResponseMessageWriter httpResponseMessageWriter = new HttpResponseMessageWriter();
        httpResponseMessageWriter.write(responseMessage, connection);
    }

}

启动服务器测试

修改BootStrap,添加对应功能

EventListener<Connection> socketEventListener3 =
                new ConnectionEventListener(
                    new HttpStaticResourceEventHandler(System.getProperty("user.dir"),
                        new DefaultHttpRequestMessageParser(new DefaultHttpRequestLineParser(),
                                new DefaultHttpQueryParameterParser(),
                                new DefaultHttpHeaderParser(),
                                new DefaultHttpBodyParser())));
SocketConnector connector3 =
                SocketConnectorFactory.build(18083, socketEventListener3);
ServerConfig serverConfig = ServerConfig.builder()
                .addConnector(connector3)
                .build();
        Server server = ServerFactory.getServer(serverConfig);
        server.start();

新建web目录,添加html、图片和js

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello Beggar Servlet Container</title>
    <link type="text/css" rel="stylesheet" href="main.css" />
</head>
<body>
<img src="spring.jpg">
<div id="content"></div>
<script src="index.js"></script>
</body>
</html>

index.js

var date = new Date();
var content = "now is " + date;
var div = document.querySelector("#content");
div.innerHTML = content;

main.css

body {
    background-color: antiquewhite;
}

#content {
    background-color: cornflowerblue;
}

本地测试:

用浏览器访问http://localhost:18083/web/index.html

显示如下

完整代码:https://github.com/pkpk1234/BeggarServletContainer/tree/step9

//TODO: 整理写死的字符串

results matching ""

    No results matching ""