# 跟闪电侠学Netty:Netty即时聊天实战与底层原理
Netty是一个异步基于**事件驱动**的**高性能网络通信**框架,可以看做是对NIO和BIO的封装,并提供了简单易用的API、Handler和工具类等,
用以快速开发高性能、高可靠性的网络服务端和客户端程序。
### 1. 创建服务端
服务端启动需要创建 `ServerBootstrap` 对象,并完成**初始化线程模型**,**配置IO模型**和**添加业务处理逻辑(Handler)**。在添加业务处理逻辑时,
调用的是 `childHandler()` 方法添加了一个 `ChannelInitializer`,代码示例如下
```java
// 负责服务端的启动
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 以下两个对象可以看做是两个线程组
// boss线程组负责监听端口,接受新的连接
NioEventLoopGroup boss = new NioEventLoopGroup();
// worker线程组负责读取数据
NioEventLoopGroup worker = new NioEventLoopGroup();
// 配置线程组并指定NIO模型
serverBootstrap.group(boss, worker).channel(NioServerSocketChannel.class)
// 定义后续每个 新连接 的读写业务逻辑
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline()
// 添加业务处理逻辑
.addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
System.out.println(msg);
}
});
}
});
// 绑定端口号
serverBootstrap.bind(2002);
```
通过调用 `.channel(NioServerSocketChannel.class)` 方法指定 `Channel` 类型为NIO类型,如果要指定为BIO类型,参数改成 `OioServerSocketChannel.class` 即可。
其中 `nioSocketChannel.pipeline()` 用来获取 `PipeLine` 对象,调用方法 `addLast()` 添加必要的业务处理逻辑,这里采用的是**责任链模式**,
会将每个Handler作为一个节点进行处理。
#### 1.1 创建客户端
客户端与服务端启动类似,不同的是,客户端需要创建 `Bootstrap` 对象来启动,并指定一个客户端线程组,
相同的是都需要完成**初始化线程模型**,**配置IO模型**和**添加业务处理逻辑(Handler)**, 代码示例如下
```java
// 负责客户端的启动
Bootstrap bootstrap = new Bootstrap();
// 客户端的线程模型
NioEventLoopGroup group = new NioEventLoopGroup();
// 指定线程组和NIO模型
bootstrap.group(group).channel(NioSocketChannel.class)
// handler() 方法封装业务处理逻辑
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {
channel.pipeline()
// 添加业务处理逻辑
.addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
System.out.println(msg);
}
});
}
});
// 连接服务端IP和端口
bootstrap.connect("127.0.0.1", 2002);
```
(注意:下文中内容均以服务端代码示例为准)
### 2. 编码和解码
客户端与服务端进行通信,通信的消息是以**二进制字节流**的形式通过 `Channel` 进行传递的,所以当我们在客户端封装好**Java业务对象**后,
需要将其按照协议转换成**字节数组**,并且当服务端接受到该**二进制字节流**时,需要将其根据协议再次解码成**Java业务对象**进行逻辑处理,
这就是**编码和解码**的过程。Netty 为我们提供了 `MessageToByteEncoder` 用于编码,`ByteToMessageDecoder` 用于解码。
#### 2.1 MessageToByteEncoder
用于将Java对象编码成字节数组并写入 `ByteBuf`,代码示例如下
```java
public class TcpEncoder extends MessageToByteEncoder<Message> {
/**
* 序列化器
*/
private final Serializer serializer;
public TcpEncoder(Serializer serializer) {
this.serializer = serializer;
}
/**
* 编码的执行逻辑
*
* @param message 需要被编码的消息对象
* @param byteBuf 将字节数组写入ByteBuf
*/
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
// 通过自定义的序列化器将对象转换成字节数组
byte[] bytes = serializer.serialize(message);
// 将字节数组写入 ByteBuf 便完成了对象的编码流程
byteBuf.writeBytes(bytes);
}
}
```
#### 2.2 ByteToMessageDecoder
它用于将接收到的二进制数据流解码成Java对象,与上述代码类似,只不过是将该过程反过来了而已,代码示例如下
```java
public class TcpDecoder extends ByteToMessageDecoder {
/**
* 序列化器
*/
private final Serializer serializer;
public TcpDecoder(Serializer serializer) {
this.serializer = serializer;
}
/**
* 解码的执行逻辑
*
* @param byteBuf 接收到的ByteBuf对象
* @param list 任何完成解码的Java对象添加到该List中即可
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> list) throws Exception {
// 根据协议自定义的解码逻辑将其解码成Java对象
Message message = serializer.deSerialize(byteBuf);
// 解码完成后添加到List中即可
list.add(message);
}
}
```
#### 2.3 注意要点
ByteBuf默认情况下使用的是**堆外内存**,不进行内存释放会发生内存溢出。不过 `ByteToMessageDecoder` 和 `MessageToByteEncoder` 这两个解码和编码
`Handler` 会自动帮我们完成内存释放的操作,无需再次手动释放。因为我们实现的 `encode()` 和 `decode()` 方法只是这两个 `Handler` 源码中执行的一个环节,
最终会在 finally 代码块中完成对内存的释放,具体内容可阅读 `MessageToByteEncoder` 中第99行 `write()` 方法源码。
#### 2.4 在服务端中添加编码解码Handler
```java
serverBootstrap.group(boss, worker).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline()
// 接收到请求时进行解码
.addLast(new TcpDecoder(serializer))
// 发送请求时进行编码
.addLast(new TcpEncoder(serializer));
}
});
```
### 3. 添加业务处理Handler
在Netty框架中,客户端与服务端的每个连接都对应着一个 `Channel`,而这个 `Channel` 的所有处理逻辑都封装在一个叫作 `ChannelPipeline` 的对象里。
`ChannelPipeline` 是一个双向链表,它使用的是**责任链模式**,每个链表节点都是一个 `Handler`,能通它能获取 `Channel` 相关的上下文信息(ChannelHandlerContext)。
Netty为我们提供了多种读取 `Channel` 中数据的 `Handler`,其中比较常用的是 `ChannelInboundHandlerAdapter` 和 `SimpleChannelInboundHandler`,
下文中我们以读取心跳消息为例。
#### 3.1 ChannelInboundHandlerAdapter
如下为处理心跳业务逻辑的 `Handler`,具体执行逻辑参考代码和注释即可
```java
public class HeartBeatHandler extends Ch