首页 > 其他分享 >Netty内置的http报文解码流程

Netty内置的http报文解码流程

时间:2023-12-09 21:56:41浏览次数:33  
标签:Netty http 报文 ctx uri public static HTTP response

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 作者:尼恩

标签:Netty,http,报文,ctx,uri,public,static,HTTP,response
From: https://www.cnblogs.com/nanshaws/p/17891468.html

相关文章

  • Apache HTTP Server 的安装与配置
    一、概要1.环境(1)RockyLinux9.3二、安装与配置1.安装(1)安装sudodnfinstallhttpd-y(2)服务sudosystemctlstarthttpdsudosystemctlenablehttpdsystemctlstatushttpd(3)防火墙sudofirewall-cmd--add-service={http,https}--permanentsudo......
  • 前端歌谣-第四拾九课-node之http模块之fs模块
    前言我是歌谣微信公众号关注前端小歌谣一起学习前端知识今天继续给大家讲解node中fs模块的讲解创建文件constfs=require("fs")fs.mkdir("./geyao",(err)=>{console.log(err)if(err&&err.code==="EEXIST"){console.log("目录已经存在")}})运行结果重命......
  • 前端歌谣-第四拾捌课-node之http模块之event模块
    前言我是歌谣微信公众号关注前端小歌谣一起学习前端知识今天继续给大家讲解node中event的讲解案例constEventEmitter=require("events")constevent=newEventEmitter()event.on("play",()=>{console.log("事件触发了")})event.emit("play")运行结果案例1varhttp=r......
  • 认识HTTP协议与apache
    万维网:(www)并非计算机网络,而是一个大型的数据库,可以实现网页与网页之间的跳转url:资源定位符描述了一个资源在服务器上的具体位置http:超文本传输协议图片视频小程序http:HyperTextTransferProtocol应用层协议,默认端口:80/tcp可以使用http协议的软件apachenginx......
  • 深入探究 Python 异步编程:利用 asyncio 和 aiohttp 构建高效并发应用
    在现代编程中,异步编程已成为处理高并发和IO密集型任务的重要方式。Python提供了强大的异步编程支持,包括asyncio库和aiohttp等框架。本文将深入探讨异步编程的概念,以及在Python中如何利用异步框架来实现高效的并发编程。1.异步编程概念异步编程允许程序在等待IO操作完成时......
  • 如何在 Angular 应用中发起 HTTP 302 redirect
    代码如下:import{RESPONSE}from'@nguniversal/express-engine/tokens'import{Response}from'express'...constructor(protected@Optional()@Inject(RESPONSE)serverResponse:Response){}...//forexample:this.serverResponse?.status......
  • HTTP 302 Redirect 解释与举例
    HTTP302Redirect解释与举例HTTP302Redirect是指HTTP协议中的一种重定向状态码,用于指示请求的资源被临时移动到其他位置。这种状态码告诉客户端发起新的请求,新的请求将指向重定向后的位置。在Web开发中,302重定向常用于实现页面跳转、URL重定向以及处理用户身份验证等场景。......
  • LWIP官方httpd使用之GET
    前言httpd的移植可以参考上篇文章LWIP官方DEMO使用之httpd服务-USTHzhanglu-博客园(cnblogs.com)此博文为学习笔记,仅介绍如何使用官方demo,无更深入分析。此博文介绍了如何通过GET返回各种数据。关键词:LWIP,HTTP,HTTPD,GET|LWIP版本|lwip-STABLE-2_2_0_RC1|GET......
  • httpclient跳过SSL证书验证的写法
    最近在请求https接口的时候,发生了异常:sun.security.validator.ValidatorException:PKIXpathbuildingfailed:sun.security.provider.certpath.SuncertPathBuilderException:unabletofindvalidcertificationpathtorequestedtarget无法找到到请求目标的有效证书路......
  • 无涯教程-Angular7 - Http Client
    HttpClient将帮助我们提供POST,GET相关方法,使用时需要导入http模块。我们需要将模块导入app.module.ts中,如下所示-import{BrowserModule}from'@angular/platform-browser';import{NgModule}from'@angular/core';import{AppRoutingModule,RoutingComponent}from......