Netty内置的http报文解码流程

发布时间 2023-12-09 21:45:28作者: 过移

netty解码

netty通过内置处理器HttpRequestDecoder和HttpObjectAggregator对Http请求报文进行解码之后,Netty会将Http请求封装成一个FullHttpRequest实例,然后发送给下一站。

Netty内置的与Http请求报文相对应的类大致有如下几个:

(1)FullHttpRequest:包含整个Http请求的信息,包含对HttpRequest首部和HttpContent请求体的结合。
(2)HttpRequest:请求首部,主要包含对Http请求行和请求头的组合。
(3)HttpContent:对Http请求体进行封装,本质上就是一个ByteBuf缓冲区实例。如果ByteBuf的长度是固定的,则请求体过大,可能包含多个HttpContent。解码的时候,最后一个解码返回对象为LastHttpContent(空的HttpContent),表示对请求体的解码已经结束。
(4)HttpMethod:主要是对Http报文请求头的封装及相关操作。
(5)HttpVersion:对Http版本的封装。
(6)HttpHeaders:包含对http报文请求头的封装及相关操作。

Netty的HttpRequest首部类中有一个String uri成员,主要是对请求uri的封装,该成员包含了Http请求的Path路径与跟随在其后的请求参数。
有关请求参数的解析,不同的Web服务器所使用的解析策略有所不同。在tomcat中,如果客户端提交的是application/x-www-form-urlencoded类型的表单post请求,则java请求参数实例除了包含跟随在uri后面的键-值对之外,请求参数还包含Http请求体body中的键-值对。在netty中,java中请求参数实例仅仅包含跟在uri后面的键-值对。
接下来介绍本文的重点:Netty的Http报文拆包方案。

一般来说,服务端收到的Http字节流可能被分成多个ByteBuf包。Netty服务端如何处理Http报文的分包问题呢?大致有以下几种策略:

(1)定长分包策略:接收端按照固定长度进行分割,发送端按照固定长度发送数据包。
(2)长度域分包策略:比如使用LengthFieldBasedFrameDecoder长度域解码器在接收端分包,而在发送端先发送4个字节表示信息的长度,紧接着发送消息的内容。
(3)分割符分割:比如说使用LineBasedFrameDecoder解码器通过换行符进行分包,或者使用DelimiterBasedFrameDecoder通过特定的分隔符进行分包。
netty结合使用上面第(2)种和第(3)种的策略完成http报文的拆包:对于请求头,应用了分隔符分包的策略,以特定分隔符("\r\n")进行拆包;对于Http请求体,应用长度字段中的分包策略,按照请求头中的内容长度进行内容拆包。

Netty的Http响应编码流程

Netty的Http响应的处理流程只需在流水线装配HttpResponseEncoder编码器即可。该编码器是一个出站处理器,有以下特点:
(1)该编码器输入的是FullHttpResponse响应实例,输出的是ByteBuf字节缓冲器。后面的处理器会将ByteBuf数据写入Channel,最终被发送到Http客户端。
(2)该编码器按照Http对入站FullHttpResponse实例的请求行,请求头,请求体进行序列化,通过请求头去判断是否含有Content-Length头或者Trunked头,然后将请求体按照对应长度规则对内容进行序列化。

如果只是发送简单的Http响应,就可以通过DefaultFullHttpResponse默认响应实现类完成。通过该默认响应类既可以设置响应的内容,又可以进行响应头的设置。

public class HttpProtocolHelper
{
    public static final int HTTP_CACHE_SECONDS = 60;

    public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
    public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
    private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");

    public static final AttributeKey<HttpVersion> PROTOCOL_VERSION_KEY =
            AttributeKey.valueOf("PROTOCOL_VERSION");
    public static final AttributeKey<Boolean> KEEP_ALIVE_KEY =
            AttributeKey.valueOf("KEEP_ALIVE_KEY");


    /**
     * 通过channel 缓存 Http 的协议版本,以及是否为长连接
     *
     * @param ctx     上下文
     * @param request 报文
     */
    public static void cacheHttpProtocol(ChannelHandlerContext ctx, final FullHttpRequest request)
    {
        //每一个连接设置一次即可,不需要重复设置
        if (ctx.channel().attr(KEEP_ALIVE_KEY).get() == null)
        {
            ctx.channel().attr(PROTOCOL_VERSION_KEY).set(request.protocolVersion());
            final boolean keepAlive = HttpUtil.isKeepAlive(request);
            ctx.channel().attr(KEEP_ALIVE_KEY).set(keepAlive);
        }
    }


    public static void setKeepAlive(ChannelHandlerContext ctx, boolean val)
    {
        ctx.channel().attr(KEEP_ALIVE_KEY).set(val);
    }

