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 序列化
 
 
 
 
posted @ 2020-05-29 16:54  L丶银甲闪闪  阅读(836)  评论(0编辑  收藏  举报