Netty入门篇
Netty概述
Netty简介
- Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能服务器和客户端。
- Netty是一个NIO客户机-服务器框架,它支持快速、简单地开发网络应用程序,如服务器和客户机。它大大简化了网络编程,如 TCP 和 UDP 套接字服务器。
- “快速和简单”并不意味着生成的应用程序将受到可维护性或性能问题的影响。Netty 经过精心设计,并积累了许多协议(如 ftp、smtp、http)的实施经验,以及各种二进制和基于文本的遗留协议。因此,Netty 成功地找到了一种方法,在不妥协的情况下实现了易于开发、性能、稳定性和灵活性。
谁在使用Netty
- Dubbo、zk、RocketMQ、ElasticSearch、Spring5(对 HTTP 协议的实现)、GRpc、Spark 等大型开源项目都在使用 Netty 作为底层通讯框架。
Netty中的核心概念
Channel
- 管道,其是对 Socket 的封装,其包含了一组 API,大大简化了直接与 Socket 进行操作的复杂性。
EventLoopGroup
- EventLoopGroup 是一个 EventLoop 线程池,包含很多的 EventLoop。
- Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命周期内只会绑定一个线程,让该线程处理一个 Channel 的所有 IO 事件。
- 一个 Channel 一旦与一个 EventLoop 相绑定,则在 Channel 的整个生命周期内是不会也不能发生变化。但一个 EventLoop 可以与多个 Channel 相绑定。即 Channel 与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。
ServerBootStrap
- 用于配置整个 Netty 代码,将各个组件关联起来。服务端使用的是 ServerBootStrap,而客户端使用的是则 BootStrap。
ChannelHandler 和 ChannelPipeline
- ChannelHandler 是对 Channel 中数据的处理器,这些处理器可以是系统本身定义好的编解码器,也可以是用户自定义的。这些处理器会被统一添加到一个 ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。
ChannelFuture
- Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,所以 Netty 中定义了一个 ChannelFuture 对象作为这个异步操作的“代言人”,表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的 addListener() 方法为该异步操作添加监听器,为其注册回调:当结果出来后马上调用执行。
- Netty 的异步编程模型都是建立在 Future 与回调概念之上的。
Netty执行流程
- Netty Server 端启动,Netty 从 parentGroup 中选出一个 NioEventLoop 对指定 port 端口的连接进行监听;Netty Client 端启动,Netty 从 eventLoopGroup 中选出一个 NioEventLoop 对 Netty Server 连接,并处理 Sever 端发送来的数据。
- Client 端连接指定 Server 的 port 端口,创建 Channel,Server 端从 childGroup 中选出一个 NioEventLoop 与该 Channel 绑定,用于处理该 Channel 中所有的操作。
- Client 端通过 Channel 像 Server 端发送数据包 ByteBuf,Server 端的 Pipeline 中的处理器依次对 Channel 中的数据包进行处理。
- Server 端如果需要向 Client 端发送数据,则需要将数据经 Pipeline 中的处理器处理形成 ByteBuf 数据包,再将数据包 ByteBuf 通过 Channel 发送给 Client 端。
- Client 端 Pipeline 中的处理器依次对 Channel 中的数据包进行处理。
牛刀小试
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.38.Final</version>
</dependency>
实现Http通信
- 通过该程序达到的目的是,对 Netty 编程的基本结构及流程有所了解。该程序是通过 Netty 实现 HTTP 请求的处理,即接收 HTTP 请求,返回 HTTP 响应。这个代码相当于“SpringMVC + Tomcat”。
- 定义服务器启动类:
public class HttpServer {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup parentGroup = new NioEventLoopGroup();
NioEventLoopGroup childGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new HttpChannelInitializer());
ChannelFuture future = bootstrap.bind(8888).sync();
System.out.println("==>> 服务端启动成功~~");
future.channel().closeFuture().sync();
} finally {
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
}
}
public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast("httpServerCodec", new HttpServerCodec());
pipeline.addLast("httpServerHandler", new HttpServerHandler());
}
}
public class HttpServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpRequest) {
HttpRequest request = (HttpRequest) msg;
System.out.println("请求方式: " + request.method().name());
System.out.println("请求URI: " + request.uri());
if ("/favicon.ico".equals(request.uri())) {
System.out.println("不处理/favicon.ico请求");
return;
}
ByteBuf content = Unpooled.copiedBuffer("Hello Netty World", CharsetUtil.UTF_8);
DefaultFullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content
);
HttpHeaders headers = response.headers();
headers.set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
headers.set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
ctx.writeAndFlush(response)
.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
- 启动程序,并访问地址:http://localhost:8888/。
实现Socket通信
- 前面的工程是一个仅存在服务端的 HTTP 请求的服务器,而 Netty 中最为最见的是 C/S 构架的 Socket 代码。所以下面我们就来看一个 Netty 的 Socket 通信代码。
- 需求:客户端连接上服务端后,其马上会向服务端发送一个数据。服务端在接收到数据后,会马上向客户端也回复一个数据。客户端每收到服务端的一个数据后,便会再向服务端发送一个数据。而服务端每收到客户端的一个数据后,便会再向客户端发送一个数据。如此反复,无穷匮也。
- 定义服务端启动类和处理器类:
public class SomeServer {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup parentGroup = new NioEventLoopGroup(1);
NioEventLoopGroup childGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new SomeServerHandler());
}
});
ChannelFuture future = bootstrap.bind(8888).sync();
System.out.println("==>> 服务端启动成功~~");
future.channel().closeFuture().sync();
} finally {
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
}
}
public class SomeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(ctx.channel().remoteAddress() + ", " + msg);
ctx.channel().writeAndFlush("From Server : " + UUID.randomUUID());
TimeUnit.SECONDS.sleep(1);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
public class SomeClient {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new SomeClientHandler());
}
});
ChannelFuture future = bootstrap.connect("localhost", 8888).sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
public class SomeClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(ctx.channel().remoteAddress() + ", " + msg);
ctx.channel().writeAndFlush("From Client: " + LocalDateTime.now());
TimeUnit.SECONDS.sleep(2);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().writeAndFlush("From client: begin talking");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
两个处理器的区别
- SimpleChannelInboundHandler中的 channelRead()方法会自动释放接收到的来自于对方的 msg 所占有的所有资源。
- ChannelInboundHandlerAdapter中的 channelRead()方法不会自动释放接收到的来自于对方的msg。
- 若对方没有向自己发送数据,则自定义处理器建议继承自 ChannelInboundHandlerAdapter。因为若继承自 SimpleChannelInboundHandler 需要重写channelRead0()方法。而重写该方法的目的是对来自于对方的数据进行处理。因为对方根本就没有发送数据,所以也就没有必要重写 channelRead0()方法。
- 若对方向自己发送了数据,而自己又需要将该数据再发送给对方,则自定义处理器建议继承自 ChannelInboundHandlerAdapter。因为 write() 方法的执行是异步的,且 SimpleChannelInboundHandler 中的 channelRead()方法会自动释放掉来自于对方的 msg。若 write()方法中正在处理 msg,而此时 SimpleChannelInboundHandler 中的 channelRead() 方法执行完毕了,将 msg 给释放了,此时就会报错。
TCP的拆包与粘包
拆包粘包简介
- Netty 在基于 TCP 协议的网络通信中,存在拆包与粘包情况。拆包与粘包同时发生在数据的发送方与接收方两方。
- 发送方通过网络每发送一批二进制数据包,那么这次所发送的数据包就称为一帧,即Frame。在进行基于 TCP 的网络传输时,TCP 协议会将用户真正要发送的数据根据当前缓存的实际情况对其进行拆分或重组,变为用于网络传输的 Frame。在 Netty 中就是将 ByteBuf 中的数据拆分或重组为二进制的 Frame。而接收方则需要将接收到的 Frame 中的数据进行重组或拆分,重新恢复为发送方发送时的 ByteBuf 数据。
- 具体来说有以下两种情况:
- ① 发送方发送的ByteBuf较大:在进行网络传输之前会被TCP底层拆分为多个Frame进行发送。这个过程称为发送方拆包;接收方在接收到后需要将这些 Frame 进行合并,这个合并的过程称为接收方粘包。
- ② 发送方发送的ByteBuf很小:无法形成一个Frame,此时TCP底层会将很多这样的小ByteBuf 合并为一个 Frame 进行传递,这个合并的过程称为发送方粘包;接收方在接收到后需要将这个 Frame 进行拆分,拆分出很多原来的 ByteBuf,这个拆分的过程称为接收方拆包。
- 当一个 Frame 无法放入整数倍个 ByteBuf 时,最后一个 ByteBuf 会发生拆包。这个 ByteBuf 中的一部分放入到了一个 Frame 中,另一部分被放入到了另一个 Frame 中,这个过程就是发送方拆包。但对于将这些 ByteBuf 放入到一个 Frame 的过程,就是发送方粘包;当接收方在接收到两个 Frame 后,对于第一个 Frame 的最后部分,与第二个 Frame 的最前部分会进行合并,这个合并的过程就是接收方粘包。但在将 Frame 中的各个 ByteBuf 拆分出来的过程,就是接收方拆包。
定义启动类
public class SomeServer {
public static void main(String[] args) throws Exception {
NioEventLoopGroup parentGroup = new NioEventLoopGroup(1);
NioEventLoopGroup childGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new SomeServerHandler());
}
});
ChannelFuture future = bootstrap.bind(8888).sync();
System.out.println("==>> 服务端启动成功~~");
future.channel().closeFuture().sync();
} finally {
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
}
}
public class SomeClient {
public static void main(String[] args) throws Exception {
NioEventLoopGroup 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();
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new SomeClientHandler());
}
});
ChannelFuture future = bootstrap.connect("localhost", 8888).sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
发送方拆包&粘包
定义服务端处理器
- 服务端作为接收方,直接将接收到的 Frame 解码为 String 后进行显示,不对这些 Frame 进行粘包与拆包。
public class SomeServerHandler extends SimpleChannelInboundHandler<String> {
private int counter;
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.printf("Server端接收到的第 [%d] 个数据包: %s %n", counter++, msg);
}
}
定义客户端处理器
- 发送方拆包:客户端作为发送方,向服务端发送两个大的 ByteBuf 数据包,这两个数据包会被拆分为若干个 Frame 进行发送。这个过程中会发生拆包与粘包。
- 发送方粘包:客户端作为发送方,向服务端发送 100 个小的 ByteBuf 数据包,这 100 个数据包会被合并为若干个 Frame 进行发送。这个过程中会发生粘包与拆包。
测试
客户端自己实现编码
- 前面 SomeClient 使用了 StringEncoder,其实可以不用 StringEncoder,而使用自己的编码逻辑:
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
byte[] bytes = message.getBytes();
ByteBuf buffer;
for (int i = 0; i < 2; i++) {
buffer = Unpooled.buffer(bytes.length);
buffer.writeBytes(bytes);
ctx.writeAndFlush(buffer);
}
}
接收方粘包&拆包
- 为了解决接收方接收到的数据的混乱性,接收方也可以对接收到的 Frame 包进行粘包与拆包。Netty 中已经定义好了很多的接收方粘包拆包解决方案,我们可以直接使用。下面就介绍几个最常用的解决方案。
- 接收方的粘包拆包实际在做的工作是解码工作。这个解码基本思想是:发送方在发送数据中添加一个分隔标记,并告诉接收方该标记是什么。这样在接收方接收到 Frame 后,其会根据事先约定好的分隔标记,将数据进行拆分或合并,产生相应的 ByteBuf 数据。这个拆分或合并的过程,称为接收方的拆包与粘包。
LineBasedFrameDecoder
- 基于行分隔符的帧解码器,即会按照行分隔符对数据进行拆包粘包,解码出 Frame。
- 客户端发送数据,在数据最后添加一个行分割符。
System.getProperty("line.separator");
pipeline.addLast(new LineBasedFrameDecoder(5000));
- 需要注意的是:这个解码器需要需要设置在 StringDecoder 之前,且确保每一帧的最大长度要大于发送方每一份分割数据的长度,否则会报错
io.netty.handler.codec.TooLongFrameException
DelimiterBasedFrameDecoder
- 基于分隔符的帧解码器,即会按照指定分隔符对数据进行拆包粘包,解码出 ByteBuf。
- 客户端发送数据添加自定义的分隔符
###--###
ByteBuf delimiter = Unpooled.copiedBuffer("###--###".getBytes());
pipeline.addLast(new DelimiterBasedFrameDecoder(6000, delimiter));
FixedLengthFrameDecoder
- 固定长度帧解码器,即会按照指定的长度对 Frame 中的数据进行拆粘包。客户端发送 100 个 12 字符长度的
Hello Netty!
,修改服务端解码器:
pipeline.addLast(new FixedLengthFrameDecoder(12));
LengthFieldBasedFrameDecoder
- 基于长度域的帧解码器,用于对 LengthFieldPrepender 编码器编码后的数据进行解码的。所以,首先要清楚 LengthFieldPrepender 编码器的编码原理。
- LengthFieldPrepender 长度域预设:
- LengthFieldBasedFrameDecoder 基于长度域的帧解码器:
public LengthFieldBasedFrameDecoder(
int maxFrameLength,
int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip) {
this(
maxFrameLength,
lengthFieldOffset, lengthFieldLength, lengthAdjustment,
initialBytesToStrip, true);
}
1、长度域 2字节、偏移量0,不剥离头部,长度矫正值0
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 0 (= do not strip header)
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
2、长度域 2字节、偏移量0,剥离头部,长度矫正值0
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 2 (= the length of the Length field)
BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes)
+--------+----------------+ +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" |
+--------+----------------+ +----------------+
3、长度域 2字节、偏移量0,不剥离头部,有长度矫正值
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = -2 (= the length of the Length field)
initialBytesToStrip = 0
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
4、长度域 3字节、偏移量2,不剥离头部,长度矫值0
lengthFieldOffset = 2 (= the length of Header 1)
lengthFieldLength = 3
lengthAdjustment = 0
initialBytesToStrip = 0
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content |
| 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
5、长度域 3字节、偏移量0,不剥离头部,长度矫值2
lengthFieldOffset = 0
lengthFieldLength = 3
lengthAdjustment = 2 (= the length of Header 1)
initialBytesToStrip = 0
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content |
| 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
6、长度域 2字节、偏移量1,剥离头部3,长度矫值1
lengthFieldOffset = 1 (= the length of HDR1)
lengthFieldLength = 2
lengthAdjustment = 1 (= the length of HDR2)
initialBytesToStrip = 3 (= the length of HDR1 + LEN)
BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
7、长度域 2字节、偏移量1,剥离头部3,长度矫值-3
lengthFieldOffset = 1
lengthFieldLength = 2
lengthAdjustment = -3 (= the length of HDR1 + LEN, negative)
initialBytesToStrip = 3
BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
Netty应用篇
基础应用
WebSocket长连接
- WebSocket 是 HTML5 中的协议,是构建在 HTTP 协议之上的一个网络通信协议,其以长连接的方式实现了客户端与服务端的全双工通信。
- HTTP/1.1 版本协议中具有 keep alive 属性,实现的是半双工通信。
- WebSocket 握手原理:
- 需求:在页面上有两个左右并排的文本域,它们的中间有一个“发送”按钮。在左侧文本域中输入文本内容后,单击发送按钮,会显示到右侧文本域中。
- 定义客户端页面:在 src/main 下定义一个目录 webapp,在其中定义 index.html 页面。
- Netty 服务端给 pipeline 添加处理器:
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(4096));
pipeline.addLast(new WebSocketServerProtocolHandler("/some"));
pipeline.addLast(new TextWebSocketServerHandler());
}
});
public class TextWebSocketServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String text = ((TextWebSocketFrame) msg).text();
ctx.channel().writeAndFlush(new TextWebSocketFrame("From Client: " + text));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
- 启动服务端,并打开 index.html 页面进行测试:
网络聊天
- 该工程是对 socket 编程的一个应用,需求是实现一个网络群聊工具,参与聊天的客户端消息是通过服务端进行广播的。
- Netty 服务端给 pipeline 添加处理器:
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(2048));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new WebChatServerHandler());
}
});
public class WebChatServerHandler extends ChannelInboundHandlerAdapter {
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Channel channel = ctx.channel();
for (Channel ch : channelGroup) {
if (channel != ch) {
ch.writeAndFlush(channel.remoteAddress() + " : " + msg + "\n");
} else {
channel.writeAndFlush("Me: " + msg + "\n");
}
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
SocketAddress address = channel.remoteAddress();
System.out.println(address + "---上线");
channelGroup.writeAndFlush(address + "上线\n");
channelGroup.add(channel);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
SocketAddress address = channel.remoteAddress();
System.out.println(address + "---下线");
channelGroup.writeAndFlush(address + "下线,在线人数:" + channelGroup.size() + "\n");
}
}
读写空闲检测
- 当客户端与服务端的连接建立好后,它们之间就可以进行通信了。但是,若某客户端与服务端间长时间没有进行通信,而 Channel 却被长时间占用,就会形成资源浪费。 Netty 提供了专门用于进行读写操作空闲检测的处理器可供使用。
- 服务端启动类 pipeline 添加处理器IdleStateHandler:
pipeline.addLast(new IdleStateHandler(0, 0, 5));
- 服务端处理器重写 userEventTriggered 方法:
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
String eventDes = null;
switch (event.state()) {
case READER_IDLE:
eventDes = "读空闲超时";
break;
case WRITER_IDLE:
eventDes = "写空闲超时";
break;
case ALL_IDLE:
eventDes = "读和写都空闲超时";
break;
}
System.out.println(eventDes);
} else {
super.userEventTriggered(ctx, evt);
}
}
心跳机制
- 所谓心跳,即在 TCP 长连接中,客户端和服务器之间定期发送的一种特殊的数据包 , 通知对方自己还“活着”, 以确保 TCP 连接的有效性。
- 需求:Client 端连接到 Server 端后,会循环执行一个定时任务:随机等待几秒,然后 ping 一下 Server 端,即发送一个心跳。当 Server 端在等待了指定时间后没有读取到 Client 端发送的心跳,Server 端会主动断开连接。
定义服务端
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new IdleStateHandler(5, 0, 0));
pipeline.addLast(new HeartBeatServerHandler());
- 服务端处理器重写 userEventTriggered 方法:
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleState state = ((IdleStateEvent) evt).state();
if (state == IdleState.READER_IDLE) {
System.out.println("将要断开连接");
ctx.close();
} else {
super.userEventTriggered(ctx, evt);
}
}
}
定义客户端
- 客户端心跳检测处理:一旦连接被关闭,则将监听器移除
服务端监听多端口
修改服务端
public class SomeServer {
private NioEventLoopGroup parentGroup = new NioEventLoopGroup(1);
private NioEventLoopGroup childGroup = new NioEventLoopGroup();
private List<ChannelFuture> futures = new ArrayList<>();
public void start(List<Integer> ports) throws Exception {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new SomeServerHandler());
}
});
for (Integer port : ports) {
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("服务器正在启动中。。。");
future.addListener(f -> {
if (f.isSuccess()) {
System.out.println("服务器已启动,监听的端口为:" + port);
}
});
futures.add(future);
}
}
public void closeAllChannel() {
System.out.println("所有Channel已经全部关闭");
for (ChannelFuture future : futures) {
future.channel().close();
}
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
}
public class ServerStarter {
public static void main(String[] args) throws Exception {
List<Integer> ports = new ArrayList<>();
ports.add(7777);
ports.add(8888);
ports.add(9999);
SomeServer server = new SomeServer();
server.start(ports);
TimeUnit.SECONDS.sleep(60);
server.closeAllChannel();
}
}
测试