netty基础07_Netty提供的消息处理器和编码解码器
1. SSL/TLS 加密
加密协议 SSL 和 TSL用来给传输的数据加密以实现数据安全;
为了支持 SSL/TLS,Java 提供了 javax.net.ssl API 的类SslContext 和 SslEngine;
Netty提供了一个名为SslHandler的ChannelHandler实现,SslHandler内部使用SslEngine做实际的工作。
Netty 的 OpenSSL/SSLEngine 实现:
Netty 还提供了使用 OpenSSL 工具包(www.openssl.org)的 SSLEngine 实现。
这个 OpenSslEngine 类提供了比 JDK 提供的 SSLEngine 实现更好的性能。
如果OpenSSL 库可用,可以将 Netty 应用程序(客户端和服务器)配置为默认使用OpenSslEngine。
如果不可用, Netty 将会回退到 JDK 实现。有关配置 OpenSSL 支持的详细说明,参见 Netty 文档:http://netty.io/wiki/forked-tomcat-native.html#wikih2-1。
注意,无论你使用 JDK 的 SSLEngine 还是使用 Netty 的 OpenSslEngine, SSL API 和数据流都是一致的。
SslHandler工作流程:
1】加密的入站数据被 SslHandler 拦截,并被解密
2】 被 SslHandler 解密数据传递给下一个入站消息处理器
3】平常数据传过 SslHandler
4】 SslHandler 加密数据并它传递出站
使用ChannelInitializer将SslHandler注册到管道
public class SslChannelInitializer extends ChannelInitializer<Channel> { private final SslContext context; private final boolean startTls; public SslChannelInitializer(SslContext context, boolean client, boolean startTls) { //1使用构造函数来传递 SSLContext 用于使用(startTls 是否启用) this.context = context; this.startTls = startTls; } @Override protected void initChannel(Channel ch) throws Exception { SSLEngine engine = context.newEngine(ch.alloc()); //2从 SslContext 获得一个新的 SslEngine 。给每个 SslHandler 实例使用一个新的SslEngine engine.setUseClientMode(client); //3设置 SslEngine 是 client 或者是 server 模式 ch.pipeline().addFirst("ssl", new SslHandler(engine, startTls)); //4添加 SslHandler 到 pipeline 作为第一个处理器 } }
SslHandler的api
2. 构建 Netty HTTP/HTTPS 应用
Netty提供了http的编码解码器来简化对http协议的开发工作;
只需要将这几个编码解码器装配在管道中,即可实现对http请求和http响应的编码解码;
1)关于http协议
HTTP 是请求-响应模式,客户端发送一个 HTTP 请求,服务就响应此请求。
http请求消息:
1】HTTP Request 第一部分是包含的头信息
2】HttpContent 里面包含的是数据,可以后续有多个 HttpContent 部分
3】LastHttpContent 标记是 HTTP request 的结束,同时可能包含头的尾部信息
4】完整的 HTTP request
http响应消息:
1】HTTP response 第一部分是包含的头信息
2】HttpContent 里面包含的是数据,可以后续有多个 HttpContent 部分
3】LastHttpContent 标记是 HTTP response 的结束,同时可能包含头的尾部信息
4】完整的 HTTP response
2)HTTP消息聚合
一个http消息由多部分组成,要看到一个完整的http消息需要用到聚合器;
Netty 提供了一个聚合器HttpObjectAggregator,用来合并消息部件到FullHttpRequest 和 FullHttpResponse
在管道中安装消息聚合器:
pipeline.addLast("aggegator", new HttpObjectAggregator(512 * 1024)); //消息最大值为512k
3) HTTP 压缩
使用 HTTP 时压缩数据可以减少传输流量;
Netty 支持“gzip”和“deflate”并提供了两个ChannelHandler用于压缩和解压缩;
在管道中安装压缩和解压处理器:
protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); if (isClient) { pipeline.addLast("codec", new HttpClientCodec()); //1 client: 添加 HttpClientCodec pipeline.addLast("decompressor",new HttpContentDecompressor()); //2 client: 添加 HttpContentDecompressor 用于处理来自服务器的压缩的内容 } else { pipeline.addLast("codec", new HttpServerCodec()); //3 server: HttpServerCodec pipeline.addLast("compressor",new HttpContentCompressor()); //4 server: HttpContentCompressor 用于压缩来自 client 支持的 HttpContentCompressor } }
4)使用https
启用 HTTPS,只需添加 SslHandler
ChannelPipeline pipeline = ch.pipeline(); SSLEngine engine = context.newEngine(ch.alloc()); pipeline.addFirst("ssl", new SslHandler(engine));
5) WebSocket
如果想实现显示实时数据之类的功能,普通的http需要多次请求很浪费资源;
WebSocket提供了一个更好的解决方案;
WebSocket的建立:
1】Client (HTTP) 与 Server 通讯
2】Server (HTTP) 与 Client 通讯
3】Client 通过 HTTP(s) 来进行 WebSocket 握手,并等待确认
4】连接协议升级至 WebSocket
netty提供了 WebSocketServerProtocolHandler来处理协议握手;
WebSocketServerProtocolHandler可以将Http消息升级成websocket帧,并处理控制帧Ping,Pong和 Close;
例如:new WebSocketServerProtocolHandler("/ws")表示将请求路径中带有"/ws"的消息升级为websocket消息;
websocket处理器处理的消息称为“帧”,分为两类:控制帧、数据帧;
websocket初始化器:
public class WebSocketServerInitializer extends ChannelInitializer<Channel> { @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast( new HttpServerCodec(), //0 http服务端编码解码器 new HttpObjectAggregator(65536), //1 添加 HttpObjectAggregator 用于提供在握手时聚合 HttpRequest //2 添加 WebSocketServerProtocolHandler,用于处理协议握手",当升级完成后,它将会处Ping,Pong和 Close 帧 new WebSocketServerProtocolHandler("/websocket"), new TextFrameHandler(), //3 自定义的TextFrameHandler 用来处理 TextWebSocketFrames new BinaryFrameHandler(), //4 自定义的BinaryFrameHandler 用来处理 BinaryWebSocketFrames new ContinuationFrameHandler()); //5 自定义的ContinuationFrameHandler 用来处理ContinuationWebSocketFrames } //自定义处理器,用来处理TextWebSocketFrames public static final class TextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { @Override public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) th rows Exception { // Handle text frame } } //自定义处理器,用来处理BinaryWebSocketFrames public static final class BinaryFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> { @Override public void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception { // Handle binary frame } } //自定义处理器,用来处理ContinuationWebSocketFrames public static final class ContinuationFrameHandler extends SimpleChannelInboundHandler<ContinuationWebSocketFrame> { @Override public void channelRead0(ChannelHandlerContext ctx, ContinuationWebSocketFrame msg) throws Exception { // Handle continuation frame } } }
3. 空闲连接以及超时
检测空闲连接和超时是为了及时释放资源。
常见的方法:对于一个不活跃的连接,发送消息(称为“心跳”)到远端,来确定它是否还活着。
netty提供了几个ChannelHandler来处理空闲和超时:
例如:处理空闲连接,如果60s没收到消息就发送心跳,如没回应则断开连接
public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> { @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS)); //1 将闲置处理器添加到管道 pipeline.addLast(new HeartbeatHandler()); //添加自定义处理器 } //自定义处理器,用来处理闲置处理器发送的idleStateEvent事件 public static final class HeartbeatHandler extends ChannelInboundHandlerAdapter { private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer( Unpooled.copiedBuffer("HEARTBEAT", CharsetUtil.ISO_8859_1)); //2 心跳发送到远端 @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate()) .addListener(ChannelFutureListener.CLOSE_ON_FAILURE); //3 发送的心跳并添加一个侦听器,如果发送操作失败将关闭连接 } else { super.userEventTriggered(ctx, evt); //4 事件不是一个 IdleStateEvent 的话,就将它传递给下一个处理程序 } } } }
4. 解码分隔符和基于长度的协议
netty提供了一些解码器,可以用来解决黏包的问题;
1)关于黏包
TCP粘包和黏包现象
1】TCP粘包是指发送方发送的若干个数据包到接收方时粘成一个包。从接收缓冲区来看,后一个包数据的头紧接着前一个数据的尾。
2】当TCP连接建立后,Client发送多个报文给Server,TCP协议保证数据可靠性,但无法保证Client发了n个包,服务端也按照n个包接收。Client端发送n个数据包,Server端可能收到n-1或n+1个包。
为什么出现粘包现象?
1】发送方原因: TCP默认会使用Nagle算法。而Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送。所以,正是Nagle算法造成了发送方有可能造成粘包现象。
2】接收方原因: TCP接收方采用缓存方式读取数据包,一次性读取多个缓存中的数据包。自然出现前一个数据包的尾和后一个收据包的头粘到一起。
如何解决粘包现象
就是要选择相应的解码器
* 添加特殊符号,接收方通过这个特殊符号将接收到的数据包拆分开 - DelimiterBasedFrameDecoder特殊分隔符解码器
* 每次发送固定长度的数据包 - FixedLengthFrameDecoder定长编码器
* 在消息头中定义长度字段,来标识消息的总长度 - LengthFieldBasedFrameDecoder自定义长度解码器
2)处理分隔符协议的decoder
一些协议以分隔符来作为每条消息的分界线,例如SMTP、POP3、IMAP、Telnet等等。
netty提供了两个解码器用来处理基于分隔符的消息;
1】 DelimiterBasedFrameDecoder
自定义的分隔符解码器;
构造函数的第一个参数表示单个消息的最大长度,当达到该长度后仍然没有查到分隔符,就抛出TooLongFrameException异常,防止由于异常码流缺失分隔符导致的内存溢出。
例如:使用DelimiterBasedFrameDecoder处理用"$_"作为每条消息的分隔符的协议
public static void main(String[] args) throws InterruptedException { EventLoopGroup worker = new NioEventLoopGroup(); Bootstrap b = new Bootstrap(); //客户端引导类 b.group(worker) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel sc) throws Exception { //设置连接符/分隔符,换行显示 ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes()); //DelimiterBasedFrameDecoder:自定义分隔符,这里是"$_" sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf)); //在管道中添加自定义分隔符解码器 //设置为字符串形式的解码:将传递的buf改为String sc.pipeline().addLast(new StringDecoder()); sc.pipeline().addLast(new ClientHandler()); } }); //连接端口 ChannelFuture cf = b.connect("127.0.0.1", 8765).sync(); cf.channel().writeAndFlush(Unpooled.copiedBuffer("aaa$_".getBytes())); cf.channel().writeAndFlush(Unpooled.copiedBuffer("bbbbb$_".getBytes())); cf.channel().writeAndFlush(Unpooled.copiedBuffer("cccccccc$_".getBytes())); cf.channel().closeFuture().sync(); worker.shutdownGracefully(); }
2】LineBasedFrameDecoder
以换行符为结束标志的解码器;
依次编译bytebuf中的可读字符,判断看是否有“\n”或者“\r\n”,如果有,就以此位置为一条消息的结束位置
例如:
public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> { @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new LineBasedFrameDecoder(65 * 1024)); //1在管道中添加换行符分隔解码器,构造方法中的参数表示一条消息最大为65k pipeline.addLast(new FrameHandler()); //2 添加自定义的处理器,用来处理解码后的每一条消息 } //自定义消息处理器实现 public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> { @Override public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { //3 bytebuf中保存了解码后的消息 // Do something with the frame } } }
3) 基于长度的协议
有些协议是以一定长度的字节为一条消息;
netty提供了基于长度的消息解码器;
1】 FixedLengthFrameDecoder
固定长度解码器;
无论一次性接收到多少的数据,他都会按照构造函数中设置的长度进行解码;
如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下一个包,到达后进行拼包,直到读取完整的包。
ch.pipeline().addLast(new FixedLengthFrameDecoder(8)); //在管道中添加8个字节长度为单位的固定长度解码器
2】 LengthFieldBasedFrameDecoder
自定义长度解码器;
构造方法中的参数:
* maxFrameLength - 发送的数据帧最大长度
* lengthFieldOffset - 定义长度域位于发送的字节数组中的下标。换句话说:发送的字节数组中下标为${lengthFieldOffset}的地方是长度域的开始地方
* lengthFieldLength - 用于描述定义的长度域的长度。换句话说:发送字节数组bytes时, 字节数组bytes[lengthFieldOffset, lengthFieldOffset+lengthFieldLength]域对应于的定义长度域部分
* lengthAdjustment - 满足公式: 发送的字节数组bytes.length - lengthFieldLength = bytes[lengthFieldOffset, lengthFieldOffset+lengthFieldLength] + lengthFieldOffset + lengthAdjustment
* initialBytesToStrip - 接收到的发送数据包,去除前initialBytesToStrip位
* failFast - true: 读取到长度域超过maxFrameLength,就抛出一个 TooLongFrameException。false: 只有真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出
* ByteOrder - 数据存储采用大端模式或小端模式
例1:
* lengthFieldOffset = 0
* lengthFieldLength = 2
* lengthAdjustment = 0 = 数据包长度(14) - lengthFieldOffset - lengthFieldLength - 长度域的值(12)
* initialBytesToStrip = 2 解码过程中,丢弃2个字节的数据
例2:
* lengthFieldOffset = 1
* lengthFieldLength = 2
* lengthAdjustment = 1 = 数据包长度(16) - lengthFieldOffset(1) - lengthFieldLength(2) - 长度域的值(12)
* initialBytesToStrip = 0 - 解码过程中,没有丢弃任何数据
5.写出大型数据
当需要传输大型数据时可能会出现传输慢、内存耗尽等问题;
netty提供了写出大型数据时的处理方案;
1) FileRegion
netty提供了FileRegion接口,以及其实现类DefaultFileRegion
作用:利用Nio的零拷贝机制来传输文件内容;
优点:消除了将文件的内容从文件系统移动到网络栈的复制过程,从而提高传输速度;
原理:
java程序中待传输的数据通常会保存在堆缓冲区中;
如果数据包含在一个在堆上分配的缓冲区中, 在通过套接字发送它之前, JVM将会在内部把你的缓冲区复制到一个直接缓冲区中;
零拷贝就是将待发送的数据保存在 直接缓冲区中,从而避免了发送前从堆到直接缓冲区的复制,从而提高传输效率;
使用 FileRegion 传输文件的内容:
FileInputStream in = new FileInputStream(file); FileRegion region = new DefaultFileRegion( in.getChannel(), 0, file.length()); channel.writeAndFlush(region).addListener( new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { Throwable cause = future.cause(); // Do something } } });
缺点:这种方式只适用于文件内容的直接传输,不包括应用程序对数据的任何处理。
例如:它对于实现了数据加密或者压缩的文件系统是不可用的——只能传输文件的原始内容。 反过来说,传输已被加密的文件则不是问题。
2) ChunkedWriteHandler
为了防止内存耗尽,可以将大型的数据分块写出;
逐块输入处理器ChunkedWriteHandler;用来处理分块后的数据;
实现方式:
netty提供了一个接口,用来表示分块后的数据
interface ChunkedInput<B>
类型参数 B 是 readChunk()方法返回的类型;
netty提供的该接口的实现类:
只需要利用这几个类的构造方法,将需要传输的数据转换成ChunkedInput即可;
然后就能用ChunkedWriteHandler处理ChunkedInput;
代码实现:
public class ChunkedWriteHandlerInitializer extends ChannelInitializer<Channel> { private final File file; private final SslContext sslCtx; public ChunkedWriteHandlerInitializer(File file, SslContext sslCtx) { this.file = file; this.sslCtx = sslCtx; } @Override protected void initChannel(Channel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new SslHandler(sslCtx.newEngine(ch.alloc()); pipeline.addLast(new ChunkedWriteHandler()); //用来处理ChunkedInput数据 pipeline.addLast(new WriteStreamHandler()); //自定义的处理器,将文件以ChunkedInput写出 } public final class WriteStreamHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); ctx.writeAndFlush(new ChunkedStream(new FileInputStream(file))); //使用ChunkedInput写出文件 } } }
6.序列化数据
JDK 提供了 ObjectOutputStream 和 ObjectInputStream,用于通过网络对 POJO 的基本数据类型和图进行序列化和反序列化;
可以被应用于任何实现了java.io.Serializable 接口的对象;
缺点是性能低;
netty提供了几种序列化的编码解码器;
使用时只需要编码器和解码器成对添加到管道即可;
1) JDK 序列化
2) 使用 JBoss Marshalling 进行序列化
3) 通过 Protocol Buffers 序列化