    public static String sanitizeUri(String uri, String dir)
    {
        // Decode the path.
        try
        {
            uri = URLDecoder.decode(uri, "UTF-8");
        } catch (UnsupportedEncodingException e)
        {
            throw new Error(e);
        }

        if (uri.isEmpty() || uri.charAt(0) != '/')
        {
            return null;
        }

        // Convert file separators.
        uri = uri.replace('/', File.separatorChar);

        // Simplistic dumb security check.
        // You will have to do something serious in the production environment.
        if (uri.contains(File.separator + '.') ||
                uri.contains('.' + File.separator) ||
                uri.charAt(0) == '.' || uri.charAt(uri.length() - 1) == '.' ||
                INSECURE_URI.matcher(uri).matches())
        {
            return null;
        }

        // Convert to absolute path.
        return dir + File.separator + uri;
    }

    private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*");

    public static void sendListing(ChannelHandlerContext ctx, final FullHttpRequest request,
                                   File dir, String dirPath)
    {
        StringBuilder buf = new StringBuilder()
                .append("<!DOCTYPE html>\r\n")
                .append("<html><head><meta charset='utf-8' /><title>")
                .append("Listing of: ")
                .append(dirPath)
                .append("</title></head><body>\r\n")

                .append("<h3>Listing of: ")
                .append(dirPath)
                .append("</h3>\r\n")

                .append("<ul>")
                .append("<li><a href=\"../\">..</a></li>\r\n");

        File[] files = dir.listFiles();
        if (files != null)
        {
            for (File f : files)
            {
                if (f.isHidden() || !f.canRead())
                {
                    continue;
                }

                String name = f.getName();
                if (!ALLOWED_FILE_NAME.matcher(name).matches())
                {
                    continue;
                }

                buf.append("<li><a href=\"")
                        .append(name)
                        .append("\">")
                        .append(name)
                        .append("</a></li>\r\n");
            }
        }

        buf.append("</ul></body></html>\r\n");

        ByteBuf buffer = ctx.alloc().buffer(buf.length());
        buffer.writeCharSequence(buf.toString(), CharsetUtil.UTF_8);

        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, buffer);
        response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");

