Netty之Http

Netty之Http

Http协议

HTTP是一个属于应用层的面向对象的协议,由于其使用简捷和快速的方式,非常适用于分布式超媒体信息系统。

HTTP协议特点

  • 支持Server/Client模式;
  • 简单——客户向服务器请求服务时,只需要指定服务的URL(统一资源定位符)并且携带对应的请求参数或消息体即可;

  • 灵活——HTTP允许传输任意类型的数据对象,内容类型由HTTP消息头中的Content-Type来进行标记;

  • 无状态——HTTP协议是无状态协议,也就是说该协议对于事务处理没有记忆能力。无状态意味着如果后续处理需要之前的信息,则必须重传,这样就会导致每次连接传送的数据量增大,另外一方面,在服务器不需要先前信息的时候它的应答就较大,负载较轻。

标示 ASCII 描述 字符
CR 13 Carriage return (回车) \n
LF 10 Line feed character(换行) \r
SP 32 Horizontal space(空格)
COLON 58 COLON(冒号) :

http协议主要使用CRLF进行分割。

请求包

图1.png

三部分组成:

  • 请求行(Line)
    主要包含三部分:Method ,URI ,协议/版本。 各部分之间使用空格(SP)分割。整个请求头使用CRLF分割。(比如:POST /1.0.0/_health_check HTTP/1.1 CRLF)
  • 请求头(header)
    格式为(name :value),用于客户端请求的描述信息。header之间以CRLF进行分割。最后一个header会多加一个CRLF。( 比如:Connection: keep-alive CRLF CRLF)
  • 请求正文
    里面主要是Post提交的数据(可支持多种格式,格式在Content-Type定义,长度是在Content-Length里面定义)。

响应包

图2.png

三部分组成:

  • 状态行(line)
    包含三部分:http版本,服务器返回状态码,描述信息。以CRLF进行分割。 ( 比如:HTTP/1.1 200 OK CRLF)
  • 响应头(header)
    格式为(name :value),用于服务器返回的描述信息。header之间以CRLF进行分割。最后一个header会多加一个CRLF (比如:Content-Type: text/html CRLF Content-Encoding:gzip CRLF CRLF)
  • 响应正文(body)
    里面主要是返回数据(可支持多种格式,格式在Content-Type定义,长度是在Content-Length里面定义)。

chunked

HTTP协议通常使用Content-Length来标识body的长度,在服务器端,需要先申请对应长度的buffer,然后再赋值。如果需要一边生产数据一边发送数据,就需要使用"Transfer-Encoding: chunked" 来代替Content-Length,也就是对数据进行分块传输。

Content-Length

  • 1:http server接收数据时,发现header中有Content-Length属性,则读取Content-Length 的值,确定需要读取body的长度。

  • 2:http server发送数据时,根据需要发送byte的长度,在header中增加 Content-Length 项,其中value为byte的长度,然后将byte 数据当做body发送到客户端。

chunked

  • 1:http server接收数据时,发现header中有Transfer-Encoding: chunked,则会按照truncked协议分批读取数据。

  • 2:http server发送数据时,如果需要分批发送到客户端,则需要在header中加上 Transfer-Encoding: chunked,然后按照truncked协议分批发送数据

truncked协议

图3.png

主要三部分:

  • chunk主要包含大小和数据,大小表示这个这个trunck包的大小,使用16进制标示。其中trunk之间的分隔符为CRLF。
  • 通过last-chunk来标识chunk发送完成。 一般读取到last-chunk(内容为0)的时候,代表chunk发送完成。

  • trailer 表示增加header等额外信息,一般情况下header是空。通过CRLF来标识整个chunked数据发送完成。

优点

  • 1:假如body的长度是10K,对于Content-Length则需要申请10K连续的buffer,而对于Transfer-Encoding: chunked可以申请1k的空间,然后循环使用10次。节省了内存空间的开销。

  • 2:如果内容的长度不可知,则可使用trunked方式能有效的解决Content-Length的问题

  • 3:http服务器压缩可以采用分块压缩,而不是整个快压缩。分块压缩可以一边进行压缩,一般发送数据,来加快数据的传输时间。

缺点

  • 1:truncked 协议解析比较复杂。

  • 2:在http转发的场景下(比如nginx) 难以处理,比如如何对分块数据进行转发。

