Netty快速入门

Netty

一、Netty简介

1.什么是Netty

​ Netty 是是jboss提供的一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持,作为一个异步 NIO 框架,Netty 的所有 IO 操作都是异步非阻塞的,通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。

​ 作为当前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于 Netty 的 NIO 框架构建。

2. Netty的特点

2.1 设计优雅。适用于各种传输类型的统一API,阻塞和非阻塞Socket基于灵活且可扩展的事件模型,可以清晰地分离关注点。
2.2 使用方便 详细记录的Javadoc,用户指南和示例 没有其他依赖项,只需要提供JDK即可。
2.3 高性能。延迟更低,减少资源消耗,最小化不必要的内存复制。
2.4 安全完整的SSL / TLS和StartTLS支持。
2.5 社区活跃,不断更新 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入。

3.常见使用场景

互联网行业 在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高新能的通信框架,往往作为基础通信组件被这些RPC框架使用。 典型的应用有:阿里分布式服务框架Dubbo的RPC使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。

游戏行业 无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和HTTP协议栈。 非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过Netty进行高性能的通信

大数据领域 经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨节点通信,它的Netty Service基于Netty框架二次封装实现。

二、Netty的功能特性和架构思想

1.Netty的整体架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E2PVz5aS-1602474934845)(C:\Users\T480\AppData\Roaming\Typora\typora-user-images\1578106630562.png)]

2.Netty线程模型

来自官网

img

来自网友整理
img

三、Netty核心API简介

1、EventLoopGroup

EventLoopGroup可以将其看作是一个线程池,其内部维护了一组EventLoop,每个EventLoop对应处理多个Channel,而一个Channel只能对应一个eventLoo

p。EventLoop如同它的名字,它是一个无限循环(Loop),在循环中不断处理接收到的事件(Event)。Netty线程模型的基石是建立在EventLoop上的。

一个Netty服务端启动时,通常会有两个NioEventLoopGroup(EventLoopGroup的间接实现类):一个是监听线程组,主要是监听客户端请求,另一个是工作线程组,主要是处理与客户端的数据通讯。
Netty客户端只有一个NioEventLoopGroup,就是用来处理与服务端通信的线程组。

2、Bootstrap 和 ServerBootstrap

ServerBootStrap是Netty服务端启动配置类,BootStrap是Netty客户端启动配置类。

2.1. ServerBootstrap
方法说明
group(bossGroup, workerGroup)绑定线程组,设置react模式的主线程池 以及 IO 操作线程池
channel(Class<? extends C> channelClass)设置通讯模式,调用的是实现io.netty.channel.Channel接口的类。如:服务端一般可以选NioServerSocketChannel。
option设置通道的选项参数, 对于服务端而言就是ServerSocketChannel, 客户端而言就是SocketChannel
handler设置主通道的处理器, 对于服务端而言就是ServerSocketChannel,也就是用来处理Acceptor的操作;对于客户端的SocketChannel,主要是用来处理 业务操作
attr设置通道的属性
childOption
childHandler
childAttr

对于服务端而言,有两种通道需要处理, 一种是ServerSocketChannel:用于处理用户连接的accept操作, 另一种是SocketChannel,表示对应客户端连接。而对于客户端,一般都只有一种channel,也就是SocketChannel。

因此以child开头的方法,都定义在ServerBootstrap中,表示处理或配置服务端接收到的对应客户端连接的SocketChannel通道。

2.2. BootStrap
方法说明
group(workGroup)绑定线程组,设置IO操作线程池
channel(Class<? extends C> channelClass)设置通讯模式,调用的是实现io.netty.channel.Channel接口的类。如:NioSocketChannel、NioServerSocketChannel,客户端一般可以选NioSocketChannel。
ChannelOptionChannelOption的各种属性在套接字选项中都有对应,下面简单的总结一下ChannelOption的含义已及使用的场景。

3、ChannelOption

3.1ChannelOption.SO_BACKLOG

​ ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小。

3.2 ChannelOption.SO_REUSEADDR

​ ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口,比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用;比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。

3.3 ChannelOption.SO_KEEPALIVE

​ Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。

3.4 ChannelOption.SO_SNDBUF和ChannelOption.SO_RCVBUF

​ ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。

3.5 ChannelOption.SO_LINGER

​ ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送

3.6 ChannelOption.TCP_NODELAY

​ ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。

4、ChannelInitializer

​ 它是一个特殊的ChannelInboundHandler,当channel注册到EventLoop上面时,用于对channel进行初始化

