使用Netty实现静态Web服务器

Netty是一个基于反应器模式、高性能的网络编程框架,提供了丰富的编解码器及各种协议的支持,包括http、http2、mqtt、dns等等。

以学习为目的,基于Netty实现一个简单的静态web服务器。
使用到的技术及开发环境:

  • Java:版本JDK11
  • Netty:版本4.1.77.Final
  • Log4j:日志框架2.17.2
  • HTTP1.1
  • HTTPS:使用到SSL/TLS
  • H2:HTTP2

1. 需求概述

基于netty实现静态web服务器。
能够提供基本的http服务,仅支持静态资源服务即可。

1.1. 支持的静态资源

支持静的态资源包括:HTML、JS、CSS、常见的图片(包括但不限于jpeg、png、svg、webp、gif等格式)、字体文件以及其他文件。
以上资源在响应到客户端时必须给与正确的MimeType(http中称MediaType),例如:html文件响应时,必须指定Content-Type:text/html响应头。
其他资源文件:非web资源文件,例如压缩文件,返回响应头Content-type:application/octet-stream。(大多数浏览器对a标签Content-Type:application/octet-stream响应头的处理方式为直接下载)。

1.2. 支持的http版本

支持HTTP1.1、HTTPS(HTTP over TLS)和H2。

1.3. 支持条件请求

返回静态资源时,生成last-modifiedEtag响应头,使浏览器能够缓存资源。支持If-modified-sinceIf-match请求头,后续请求依此判断缓存的资源是否有效。

1.4 支持范围请求

支持Range请求头,能够依据此头返回部分内容(状态码:206)。

1.5 可配置性

web服务器的一些配置能够可配置,支持的配置方式包括但不限于:

  • 程序命令行配置
  • jvm系统属性配置
  • 环境变量配置

支持的配置项,包括但不限于:

  • 监听的地址和端口号
  • web部署目录
  • SSL证书私钥文件
  • SSL证书文件 (用于实现https)

1.6.日志记录

能够记录服务器运行日志且日志参数可配置。

1.7.启动脚本

程序打包后以类似于Tomcat的方式提供一个启动脚本,一键启动服务器。
至少支持Windows和linux系统。

2. Netty与HTTP相关编解码器和处理器

2.1 HttpServerCodec编解码器

该编解码器是通道双向编解码器,包含了请求解码器和响应编码器,是Netty中Http服务器实现的核心类。下面是最全的构造方法:

public HttpServerCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders,
                           int initialBufferSize, boolean allowDuplicateContentLengths, boolean allowPartialChunks) {
        init(new HttpServerRequestDecoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders,
                                          initialBufferSize, allowDuplicateContentLengths, allowPartialChunks),
             new HttpServerResponseEncoder());
}

各参数解释:
控制内存使用的参数:

  • maxInitialLineLength:HTTP请求行的最大长度(例如:GET / HTTP/1.1),如果长度超过该值将抛出TooLongHttpHeaderException异常。
  • maxHeaderSize:HTTP请求中允许的最大请求头总和,如果超过此值将抛出 TooLongHttpHeaderException。
  • maxChunkSize:分割请求体的阈值,当请求体超过该值时被分割为更小的HttpContents。

控制解析行为的参数:

  • validateHeaders:是否校验请求头
  • initialBufferSize:初始化的buff大小
  • allowDuplicateContentLengths:请求头中是否允许出现重复的Content-Length头。如果设为false将拒绝该报文,设为true也只允许Content-Length的值为相同的十进制,否则拒绝。
  • allowPartialChunks:如果接收到一个chunk超出了buff可读的字节数并且该值为true,该chunk将被分割成多个HttpContent。否则,如果chunk的大小没有超出maxChunkSize并且该值设为false,buff中数据不会解析成httpContent,直到可读的字节数大于或等于chunk的大小。

2.2. HttpObjectAggregator聚合器

