美文网首页
从零手写实现 nginx-04-基于 netty http 出入

从零手写实现 nginx-04-基于 netty http 出入

作者: 老马啸西风2020 | 来源:发表于2024-06-03 09:02 被阅读0次

    前言

    大家好,我是老马。很高兴遇到你。

    我们希望实现最简单的 http 服务信息,可以处理静态文件。

    如果你想知道 servlet 如何处理的,可以参考我的另一个项目:

    手写从零实现简易版 tomcat minicat

    netty 相关

    如果你对 netty 不是很熟悉,可以读一下

    Netty 权威指南-01-BIO 案例

    Netty 权威指南-02-NIO 案例

    Netty 权威指南-03-AIO 案例

    Netty 权威指南-04-为什么选择 Netty?Netty 入门教程

    手写 nginx 系列

    如果你对 nginx 原理感兴趣,可以阅读:

    从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?

    从零手写实现 nginx-02-nginx 的核心能力

    从零手写实现 nginx-03-nginx 基于 Netty 实现

    从零手写实现 nginx-04-基于 netty http 出入参优化处理

    从零手写实现 nginx-05-MIME类型(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展类型)

    从零手写实现 nginx-06-文件夹自动索引

    从零手写实现 nginx-07-大文件下载

    从零手写实现 nginx-08-范围查询

    从零手写实现 nginx-09-文件压缩

    从零手写实现 nginx-10-sendfile 零拷贝

    从零手写实现 nginx-11-file+range 合并

    从零手写实现 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

    相关文章

      网友评论

          本文标题:从零手写实现 nginx-04-基于 netty http 出入

          本文链接:https://www.haomeiwen.com/subject/gyfeqjtx.html