ChannelInitializer继承于ChannelInboundHandler接口。ChannelInitializer是一个抽象类,不能直接使用。

4.1 抽象方法 initChannel

​ ChannelInitializer的实现类必须要重写这个方法,这个方法在Channel被注册到EventLoop的时候会被调用

ChannelInitializer的主要目的是为程序员提供了一个简单的工具,用于在某个Channel注册到EventLoop后,对这个Channel执行一些初始化操作。ChannelInitializer虽然会在一开始会被注册到Channel相关的pipeline里,但是在初始化完成之后,ChannelInitializer会将自己从pipeline中移除,不会影响后续的操作。

5、ChannelPipeline

​ 一个包含channelHandler的list,用来设置channelHandler的执行顺序。ChannelPipeline可以理解为ChannelHandler的容器,所有ChannelHandler都会注册到ChannelPipeline中,并按顺序组织起来。

ChannelPipeline = Channel + Pipeline,也就是说首先它与Channel绑定,然后它是起到类似于管道的作用:字节流在ChannelPipeline上流动,流动的过程中被ChannelHandler修饰,最终输出。

6、ChannelHandler

​ ChannelHandler是用来处理业务逻辑的代码,ChannelHandler类似于Servlet的Filter过滤器,负责对I/O事件或者I/O操作进行拦截和处理,它可以选择性地拦截和处理自己感兴趣的事件,也可以透传和终止事件的传递。基于ChannelHandler接口,用户可以方便地进行业务逻辑定制,例如打印日志、统一封装异常信息、性能统计和消息编解码等。

ChannelInboundHandlerAdapter

​ 一般用netty来发送和接收数据都会继承ChannelInboundHandlerAdapter这个抽象类。

ChannelInboundHandlerAdapter是ChannelInboundHandler的一个简单实现,默认情况下不会做任何处理,只是简单的将操作通过fire方法传递到ChannelPipeline中的下一个ChannelHandler中让链中的下一个ChannelHandler去处理。

需要注意的是信息经过channelRead方法处理之后不会自动释放(因为信息不会被自动释放所以能将消息传递给下一个ChannelHandler处理)。

我们常用的inbound事件有:

- channelRegistered(ChannelHandlerContext) //channel注册事件
- channelActive(ChannelHandlerContext)//通道激活时触发,当客户端connect成功后,服务端就会接收到这个事件,从而可以把客户端的Channel记录下来,供后面复用
- exceptionCaught(ChannelHandlerContext, Throwable)//出错时会触发,做一些错误处理
- userEventTriggered(ChannelHandlerContext, Object)//用户自定义事件
- channelRead(ChannelHandlerContext, Object) //当收到对方发来的数据后,就会触发,参数msg就是发来的信息,可以是基础类型,也可以是序列化的复杂对象。

7、Channel

​ Channel代表一个连接,一个请求就是一个连接。

8、Future or ChannelFuture

​ Future提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。netty的每一个出站操作都会返回一个ChannelFuture。future上面可以注册一个监听器,当对应的事件发生后会触发该监听器。

9、ChannelHandlerContext

允许与其关联的ChannelHandler与它相关联的ChannlePipeline和其它ChannelHandler来进行交互。它可以通知相同ChannelPipeline中的下一个ChannelHandler,也可以对其所属的ChannelPipeline进行动态修改。

四、编码实现

服务器端

package cn.xbb.netty;

import cn.xbb.nettyhandler.ServerIOHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * 服务器
 */
public class NettyServer {
    public static void main(String[] args) throws Exception {
        //1.创建处理任务的线程池
        EventLoopGroup acceptGroup = new NioEventLoopGroup();// 监听客户端连接请求
        EventLoopGroup workerGroup = new NioEventLoopGroup();// 处理IO事件
        //2.创建服务器启动配置类
        ServerBootstrap server = new ServerBootstrap();
        //3.绑定线程池
        server.group(acceptGroup, workerGroup);
        //4.设置通道类型
        server.channel(NioServerSocketChannel.class);
        //5.设置处理客户端连接的通道监听处理器,这里属于AIO异步的监听实现机制
        server.childHandler(new ChannelInitializer<SocketChannel>() {
            // 初始化通道中的管道参数
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                //获取连接通道中的管道(装饰)
                ChannelPipeline pipeline = socketChannel.pipeline();
                //设置IO事件业务处理器
                pipeline.addLast(new ServerIOHandler());

            }
        });
        //6.绑定监听端口,启动服务
        ChannelFuture channelFuture = server.bind(9000).sync();
        /**
         * Unsafe?
         * 开启一个channel的监听器,如果未来channel关闭了会释放子线程,同时sync让主线程同步等待子线程结果
         */
        channelFuture.channel().closeFuture().sync();
        //8.优雅关闭线程,保证已接受的请求处理不丢失
        workerGroup.shutdownGracefully();
        acceptGroup.shutdownGracefully();

    }
}