该处理器作用是聚合多个Http请求或响应部分为一个完整的FullHttpRequest 或FullHttpResponse。
构造函数如下:

public HttpObjectAggregator(int maxContentLength, boolean closeOnExpectationFailed) {
        super(maxContentLength);
        this.closeOnExpectationFailed = closeOnExpectationFailed;
}

参数解释:

  • maxContentLength:支持的聚合的最大内容长度(单位字节)
  • closeOnExpectationFailed: 如果发现了100-continue响应但是内容太大,true意味着关闭连接。否则连接将保持打开,数据将被消费和丢弃直到下一个请求到达。

2.3 SSLHandler SSL支持

Netty使用SSLHandler来实现高层应用的数据加密。
SSLHandler的初始化:

SslContext sslContext = SslContextBuilder
                        .forServer(sslConfig.getCert(), sslConfig.getPrivateKey())
                        .clientAuth(ClientAuth.NONE)
                        .build();
SSLEngine sslEngine = sslContext.newEngine(channel.alloc());                                sslEngine.setUseClientMode(false);
SslHandler sslHandler = new SslHandler(sslEngine, false);     
channel.pipeline().addLast("ssl", sslHandler);                  

3. 实现

3.1 WebServer

package com.github.cloudgyb.webserver;

import com.github.cloudgyb.webserver.config.SSLConfig;
import com.github.cloudgyb.webserver.config.WebServerConfig;
import com.github.cloudgyb.webserver.http.HttpRequestHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.ssl.*;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;

/**
 * 用于启动web服务器
 *
 * @author cloudgyb
 */
public class WebServer {
    private final static Logger logger = LoggerFactory.getLogger(WebServer.class);

    public static void start(WebServerConfig config) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        NioEventLoopGroup boosGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        HttpRequestHandler httpRequestHandler = new HttpRequestHandler(config);
        SSLConfig sslConfig = config.getSslConfig();
        boolean hasSSLConfig = false;
        SslContext sslContext = null;
        if (sslConfig != null) {
            try {
                sslContext = SslContextBuilder
                        .forServer(sslConfig.getCert(), sslConfig.getPrivateKey())
                        .clientAuth(ClientAuth.NONE)
                        .build();
                hasSSLConfig = true;
            } catch (SSLException e) {
                throw new RuntimeException(e);
            }
        }
        try {
            boolean finalHasSSLConfig = hasSSLConfig;
            SslContext finalSslContext = sslContext;
            ChannelFuture cf = serverBootstrap.group(boosGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, config.getTcpBacklog())
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel channel) {
                            if (finalHasSSLConfig) {
                                SSLEngine sslEngine = finalSslContext.newEngine(channel.alloc());
                                sslEngine.setUseClientMode(false);
                                SslHandler sslHandler = new SslHandler(sslEngine, false);
                                channel.pipeline()
                                        .addLast("ssl", sslHandler);
                            }
                            channel.pipeline()
                                    .addLast("httpRequestDecoder", new HttpServerCodec())
                                    .addLast("httpAgg", new HttpObjectAggregator(512))
                                    .addLast("chunked", new ChunkedWriteHandler())
                                    .addLast("httpRequestHandler", httpRequestHandler);
                        }
                    })
                    .bind(config.getHost(), config.getPort());
            logger.info("Web Server listen at " + config.getHost() + ":" + config.getPort() + "...");
            cf.sync().channel().closeFuture().sync();
        } catch (InterruptedException e) {
            logger.info("被中断退出...");
            throw new RuntimeException(e);
        } finally {
            workerGroup.shutdownGracefully();
            boosGroup.shutdownGracefully();
        }
    }
}

完整代码在github:https://github.com/cloudgyb/static-webserver

4. 参考文献

RFC7230: Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing
RFC7231: Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content
RFC7232: Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests
RFC7233: Hypertext Transfer Protocol (HTTP/1.1): Range Requests
MDN: MDN HTTP

你可能感兴趣的:(HTTP,Java,https,HTTP,web服务器,Netty,Java)