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
基于netty实现静态web服务器。
能够提供基本的http服务,仅支持静态资源服务即可。
支持静的态资源包括: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响应头的处理方式为直接下载)。
支持HTTP1.1、HTTPS(HTTP over TLS)和H2。
1.3. 支持条件请求返回静态资源时,生成last-modified和Etag响应头,使浏览器能够缓存资源。支持If-modified-since和If-match请求头,后续请求依此判断缓存的资源是否有效。
1.4 支持范围请求支持Range请求头,能够依据此头返回部分内容(状态码:206)。
1.5 可配置性web服务器的一些配置能够可配置,支持的配置方式包括但不限于:
- 程序命令行配置
- jvm系统属性配置
- 环境变量配置
支持的配置项,包括但不限于:
- 监听的地址和端口号
- web部署目录
- SSL证书私钥文件
- SSL证书文件 (用于实现https)
能够记录服务器运行日志且日志参数可配置。
1.7.启动脚本程序打包后以类似于Tomcat的方式提供一个启动脚本,一键启动服务器。
至少支持Windows和linux系统。
该编解码器是通道双向编解码器,包含了请求解码器和响应编码器,是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的大小。
该处理器作用是聚合多个Http请求或响应部分为一个完整的FullHttpRequest 或FullHttpResponse。
构造函数如下:
public HttpObjectAggregator(int maxContentLength, boolean closeOnExpectationFailed) {
super(maxContentLength);
this.closeOnExpectationFailed = closeOnExpectationFailed;
}
参数解释:
- maxContentLength:支持的聚合的最大内容长度(单位字节)
- closeOnExpectationFailed: 如果发现了100-continue响应但是内容太大,true意味着关闭连接。否则连接将保持打开,数据将被消费和丢弃直到下一个请求到达。
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;
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() {
@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