package cn.xbb.nettyhandler;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

import java.util.Date;

/**
 * 业务处理器
 */
public class ServerIOHandler extends ChannelInboundHandlerAdapter {
    // 接收客户端请求信息
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //msg是一个ByteBuf类型
        ByteBuf request = (ByteBuf) msg;
        System.out.println("服务器已收到请求信息:"+request.toString(CharsetUtil.UTF_8));
        // 开始响应结果
        String respMsg = "当前时间:"+new Date();
        ByteBuf response = Unpooled.wrappedBuffer(respMsg.getBytes());
        ChannelFuture future = ctx.writeAndFlush(response);
        // 注册监听器
        future.addListener(ChannelFutureListener.CLOSE);//响应结束关闭通道
        future.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);// 监听是否出现异常
        future.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);// 失败关闭通道
    }
    // 出现异常的时候执行
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
    }
}

客户端

package cn.xbb.netty;

import cn.xbb.nettyhandler.ClientIOHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;

import java.net.InetAddress;
import java.net.InetSocketAddress;

/**
 * 客户端
 */
public class NettyClient {
    public static void main(String[] args) throws Exception {
        //1.创建工作线程池
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        //2.创建客户端启动对象
        Bootstrap client = new Bootstrap();
        //3.绑定线程
        client.group(workerGroup);
        //4.设置通道类型
        client.channel(NioSocketChannel.class);
        //5.初始化通讯通道处理
        client.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                pipeline.addLast(new ClientIOHandler());
            }
        });
        //6.连接服务器
        ChannelFuture channelFuture = client.connect(new InetSocketAddress("localhost", 9000));
        channelFuture.addListener(new GenericFutureListener<Future<? super Void>>() {
            @Override
            public void operationComplete(Future<? super Void> future) throws Exception {
                System.out.println("连接服务器成功");
            }
        });
        //7.关闭通道
        channelFuture.channel().closeFuture().sync();
        //8.关闭线程
        workerGroup.shutdownGracefully();

    }
}

package cn.xbb.nettyhandler;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.EventExecutorGroup;

/**
 * 业务处理器
 */
public class ClientIOHandler extends ChannelInboundHandlerAdapter {
    // 连接成功建立,发送请求
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf request = Unpooled.wrappedBuffer("现在是什么时间了".getBytes());
        ctx.writeAndFlush(request);

    }

    //读取通道的数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf response = (ByteBuf) msg;
        System.out.println("收到响应信息:" + response.toString(CharsetUtil.UTF_8));
    }

    //异常处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
    }
}

五、Netty中的reactor反应器模式

1.reactor反应器模式简介

反应器模式是一种为处理服务请求并发提交到一个或者多个服务处理程序的事件设计模式。对于高性能的网络编程架构设计都离不开反应器模式。反应器模式由reactor反应器线程、Handler处理器两个核心角色组成:

reactor反应器线程的主要职责是负责响应IO事件,并分发到Handler处理器。
Handler处理器的主要职责是执行业务处理逻辑。

反应器模式的处理流程

第1步:通道注册。

IO源于通道(Channel)。IO是和通道(对应于底层连接而言)强相关的。一个IO事件,一定属于某个通道。但是,如果要查询通道的事件,首先要将通道注册到选择器。只需通道提前注册到Selector选择器即可,IO事件会被选择器查询到。

第2步:查询选择。

在反应器模式中,一个反应器(或者SubReactor子反应器)会负责一个线程;不断地轮询,查询选择器中的IO事件(选择键)。

第3步:事件分发。

如果查询到IO事件,则分发给与IO事件有绑定关系的Handler业务处理器。

第4步:完成真正的IO操作和业务处理,这一步由Handler业务处理器负责。

2.Netty中Reactor反应器的实践