        sendAndCleanupConnection(ctx, response);
    }

    public static void sendRedirect(ChannelHandlerContext ctx, final FullHttpRequest request, String newUri)
    {
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND, Unpooled.EMPTY_BUFFER);
        response.headers().set(LOCATION, newUri);

        sendAndCleanupConnection(ctx, response);
    }

    public static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status)
    {
        HttpVersion version = getHttpVersion(ctx);
        FullHttpResponse response = new DefaultFullHttpResponse(
                version, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));
        response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");

        sendAndCleanupConnection(ctx, response);
    }

    /**
     * 发送普通文本响应
     *
     * @param ctx     上下文
     * @param content 响应内容
     */
    public static void sendContent(ChannelHandlerContext ctx, String content)
    {
        HttpVersion version = getHttpVersion(ctx);
        FullHttpResponse response = new DefaultFullHttpResponse(
                version, OK, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8));
        response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");

        sendAndCleanupConnection(ctx, response);
    }

    /**
     * 发送html页面响应
     *
     * @param ctx     上下文
     * @param content 响应内容
     */
    public static void sendWebPage(ChannelHandlerContext ctx, String content)
    {
        HttpVersion version = getHttpVersion(ctx);
        FullHttpResponse response = new DefaultFullHttpResponse(
                version, OK, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8));
        response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");

        sendAndCleanupConnection(ctx, response);
    }

    /**
     * 发送Json格式的响应
     *
     * @param ctx     上下文
     * @param content 响应内容
     */
    public static void sendJsonContent(ChannelHandlerContext ctx, String content)
    {
        HttpVersion version = getHttpVersion(ctx);
        /**
         * 构造一个默认的FullHttpResponse实例
         */
        FullHttpResponse response = new DefaultFullHttpResponse(
                version, OK, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8));
        /**
         * 设置响应头
         */
        response.headers().set(CONTENT_TYPE, "application/json; charset=UTF-8");
        /**
         * 发送响应内容
         */
        sendAndCleanupConnection(ctx, response);
    }

    /**
     * 发送响应
     */
    public static void sendAndCleanupConnection(ChannelHandlerContext ctx, FullHttpResponse response)
    {
        final boolean keepAlive = ctx.channel().attr(KEEP_ALIVE_KEY).get();
        HttpUtil.setContentLength(response, response.content().readableBytes());
        if (!keepAlive)
        {
            // 如果不是长连接,设置 connection:close 头部
            response.headers().set(CONNECTION, CLOSE);
        } else if (isHTTP_1_0(ctx))
        {
            // 如果是1.0版本的长连接,设置 connection:keep-alive 头部
            response.headers().set(CONNECTION, KEEP_ALIVE);
        }

        //发送内容
        ChannelFuture writePromise = ctx.channel().writeAndFlush(response);

        if (!keepAlive)
        {
            // 如果不是长连接,发送完成之后,关闭连接
            writePromise.addListener(ChannelFutureListener.CLOSE);
        }
    }

    private static HttpVersion getHttpVersion(ChannelHandlerContext ctx)
    {
        HttpVersion version;
        if (isHTTP_1_0(ctx))
        {
            version = HTTP_1_0;
        } else
        {
            version = HTTP_1_1;
        }
        return version;
    }

    /**
     * When file timestamp is the same as what the browser is sending up, send a "304 Not Modified"
     *
     * @param ctx Context
     */
    public static void sendNotModified(ChannelHandlerContext ctx)
    {
        HttpVersion version = getHttpVersion(ctx);
        FullHttpResponse response = new DefaultFullHttpResponse(version, NOT_MODIFIED, Unpooled.EMPTY_BUFFER);
        setDateHeader(response);

        sendAndCleanupConnection(ctx, response);
    }


    public static boolean isHTTP_1_0(ChannelHandlerContext ctx)
    {

        HttpVersion protocol_version =
                ctx.channel().attr(PROTOCOL_VERSION_KEY).get();
        if (null == protocol_version)
        {
            return false;
        }
        if (protocol_version.equals(HTTP_1_0))
        {
            return true;
        }
        return false;
    }

    /**
     * Sets the Date header for the HTTP response
     *
     * @param response HTTP response
     */
    public static void setDateHeader(FullHttpResponse response)
    {
        SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
        dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));

        Calendar time = new GregorianCalendar();
        response.headers().set(DATE, dateFormatter.format(time.getTime()));
    }

    /**
     * Sets the Date and Cache headers for the HTTP Response
     *
     * @param response    HTTP response
     * @param fileToCache file to extract content type
     */
    public static void setDateAndCacheHeaders(HttpResponse response, File fileToCache)
    {
        SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
        dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));

        // Date header
        Calendar time = new GregorianCalendar();
        response.headers().set(DATE, dateFormatter.format(time.getTime()));

        //设置缓存过期时间
        time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);
        response.headers().set(EXPIRES, dateFormatter.format(time.getTime()));
        response.headers().set(CACHE_CONTROL, "private, max-platform=" + HTTP_CACHE_SECONDS);

        //最近修改时间
        String lastModified = dateFormatter.format(new Date(fileToCache.lastModified()));
        response.headers().set(LAST_MODIFIED, lastModified);
    }

    /**
     * Sets the content type header for the HTTP Response
     *
     * @param response HTTP response
     * @param file     file to extract content type
     */
    public static void setContentTypeHeader(HttpResponse response, File file)
    {
        MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
        response.headers().set(CONTENT_TYPE,
                mimeTypesMap.getContentType(file.getPath()));
    }


    public static void setKeepAlive(ChannelHandlerContext ctx, HttpResponse response)
    {
        final boolean keepAlive = ctx.channel().attr(KEEP_ALIVE_KEY).get();

        if (!keepAlive)
        {
            response.headers().set(CONNECTION, CLOSE);

        } else if (isHTTP_1_0(ctx))
        {
            response.headers().set(CONNECTION, KEEP_ALIVE);
        }

    }

    public static boolean isKeepAlive(ChannelHandlerContext ctx)
    {
        boolean keepAlive = ctx.channel().attr(KEEP_ALIVE_KEY).get();
        return keepAlive;
    }

    /**
     * 发送目录或者错误信息,如果是文件,则返回
     *
     * @param ctx     上下文
     * @param request 请求
     * @return 文件对象
     */
    public static File sendErrorOrDirectory(ChannelHandlerContext ctx, FullHttpRequest request)
    {
        /**
         * 路径不对
         */
        final String uri = request.uri();
        final String path = HttpProtocolHelper.sanitizeUri(uri, SystemConfig.getFileServerDir());
        if (path == null)
        {
            HttpProtocolHelper.sendError(ctx, FORBIDDEN);
            return null;
        }
        File file = new File(path);

        /**
         * 文件不存在
         */
        if (!file.exists())
        {
            HttpProtocolHelper.sendError(ctx, NOT_FOUND);
            return null;
        }


        /**
         * 发送文件目录
         */
        if (file.isDirectory())
        {
            if (uri.endsWith("/"))
            {
                HttpProtocolHelper.sendListing(ctx, request, file, uri);
            } else
            {
                HttpProtocolHelper.sendRedirect(ctx, request, uri + '/');
            }
            return null;
        }
        /**
         * 文件不可用访问
         */
        if (!file.isFile())
        {
            HttpProtocolHelper.sendError(ctx, FORBIDDEN);
            return null;
        }

        return file;
    }

    /**
     * 根据文件,获取只读的随机访问文件实例
     *
     * @param ctx  上下文
     * @param file 文件
     * @return 随机访问文件实例
     */
    public static RandomAccessFile openFile(ChannelHandlerContext ctx, File file)
    {
        RandomAccessFile raf = null;
        try
        {
            raf = new RandomAccessFile(file, "r");
        } catch (FileNotFoundException ignore)
        {
            HttpProtocolHelper.sendError(ctx, NOT_FOUND);
            return null;
        }
        return raf;
    }
}

参考文献:java高并发核心编程Nio、Netty、Redis、ZooKeeper 作者:尼恩