前言
大家好,我是老马。很高兴遇到你。
我们希望实现最简单的 http 服务信息,可以处理静态文件。
如果你想知道 servlet 如何处理的,可以参考我的另一个项目:
手写从零实现简易版 tomcat minicat
netty 相关
如果你对 netty 不是很熟悉,可以读一下
Netty 权威指南-04-为什么选择 Netty?Netty 入门教程
手写 nginx 系列
如果你对 nginx 原理感兴趣,可以阅读:
从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?
从零手写实现 nginx-03-nginx 基于 Netty 实现
从零手写实现 nginx-04-基于 netty http 出入参优化处理
从零手写实现 nginx-05-MIME类型(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展类型)
从零手写实现 nginx-12-keep-alive 连接复用
从零手写实现 nginx-13-nginx.conf 配置文件介绍
从零手写实现 nginx-14-nginx.conf 和 hocon 格式有关系吗?
从零手写实现 nginx-15-nginx.conf 如何通过 java 解析处理?
从零手写实现 nginx-16-nginx 支持配置多个 server
前言
我们上一篇文章中,使用 netty 优化我们的 io 模型。
对于请求和响应是基于自己的代码封装实现的。
但是 http 协议本身比较复杂,自己实现起来要耗费大量的时间。
那么,有没有现成的实现呢?
答案是 netty 已经帮我们封装好了。
核心代码
启动类
我们对启动类调整如下:
/**
* netty 实现
*
* @author 老马啸西风
* @since 0.2.0
*/
public class NginxServerNetty implements INginxServer {
//basic ...
@Override
public void start() {
// 服务器监听的端口号
String host = InnerNetUtil.getHost();
int port = nginxConfig.getHttpServerListen();
EventLoopGroup bossGroup = new NioEventLoopGroup();
//worker 线程池的数量默认为 CPU 核心数的两倍
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpRequestDecoder()); // 请求消息解码器
p.addLast(new HttpObjectAggregator(65536)); // 目的是将多个消息转换为单一的request或者response对象
p.addLast(new HttpResponseEncoder()); // 响应解码器
p.addLast(new ChunkedWriteHandler()); // 目的是支持异步大文件传输
// 业务逻辑
p.addLast(new NginxNettyServerHandler(nginxConfig));
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// Bind and start to accept incoming connections.
ChannelFuture future = serverBootstrap.bind(port).sync();
log.info("[Nginx4j] listen on http://{}:{}", host, port);
// Wait until the server socket is closed.
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
// 省略...
}
}
}
NginxNettyServerHandler 业务逻辑
这个类可以变得非常简单
/**
* netty 处理类
* @author 老马啸西风
* @since 0.2.0
*/
public class NginxNettyServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
//...
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
logger.info("[Nginx] channelRead writeAndFlush start request={}", request);
// 分发
final NginxRequestDispatch requestDispatch = nginxConfig.getNginxRequestDispatch();
FullHttpResponse response = requestDispatch.dispatch(request, nginxConfig);
// 结果响应
ChannelFuture lastContentFuture = ctx.writeAndFlush(response);
//如果不支持keep-Alive,服务器端主动关闭请求
if (!HttpUtil.isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
logger.info("[Nginx] channelRead writeAndFlush DONE response={}", response);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("[Nginx] exceptionCaught", cause);
ctx.close();
}
}
分发处理
分发处理的逻辑,主要是构建响应内容。
我们先实现最基本的能力:
/**
* 内容的分发处理
*
* @param requestInfoBo 请求
* @param nginxConfig 配置
* @return 结果
* @author 老马啸西风
*/
public FullHttpResponse dispatch(final FullHttpRequest requestInfoBo, final NginxConfig nginxConfig) {
// 消息解析不正确
/*如果无法解码400*/
if (!requestInfoBo.decoderResult().isSuccess()) {
log.warn("[Nginx] base request for http={}", requestInfoBo);
return buildCommentResp(null, HttpResponseStatus.BAD_REQUEST, requestInfoBo, nginxConfig);
}
final String basicPath = nginxConfig.getHttpServerRoot();
final String path = requestInfoBo.uri();
boolean isRootPath = isRootPath(requestInfoBo, nginxConfig);
// 根路径
if(isRootPath) {
log.info("[Nginx] current req meet root path");
String indexContent = nginxConfig.getNginxIndexContent().getContent(nginxConfig);
return buildCommentResp(indexContent, HttpResponseStatus.OK, requestInfoBo, nginxConfig);
}
// other
String fullPath = FileUtil.buildFullPath(basicPath, path);
if(FileUtil.exists(fullPath)) {
String fileContent = FileUtil.getFileContent(fullPath);
return buildCommentResp(fileContent, HttpResponseStatus.OK, requestInfoBo, nginxConfig);
} else {
return buildCommentResp(null, HttpResponseStatus.NOT_FOUND, requestInfoBo, nginxConfig);
}
}
核心逻辑:
1)如果请求体解析失败,直接返回。
2)根路径,则返回 index 内容
3)否则解析处理文件内容,不存在则返回 404
resp 构建的方法暂时简单实现如下,后续我们会持续改进
/**
* String format = "HTTP/1.1 200 OK\r\n" +
* "Content-Type: text/plain\r\n" +
* "\r\n" +
* "%s";
*
* @param rawText 原始内容
* @param status 结果枚举
* @param request 请求内容
* @param nginxConfig 配置
* @return 结果
* @author 老马啸西风
*/
protected FullHttpResponse buildCommentResp(String rawText,
final HttpResponseStatus status,
final FullHttpRequest request,
final NginxConfig nginxConfig) {
String defaultContent = status.toString();
if(StringUtil.isNotEmpty(rawText)) {
defaultContent = rawText;
}
// 构造响应
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
status, Unpooled.copiedBuffer(defaultContent, CharsetUtil.UTF_8));
// 头信息
// TODO: 根据文件变化
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
//如果request中有KEEP ALIVE信息
if (HttpUtil.isKeepAlive(request)) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
return response;
}
小结
本节我们使用 netty 简化出入参的处理。
但是响应的构建还不够完善,我们下一节来一起优化一下响应的处理。
我是老马,期待与你的下次重逢。
开源地址
为了便于大家学习,已经将 nginx 开源
https://github.com/houbb/nginx4j
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。