一.认识Http请求
Netty中,可以注册多个handler。ChannelInboundHandler按照注册的先后顺序执行;ChannelOutboundHandler按照注册的先后顺序逆序执行,如下图所示,按照注册的先后顺序对Handler进行排序,request进入Netty后的执行顺序为:
在动手写Netty框架之前,我们先要了解http请求的组成,如下图:
- HTTP Request 第一部分是包含的头信息
- HttpContent 里面包含的是数据,可以后续有多个 HttpContent 部分
- LastHttpContent 标记是 HTTP request 的结束,同时可能包含头的尾部信息
- 完整的 HTTP request,由1,2,3组成
- HTTP response 第一部分是包含的头信息
- HttpContent 里面包含的是数据,可以后续有多个 HttpContent 部分
- LastHttpContent 标记是 HTTP response 的结束,同时可能包含头的尾部信息
- 完整的 HTTP response,由1,2,3组成
从request的介绍我们可以看出来,一次http请求并不是通过一次对话完成的,他中间可能有很次的连接。每一次对话都会建立一个channel,并且一个ChannelInboundHandler一般是不会同时去处理多个Channel的。
如何在一个Channel里面处理一次完整的Http请求?这就要用到我们上图提到的FullHttpRequest,我们只需要在使用netty处理channel的时候,只处理消息是FullHttpRequest的Channel,这样我们就能在一个ChannelHandler中处理一个完整的Http请求了。
HTTP基础实现类:
二.HTTP编码器、解码器
Netty为HTTP消息提供了ChannelHandler编码器和解码器:
网络通信最终都是通过字节流进行传输的。 ByteBuf
是 Netty 提供的一个字节容器,其内部是一个字节数组。 当我们通过 Netty 传输数据的时候,就是通过 ByteBuf
进行的。
HTTP Server 端用于接收 HTTP Request,然后发送 HTTP Response。因此我们只需要 HttpRequestDecoder
和 HttpResponseEncoder
即可。
为了能够表示 HTTP 中的各种消息,Netty 设计了抽象了一套完整的 HTTP 消息结构图,核心继承关系如下图所示。
-
HttpObject
: 整个 HTTP 消息体系结构的最上层接口。HttpObject
接口下又有HttpMessage
和HttpContent
两大核心接口。 -
HttpMessage
: 定义 HTTP 消息,为HttpRequest
和HttpResponse
提供通用属性 -
HttpRequest
:HttpRequest
对应 HTTP request。通过HttpRequest
我们可以访问查询参数(Query Parameters)和 Cookie。和 Servlet API 不同的是,查询参数是通过QueryStringEncoder
和QueryStringDecoder
来构造和解析查询查询参数。 -
HttpResponse
:HttpResponse
对应 HTTP response。和HttpMessage
相比,HttpResponse
增加了 status(相应状态码) 属性及其对应的方法。 -
HttpContent
: 分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制(HTTP/1.1 才有),允许 HTTP 由应用服务器发送给客户端应用( 通常是网页浏览器)的数据可以分成多“块”(数据量比较大的情况)。我们可以把HttpContent
看作是这一块一块的数据。 -
LastHttpContent
: 标识 HTTP 请求结束,同时包含HttpHeaders
对象。 -
FullHttpRequest
和FullHttpResponse
:HttpMessage
和HttpContent
聚合后得到的对象。
Netty 自带了常用的编解码器:
- HttpRequestEncoder: 编码器,用于客户端,向服务器发送请求
- HttpResponseEecoder: 编码器,用于服务端,向客户端发送响应
- HttpResponseDecoder: 解码器,用于客户端,接收来自服务端的请求
- HttpRequestDecoder:解码器,用于服务端,接收来自客户端的请求
除了独立的编码器、解码器,Netty还提供了编解码器:
- HttpClientCodec: 用于客户端的编解码器,等效于HttpRequestEncoder和HttpResponseDecoder的组合
- HttpServerCodec:用于服务端的编解码器,等效于HttpRequsetDecoder和 HttpResponseEncoder的组合
- HttpObjectAggregator:聚合器,由于 HTTP 的请求和响应可能由许多部分组成,需要聚合它们以形成完整的消息,HttpObjectAggregator 可以将多个消息部分合并为 FullHttpRequest 或者 FullHttpResponse 消息。
- HttpContentCompressor:压缩,用户服务端,压缩要传输的数据,支持 gzip 和 deflate 压缩格式。
- HttpContentDecompressor:解压缩,用于客户端,解压缩服务端传输的数据。
@Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); SSLEngine sslEngine = sslContext.newEngine(ch.alloc()); if (isClient) { //使用 HTTPS,添加 SSL 认证 pipeline.addFirst("ssl", new SslHandler(sslEngine, true)); pipeline.addLast("codec", new HttpClientCodec()); //1、建议开启压缩功能以尽可能多地减少传输数据的大小 //2、客户端处理来自服务器的压缩内容 pipeline.addLast("decompressor", new HttpContentDecompressor()); }else { pipeline.addFirst("ssl", new SslHandler(sslEngine)); //HttpServerCodec:将HTTP客户端请求转成HttpRequest对象,将HttpResponse对象编码成HTTP响应发送给客户端。 pipeline.addLast("codec", new HttpServerCodec()); //服务端,压缩数据 pipeline.addLast("compressor", new HttpContentCompressor()); } //目的多个消息转换为一个单一的FullHttpRequest或是FullHttpResponse //将最大的消息为 512KB 的HttpObjectAggregator 添加到 ChannelPipeline //在消息大于这个之后会抛出一个 TooLongFrameException 异常。 pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024)); }
tips:当使用 HTTP 时,建议开启压缩功能以尽可能多地减小传输数据的大小。虽然压缩会带来一些 CPU 时钟周期上的开销。
三.拆包和粘包的解决方案
TCP 传输过程中,客户端发送了两个数据包,而服务端却只收到一个数据包,客户端的两个数据包粘连在一起,称为粘包;
TCP 传输过程中,客户端发送了两个数据包,服务端虽然收到了两个数据包,但是两个数据包都是不完整的,或多了数据,或少了数据,称为拆包;
发生TCP粘包、拆包主要是由于下面一些原因:
1、应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
2、应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
3、进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。
4、接收方法不及时读取套接字缓冲区数据,这将发生粘包。
Netty 预定义了一些解码器用于解决粘包和拆包现象,其中大体分为两类:
基于分隔符的协议:在数据包之间使用定义的字符来标记消息或者消息段的开头或者结尾。这样,接收端通过这个字符就可以将不同的数据包拆分开。
基于长度的协议:发送端给每个数据包添加包头部,头部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包头部的长度字段,便知道每一个数据包的实际长度了。
基于分隔符的协议
public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> { @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast( // 将提取到的桢转发给下一个Channelhandler new LineBasedFrameDecoder(64 * 1024), // 添加 FrameHandler 以接收帧 new FrameHandler() ); } public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> { @Override protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { //Do something with the data extracted from the frame } } }
基于长度的协议
LengthFieldBasedFrameDecoder 是 Netty 基于长度协议解决拆包粘包问题的一个重要的类,主要结构就是 header+body 结构。我们只需要传入正确的参数就可以发送和接收正确的数据,那吗重点就在于这几个参数的意义。下面我们就具体了解一下这几个参数的意义。先来看一下LengthFieldBasedFrameDecoder主要的构造方法:
public LengthFieldBasedFrameDecoder( int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip)
maxFrameLength:最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃。
lengthFieldOffset:长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域。
lengthFieldLength:长度域字节数。用几个字节来表示数据长度。
lengthAdjustment:数据长度修正。因为长度域指定的长度可以使 header+body 的整个长度,也可以只是body的长度。如果表示header+body的整个长度,那么我们需要修正数据长度。
initialBytesToStrip:跳过的字节数。如果你需要接收 header+body 的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。
public class LengthBasedInitializer extends ChannelInitializer<Channel> { @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast( new LengthFieldBasedFrameDecoder(64 * 1024, 0, 8), new FrameHandler() ); } public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> { @Override protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { //处理桢的数据 } } }
tips:UDP协议不会发生沾包或拆包现象, 因为UDP是基于报文发送的,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开。
https://www.cnblogs.com/jmcui/p/9550119.html
参考:
https://www.cnblogs.com/jmcui/p/9550119.html
https://skyao.gitbooks.io/learning-netty/content/http/object/http_classes.html
https://blog.csdn.net/u013252773/article/details/21254257