Java-网络编程-Netty
前言
Netty简介
1 HelloWolrd
1 服务端程序
//创建两个线程组
EventLoopGroup connectGroup = new NioEventLoopGroup();//接受客户端连接
EventLoopGroup workGroup = new NioEventLoopGroup();//处理实际业务操作
try {//创建server配置类
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(connectGroup, workGroup)//支持链式编程,进行配置
//指定使用NioServerSocketChannel这种类型(服务端)的通道
.channel(NioServerSocketChannel.class)
//ChannelInitializer:服务器Channel通道初始化设置的抽象类
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override //初始化操作:编解码,绑定处理逻辑等
protected void initChannel(SocketChannel channel) throws Exception {
//使用 childHandler 去绑定具体的 事件处理器
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new ServerHandler());//绑定服务端数据处理
}
});
//绑定端口,调用sync()方法来执行同步阻塞,直到绑定完成
ChannelFuture sync = bootstrap.bind(9527).sync();
//获取该Channel的CloseFuture,并且阻塞当前线程直到绑定的端口关闭才会执行关闭通道
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//关闭线程组
connectGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
2 客户端程序
//实际业务处理线程组
EventLoopGroup workGroup = new NioEventLoopGroup();
//创建客户端配置类
Bootstrap bootstrap = new Bootstrap();
//链式配置
bootstrap.group(workGroup)
.channel(NioSocketChannel.class) //指定客户端类型通道
.handler(new ChannelInitializer<SocketChannel>() { //配置初始化
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//设置超时处理,如果指定时间内没有接受到数据,则抛出异常,关闭通道断开连接
socketChannel.pipeline().addLast(new ReadTimeoutHandler(5));//秒
//如果指定时间内没有写数据,则关闭通道,服务端和客户端均可设置,因为是互相读写
//socketChannel.pipeline().addLast( new WriteTimeoutHandler(5));
socketChannel.pipeline().addLast(new ClientHandler());//绑定客户端数据处理对象
}
});
//与服务端连接,调用sync()方法来执行同步阻塞,直到连接完成
ChannelFuture sync = bootstrap.connect("127.0.0.1", 9527).sync();
//向服务端发送数据 Unpooled: netty提供的工具类,可以将其他类型转为buf类型
sync.channel().writeAndFlush(Unpooled.copiedBuffer("我是客户端".getBytes()));
TimeUnit.SECONDS.sleep(6);
sync.channel().writeAndFlush(Unpooled.copiedBuffer("第二次发送".getBytes()));
//开启同步阻塞监听,直到断开连接才关闭通道
sync.channel().closeFuture().sync();
workGroup.shutdownGracefully();
2 使用与进阶
1 TCP拆包和粘包解决
1 什么是TCP拆包,粘包?
TCP是个流协议,即没有界限的一串数据,就好比河里的流水,连城一片的,TCP底层并不了接上层业务,他会根据TCP缓冲区进行包的划分,所以在业务上,一个完整的包可能会被拆分成多个包发送,也有可能多个小的包封装成一个大的包发送,这就是拆包和粘包,如下图:
2 解决办法
由于TCP底层不了解业务数据,所以只能上层应用解决,目前业界主流协议解决方案,主要有如下几个:
- 1 消息定长
- 2 在包尾增加回车换行符进行分割,例如FTP协议
- 3 将消息分为消息头和消息体
- 4 更复杂的应用层协议
3 Netty的编解码器
Netty默认提供了多种编解码器,用于解决粘包拆包问题:
比如:
//第一个参数表示单个消息的最大长度 第二个参数就是特殊分隔符
// (当消息到了最大长度还没查到分隔符,那么就会报错TooLongFrameException异常)
pipeline.addLast(new DelimiterBasedFrameDecoder(1024, buf));
//设置字符串编码,设置后服务端发送的数据就会转成String
pipeline.addLast(new StringEncoder());
//还可以设置字符串形式的解码,设置后服务端接受到的数据就会自动转成String
pipeline.addLast(new StringDecoder());//必须放到数据处理之前
2 分隔符和定长解码器应用
1 分隔符解码器
服务端:
//设置分隔符解码器
///第一个参数表示单个消息的最大长度 第二个参数就是特殊分隔符
// (当消息到了最大长度还没查到分隔符,那么就会报错TooLongFrameException异常)
pipeline.addLast(new DelimiterBasedFrameDecoder(1024, buf));
//设置字符串编码,设置后服务端发送的数据就会转成String
pipeline.addLast(new StringEncoder());
//还可以设置字符串形式的解码,设置后服务端接受到的数据就会自动转成String
pipeline.addLast(new StringDecoder());//必须放到数据处理之前
客户端:
//实际业务处理线程组
EventLoopGroup workGroup = new NioEventLoopGroup();
//创建客户端配置类
Bootstrap bootstrap = new Bootstrap();
//链式配置
bootstrap.group(workGroup)
.channel(NioSocketChannel.class) //指定客户端类型通道
.handler(new ChannelInitializer<SocketChannel>() { //配置初始化
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//初始化时候设置特殊分隔符
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());//转成bytebuf
socketChannel.pipeline().
//设置分隔符解码器
///第一个参数表示单个消息的最大长度 第二个参数就是特殊分隔符
// (当消息到了最大长度还没查到分隔符,那么就会报错TooLongFrameException异常)
addLast(new DelimiterBasedFrameDecoder(1024, buf))
//设置字符串编码,设置后客户端发送的数据就会转成String
.addLast(new StringEncoder())
//还可以设置字符串形式的解码,设置后客户端接受到的数据就会自动转成String
.addLast(new StringDecoder())//必须放到数据处理之前
.addLast(new ClientHandler());//绑定客户端数据处理对象
}
});
//与服务端连接,调用sync()方法来执行同步阻塞,直到连接完成
ChannelFuture sync = bootstrap.connect("127.0.0.1", 9527).sync();
//向服务端发送数据 Unpooled: netty提供的工具类,可以将其他类型转为buf类型
sync.channel().writeAndFlush("我是客户端$_"); //使用了String编码器,可直接发送String
sync.channel().writeAndFlush("我现在需要发送数据$_");
sync.channel().writeAndFlush("你准备好了吗$_");
//开启同步阻塞监听,直到断开连接才关闭通道
sync.channel().closeFuture().sync();
workGroup.shutdownGracefully();
2 定长解码器
服务端:
ChannelPipeline pipeline = channel.pipeline();
//设置分隔符解码
//使用5个字节长度,不够需要补齐
pipeline.addLast(new FixedLengthFrameDecoder(5));
//设置字符串编码,设置后服务端发送的数据就会转成String
pipeline.addLast(new StringEncoder());
//还可以设置字符串形式的解码,设置后服务端接受到的数据就会自动转成String
pipeline.addLast(new StringDecoder());//必须放到数据处理之前
pipeline.addLast(new ServerHandler());//绑定服务端数据处理
客户端:
//实际业务处理线程组
EventLoopGroup workGroup = new NioEventLoopGroup();
//创建客户端配置类
Bootstrap bootstrap = new Bootstrap();
//链式配置
bootstrap.group(workGroup)
.channel(NioSocketChannel.class) //指定客户端类型通道
.handler(new ChannelInitializer<SocketChannel>() { //配置初始化
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().
//设置分隔符解码器
// 设置定长,5个字节长度,不够的需要补齐空格或者其他
addLast(new FixedLengthFrameDecoder(5))
//设置字符串编码,设置后客户端发送的数据就会转成String
.addLast(new StringEncoder())
//还可以设置字符串形式的解码,设置后客户端接受到的数据就会自动转成String
.addLast(new StringDecoder())//必须放到数据处理之前
.addLast(new ClientHandler());//绑定客户端数据处理对象
}
});
//与服务端连接,调用sync()方法来执行同步阻塞,直到连接完成
ChannelFuture sync = bootstrap.connect("127.0.0.1", 9527).sync();
//向服务端发送数据 Unpooled: netty提供的工具类,可以将其他类型转为buf类型
sync.channel().writeAndFlush("12345"); //使用了String编码器,可直接发送String
sync.channel().writeAndFlush("1234567"); //超出会在下一次一起发送
sync.channel().writeAndFlush("123");
sync.channel().writeAndFlush("123"); // 长度不够,不会发送,一直等
//开启同步阻塞监听,直到断开连接才关闭通道
sync.channel().closeFuture().sync();
workGroup.shutdownGracefully();
如图,运行服务端收到的包:
3 换行符解码器
// 换行符为结束的解码器,即按行切割,依次遍历 ByteBuf的可读字节,判断是否有换行“\n” 或者 “\r\n”,如果有,从可读索引到结束为止区间就组成了一行
pipeline.addLast(new LineBasedFrameDecoder(5));
//设置字符串编码,设置后服务端发送的数据就会转成String
pipeline.addLast(new StringEncoder());
//还可以设置字符串形式的解码,设置后服务端接受到的数据就会自动转成String
pipeline.addLast(new StringDecoder());//必须放到数据处理之前
pipeline.addLast(new ServerHandler());//绑定服务端数据处理
4 可以使用telnet进行测试
成功后可以向服务器发送消息
3 编解码技术与java序列化
当进行跨进程服务调用时,需要把被传输的java对象编码为字节数组或者ByteBuffer对象,当远程服务读取到ByteBuffer或者字节数组时,需要将其解码为发送时的java对象,这是编解码技术
java序列化只是编解码技术的一种,但是它无法跨语言,并且序列化后的码流太大,,并且性能太低,所以一般主流的RPC框架都不使用它
0 java序列化
java序列化只需要POJO对象实现Serializable接口,生成序列化ID,就可以通过ObjectInput和Objectoutput进行序列化和反序列化
实现如下:
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(connectGroup, workGroup)//支持链式编程,进行配置
//指定使用NioServerSocketChannel这种类型(服务端)的通道
.channel(NioServerSocketChannel.class)
//ChannelInitializer:服务器Channel通道初始化设置的抽象类
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override //初始化操作:编解码,绑定处理逻辑等
protected void initChannel(SocketChannel channel) throws Exception {
//使用 childHandler 去绑定具体的 事件处理器
ChannelPipeline pipeline = channel.pipeline();
// 设置java序列化解码器,单个对象序列化后的字节最大长度设置为1M,
// 使用 weakCachingConcurrentResolver创建线程安全的 WeakReferenceMap 对类加载器进行缓存,支持多线程访问,当虚拟机内存不足时,会释放缓存中的内存,防止内存泄漏
pipeline.addLast(new ObjectDecoder(1024*1024, ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader())));
pipeline.addLast(new ObjectEncoder()); //发送消息时自动进行编码
pipeline.addLast(new ServerHandler());//绑定服务端数据处理
业界主流的编码框架
1 Protobuf google
2 thrift FaceBook
thrift通过IDL描述接口和数据结构定义,支持8种java基本类型和Map,List,Set,支持可选和必选的定义,功能非常强大,因此可以定义数据结构中字段的顺序,
2 Marshalling JBoss
使用非常简单,demo如下:
服务端配置:
ServerBootstrap bootstrap = new ServerBootstrap();//配置
bootstrap.group(wgroup, croup)
.channel(NioServerSocketChannel.class)//指定通道类型
.option(ChannelOption.SO_BACKLOG, 1024)
.handler(new LoggingHandler(LogLevel.INFO))//设置日志输出
.childHandler(new ChannelInitializer<SocketChannel>() { //初始化设置
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//设置Marshalling编解码
socketChannel.pipeline().addLast(MarshallingCodeFactory.createMarshallingEncoder());
socketChannel.pipeline().addLast(MarshallingCodeFactory.createMarshallingDecoder());
socketChannel.pipeline().addLast(new ServerHandler());//服务端业务处理
}
});
工厂MarshallingCodeFactory:
//Marshalling 编解码工厂类
public class MarshallingCodeFactory {
//创建解码器
public static MarshallingDecoder createMarshallingDecoder() {
//首先通过Marshalling工具类的getProvidedMarshallerFactory静态方法获取MarshallerFactory实例
//参数“serial”表示创建的是Java序列化工厂对象
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
//创建了MarshallingConfiguration对象
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);//将它的版本号设置为5
//然后根据MarshallerFactory和MarshallingConfiguration创建UnmarshallerProvider实例
UnmarshallerProvider provider = new DefaultUnmarshallerProvider(marshallerFactory, configuration);
//最后通过构造函数创建Netty的MarshallingDecoder对象
//它有两个参数,分别是UnmarshallerProvider和单个消息序列化后的最大长度。(如果序列化后长度大于此长度,则无法接受到对象)
MarshallingDecoder decoder = new MarshallingDecoder(provider, 1024 * 1024 * 1);
return decoder;
}
//创建编码器
public static MarshallingEncoder createMarshallingEncoder() {
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(5);
//创建MarshallerProvider对象,它用于创建Netty提供的MarshallingEncoder实例
MarshallerProvider provider = new DefaultMarshallerProvider(marshallerFactory, configuration);
//MarshallingEncoder用于将实现序列化接口的POJO对象序列化为二进制数组。
MarshallingEncoder encoder = new MarshallingEncoder(provider);
return encoder;
}
}
4 Http协议开发
netty的http协议栈性能非常优异,也可靠,非常适合在非web容器的场景下使用,比起tomcat,更加轻量和小巧,灵活性和定制性也好,netty已经提供了HTTP协议栈开发所需要的Handler
HTTP协议交互步骤如下:
1、Client向Server发送http请求。
2、Server端对http请求进行解析。
3、Server端向client发送http响应。
4、Client对http响应进行解析。
主要服务端代码如下:
netty服务器:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());//把流数据解析为httpRequest
//把多个消息转化成一个消息(FullHttpRequest或者FullHttpResponse),原因是HTTP解码器在每个HTTP消息中会生成多个消息对象。
ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());//http服务器端对response编码
//支持处理异步发送大数据文件,但不占用过多的内存,防止发生内存泄漏
ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
//这个是我们自定义的,处理文件服务器逻辑。主要功能还是在这个文件中
ch.pipeline().addLast("http-fileServerHandler", new HttpFileServerHandler(url));
}
});
ChannelFuture future = b.bind("172.16.1.188",8080).sync();//这里写你本机的IP地址
System.out.println("HTTP 文件目录服务器启动,网址是:"+"http://172.16.1.188:");
future.channel().closeFuture().sync();
} catch (Exception e) {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
5 Websocket协议开发
websocket是HTML5 提供的一种全双工通信的网络技术,websocket基于TCP进行双向全双工消息传递,相比于HTTP的半双工,性能有了很大提升。
6 UDP协议开发
代码如下:
netty服务器:
/*
* UDP没有连接,只监听端口
* UDP 无法从Channel获取远程客户端的ip和端口号,
* 而是通过发过来的DatagramPacket中的sender获取发送消息客户端的ip和端口号
* UDP不需要粘包拆包,每个包都是完整的
* netty UDP一般接收的是DatagramPacket包,里面封装了消息对象
* */
public void run(int port) throws Exception {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();//udp不能使用ServerBootstrap
bootstrap.group(group).channel(NioDatagramChannel.class)//udp通道
.option(ChannelOption.SO_BROADCAST, true) //允许发送广播
.handler(new UdpServerHandler());//服务端业务处理
//绑定端口 //等待端口关闭
bootstrap.bind(port).sync().channel().closeFuture().sync();
}
public static void main(String[] args) throws Exception {
new UdpServer().run(9527);//绑定端口,启动服务
new UdpServer().run(9528);
}
handler:
//udp数据格式
public class UdpServerHandler extends SimpleChannelInboundHandler<DatagramPacket> {
//udp通道只有一个,关闭了就不能接受了
protected void channelRead0(ChannelHandlerContext channelHandlerContext, DatagramPacket datagramPacket) throws Exception {
String req = datagramPacket.content().toString(CharsetUtil.UTF_8);
System.out.println(req);
//向客户端发送消息,构建udp消息对象 通过Packet中的sender获取发送消息客户端的ip和端口号
DatagramPacket dp = new DatagramPacket(Unpooled.copiedBuffer("我收到了你的消息".getBytes()), datagramPacket.sender());
channelHandlerContext.channel().writeAndFlush(dp);
}
}
3 源码解读
1 ByteBuf和相关辅助类
Netty的ByteBuf主要是为了弥补一些jdk nio ByteBuffer的不足(使用复杂,性能不好)。
ByteBuf 是byte数组的缓冲区,提供了以下基本功能:
1 先熟悉一下NIO的 ByteBuffer
ByteBuffer依靠flip()来切换模式,在读模式下调用flip()切换为写模式,在写模式下limit和capacity相等,position标识当前写的位置。
在写模式下调用flip()切换为读模式,在读模式下position回到起始位置开始读,limit回到position位置表示能读到多少数据,capacity不变表示缓存区容量大小。
capacity:在读/写模式下都是固定的,就是缓冲区容量大小。
position:读/写位置指针,表示当前读(写)到什么位置。
limit:在写模式下表示最多能写入多少数据,此时和capacity相同。在读模式下表示最多能读多少数据,此时它的值等于缓存区中实际数据量的大小。
如图:
2 netty的ByteBuf
ByteBuf主要是通过readerIndex 和 writerIndex两个指针进行数据的读和写,整个ByteBuf被这两个指针最多分成三个部分,分别是可丢弃部分,可读部分和可写部分
1 初始化的时候,整个缓冲区还没有数据,读写指针都指向0,所有的内容都是可写部分,此时还没有可读部分和可丢弃部分,如下:
2 当写完N个字节数据后,读指针仍然是0,因为还没有开始进行读事件,写指针向后移动了N个字节的位置,如下:
3 当开始读数据并且读取M个字节数据之后(M<N)写指针位置不变,读指针后移动了M个字节的位置,如下:
4 当可丢弃部分数据被清空之后,readerindex重新回到起始位置,writerindex的位置为writerindex的值减去之前的readerindex,也就是M,相关图示如下:
5 调用clear之后,writerindex和readerinde全部复位为0。它不会清除缓冲区内容(例如,用填充0),而只是清除两个指针
3 ByteBuf的API
PooledByteBufAllocator aDefault = PooledByteBufAllocator.DEFAULT;
ByteBuf buffer = aDefault.buffer();
ByteBuf buffer2 = aDefault.buffer();
// 从readerIndex 开始获取字节值,readerIndex增加1
buffer.readByte();
// 从readerIndex 开始获取无符号字节值,readerIndex增加1, readUnsignedByte()就等价于:ByteBuf.readByte()&0xFF
buffer.readUnsignedByte();
// 返回表示ByteBuf当前可读取的字节数,它的值等于writerIndex减去readerIndex
buffer.readableBytes();
buffer.readerIndex();
// 从readerIndex 开始获取24位整型值,readerIndex增加3
buffer.readMedium();
buffer.readUnsignedMedium();
// 从readerIndex 开始获取整型值,readerIndex增加4
buffer.readInt();
// 从readerIndex 开始获取long值,readerIndex增加8
buffer.readLong();
// 将当前buffer中的数据,读取到一个新创建的buffer中,并返回,返回的buffer,readerIndex是0 ,writerIndex是读取的长度
buffer.readBytes(10);
//将当前buffer中的数据读取到目标Bytebuffer中,直到没有剩余的可写空间
buffer.readBytes(buffer2);
//将当前buffer中的数据读取到目标Bytebuffer中,读取长度为固定值10
buffer.readBytes(buffer2,10);
//将当前buffer中的数据读取到目标Bytebuffer中,读取长度为固定值10,目标buffer的起始索引为指定位置(2)
buffer.readBytes(buffer2,2,10);
//将当前buffer中的数据读取到 字节数组中
buffer.readBytes(new byte[21]);
//将当前buffer中的数据读取到 字节数组中,读取长度为固定值10,目标数组的起始索引为指定位置(2)
buffer.readBytes(new byte[21],2,10);
// 将参数写入到 buffer中, writerIndex 增加1
buffer.writeByte(10);
// 将参数buffer2 的可读字节写入到 buffer中
buffer.writeBytes(buffer2);
// 将参数写入到 buffer中, writerIndex 增加4
buffer.writeInt(10);
// 重用已被读取的可丢弃的空间,防止buffer的动态扩张,但是此操作会发生数组内存复制
buffer.discardReadBytes();
// 将 readerIndex 和 writerIndex 重置为0
buffer.clear();
// 如果需要回滚操作: 将当前 readerIndex 备份到 markReaderIndex
buffer.markReaderIndex();
// 将 markReaderIndex 设置为当前 readerIndex
buffer.resetReaderIndex();
// 从buffer中找到 字节 3 首次出现的位置,没有返回-1, 查找区间 0-10
buffer.indexOf(0,10, (byte) 3);
// 从buffer中查找字节3首次出现的位置,起始readerIndex ,结束位置writerIndex,找不到返回 -1
buffer.bytesBefore((byte) 3);
// 从buffer中查找字节3首次出现的位置,起始readerIndex ,结束位置readerIndex +10 ,找不到返回 -1
buffer.bytesBefore(10,(byte) 3);
// 从buffer中查找字节3首次出现的位置,起始 5 ,结束位置 5 + 10,找不到返回 -1
buffer.bytesBefore(5,10,(byte) 3);
// 遍历 buffer 的可读字节数组,与 ByteProcessor 设置的条件进行查找对比,满足条件返回索引位置,否则返回 -1
// ByteProcessor接口对常用的查找关键字进行了定义和抽象
buffer.forEachByte(ByteProcessor.FIND_CR);
// 返回当前 buffer 的复制对象,共享缓冲区,即修改值会影响原buffer,但是有自己独立的读写索引
buffer.duplicate();
// 返回当前 buffer 的复制对象,一个新的对象,互不影响
buffer.copy();
// 返回的buffer的可读子缓冲区,即起始readerIndex ,结束位置writerIndex,共享缓存区,但是有独立的读写索引
buffer.slice();
// 指定位置读取
buffer.getByte(5);
// 指定位置写
buffer.setByte(5,10);
// 转换成 jdk的 ByteBuffer
buffer.nioBuffer();
4 源码解读
1 从内存分配看,ByteBuf分为堆内存和直接内存两大类
2 从内存回收看,ByteBuf分为池化和非池化两大来,主要类继承关系如下图:
5 ByteBuf的辅助类
ByteBufAllocator 是 Netty 内存分配最顶层的抽象,负责分配所有类型的内存。有两类实现:
1 基于内存池的:PooledByteBufAllocator
2 基于非内存池的:UnpooledByteBufAllocator
2 Channel和Unsafe
1 Channel是netty抽象出来的 网络IO 读写相关的接口,主要设计理念:采用facade模式,将网络IO以及其相关的操作封装起来,统一对外提供一个简单的APIChannel的定义尽量大而全,公共功能在抽象父类实现,由不通子类实现不同的功能,最大限度实现功能和接口的重用具体实现采用聚合的方式,运用更灵活, Channel接口定义:
public interface Channel extends AttributeMap, ChannelOutboundInvoker, Comparable<io.netty.channel.Channel> {
// Channel唯一标识
ChannelId id();
// Channel需要注册到EventLoop上,用于处理IO事件,此方法可以获取到channel注册到的EventLoop,
EventLoop eventLoop();
// 对于服务端: parent是空, 对于客户端:parent是创建它的ServerSocketChannel
Channel parent();
// 获取当前channel的配置信息,比如 连接超时配置等
ChannelConfig config();
// 判断当前channel是否已经打开
boolean isOpen();
// 判断当前channel是否已经注册到EventLoop上
boolean isRegistered();
// 判断当前channel是否处于激活状态上
boolean isActive();
// 获取元数据的描述信息,比如TCP参数配置等
ChannelMetadata metadata();
// 后去本地绑定地址
SocketAddress localAddress();
// 获取远程通信地址
SocketAddress remoteAddress();
//调用该方法的目的并不是关闭通道,而是预先创建一个凭证(Future),等通道关闭时,会通知该 Future,用户可以通过该 Future 注册事件
ChannelFuture closeFuture();
/**
* 从当前Chanel中读取数据到第一个inbound缓冲区,如果读取成功,则触发 ChanelHandler.channelRead事件
* 读取API操作完之后,紧接着会触发 ChanelHandler.channelReadComplete事件
*/
Channel read();
/**
* 将写入到环形数组中的数据,写入目标Channel中,真正的发送出去
*/
Channel flush();
// Unsafe 是 Channel的辅助接口, 用户不能直接调用,实际的IO读写都是此接口来完成的
public interface Unsafe {
// 绑定指定的本地socket地址
void bind(SocketAddress var1, ChannelPromise var2);
// 客户端使用指定的服务端地址发起连接,
void connect(SocketAddress var1, SocketAddress var2, ChannelPromise var3);
// 请求断开与远程通信对端的连接,ChannelPromise获取操作结果,该操作会级联ChannelPipeline 中的所有 ChanelHandler.disconnect事件
void disconnect(ChannelPromise var1);
// 主动关闭当前连接,ChannelPromise获取操作结果,该操作会级联ChannelPipeline 中的所有 ChanelHandler.close事件
void close(ChannelPromise var1);
//将当前消息 var1 ,通过ChannelPipeline 写入到发送消息的环形数组中,并没有真正的发送,ChannelPromise来获取写入操作的结果
void write(Object var1, ChannelPromise var2);
void flush();
}
}
2 主要实现类:
服务端:NioServerSocketChannel
客户端:NioSocketChannel
测试类: EmbeddedChannel
UDP数据包处理:DatagramChannel
3 ChannelPipeline和ChannelHandler
ChannelPipeline 是线程安全的!!但是 ChannelHandler 不是,所以需要考虑ChannelHandler的线程安全
Channel的ChannelPipeline和ChannelHandler机制类似于Servlet和Filter:
netty将Channel的数据管道抽象成ChannelPipeline,消息在ChannelPipeline中传递和流动,即ChannelPipeline 是 ChannelHandler的容器,负责 ChannelHandler 的管理和调度。
由ChannelHandler对IO事件进行拦截和处理,ChannelPipeline持有ChannelHandler事件拦截器的链表(即所有的ChannelHandler),可以通过增删ChannelHandler来实现不通的业务逻辑定制
ChannelHandler支持注解: @Shable(类注解,多个Pipeline共用同一个Handler对象)和@Skip(方法注解,方法会被跳过,不执行)
1 消息的读取全流程:
1 channel.read 读取到 ByteBuf,触发了ChannelRead事件,NioEventLoop调用ChannelPipeline的fireChannelRead方法,将消息传递到ChannelPipeline 中,消息依次被ChannelHandler处理,过程中任何ChannelHandler都可以中断处理:
2 ChannelHandler 分为两种:入站和出站,执行都是有顺序的!
1 ChannelInboundHandler 处理入站事件
入站事件是被动接收事件,例如接收远端数据,通道注册成功,通道变的活跃等等。
入站事件流向是从 ChannelPipeline 管理的 ChannelInboundHandler 列表头到尾。因为入站事件一般都是从远端发送过来,所以流向是从头到尾。
采用拦截器的模式,由ChannelInboundHandler 决定是否处理列表中的下一个 ChannelInboundHandler。
2 ChannelOutboundHandler 处理出站事件
出站事件是主动触发事件,例如绑定,注册,连接,断开,写入等等。
出站事件流向是从 ChannelPipeline 管理的 ChannelOutboundHandler 列表尾到头。因为出站事件最后要发送到远端,所以从尾到头。
采用拦截器的模式,由ChannelInboundHandler 决定是否处理列表中的下一个 ChannelInboundHandler (因为是从尾到头,这里的下一个,在列表中其实是上一个)。
3 ChannelHandlerAdapter说明
用户如果需要自定义 ChannelHandler,则需要实现该接口,对于很多方法都需要覆写,代码非常麻烦,使用 ChannelHandlerAdapter,则会非常简洁(自动实现了很多方法,都使用Skip跳过了)
只需要继承类,实现自定义功能即可,如:
4 Pipeline构建和使用
netty会为每个连接创建一个单独的Pipeline,我们需要将ChannelHandler添加即可
5 ByteToMessageDecoder说明
为了方便业务将ByteBuf转换成POJO对象,Netty提供了ByteToMessageDecoder抽象工具解码类,用户的解码器,只需要实现decode方法,即可实现congByteBuf到POJO对象的功能
但是ByteToMessageDecoder 并没有考虑粘包,组包的情况,需要用户自己处理
6 MessageToMessageDecoder说明
MessageToMessageDecoder实际上是netty的二次解码,将一个POJO对象二次解码为其他对象,因为在ByteToMessageDecoder之后,所以撑为二次解码
7 LengthFieldBasedFrameDecoder netty提供的自定义半包解码器
LengthFieldBasedFrameDecoder是netty提供的一种基于灵活长度的解码器。在数据包中,加了一个长度字段(长度域),保存上层包的长度。解码的时候,会按照这个长度,进行上层ByteBuf应用包的提取
只需要在构造器传入正确得参数即可
LengthFieldBasedFrameDecoder 提供了很多个参数,可以组合使用满足各种场景:
(1) maxFrameLength - 发送的数据包最大长度;
(2) lengthFieldOffset - 长度域偏移量,指的是长度域位于整个数据包字节数组中的下标;
(3) lengthFieldLength - 长度域的自己的字节数长度。
(4) lengthAdjustment – 长度域的偏移量矫正。 如果长度域的值,除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长 - 长度域的值 – 长度域偏移 – 长度域长。
(5) initialBytesToStrip – 丢弃的起始字节数。丢弃处于有效数据前面的字节数量。比如前面有4个节点的长度域,则它的值为4。
举个例子:
自定义长度解码器的构造参数值如下:
LengthFieldBasedFrameDecoder spliter=new LengthFieldBasedFrameDecoder(1024,0,4,0,4);
第一个参数为1024,表示数据包的最大长度为1024;
第二个参数0,表示长度域的偏移量为0,也就是长度域放在了最前面,处于包的起始位置,如果为4 ,则表明第四个字节位置才是长度的数值;
第三个参数为4,表示长度域占用4个字节;
第四个参数为0,表示长度域保存的值,仅仅为有效数据长度,不包含其他域(如长度域)的长度;
第五个参数为4,表示最终的取到的目标数据包,抛弃最前面的4个字节数据,长度域的值被抛弃,如果为0 ,则获取的包会包含了长度
8 MessageToMessageEncoder说明
将一个POJO对象转换成另一个POJO对象,自定义的编码器,可以继承MessageToMessageEncoder 来实现,
9 LengthFieldPrepender 基于长度的编码器,跟LengthFieldBasedFrameDecoder 是一对
LengthFieldPrepender 是一个长度前置编码器,它负责在消息的头部设置消息的长度。
在 LengthFieldPrepender 中,有 4 个成员变量。
byteOrder:设置字节序,默认大字端,在缓冲区处理数据是以大字端方式,还是以小字端方式;
lengthFieldLength:数据长度所占用的字节数,没有默认值,必须设置;
lengthIncludesLengthFieldLength:默认false,数据长度中是否包含数据长度本身的长度;
lengthAdjustment:默认0,长度调整字节数,消息体的长度等于数据长度加上长度调整字节数。
在 LengthFieldPrepender 中,共 5 个构造函数,成员变量 lengthFieldLength 为必传参数,其他都有默认值。在第 5 个构造函数中完成所有成员变量的初始化。
10 利用EmbeddedChannel做单元测试
4 EventLoop和EventLoopGroup
Netty的线程模型:Netty支持Reactor模式的三种模型,具体哪一种取决于启动的参数,但是一般实际中都使用主从 Reactor 多线程:
Nett y抽象出两组线程池:BossGroup 和 WorkerGroup
BossGroup 专门负责接收客户端的连接
WorkerGroup 专门负责网络的读写
BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup,NioEventLoopGroup 相当于一个 事件循环组,这个组中 含有多个事件循环 ,每一个事件循环是 NioEventLoop
NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯。
NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
每个Boss Group 中的 NioEventLoop 循环执行的步骤:
1)轮询accept 事件
2)处理accept 事件,与client建立连接 , 生成 NioScocketChannel,并将其注册 Worker Group 上的某个 NIOEventLoop 上的 selector
3)处理任务队列的任务,即 runAllTasks
每个 Worker Group 中的 NIOEventLoop 循环执行的步骤:
1)轮询 read/write 事件
2)处理 I/O 事件, 即 read/write 事件,在对应的 NioScocketChannel 上处理
3)处理任务队列的任务 , 即 runAllTasks
每个Worker NIOEventLoop 处理业务时,会使用 pipeline(管道)。pipline中包含了 channel,即通过pipline可以获取到对应的 channel,并且pipline维护了很多的 handler(处理器)来对我们的数据进行一系列的处理。
5 ServerBootStrap
ServerBootStrap是Netty服务端启动配置类,BootStrap是Netty客户端启动配置类。 主要API有:
- group(bossGroup, workerGroup) 绑定线程组,设置react模式的主线程池 以及 IO 操作线程池:
- channel(Class<? extends C> channelClass) 设置通讯模式,调用的是实现io.netty.channel.Channel接口的类。如:NioSocketChannel、NioServerSocketChannel,客户端一般选NioSocketChannel。
- childOption: 设置通道的选项参数, 对于客户端而言就是SocketChannel;服务断则是 ServerSocketChannel
- childHandlerhandler: 设置主通道的处理器, 对于客户端的SocketChannel,主要是用来处理 业务操作
- childAttr: 设置通道的属性;
客户端是 option / handler / attr 方法, 服务端都是开头有child的 childHandler / childOption / childAttr 方法
1 常用的childOption
.childOption(ChannelOption.TCP_NODELAY, true)/* 为Nagle算法的原因(Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次),可能导致数据延迟发送,如果不希望延迟发送时候,该参数设置为ture*/
.childOption(ChannelOption.SO_KEEPALIVE, true)/* TCP会主动探测空闲连接的有效性。可以将此功能视为TCP的心跳机制,需要注意的是:默认的心跳间隔是7200s即2小时。Netty默认关闭该功能 */
.childOption(ChannelOption.SO_REUSEADDR, true) /* 允许重复使用本地地址和端口,比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,这个在服务器程序中比较常使用 */
/* ALLOCATOR: 用来控制使用过的ByteBuff池化\非池化、堆内存\直接内存
* // true表示使用直接内存
* new PooledByteBufAllocator(true);
* // false表示使用堆内存
* new PooledByteBufAllocator(false);
* // ture表示使用直接内存
* new UnpooledByteBufAllocator(true);
* // false表示使用堆内存
* new UnpooledByteBufAllocator(false);
*/
.childOption(ChannelOption.ALLOCATOR, new PooledByteBufAllocator(false))// heap buf 's better
.childOption(ChannelOption.SO_RCVBUF, 1048576)/* 设置缓冲区大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功 */
6 Futher和Promise
1 ChannelFuture是Channel异步IO操作的结果。
Netty中的所有IO操作都是异步的。这意味着任何IO调用都将立即返回,而不能保证所请求的IO操作在调用结束时完成。
相反,将返回一个带有ChannelFuture的实例,该实例将提供有关IO操作的结果或状态的信息。
ChannelFuture要么是未完成状态,要么是已完成状态。IO操作刚开始时,将创建一个新的Future对象。
新的Future对象最初处于未完成的状态,因为IO操作尚未完成,所以既不会执行成功、执行失败,也不会取消执行。
如果IO操作因为执行成功、执行失败或者执行取消导致操作完成,则将被标记为已完成的状态,并带有更多特定信息,例如失败原因。请注意,即使执行失败和取消执行也属于完成状态。
由ChannelFutureListener提供的通知机制可以使我们不需要手动去循环查询操作是否已经执行结束。
2 ChannelPromise 接口扩展了 Promise 和 ChannelFuture,绑定了 Channel,既可写异步执行结构,又具备了监听者的 功能,是 Netty 实际编程使用的表示异步执行的接口。
5 总结与新特性
1 Netty架构刨析
1 Netty 采用了比较典型的三层网络架构进行设计,逻辑架构图如下所示:
第一层:Reactor 通信调度层,它由一系列辅助类完成,包括 Reactor 线程 NioEventLoop 以及其父类、NioSocketChannel/NioServerSocketChannel 以及其父类、ByteBuffer 以及由其衍生出来的各种 Buffer、Unsafe 以及其衍生出的各种内部类等。该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读事件、写事件等等,将这些事件触发到 PipeLine 中,由 PipeLine 充当的职责链来进行后续的处理。
第二层:职责链 PipeLine,它负责事件在职责链中的有序传播,同时负责动态的编排职责链,职责链可以选择监听和处理自己关心的事件,它可以拦截处理和向后/向前传播事件,不同的应用的 Handler 节点的功能也不同,通常情况下,往往会开发编解码 Hanlder 用于消息的编解码,它可以将外部的协议消息转换成内部的 POJO 对象,这样上层业务侧只需要关心处理业务逻辑即可,不需要感知底层的协议差异和线程模型差异,实现了架构层面的分层隔离。
第三层:业务逻辑处理层,可以分为两类:
纯粹的业务逻辑处理,例如订单处理。
应用层协议管理,例如HTTP协议、FTP协议等。
2 高性能说明
3 可定制性和可扩展性
3 Netty新版本新特性
4 netty的启动环境变量说明
配置参数名 | 功能说明 |
---|---|
io.netty.allocator.numHeapArenas | 内存池堆内存内存区域的个数。默认值:Math.min(runtime.availableProcessors(),Runtime.getRuntime().maxMemory()/defaultChunkSize/2/3) |
io.netty.allocator.numDirectArenas | 内存池直接内存内存区域的个数。默认值:Math.min(runtime.availableProcessors(),Runtime.getRuntime().maxMemory()/defaultChunkSize /2/3) |
io.netty.allocator.pageSize | 一个page的内存大小,默认值8192 |
io.netty.allocator.maxOrder | 用于计算内存池中一个 Chunk内存的大小:默认值11,计算公式如下:1 Chunk = 8192<<11 = 16MB |
io.netty.allocator.chunkSize | 一个 Chunk内存的大小,如果没有设置,默认值为 pageSize<< maxOrder=16M |
io.netty.noKeySetOptimization | Netty的 JDK SelectionKey优化开关,默认关闭,设置true开启,性能优化开关,对上层用户不感知 |
io.netty.selectorAutoRebuildThreshold | 重建 selector的阈值,修复 JDK NIO多路复用器死循环问题。默认值为512 |
io.netty.threadLocalDirectBufferSize | 线程本地变量直接内存缓冲区大小,默认64KB |
io.netty.machineId | 用户设置的机器id,默认会根据MAC地址自动生成 |
io.netty.processId | 用户设置的流程ID,默认会使用随机数生成 |
io.netty.eventLoopThreads | Reactor线程 NioEventLoop的个数,默认值CPU个数×2 |
io.netty.noJdkZlibDecoder | 是否使用 JDK Zlib压缩解码器,默认不使用 |
io.netty.noPreferDirect | 是否允许通过底层AP直接访问直接内存。默认值:允许 |
io.netty.noUnsafe | 是否允许使用 sun.misc.Unsafe,默认允许。注意:使用sun的私有类库存在平台可移植问题:另外, sun.miscUnsafe类是不安全的,如果操作失败,不是抛出异常,而是虚拟机 core dump。不建议使用 Unsafe |
io.netty.noJavassist | 是否允许使用 Javassist类库,默认允许 |
io.netty.initialSeedUniquifier | 本地线程相关的随机种子初始值,默认值为0 |