前些天分享了关于Socket与NIO通信编程的文章,没看过的朋友可以阅读一下
基于传统NIO进行通信编程是很让人头秃的一件事,幸好有大佬已经提供了完整的解决方案:Netty。Netty的API很简洁,屏蔽了NIO中复杂的细节,使用起来非常方便顺手。下面来看一下Netty的来龙去脉并用它实现一个简单又不失全面的通信demo。
Netty的历史
Netty是由Trustin Lee(韩国)于2004年开发,各版本的时间线如下
-
2004年6月 Netty2 发布
声称是Java社区中第一个基于事件驱动的应用网络框架
-
2008年10月 Netty3 发布
-
2013年7月 Netty4 发布
-
2013年12 发布Netty5.0.0.Alpha 1
-
2015年11月废弃5.0.0
Trustin Lee在一开始编写Netty时,任职于JBOSS公司,所以Netty3.x及之前的版本包名都是 org.jboss.netty。而编写Netty4.x 时,他自己搞了Netty社区出来,用来研发及维护,相应的包名也改为了 io.netty。
另外,MINA这个框架也是作者参与研发的。有个人叫Alex,他为 Apache Directory 开发网络框架,但是觉得不好用,看到 Netty2 后找到作者邀请合作开发,结合两种框架,随后有了MINA。
如今Netty和MINA都在更新维护中,但是Netty作为Trustin Lee的亲儿子,当然会更受他推崇,根据现在的使用情况来看,使用Netty也确实是主流。
Netty5.0版本为什么做出来后又被废弃了呢,主要是因为
- 过于复杂,并且性能上没有优势
- 精力有限,维护不过来了
所以干脆不要它了。
相比原生NIO,Netty的优点
netty做的更多:
- 支持常用的应用层协议
- 解决传输问题:粘包、半包现象
- 支持流量整形, 流量控制,黑白名单等
- 完善的断连、idle等异常处理
netty做的更好:
- 规避了JDK NIO bug
经典epoll bug:异常唤醒导致cpu 100%
- api更友好、更强大
- 隔离变化,屏蔽细节,oio、nio的模式切换代码改动量很小
很多项目都已经用上了Netty
- 数据库:Cassandra
- 大数据:Spark、Hadoop
- Message Queue:Rocketmq
- 检索:Elasticsearch
- 框架:gRPC、Aparche Dubbo、Spring5
- 分布式协调器:Zookeeper
- 工具类:async-http-client
Netty的关键组件
-
Channel(通道):
Channel
是Netty中数据的载体,代表了一个开放的连接,可以进行读取、写入、绑定等操作。在Netty中,Channel
是与底层I/O操作相关联的。 -
EventLoop(事件循环):
EventLoop
是Netty中处理所有事件的核心抽象。它负责处理连接的创建、数据的读写、定时任务等。Netty应用程序通常包含一个或多个EventLoop
,每个EventLoop
都可以被多个Channel注册,一个Channel
的所有事件都归该EventLoop处理。 -
ChannelHandler(通道处理器):
ChannelHandler
是用于处理Channel
中各种事件的接口。它允许开发者自定义业务逻辑,例如编解码、数据处理、异常处理等。ChannelHandler
可以被添加到ChannelPipeline
中,组成一个处理输入、输出数据的流水线链条。 -
ChannelPipeline(通道流水线):
ChannelPipeline
是ChannelHandler
链的容器。每个Channel
都有一个关联的ChannelPipeline
,用于维护和执行ChannelHandler
链。数据通过ChannelPipeline
时,经过一系列的ChannelHandler
,形成一条处理链。 -
Bootstrap和ServerBootstrap:
Bootstrap
和ServerBootstrap
是Netty用于启动和配置Channel
的辅助类。Bootstrap
用于客户端,而ServerBootstrap
用于服务器端。它们简化了启动过程,提供了一些配置选项。 -
ChannelFuture(通道异步操作结果):
ChannelFuture
表示一个异步操作的结果,例如Channel
的连接、数据的写入等。Netty中的很多操作都是异步的,ChannelFuture
用于监听和处理这些异步操作。 -
ByteBuf(字节缓冲区):
ByteBuf
是Netty中用于处理字节数据的缓冲区。与java.nio.ByteBuffer
相比,ByteBuf
提供了更加灵活、强大的API,支持动态扩容和零拷贝等特性。 -
Codec(编解码器):
Codec
是编解码器的抽象,用于在ChannelPipeline
中进行数据的编解码。Netty提供了许多内置的编解码器,例如StringEncoder
、StringDecoder
等。 -
ChannelOption(通道选项):
ChannelOption
用于配置Channel
的各种参数,例如TCP参数、连接超时时间等。通过ChannelOption
可以灵活配置Channel
的行为。
对于各个组件的实现细节,推荐去看一个【Netty实战】这本书,书中对Netty介绍的很详细,这里仅介绍下编程时接触最多的ChannelHandler。
最重要的两个ChannelHandler是ChannelInboundHandler与ChannelOutboundHandler。
在一个channel有读事件触发后,channel会顺着ChannelPipeline顺序走完所有ChannelInboundHandler。而当写事件触发后,channel会顺着ChannelPipeline倒序走完所有ChannelOutboundHandler。ChannelInboundHandler与ChannelOutboundHandler在ChannelPipeline中的顺序与其使用ChannelInitializer进行初始化时的添加顺序一致,所以每一个Handler都要放在正确的位置,代码才能正常工作。
假设伪代码如下
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//给pipeline管道设置Handler处理链,这里要特别注意Handler的顺序
socketChannel.pipeline()
.addLast( new ChannelInboundHandler1() )
.addLast( new ChannelOutboundHandler1() )
.addLast( new ChannelInboundHandler2() )
.addLast( new ChannelOutboundHandler2() )
.addLast( new ChannelInboundHandler3() )
.addLast( new ChannelOutboundHandler3() );
}
});
那么读写数据时Handler的处理先后顺序如下
下面通过一个demo将上面提到的组件串联使用起来,看看它们是怎么打配合的,废话不多说
上代码
代码分为服务端和客户端两块内容,文章结尾有源码地址,感兴趣的同学可以pull下来自己调试一下。由于涉及的类比较多,我也推荐你对着源码看文章,这样会比较清晰。
服务端代码
服务启动类
使用Netty进行网络通信编程的话,一般使用nio的形式,所以本次代码的通信模式也是nio模式
public class NettyServer {
public static void run() throws InterruptedException {
// 创建两个线程组 boosGroup、workerGroup。 因为服务端只用到一个端口来监听接入请求,所以boosGroup的线程数设为1(不设置的话实际也不会多启无用的线程,只是多new了几个无用的EventLoop对象)
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
// 因为LoggingHandler是可以共享使用的,所以这里先将它new出来,后面所有的ChannelPipeline中都用它
LoggingHandler logHandler = new LoggingHandler();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 设置两个线程组boosGroup和workerGroup
// boosGroup用来处理socket的连接请求,由ServerSocketChannel生成SocketChannel,然后从workerGroup中挑选一个eventLoop,将SocketChannel交给它进行接下来的通信工作
serverBootstrap.group(bossGroup, workerGroup)
// 设置服务端通道实现类型
.channel(NioServerSocketChannel.class)
// 设置最大的等待连接数量
.option(ChannelOption.SO_BACKLOG, 1024)
// 如果TCP_NODELAY没有设置为true,那么底层的TCP为了能减少交互次数,会将网络数据积累到一定的数量后,
// 服务器端才发送出去,会造成一定的延迟。在互联网应用中,通常希望服务是低延迟的,建议将TCP_NODELAY设置为true。
.childOption(NioChannelOption.TCP_NODELAY, true)
// 为ServerSocketChannel设置handler
.handler(logHandler)
// 为SocketChannel设置pipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//给pipeline管道设置Handler处理链,这里要特别注意Handler的顺序
socketChannel.pipeline()
// netty日志
.addLast("logHandler", logHandler)
// 空闲检测
.addLast("serviceIdleCheckHandler", new ServiceIdleCheckHandler())
// tcp粘包、半包处理
.addLast("tcpPacketDecoder", new TcpPacketDecoder())
.addLast("tcpPacketEncoder", new TcpPacketEncoder())
// 将ByteBuf解析为双端约定好的通信协议对象
.addLast("protocolDecoder", new ProtocolDecoder())
.addLast("protocolEncoder", new ProtocolEncoder())
// 具体业务分发处理
.addLast("serviceProcessHandler", new ServiceProcessHandler());
}
});
// 绑定端口号,启动服务端
ChannelFuture channelFuture = serverBootstrap.bind(9001).sync();
// 等待channel关闭并且阻塞当期线程直到它完成
channelFuture.channel().closeFuture().sync();
} finally {
// 优雅的关闭线程组
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
run();
}
}
主要流程没啥说的,这里主要讲一下我设置的这几个Handler。
LoggingHandler:这是Netty内置的一个Handler用于打印Netty内部的日志,使用它我们可以直观的看到Netty正在干什么事,或者说当前这个进程正在发生哪些网络事件。
17:09:04.671 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf838db11, L:/127.0.0.1:9001 - R:/127.0.0.1:55369] READ COMPLETE
17:09:09.534 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf838db11, L:/127.0.0.1:9001 - R:/127.0.0.1:55369] READ: 66B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 40 00 00 00 01 00 00 00 01 66 31 63 65 64 65 |.@........f1cede|
|00000010| 39 39 61 30 34 34 34 65 63 66 39 63 63 39 65 38 |99a0444ecf9cc9e8|
|00000020| 65 62 35 39 37 31 31 32 31 34 7b 22 74 69 6d 65 |eb59711214{"time|
|00000030| 22 3a 34 34 33 38 33 36 36 35 30 30 34 35 31 36 |":44383665004516|
|00000040| 36 7d |6} |
+--------+-------------------------------------------------+----------------+
17:09:09.537 [nioEventLoopGroup-3-1] INFO com.example.rpcdemo.nio.netty.handler.ServiceProcessHandler - 消息到达ServiceProcessHandler,开始执行请求处理的具体逻辑,{"messageBody":{"time":443836650045166},"messageHeader":{"opCode":1,"streamId":"f1cede99a0444ecf9cc9e8eb59711214","version":1}}
17:09:09.537 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf838db11, L:/127.0.0.1:9001 - R:/127.0.0.1:55369] WRITE: 2B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 40 |.@ |
+--------+-------------------------------------------------+----------------+
17:09:09.538 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf838db11, L:/127.0.0.1:9001 - R:/127.0.0.1:55369] WRITE: 64B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 01 00 00 00 01 66 31 63 65 64 65 39 39 |........f1cede99|
|00000010| 61 30 34 34 34 65 63 66 39 63 63 39 65 38 65 62 |a0444ecf9cc9e8eb|
|00000020| 35 39 37 31 31 32 31 34 7b 22 74 69 6d 65 22 3a |59711214{"time":|
|00000030| 34 34 33 38 33 36 36 35 30 30 34 35 31 36 36 7d |443836650045166}|
+--------+-------------------------------------------------+----------------+
17:09:09.538 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xf838db11, L:/127.0.0.1:9001 - R:/127.0.0.1:55369] FLUSH
ServiceIdleCheckHandler:这是我自定义的空闲检测的类,继承自IdleStateHandler
public class ServiceIdleCheckHandler extends IdleStateHandler {
/**
* 空闲检测策略:如果10秒内该Channel都没有读事件发生时,则该Channel视为"空闲"
*/
public ServiceIdleCheckHandler() {
super(false, 10, 0, 0, TimeUnit.SECONDS);
}
/**
* 当空闲检测检测到空闲后,会触发这个方法
* (我这个服务器很傲娇,如果你客户端一直不跟我联系,我就主动跟你断绝关系)
* IdleStateHandler中原方法是向pipeline中传递一个IdleStateEvent事件,这里我们改变一下处理方式,改为直接关闭这个"空闲"的channel
*/
@Override
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
if (evt == IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT) {
String channelStr = ctx.channel().toString();
log.info("检测到空闲通道,即将关闭该通道,{}", channelStr);
ctx.close();
log.info("通道已关闭,{}", channelStr);
return;
}
super.channelIdle(ctx, evt);
}
}
TcpPacketDecoder,TcpPacketEncoder: 处理tcp粘包、半包问题的编解码器
这里先说下什么是粘包、半包现象,举个例子
发送方发送了两条消息:ABC DEF
接收方收到消息:
- 粘包现象发生时: ABCDEF
- 半包现象发生时:AB CD EF
为什么会发生粘包现象?
- 发送方每次写入数据 < 套接字缓冲区大小,发送方多个小数据包被合并成一个TCP段传输。
- 接收方读取套接字缓冲区数据不够及时,导致多个数据包在缓冲区内合并。
- 发送方写入数据 > 套接字缓冲区大小,发送方一个数据包被拆成多个TCP段传输。
- 发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包
根本原因:TCP是流式协议,消息无边界。
解决办法:为消息设置边界。
为消息设置边界通常有三种方法:
- 消息定长:规定每个消息的长度,接收方按照固定长度读取数据。
- 消息分隔符:在消息中添加分隔符,接收方根据分隔符拆分消息。
- 使用特定协议:定义协议头,包含消息的长度等信息。
Netty也支持这三种处理方式,并提供了具体的类来进行支持
解决方式 | 解码 | 编码 |
---|---|---|
消息定长 | FixedLengthFrameDecoder | 不内置,几乎没人用 |
消息分隔符 | DelimiterBasedFrameDecoder | 不内置,太简单 |
协议头中存储消息长度 | LengthFieldBasedFrameDecoder | LengthFieldPrepender |
本次代码示例中使用的就是第三种方式,“在协议头中存储消息长度”,这也是业界比较推荐的处理方式。
看一下代码实现:
/**
* 功能: 解决TCP传输的粘包,半包问题
* <p>
* 各参数说明(按参数顺序)
* maxFrameLength:指定解码时,帧的最大长度。如果超过这个长度,LengthFieldBasedFrameDecoder 将抛出 TooLongFrameException 异常。
* lengthFieldOffset:指定长度字段的偏移量,即长度字段在帧中的起始位置的偏移量。例如,如果长度字段位于帧的起始位置,则 lengthFieldOffset 为 0。
* lengthFieldLength:指定长度字段的字节长度。常见的取值为 1、2、4 字节。
* lengthAdjustment:长度字段的值表示的是整个帧的长度,如果帧包含其他固定长度的字段,需要进行长度的调整。lengthAdjustment 就是用来进行这种调整的。
* initialBytesToStrip:指定从解码帧中跳过的字节数。在某些协议中,头部信息可能包含一些与长度字段无关的额外信息,这些信息需要跳过。
* failFast:如果设置为 true,则表示在帧长度超过 maxFrameLength 时立即抛出异常。如果设置为 false,则表示只有在实际解码时发现帧长度超过 maxFrameLength 时才抛出异常。
* </p>
* 时间: 2024/2/26 18:38
*/
public class TcpPacketDecoder extends LengthFieldBasedFrameDecoder {
public TcpPacketDecoder() {
super(Integer.MAX_VALUE, 0, 2, 0, 2, true);
}
}
/**
* 功能: 解决TCP传输的粘包,半包问题
* 各参数说明(按参数顺序)
* lengthFieldLength: 表示长度字段的字节数。仅支持 1,2,3,4,8
* lengthAdjustment: 表示长度字段值的调整值。长度字段的值表示的是整个帧的长度,如果帧包含其他固定长度的字段,需要进行长度的调整。lengthAdjustment 就是用来进行这种调整的。
* lengthIncludesLengthFieldLength: 表示长度字段的值是否包含长度字段的长度。如果设置为 true,则表示长度字段的值包含长度字段自身的长度;如果设置为 false,则表示长度字段的值仅包含有效数据的长度,不包含长度字段本身的长度。
* 时间: 2024/2/27 23:59
*/
public class TcpPacketEncoder extends LengthFieldPrepender {
public TcpPacketEncoder() {
super(2, 0, false);
}
}
ProtocolDecoder,ProtocolEncoder: 客户端与服务端进行通信的自定义协议的编解码器,用于将字节流解析成Java协议对象,或将Java协议对象编码成字节流。
本次代码通信协议使用json形式,协议对象为Message,有两个属性:请求头MessageHeader和MessageBody。请求对象RequestMessage和返回对象ResponseMessage分别继承于Message,类图如下:详情请看源码(文末有分享链接)
对应的的部分代码如下:
/**
* 功能: 将ByteBuf的内容解析为约定的协议对象
*/
public class ProtocolDecoder extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> out) throws Exception {
RequestMessage requestMessage = new RequestMessage();
requestMessage.decode(byteBuf);
out.add(requestMessage);
}
}
/**
* 功能: 将协议对象转为ByteBuf
*/
public class ProtocolEncoder extends MessageToMessageEncoder<ResponseMessage> {
@Override
protected void encode(ChannelHandlerContext ctx, ResponseMessage responseMessage, List<Object> out) throws Exception {
ByteBuf buffer = ctx.alloc().buffer();
responseMessage.encode(buffer);
out.add(buffer);
}
}
/**
* 功能: netty传输的自定义协议的消息体
*/
@Data
public abstract class Message<T extends MessageBody> {
MessageHeader messageHeader;
T messageBody;
/**
* 将ByteBuf解析为Message对象
*
* @param byteBuf
*/
public void decode(ByteBuf byteBuf) {
// 先读取请求信息中的头部字段
int version = byteBuf.readInt();
int opCode = byteBuf.readInt();
String streamId = byteBuf.readCharSequence(32, CharsetUtil.UTF_8).toString();
this.messageHeader = new MessageHeader(version, opCode, streamId);
// 剩余的内容都是消息体了
String bodyStr = byteBuf.toString(CharsetUtil.UTF_8);
Class<T> messageBodyClass = getMessageBodyClassFromOpCode(opCode);
this.messageBody = JSONObject.parseObject(bodyStr, messageBodyClass, JSONReader.Feature.SupportClassForName);
}
public abstract Class<T> getMessageBodyClassFromOpCode(int opCode);
public void encode(ByteBuf buffer) {
// 先将返回信息中的头部信息写入buffer
buffer.writeInt(this.messageHeader.getVersion());
buffer.writeInt(this.messageHeader.getOpCode());
buffer.writeCharSequence(this.messageHeader.getStreamId(), CharsetUtil.UTF_8);
// 再将body体转为json写入buffer
buffer.writeCharSequence(JSON.toJSONString(this.messageBody), CharsetUtil.UTF_8);
}
}
public class RequestMessage extends Message<Operation> {
public RequestMessage(String streamId, Operation operation) {
MessageHeader messageHeader = new MessageHeader();
messageHeader.setStreamId(streamId);
messageHeader.setOpCode(OperationEnum.fromOperationClazz(operation.getClass()).getOpCode());
this.messageHeader = messageHeader;
this.messageBody = operation;
}
@Override
public Class getMessageBodyClassFromOpCode(int opCode) {
return OperationEnum.fromOpCode(opCode).getOperationClazz();
}
}
public class ResponseMessage extends Message<OperationResult> {
@Override
public Class getMessageBodyClassFromOpCode(int opCode) {
return OperationEnum.fromOpCode(opCode).getOperationResultClazz();
}
}
这里提一下上面的两层编解码的关系
关于ByteToMessageDecoder与MessageToMessageDecoder
一次解码器:ByteToMessageDecoder
io.netty.buffer.ByteBuf (原始数据流)-> io.netty.buffer.ByteBuf (用户数据)
解决tcp的粘包、半包问题就属于一次解码。
二次解码器:MessageToMessageDecoder<I>
io.netty.buffer.ByteBuf (用户数据)-> Java Object
二次解码后ChannelPipeline中的流转的就是解析好的Java协议对象了。
Netty的设计中将两次解码分层处理了,这样每层的任务比较清晰,也能达到解耦的效果。
常见的“二次”编解码方式
- Java 序列化
- Marshaling
- XML
- JSON
- MessagePack
- Protobuf
- 其他
继续回到Handler的介绍上
ServiceProcessHandler:进行具体业务处理的Handler
本次代码中服务端接收的请求分为两类:探活请求与业务请求,具体的处理逻辑封装在了各自的Operation对象中。
public class ServiceProcessHandler extends SimpleChannelInboundHandler<RequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RequestMessage requestMessage) throws Exception {
log.info("消息到达ServiceProcessHandler,开始执行请求处理的具体逻辑,{}", JSON.toJSONString(requestMessage));
OperationResult operationResult = requestMessage.getMessageBody().execute();
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setMessageHeader(requestMessage.getMessageHeader());
responseMessage.setMessageBody(operationResult);
if (ctx.channel().isActive() && ctx.channel().isWritable()) {
ctx.writeAndFlush(responseMessage);
} else {
log.error("channel当前不可写,丢弃此消息:" + JSON.toJSONString(responseMessage));
}
}
}
public class BusinessOperation extends Operation {
private long userId;
@Override
public OperationResult execute() {
log.info("收到BusinessOperation消息");
BusinessOperationResult operationResult = new BusinessOperationResult(userId, 18, "张三");
log.info("BusinessOperation消息处理完成");
return operationResult;
}
}
public class KeepaliveOperation extends Operation {
private long time;
public KeepaliveOperation() {
this.time = System.nanoTime();
}
@Override
public OperationResult execute() {
return new KeepaliveOperationResult(time);
}
}
ServiceProcessHandler处理完请求后就封装好ResponseMessage,经过两层编码后返回给客户端了。
客户端代码
客户端启动类
public class NettyClient {
//此客户端对象拥有的channel
private Channel channel;
// 定义一个静态的EVENT_LOOP_GROUP,客户端所有channel共用这一个EVENT_LOOP_GROUP
public static final EventLoopGroup EVENT_LOOP_GROUP = new NioEventLoopGroup();
// 请求等待中心,存放等待请求结果返回的Future对象
public static final RequestPendingCenter REQUEST_PENDING_CENTER = new RequestPendingCenter();
public synchronized void initClient() {
try {
// 双重检查,防止并发访问时多次初始化
if (this.channel != null) {
return;
}
log.info("开始初始化客户端并建立连接");
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(EVENT_LOOP_GROUP)
.option(ChannelOption.TCP_NODELAY, true)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
// 空闲检测
.addLast("idleCheck", new ClientIdleCheckHandler())
// netty日志
.addLast("logHandler", new LoggingHandler())
// tcp粘包、半包处理
.addLast("tcpPacketDecoder", new TcpPacketDecoder())
.addLast("tcpPacketEncoder", new TcpPacketEncoder())
// 将ByteBuf解析为双端约定好的通信协议对象
.addLast("protocolDecoder", new ProtocolDecoder())
.addLast("protocolEncoder", new ProtocolEncoder())
// 空闲检测具体处理逻辑
.addLast("keepaliveHandler", new KeepaliveHandler())
// 返回结果分发处理
.addLast("responseDispatcherHandler", new ResponseDispatcherHandler(REQUEST_PENDING_CENTER));
}
});
//连接服务端
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9001).sync();
//将channel赋给实例属性,方便对channel进行复用
this.channel = channelFuture.channel();
log.info("客户端初始化成功");
//对通道关闭进行监听,这里不能一直等待,否则会阻塞线程
// channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 假设 EVENT_LOOP_GROUP 被用于了多个channel, 此channel关闭时,不能将 EVENT_LOOP_GROUP 关掉,确实需要关闭时通过 closeAll 方法关闭
// EVENT_LOOP_GROUP.shutdownGracefully();
}
}
/**
* 发送消息
*/
public OperationResult send(Operation operation) throws ExecutionException, InterruptedException {
if (this.channel == null) {
initClient();
}
String streamId = IdUtil.fastSimpleUUID();
// 创建一个Future对象,并放入"请求等待中心"中,等待结果返回
OperationRequestFuture requestFuture = new OperationRequestFuture();
REQUEST_PENDING_CENTER.add(streamId, requestFuture);
// 发送消息给对端
RequestMessage requestMessage = new RequestMessage(streamId, operation);
this.channel.writeAndFlush(requestMessage);
// 阻塞等待返回结果,这是Future的特性
// 当ResponseDispatcherHandler将返回结果set进Future对象后,requestFuture.get()就能拿到结果并结束阻塞了
OperationResult operationResult = requestFuture.get();
return operationResult;
}
/**
* 关闭eventLoop线程池
*/
public static void closeAll() {
EVENT_LOOP_GROUP.shutdownGracefully();
}
/**
* 通过main方法启动客户端
*/
public static void main(String[] args) throws InterruptedException, ExecutionException {
try {
NettyClient nettyClient = new NettyClient();
for (int i = 1; i <= 10; i++) {
BusinessOperation businessOperation = new BusinessOperation();
businessOperation.setUserId(i);
OperationResult result = nettyClient.send(businessOperation);
log.info("future服务端返回结果:{}", JSON.toJSONString(result));
ThreadUtil.sleep(10, TimeUnit.SECONDS);
}
log.info("调用结束");
} finally {
NettyClient.closeAll();
}
}
}
客户端与服务端的“两次”编解码方式基本相同,主要的不同在于空闲检测的处理与响应结果的处理上。
ClientIdleCheckHandler,KeepaliveHandler:空闲检测与相应处理。
本次代码中,客户端检测5秒内是否有写事件发生,如果没有的话,就向服务端发送一个探活包,以保活它与服务端之间的连接。(毕竟我们这个服务端比较傲娇,10秒内没收到客户端消息的话就主动断开连接了)
public class ClientIdleCheckHandler extends IdleStateHandler {
public ClientIdleCheckHandler() {
super(0, 5, 0, TimeUnit.SECONDS);
}
}
public class KeepaliveHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt == IdleStateEvent.FIRST_WRITER_IDLE_STATE_EVENT) {
log.info("检测到写空闲,即将发送探活包");
KeepaliveOperation keepaliveOperation = new KeepaliveOperation();
String streamId = IdUtil.fastSimpleUUID();
RequestMessage requestMessage = new RequestMessage(streamId, keepaliveOperation);
ctx.writeAndFlush(requestMessage);
log.info("探活包发送完成");
}
super.userEventTriggered(ctx, evt);
}
}
ResponseDispatcherHandler:接收到服务端的返回结果,并赋值给对应Future。
客户端发消息的设计里涉及到对Future的使用,因为客户端也是使用的Netty的nio模式,所以请求发出后,代码并不会阻塞等待结果返回,那怎么实现阻塞等待的效果呢?那就使用Future。这里我使用的Future是Netty中内置的DefaultPromise,这个Future支持我们手动给Future赋值以取消阻塞。google guava包中的SettableFuture也能实现类似的效果。
使用Future进行阻塞等待的流程如下图:
代码如下:
public class OperationRequestFuture extends DefaultPromise<OperationResult> {
}
/**
* 功能: 请求等待中心,存放等待返回结果的Future
*/
@Slf4j
public class RequestPendingCenter {
private Map<String, OperationRequestFuture> requestFutureMap = new ConcurrentHashMap<>();
public void add(String streamId, OperationRequestFuture operationRequestFuture) {
requestFutureMap.put(streamId, operationRequestFuture);
}
public void set(String streamId, OperationResult operationResult) {
if (requestFutureMap.containsKey(streamId)) {
requestFutureMap.get(streamId).setSuccess(operationResult);
requestFutureMap.remove(streamId);
return;
}
log.error("streamId:{} 在请求等待集合中不存在", streamId);
}
}
public class ResponseDispatcherHandler extends SimpleChannelInboundHandler<ResponseMessage> {
private RequestPendingCenter requestPendingCenter;
public ResponseDispatcherHandler(RequestPendingCenter requestPendingCenter) {
this.requestPendingCenter = requestPendingCenter;
}
/**
* 这里处理服务端返回结果,并放入Future中
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, ResponseMessage responseMessage) throws Exception {
String streamId = responseMessage.getMessageHeader().getStreamId();
OperationResult operationResult = responseMessage.getMessageBody();
log.info("收到服务端回复:{}", JSON.toJSONString(operationResult));
// 通过"请求等待中心",将返回结果赋值给指定Future
requestPendingCenter.set(streamId, operationResult);
}
}
至此,客户端与服务端就编写完成了,后面我会继续基于这个代码实现一个spring+netty的rpc调用工程,敬请期待!
源码分享
为了方便调试,我将客户端与服务端分了两个项目
https://gitee.com/huo-ming-lu/RpcDemoServer
https://gitee.com/huo-ming-lu/RpcDemoClient
本篇文章涉及到的类均在 com.example.rpcdemo.nio.netty 包下,敬请查阅。