Netty 二
Netty模块组件
- ServerBootstrap& Bootstrap:
- 在 Netty, ServerBootstrap类是服务端的启动引导类, Bootstrap类是客户端的启动引导类. 主要作用是配置整个 Netty程序
- 常见方法:
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup), 该方法用于服务器端设置两个 EventLoopGroup(一个Boss Group, 一个Worker Group)
public B group(EventLoopGroup group), 该方法用于客户端设置一个 NioEventLoopGroup
public B channel(Class<? extends C> channelClass),该方法用来设置 SocketChannel
public B option(ChannelOption option, T value), 该方法用来设置服务端通道的的 ChannelOption配置
public ServerBootstrap childOption(ChannelOption childOption, T value), 该方法用来设置给接收到的通道添加 ChannelOption配置
public ServerBootstrap childHandler(ChannelHandler childHandler), 该方法用来设置自定义业务处理类
public ChannelFuture bind(int inetPort), 该方法用于服务器端设置监控端口号
public ChannelFuture connect(String inetHost, int inetPort), 该方法用于客户端连接服务器端
- Future& ChannelFuture:
- Netty中所有的 IO操作都是异步的, 也就是不能立刻得知消息是否被正确处理, 而是需要通过监听获取结果
- 常见方法: sync()等待异步操作执行完毕
- Channel:
- 通过 Channel可获得当前网络连接的通道的状态以及配置参数 如接收缓冲区大小
- Channel提供异步的网络 I/O操作 如建立连接, 读写, 绑定端口
- 调用后返回一个 ChannelFuture实例, 可以注册监听器来获得完成结果
- 支持关联 I/O操作与对应的处理程序
- 不同协议, 不同阻塞类型的连接都有不同的 Channel类型与之对应, 常用的 Channel类型:
NioSocketChannel, 异步的客户端 TCP Socket连接
NioServerSocketChannel, 异步的服务器端 TCP Socket连接
NioDatagramChannel, 异步的 UDP连接
NioSctpChannel, 异步的客户端 Sctp连接
NioSctpServerChannel, 异步的 Sctp服务器端连接
- Selector:
- Netty基于 Selector对象实现 I/O多路复用, 通过 Selector一个线程可以监听多个连接的 Channel事件
- 当向一个 Selector中注册 Channel后, Selector内部机制就可以自动不断地查询这些注册的 Channel是否有已就绪的 I/O事件 如可读, 可写, 网络连接完成等
- ChannelHandler及其实现类:
- ChannelHandler是一个接口, 处理 I/O事件或拦截 I/O操作, 并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序
- ChannelHandler本身并没有提供很多方法, 可继承它的子类
- ChannelHandler 及其实现类一览图(后)
ChannelInboundHandler用于处理入站 I/O事件
ChannelOutboundHandler用于处理出站 I/O操作
ChannelInboundHandlerAdapter(适配器), 用于处理入站 I/O事件
ChannelOutboundHandlerAdapter(适配器), 用于处理出站 I/O操作
ChannelDuplexHandler(适配器), 用于处理入站和出站事件
- 我们经常需要自定义一个 Handler类去继承 ChannelInboundHandlerAdapter, 然后通过重写相应方法实现业务逻辑
public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
public ChannelInboundHandlerAdapter() {}
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelRegistered();
}
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelUnregistered();
}
// 通道就绪事件
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
}
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelInactive();
}
// 通道读取数据事件
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
}
- Pipeline& ChannelPipeline
- ChannelPipeline是一个 Handler的集合, 它负责处理和拦截 inbound或 outbound的事件和操作, 相当于一个贯穿 Netty的链(可以理解为 ChannelPipeline是保存 ChannelHandler的 List, 用于处理或拦截 Channel的入站事件和出站操作)
- ChannelPipeline实现了一种拦截过滤器模式, 使用户可以完全控制事件的处理方式, 以及 Channel中各个 ChannelHandler, 如何相互交互
- 在 Netty中每个 Channel都有且仅有一个 ChannelPipeline与之对应, 它们的组成关系如下
(-) 一个 Channel包含了一个 ChannelPipeline, 而 ChannelPipeline中又维护了一个由 ChannelHandlerContext组成的双向链表, 并且每个 ChannelHandlerContext中又关联着一个 ChannelHandler
(-) 入站事件和出站事件在一个双向链表中, 入站事件会从链表 head往后传递到最后一个入站的 handler, 出站事件会从链表 tail往前传递到最前一个出站的 handler, 两种类型的 handler互不干扰
4) 常用方法:
ChannelPipeline addFirst(ChannelHandler… handlers), 把一个业务处理类(handler), 添加到链中的第一个位置
ChannelPipeline addLast(ChannelHandler… handlers), 把一个业务处理类(handler), 添加到链中的最后一个位置
- ChannelHandlerContext
- 保存 Channel相关的所有上下文信息, 同时关联一个 ChannelHandler对象
- ChannelHandlerContext中包含一个具体的事件处理器 ChannelHandler, 同时 ChannelHandlerContext中也绑定了对应的 pipeline和 Channel的信息
- 常用方法:
ChannelFuture close(), 关闭通道
ChannelOutboundInvoker flush(), 刷新
ChannelFuture writeAndFlush(Object msg), 将数据写到 ChannelPipeline中当前 ChannelHandler的下一个 ChannelHandler开始处理(出站)
- ChannelOption
- Netty在创建 Channel实例后, 设置 ChannelOption参数
- ChannelOption参数如下:
ChannelOption.SO_BACKLOG: 对应 TCP/IP协议 listen函数中的 backlog参数, 用来初始化服务器可连接队列大小. 服务端处理客户端连接请求是顺序处理的, 所以同一时间只能处理一个客户端连接. 多个客户端来的时候, 服务端将不能处理的客户端连接请求放在队列中等待处理, backlog参数指定了队列的大小
ChannelOption.SO_KEEPALIVE: 一直保持连接活动状态
- EventLoopGroup和其实现类 NioEventLoopGroup
- EventLoopGroup是一组 EventLoop的抽象, Netty为了更好的利用多核 CPU资源, 一般会有多个 EventLoop同时工作, 每个 EventLoop维护着一个 Selector实例
- EventLoopGroup提供 next接口, 可以从组里面按照一定规则获取其中一个 EventLoop来处理任务. 在 Netty服务器端编程中, 我们一般都需要提供两个 EventLoopGroup, 如 BossEventLoopGroup和 WorkerEventLoopGroup
- 通常一个服务端口即一个 ServerSocketChannel对应一个 Selector和一个 EventLoop线程. BossEventLoop负责接收客户端的连接并将 SocketChannel交给 WorkerEventLoopGroup来进行 IO处理, 如下图所示
(-) BossEventLoopGroup通常是一个单线程的 EventLoop, EventLoop维护着一个注册了 ServerSocketChannel的 Selector实例 BossEventLoop不断轮询 Selector将连接事件分离出来
(-) 通常是 OP_ACCEPT事件, 然后将接收到的 SocketChannel交给 WorkerEventLoopGroup
(-) WorkerEventLoopGroup会由 next选择其中一个 EventLoop来将这个 SocketChannel注册到其维护的 Selector并对其后续的 IO事件进行处理
4) 常用方法:
public NioEventLoopGroup(), 构造方法
public Future<?> shutdownGracefully(), 断开连接, 关闭线程
- Unpooled类
- Netty 提供一个专门用来操作缓冲区(即 Netty 的数据容器)的工具类
- 常用方法:
public static ByteBuf copiedBuffer(CharSequence string, Charset charset), 通过给定的数据和字符编码返回一个 ByteBuf对象
# ByteBuf的基本使用
public class NettyByteBuf {
public static void main(String[] args) {
// 1. 创建 netty的 ByteBuf对象, 该对象包含一个数组 arr, 是一个 byte[10]
// 2. ByteBuf不需要使用 flip()反转
// 3. 底层维护了 readerindex和 writerIndex, 通过 readerindex, writerIndex和 capacity
// readerindex 已经读取的区域
// readerindex ~ writerIndex, 可读的区域
// writerIndex ~ capacity, 可写的区域
ByteBuf buffer = Unpooled.buffer(10);
for (int i = 0; i < 8; i++) {
buffer.writeByte(i);
}
System.out.println("capacity=" + buffer.capacity()); // 10
System.out.println("readerIndex=" + buffer.readerIndex()); // 0
System.out.println("writerIndex=" + buffer.writerIndex()); // 8
for (int i = 0; i < 3; i++) {
System.out.println(buffer.getByte(i)); // 此方式不累加 readerIndex
}
System.out.println("readerIndex=" + buffer.readerIndex()); // 0
System.out.println("writerIndex=" + buffer.writerIndex()); // 8
for (int i = 0; i < 5; i++) {
System.out.println(buffer.readByte()); // 此方式会累加 readerIndex
}
System.out.println("readerIndex=" + buffer.readerIndex()); // 5
System.out.println("writerIndex=" + buffer.writerIndex()); // 8
}
}
# Unpooled获取 ByteBuf的基本使用:
public class NettyByteBuf {
public static void main(String[] args) {
// 创建 ByteBuf
ByteBuf byteBuf = Unpooled.copiedBuffer("hello, world!", CharsetUtil.UTF_8);
// 使用相关的方法
if (byteBuf.hasArray()) {
byte[] content = byteBuf.array();
// 将 content 转成字符串
System.out.println(new String(content, CharsetUtil.UTF_8));
System.out.println("byteBuf=" + byteBuf); // byteBuf=UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 13, cap: 39)
System.out.println(byteBuf.arrayOffset()); // 0
System.out.println(byteBuf.readerIndex()); // 0
System.out.println(byteBuf.writerIndex()); // 13
System.out.println(byteBuf.capacity()); // 39
System.out.println(byteBuf.getByte(0)); // 104
int len = byteBuf.readableBytes();
System.out.println("len=" + len); // len=13
// 使用 for取出各个字节
for (int i = 0; i < len; i++) {
System.out.println((char) byteBuf.getByte(i));
}
// 按照某个范围读取
System.out.println(byteBuf.getCharSequence(0, 4, CharsetUtil.UTF_8)); // hell
System.out.println(byteBuf.getCharSequence(4, 6, CharsetUtil.UTF_8)); // o, wor
}
}
}
- Netty实例(群聊系统)
public class GroupChatServer {
private int port;
public GroupChatServer(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 向 pipeline加解码器
pipeline.addLast("decoder", new StringDecoder());
// 向 pipeline加编码器
pipeline.addLast("encoder", new StringEncoder());
// 加自己的业务处理(handler)
pipeline.addLast(new GroupChatServerHandler());
}
});
ChannelFuture cf = b.bind(port).sync();
// 监听关闭
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new GroupChatServer(7000).run();
}
}
public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {
// 使用 map, 管理所有 channel; 或此处可以使用 db来管理
//public static ConcurrentMap<String, Channel> channels = new ConcurrentHashMap<>();
// 创建 ChannelGroup, 管理所有 channel;
// GlobalEventExecutor.INSTANCE是全局的事件执行器, 是一个单例
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 一个用户首次建立连接, 第一个被执行的方法
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
// 该方法会将 ChannelGroup中所有的 channel遍历, 并发送消息
channelGroup.writeAndFlush(LocalDateTime.now() + " [客户] " + channel.remoteAddress() + " 加入聊天!\n");
// 将当前通道加到 ChannelGroup
channelGroup.add(channel);
}
// 触发执行 channelInactive方法后, 触发此方法
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
// 发送消息给当前在线的所有用户
channelGroup.writeAndFlush(LocalDateTime.now() + " [客户] " + channel.remoteAddress() + " 离开了!\n");
System.out.println("User count=" + channelGroup.size());
}
// channel处于, 活动状态时(上线), 触发
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " 上线了! User count=" + channelGroup.size());
}
// channel处于, 不活动状态时(下线), 触发
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " 离线了! User count=" + channelGroup.size());
}
// 读取数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
Channel currentChannel = ctx.channel();
// 遍历 channelGroup
channelGroup.forEach(ch -> {
if (currentChannel == ch) {
ch.writeAndFlush(LocalDateTime.now() + " [我] 发送了消息: " + msg);
} else {
ch.writeAndFlush(LocalDateTime.now() + " [客户] " + currentChannel.remoteAddress() + " 发送了消息: " + msg);
}
});
}
// 捕获异常
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close(); // 关闭通道
}
}
public class GroupChatClient {
private final String host;
private final int port;
public GroupChatClient(String host, int port) {
this.host = host;
this.port = port;
}
public void run() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 加相关 handler
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
// 加自定义 handler
pipeline.addLast(new GroupChatClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
// 取得 channel
Channel channel = channelFuture.channel();
System.out.println("localAddress: " + channel.localAddress());
// 创建标准输入流, 用于客户端输入信息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
// 通过 channel发信息到服务器端
channel.writeAndFlush(msg + "\r\n");
}
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new GroupChatClient("127.0.0.1", 7000).run();
}
}
public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
// 客户端读取信息
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(msg.trim());
}
}
- Netty实例(心跳检测机制)
public class MyServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.handler(new LoggingHandler(LogLevel.INFO)); // 配置服务器日志方式
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
/*
1. IdleStateHandler 是netty 提供的处理空闲状态的处理器
2. long readerIdleTime: 表示多长时间没有读, 就会发送一个心跳检测包检测是否连接
3. long writerIdleTime: 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接
4. long allIdleTime: 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接
6. 当 IdleStateEvent触发后, 就会传递给管道的下一个 handler去处理
通过调用(触发)下一个 handler的 userEventTiggered, 处理 IdleStateEvent(读空闲,写空闲,读写空闲)*/
pipeline.addLast(new IdleStateHandler(8, 11, 5, TimeUnit.SECONDS));
// 加空闲检测处理器(自定义 handler)
pipeline.addLast(new MyServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class MyServerHandler extends ChannelInboundHandlerAdapter {
/**
* @param ctx 上下文
* @param evt 事件
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲";
break;
case WRITER_IDLE:
eventType = "写空闲";
break;
case ALL_IDLE:
eventType = "读写空闲";
break;
}
System.out.println(ctx.channel().remoteAddress() + " " + eventType);
// 当发生空闲, 关闭通道
// ctx.channel().close();
}
}
}
- Netty实例(通过 WebSocket实现服务器和客户端的长连接)
public class MyServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 因为基于 http协议, 需要配置 http的编解码器
pipeline.addLast(new HttpServerCodec());
// 是以块的方式写, 添加 ChunkedWriteHandler处理器(它解决大文件或码流传输过程中可能发生的内存溢出问题)
pipeline.addLast(new ChunkedWriteHandler());
/* 当浏览器发送大量数据时, 就会发出多次 http请求:
- http数据在传输过程中是分段, HttpObjectAggregator可以将多个段聚合(将数据解码成 FullHttpRequest)*/
pipeline.addLast(new HttpObjectAggregator(8192));
/*
1. 对应 websocket, 它的数据是以帧(frame)形式传递
2. 可以看到 WebSocketFrame下面有六个子类
4. WebSocketServerProtocolHandler核心功能是将 http协议升级为 ws协议, 保持长连接
5. 会将状态码升级为 101*/
pipeline.addLast(new WebSocketServerProtocolHandler("/abc"));
// 配置 TextWebSocketFrame处理器(自定义 handler)
pipeline.addLast(new MyTextWebSocketFrameHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
// TextWebSocketFrame类型, 表示一个文本帧(frame)
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
// 读数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("服务器收到消息 " + msg.text());
// 回复消息
ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间 " + LocalDateTime.now() + " " + msg.text()));
}
// 一个用户首次建立连接, 第一个被执行的方法
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// id表示通道的唯一编号, LongText唯一, ShortText较短可能重复
System.out.println("handlerAdded.channel.long.id=" + ctx.channel().id().asLongText()); // 84ef18fffe1640e0-000021d8-00000001-0721075f4dddb688-96c3f291
System.out.println("handlerAdded.channel.short.id=" + ctx.channel().id().asShortText()); // 96c3f291
}
// 客户端离线时触发
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved.channel.long.id=" + ctx.channel().id().asLongText());
}
// 发生异常时触发
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getMessage());
ctx.close(); //关闭连接
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
var socket;
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:7000/abc");
// 收到服务器端回送的消息时触发
socket.onmessage = function (ev) {
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + ev.data;
}
// 感知到连接开启时触发
socket.onopen = function (ev) {
var rt = document.getElementById("responseText");
rt.value = "连接已开启."
}
// 感知到连接关闭时触发
socket.onclose = function (ev) {
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + "连接已关闭."
}
} else {
alert("当前浏览器不支持 Websocket!")
}
// 发送消息到服务器
function send(message) {
if (!window.socket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
socket.send(message)
} else {
alert("连接未开启!");
}
}
</script>
<form onsubmit="return false">
<textarea name="message" style="height: 200px; width: 400px"></textarea>
<input type="button" value="发生消息" onclick="send(this.form.message.value)">
<textarea id="responseText" style="height: 200px; width: 400px"></textarea>
<input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
</form>
</body>
</html>
如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!