在Netty中NioEventLoop为反应器,一个反应器可以注册对应多个通道,在这个反应器内部封装了一个选择器成员,这个选择器会执行相应IO事件的查询(可读/可写/连接/接收),而后进行事件的分发。
当通道发生IO事件时,反应器会将其分发给ChannelInboundHandler/ChannelOutboundHandler进行处理。我们在开发过程中,经常会继承通道处理适配器ChannelInboundHandlerAdapter/ChannelOutboundHandlerAdapter自定义自己的业务处理逻辑。

总的来说:
反应器与通道之间是一对多关系。即一个反应器可以查询很多个通道的IO事件。

通道和处理器实例是多对多关系。一个通道的IO事件可以被多个Handler处理。而一个Handler也能绑定到很多个通道上,处理多个通道的IO事件。为了处理好通道和Handler之间的关系,Netty设计了一个叫ChannelPipleline的通道流水线,这个对象类似于一个管道,将绑定到一个通道的多个Handler串在一起,形成一条流水线。需要注意的是每一个通道都有一条Handler处理器的流水线。我们可以认为流水线就是通道的一个管家,为通道管理好了它的多个处理器。

六、解码器(Decoder)与编码器(Encoder)重要组件

​ Netty处理数据是从底层Java通道读到它的ByteBuf二进制数据,后传入通道的流水线进行后续的处理。

在入站处理过程中需要将ByteBuf二进制类型解码成Java对象,这个过程为解码。

在出站处理过程中,需要将业务处理的结果(Java对象)编码为ByteBuf二进制数据,而后进行传输。

解码器实现

public class MsgDecoder extends MessageToMessageDecoder{
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, Object msg, List out) throws Exception {
        ByteBuf buffer = (ByteBuf) msg;
        byte[] bytes = new byte[buffer.readableBytes()];
        buffer.readBytes(bytes);
        Object obj = SerializationUtils.deserialize(bytes);
        out.add(obj);

    }
}

编码器

public class MsgEncoder extends MessageToMessageEncoder {
    // 处理object 然后添加到 list中
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, List list) throws Exception {
        byte[] bytes = SerializationUtils.serialize((Serializable) o);
        ByteBuf byteBuf = Unpooled.buffer();
        byteBuf.writeBytes(bytes);
        list.add(byteBuf);
    }
}

七、粘包和拆包详解

1.什么是TCP粘包/拆包问题

  • TCP是个“流”协议,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

  • 假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种情况,如下:

    (1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;

    (2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;

    (3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;

    (4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。

如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。

2.如何解决

第一种方案 :消息定长。发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

第二种方案 :设置消息边界。服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如FTP协议。

第三种方案 :将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。

第四种方案 :更复杂的应用层协议。

3.Netty中解决粘包拆包问题实现

编码器io.netty.handler.codec.LengthFieldPrepender负责在待发送的ByteBuf消息头上增加一个长度字段,用来标识消息的⻓度

解码器io.netty.handler.codec.LengthFieldBasedFrameDecoder是基于消息长度的半包的解码器

在服务器端或客户端加入如下代码即可:

ChannelPipeline pipeline = ch.pipeline();
/**解码 
	根据消息长度获取消息体
	参数⼀:消息包的最大长度 
	参数二:长度域的偏移量 
	参数三:长度字段所占字节大小 
	参数四:长度补偿 
	参数五:从数据帧中跳过的字节数
*/
pipeline.addLast(new LengthFieldBasedFrameDecoder(65535,0,2,0,2));
/** 编码 
	负责在待发送的ByteBuf消息头上增加一个长度字段,用来表示消息体的长度 
	参数代表:长度字段所占字节大小
*/
channelPipeline.addLast(new LengthFieldPrepender(2));

zouxf

.LengthFieldPrepender负责在待发送的ByteBuf消息头上增加一个长度字段,用来标识消息的⻓度

解码器io.netty.handler.codec.LengthFieldBasedFrameDecoder是基于消息长度的半包的解码器

在服务器端或客户端加入如下代码即可:

ChannelPipeline pipeline = ch.pipeline();
/**解码 
	根据消息长度获取消息体
	参数⼀:消息包的最大长度 
	参数二:长度域的偏移量 
	参数三:长度字段所占字节大小 
	参数四:长度补偿 
	参数五:从数据帧中跳过的字节数
*/
pipeline.addLast(new LengthFieldBasedFrameDecoder(65535,0,2,0,2));
/** 编码 
	负责在待发送的ByteBuf消息头上增加一个长度字段,用来表示消息体的长度 
	参数代表:长度字段所占字节大小
*/
channelPipeline.addLast(new LengthFieldPrepender(2));
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值