http针对拆包粘包解决方案

  • 1:请求行的边界是CRLF,如果读取到CRLF,则意味着请求行的信息已经读取完成。

  • 2:Header的边界是CRLF,如果连续读取两个CRLF,则意味着header的信息读取完成。

  • 3:body的长度是有Content-Length 来进行确定。如果没有Content-Length ,则是chunked协议

Netty对Http的处理

对请求request的抽象:

图4.png
  • HttpMethod:主要是对method的封装,包含method序列化的操作

  • HttpVersion: 对version的封装,netty包含1.0和1.1的版本

  • QueryStringDecoder: 主要是对url进行封装,解析path和url上面的参数。(Tips:在tomcat中如果提交的post请求是application/x-www-form-urlencoded,则getParameter获取的是包含url后面和body里面所有的参数,而在netty中,获取的仅仅是url上面的参数)

  • HttpHeaders:包含对header的内容进行封装及操作

  • HttpContent:是对body进行封装,本质上就是一个ByteBuf。如果ByteBuf的长度是固定的,则请求的body过大,可能包含多个HttpContent,其中最后一个为LastHttpContent(空的HttpContent),用来说明body的结束。

  • HttpRequest:主要包含对Request Line和Header的组合

  • FullHttpRequest: 主要包含对HttpRequest 和httpContent 的组合

request处理流程

只需要在netty 的pipeLine 中配置 HttpRequestDecoder 和HttpObjectAggregator。

图5.png
  • 1:如果把解析这块理解是一个黑盒的话,则输入是ByteBuf,输出是FullHttpRequest。通过该对象便可获取到所有与http协议有关的信息。

  • 2:HttpRequestDecoder 先通过 RequestLine 和Heade解析成HttpRequest对象,传入到HttpObjectAggregator。然后再通过body解析出httpConten对象,传入到HttpObjectAggregator。当HttpObjectAggregator 发现是LastHttpContent ,则代表http协议解析完成,封装FullHttpRequest。

  • 3:对于body内容的读取涉及到Content-Length和trunked两种方式。两种方式只是在解析协议时处理的不一致,最终输出是一致的。

HttpObjectAggregator

public class HttpObjectAggregator
extends MessageAggregator<HttpObject,HttpMessage,HttpContent,FullHttpMessage>

一个ChannelHandler,它将HttpMessage及其后续的HttpContents聚合为一个没有后续HttpContents的单个FullHttpRequest或FullHttpResponse(取决于它是否用于处理请求或响应)。 当您不想处理传输编码为“块状”的HTTP消息时,此功能很有用。 如果用于处理响应,则将此处理程序插入到ChannelPipeline中的HttpResponseDecoder之后;如果用于处理请求,则将其插入到ChannelPipeline中的HttpRequestDecoder和HttpResponseEncoder之后。

  ChannelPipeline p = ...;
  ...
  p.addLast("decoder", new HttpRequestDecoder());
  p.addLast("encoder", new HttpResponseEncoder());
  p.addLast("aggregator", new HttpObjectAggregator(1048576));
  ...
  p.addLast("handler", new HttpRequestHandler());
  

为了方便起见,请考虑将HttpServerCodec放在HttpObjectAggregator之前,因为它既充当HttpRequestDecoder,又充当HttpResponseEncoder。

请注意,HttpObjectAggregator可能最终会发送HttpResponse:

Response Status Condition When Sent
100 Continue A '100-continue' expectation is received and the 'content-length' doesn't exceed maxContentLength
417 Expectation Failed A '100-continue' expectation is received and the 'content-length' exceeds maxContentLength
413 Request Entity Too Large Either the 'content-length' or the bytes received so far exceed maxContentLength

构造函数:


//maxContentLength-聚合内容的最大长度(以字节为单位)。 如果聚合内容的长度超过此值,则将调用handleOversizedMessage(ChannelHandlerContext,HttpMessage)。
public HttpObjectAggregator(int maxContentLength)



QueryStringDecoder


public class QueryStringDecoder
extends java.lang.Object

将HTTP查询字符串拆分为路径字符串和键值参数对。 该解码器仅供一次使用。 为每个URI创建一个新实例:

 QueryStringDecoder decoder = new QueryStringDecoder("/hello?recipient=world&x=1;y=2");
 assert decoder.path().equals("/hello");
 assert decoder.parameters().get("recipient").get(0).equals("world");
 assert decoder.parameters().get("x").get(0).equals("1");
 assert decoder.parameters().get("y").get(0).equals("2");

