1.ServerBootStrap和BootStrap
这两个主要是我们netty服务端和客户端的启动对象,主要实现了netty服务的配置功能,能够将多个netty组件装载到bootstap对象上。
主要方法有:
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) //用来配置服务端的主从工作组
public B group(EventLoopGroup group) //用来配置客户端的工作组
public B channel(Class<? extends C> channelClass) //配置服务端的通道实现类,通常是ServerSocketChannel.class
public <T> B option(ChannelOption<T> option, T value) //用来给 ServerSocketChannel添加配置
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) //用来给与服务端建立的通道(SocketChannel)进行配置
public ServerBootstrap childHandler(ChannelHandler childHandler) //用来给我们的子工作组关联的pipeline配置自定义的handler
public ChannelFuture bind(int inetPort) //用于服务端绑定端口,会返回一个ChannelFuture对象
public ChannelFuture connect(String inetHost, int inetPort) //用于客户端连接服务器
option常见参数:
ChannelOption.SO_BACKLOG, 128
这个参数代表当服务端处理客户端请求的线程数已达到线程池峰值时,会将已建立tcp连接的客户端存放到等待队列中,这个参数则配置了我们等待队列的大小,默认配置50
ChannelOption.SO_KEEPALIVE, true
客户端与服务端建立连接后,两小时内客户端都处于空闲状态,那么服务端就会发送一个ack探测包给客户端,判断双方的连接是否有效,一定事件内无应答,服务端会清除当前连接。设置为false的话,服务器就会一直保持宕机的客户端连接
ChannelOption.TCP_NODELAY, true
在tcp协议中,为了尽可能的利用网络的带宽,会基于Nagle算法,将多个数据块合并成一个大的数据块进行传输,减少网络的交互次数(需要解决tcp粘包拆包问题)。设置为true的话,就会关闭Nagle算法,每当数据准备好就会立马进行传输。默认设置为false。
2.ChannelFuture
Netty中的操作都是异步执行的,因此不能立刻得知处理结果。通过ChannelFuture对象可以注册一个监听器,对相应事件的处理返回结果进行监听,并进行相应处理。
常见方法:
Channel channel() //获取正在执行io操作的通道
ChannelFuture sync() //等待异步处理
3.Channel
Channel主要负责执行网络的IO操作,由于Netty是通过异步调用来执行的,因此任何IO调用都会立即返回,不能保证返回时IO操作已完成。这里我们可以通过返回的ChannelFuture对象来注册监听器,监控IO操作结果。
常见的Channel类型:
NioSocketChannel //客户端 TCP Socket 连接。
NioServerSocketChannel //服务端 TCP Socket 连接。
NioDatagramChannel // UDP 连接。
NioSctpChannel //客户端 Sctp 连接。
NioSctpServerChannel //Sctp 服务器端连接
4.Selector
selector作为Netty的核心组件,通过少量的线程就能够监听大量的通道的事件。当Channel注册到Selector中,它会轮询判断每个channel中是否有事件发送,可以十分高效的管理多个channel。
5.ChannelHandler
ChannelHandler是一个处理器接口,定义了相应的事件处理方法。
常见子类的接口有ChannelInboundHandler和ChannelOutboundHandler。
ChannelInboundHandler主要定义了处理入站IO事件的方法,而ChannelOutboundHandler则定义了出站IO事件的方法
其实现类为ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。
ChannelInboundHandlerAdapter常见方法
1. channelRegistered //当前Channel被注册到EventLoop
2. channelUnregistered //Channel从EventLoop中注销
3. channelActive //Channel已激活
4. channelInactive //Channel结束生命周期
5. channelRead //从当前Channel的另一端读取消息
6. channelReadComplete //消息读取完成后执行
7. userEventTriggered //一个用户事件被处罚
8. channelWritabilityChanged //改变通道的可写状态,可以使用Channel.isWritable()检查
9. exceptionCaught //重写父类ChannelHandler的方法,处理异常
在自定义Handler时,我们可以继承这两个类,并重写相应的事件方法。
在服务端我们通常继承ChannelInboundHandlerAdapter来实现入站io事件的处理,客户端可以继承ChannelInboundHandlerAdapter或者是SimpleChannelInboundHandler来处理io事件。
SimpleChannelInboundHandler是ChannelInboundHandlerAdapter的子类,它在ChannelInboundHandlerAdapter基础上进行了一定的拓展。
在继承SimpleChannelInboundHandler时,我们可以通过泛型指定接收的消息类型,然后需要实现一个channelRead0方法,这里会将我们指定类型的消息作为参数传入,我们可以通过对当前类型的消息进行处理。当该方法返回时,SimpleChannelInboundHandler自动release掉数据占用的Bytebuf资源(自动调用Bytebuf.release()),释放指向保存该消息的ByteBuf的内存引用。而ChannelInboundHandlerAdapter是不会进行这一释放的操作的。
为什么我们的服务端不去使用SimpleChannelInboundHandler呢?在服务端,我们在接收消息后需要将传入消息回送给发送者,而 write() 操作是异步的,直到 channelRead() 方法返回后可能仍然没有完成,如果继承SimpleChannelInboundHandler,会立即释放ByteBuf的资源,导致写入异常。
6.Pipeline
Pipeline实际上是一个handler的集合,它主要负责拦截入站/出战的IO操作,底层是由一个双向链表实现的。
每个channel都与一个pipeline相对应,在入站io操作时,会从我们的head节点的handler顺序执行,通过channelHandlerContext对象将处理结果传递给下一个handler,直到tail节点出站,而出站io操作则是从tail到head节点。
通过ChannelPipeline的addFirst方法往链表的头部添加handler,而addLast则是往尾部添加handler。
channelHandlerContext中 包 含 一 个 具 体 的 事 件 处 理 器 ChannelHandler , 同 时ChannelHandlerContext 中也绑定了对应的 pipeline 和 Channel 的信息,方便对 ChannelHandler进行调用
channelHandlerContext常用方法:
ChannelFuture close() //关闭Channel通道
ChannelOutboundInvoker flush() //刷新
ChannelFuture writeAndFlush(Object msg) //将 数 据 写 到 ChannelPipeline 中 当 前
ChannelHandler 的下一个 ChannelHandler 开始处理(出站)
7.ByteBuf
ByteBuf是我们在进行TCP网络通信时最常用的一种缓冲区,它是将数据转换成二进制进行传输。
在netty中,分别包含一下几种使用模式
1.堆缓冲区
最常用的 ByteBuf 模式是将数据存储在 JVM 的堆空间中。 这种模式被称为支撑数组(backing array), 它能在没有使用池化的情况下提供快速的分配和释放
直接缓冲区
直接缓冲区的内容将驻留在常规的会被垃圾回收的堆之外。直接缓冲区对于网络数据传输是理想的选择。因为如果你的数据包含在一个在堆上分配的缓冲区中,那么事实上,在通过套接字发送它之前,JVM将会在内部把你的缓冲区复制到一个直接缓冲区中。
直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。
经验表明,Bytebuf的最佳实践是在IO通信线程的读写缓冲区使用DirectByteBuf,后端业务使用HeapByteBuf。
复合缓冲区
让我们考虑一下一个由两部分——头部和主体——组成的将通过 HTTP 协议传输的消息。这两部分由应用程序的不同模块产生, 将会在消息被发送的时候组装。该应用程序可以选择为多个消息重用相同的消息主体。当这种情况发生时,对于每个消息都将会创建一个新的头部。
因为我们不想为每个消息都重新分配这两个缓冲区,所以使用 CompositeByteBuf 是一个
完美的选择。
需要注意的是, Netty使用了CompositeByteBuf来优化套接字的I/O操作,尽可能地消除了
由JDK的缓冲区实现所导致的性能以及内存使用率的惩罚。这种优化发生在Netty的核心代码中,因此不会被暴露出来,但是你应该知道它所带来的影响。
ByteBuf 的分配方式
池化的分配 PooledByteBufAllocator是ByteBufAllocator的默认方式
可以通过 Channel(每个都可以有一个不同的 ByteBufAllocator 实例)或者绑定到
ChannelHandler 的 ChannelHandlerContext 获取一个到 ByteBufAllocator 的引用。
池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。此实现使用了一种称为jemalloc的已被大量现代操作系统所采用的高效方法来分配内存。
该方式在netty中是默认方式。
非池化的分配 UnpooledByteBufAllocator
可能某些情况下,你未能获取一个到 ByteBufAllocator 的引用。对于这种情况,Netty 提供了一个简单的称为 Unpooled 的工具类, 它提供了静态的辅助方法来创建未池化的 ByteBuf实例。
在byteBuf中,有三条属性很重要,分别是readerIndex、writerIndex以及capacity
在byteBuf初始化时,readerIndex和writerIndex都为0,capacity为缓冲区容量.
当readerIndex=writerIndex=0时,此时缓冲区内为空,无数据可读
从通道中写入数据到缓冲区后,writerIndex变大,达到capacity-1时就无法在写入数据到缓冲区了(从0开始写),此时可读字节为readerIndex~writerIndex这一区间,可写字节在writerIndex~capacity-1这一区间。
当从缓冲区读数据到通道中时,readerIndex变大,达到writerIndex时,数据全部输出完了,无可读字节 ,此时可读字节readerIndex~writerIndex,可丢弃字节为0~readerIndex。
编码练习
搭建一个简单的基于netty框架的聊天室
实现流程:
聊天服务器搭建:
1.构建两个工作组线程池,负责监听客户端的连接以及读写事件。
2.创建启动对象,并进行相关配置。
3.绑定相应端口,启动服务端
代码:
public class NettyChatServer {
public static void main(String[] args) {
//1.构建两个Reactor工作组,bossGroup负责监听accept,workerGroup负责监听read和write
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //默认创建最多存在8个线程的线程池
try {
//2.创建服务端启动对象,并进行相关配置
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup) //配置服务端工作组
.channel(NioServerSocketChannel.class) //配置服务端通道实现类
.option(ChannelOption.SO_BACKLOG, 128) //配置服务端处理客户端请求的线程数达到峰值时,已经完成tcp三次握手的客户端会进入等待队列等候处理
//SO_BACKLOG代表等待队列的大小
.childOption(ChannelOption.SO_KEEPALIVE, true) //客户端连接是否保持连接
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new StringEncoder());
socketChannel.pipeline().addLast(new NettyChatServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(6666); //绑定端口号
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
服务器端自定义处理器:
1.客户端建立连接时,将SocketChannel保存到一个全局的map中,并通知map中的其他客户端。
2.对各个事件进行相应的处理
代码:
public class NettyChatServerHandler extends ChannelInboundHandlerAdapter {
private static Map<String, Channel> channelMap = new HashMap<String, Channel>();
//客户端已建立连接,通道处于就绪状态
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("事件channelActive");
Channel channel = ctx.channel();
String remoteAddress = channel.remoteAddress().toString().substring(1);
System.out.println("客户端-" + remoteAddress + "已上线");
//通知其他已连接客户端
for (String key : channelMap.keySet()) {
if(!key.equals(remoteAddress)) {
channelMap.get(key).writeAndFlush(Unpooled.copiedBuffer("客户端-" +
channel.remoteAddress() + "上线了", CharsetUtil.UTF_8));
}
}
channelMap.put(remoteAddress, channel);
}
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("事件channelInactive");
Channel channel = ctx.channel();
String remoteAddress = channel.remoteAddress().toString().substring(1);
System.out.println("客户端-" + remoteAddress + "已离线");
//通知其他已连接客户端
for (String key : channelMap.keySet()) {
if(!key.equals(remoteAddress)) {
channelMap.get(key).writeAndFlush(Unpooled.copiedBuffer("客户端-" +
channel.remoteAddress() + "离线了", CharsetUtil.UTF_8));
}
}
channelMap.remove(remoteAddress);
}
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("事件channelRead");
Channel channel = ctx.channel();
String remoteAddress = channel.remoteAddress().toString().substring(1);
System.out.println("客户端-" + remoteAddress + ":" + new String(msg.toString().getBytes(), Charset.forName("UTF-8")));
//通知其他客户端
channelMap.keySet().forEach(key -> {
if(!key.equals(remoteAddress)) {
channelMap.get(key).writeAndFlush(Unpooled.copiedBuffer("客户端-" +
channel.remoteAddress() + " : " + msg.toString(), CharsetUtil.UTF_8));
}
});
System.out.println("消息已转发至其他客户端");
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("执行client-exceptionCaught");
cause.printStackTrace();
//直接关闭管道的连接
ctx.close();
}
}
客户端:
1.创建一个工作组线程池监听事件并响应
2.配置bootStrap启动对象,并通过bootStrap启动对象与服务端建立连接
代码:
public class NettyChatClient extends ChannelInboundHandlerAdapter {
private static EventLoopGroup loopGroup;
public static void main(String[] args) {
loopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(loopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringDecoder());
nioSocketChannel.pipeline().addLast(new StringEncoder());
nioSocketChannel.pipeline().addLast(new NettyChatClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666);
Scanner scanner = new Scanner(System.in);
while(scanner.hasNextLine()) {
channelFuture.channel().writeAndFlush(Unpooled.copiedBuffer(scanner.nextLine(), CharsetUtil.UTF_8));
}
channelFuture.channel().close().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
loopGroup.shutdownGracefully();
}
}
}
客户端自定义处理器:
代码:
public class NettyChatClient extends ChannelInboundHandlerAdapter {
private static EventLoopGroup loopGroup;
public static void main(String[] args) {
loopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(loopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new StringDecoder());
nioSocketChannel.pipeline().addLast(new StringEncoder());
nioSocketChannel.pipeline().addLast(new NettyChatClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666);
Scanner scanner = new Scanner(System.in);
while(scanner.hasNextLine()) {
channelFuture.channel().writeAndFlush(Unpooled.copiedBuffer(scanner.nextLine(), CharsetUtil.UTF_8));
}
channelFuture.channel().close().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
loopGroup.shutdownGracefully();
}
}
}