该解码器还可以解码HTTP POST请求的内容,其内容类型为application / x-www-form-urlencoded:

 QueryStringDecoder decoder = new QueryStringDecoder("recipient=world&x=1;y=2", false);


例子

客户端发送http请求,服务端响应


public class HttpServer {


    public void bind(int port) {

        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();

        try {

            ServerBootstrap sb = new ServerBootstrap();

            sb.group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {

                            ChannelPipeline pipeline = ch.pipeline();

                            pipeline.addLast(new HttpServerCodec());

                            pipeline.addLast(new HttpObjectAggregator(61024));

                            pipeline.addLast(new HttpServerHandler());


                        }
                    });


            ChannelFuture future = sb.bind(port).sync();
            System.out.println("Server...............................");
            future.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {

            boss.shutdownGracefully();
            worker.shutdownGracefully();

        }


    }

    public static void main(String[] args) {

        new HttpServer().bind(9921);
        
    }

}




public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{

    private static final Logger logger = LoggerFactory.getLogger(HttpServerHandler.class);


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();

    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {

        // 获取请求头
        HttpHeaders httpHeaders = msg.headers();
//        System.out.println(httpHeaders.toString());
        // 查看请求头的详细信息
        logger.info(httpHeaders.toString());
        logger.info("*****************************");
        // 打印uri
        logger.info(msg.uri());

        // 获取Content-Type信息
        String strContentType = httpHeaders.get("Content-Type").trim();
        Map<String, Object> mapReturnData = new HashMap<String, Object>();
        System.out.println("ContentType:" + strContentType);

        // 处理get请求
        QueryStringDecoder decoder = new QueryStringDecoder(
                msg.getUri());
        if (msg.getMethod() == HttpMethod.GET) {
            Map<String, List<String>> get_parame = decoder.parameters();

            System.out.println("GET方式:" + get_parame.toString());

            response_message(ctx);
        } else if (msg.getMethod() == HttpMethod.POST) {

            if (strContentType.contains("application/json")) {
                    ByteBuf content = msg.content();
                    Map<String, List<String>> post_parame = decoder.parameters();
                    System.out.println("POST方式:" + post_parame.toString());
                    if (content.capacity() != 0 ) {
                        byte[] resquestContent = new byte[content.readableBytes()];
                        content.readBytes(resquestContent);
                        String strContent = new String(resquestContent, "UTF-8");
                        JSONObject jsonParamRoot = JSONObject.parseObject(strContent);
                        for (String key : jsonParamRoot.keySet()) {
                            mapReturnData.put(key, jsonParamRoot.get(key));
                        }
                    }
                }

//                logger.info("POST: " + mapReturnData.toString());
        }


    }


    // 服务端回应数据
    private void response_message(ChannelHandlerContext ctx) {

        // 构造返回数据
        JSONObject jsonRootObj = new JSONObject();
        JSONObject jsonUserInfo = new JSONObject();
        jsonUserInfo.put("id", 1);
        jsonUserInfo.put("name", "张三");
        jsonUserInfo.put("password", "123");
        jsonRootObj.put("userInfo", jsonUserInfo);
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
        response.headers().set(CONTENT_TYPE, "application/json; charset=UTF-8");
        StringBuilder bufRespose = new StringBuilder();
        bufRespose.append(jsonRootObj.toJSONString());
        ByteBuf buffer = Unpooled.copiedBuffer(bufRespose, CharsetUtil.UTF_8);
        response.content().writeBytes(buffer);
        buffer.release();
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);


    }
}

PostMan测试:

图6.png

参考文章

https://blog.csdn.net/u014209205/article/details/96426595

https://www.jhonrain.org/2019/10/15/Netty%E7%B3%BB%E5%88%97-%E4%B8%AD%E7%BA%A7%E7%AF%87-008-Netty%E7%BB%93%E5%90%88Http%E5%8D%8F%E8%AE%AE%E5%BC%80%E5%8F%91-%E4%B8%80-Http%E5%8D%8F%E8%AE%AE%E8%AE%A4%E8%AF%86/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容