通过上一节的演示,我们自定义了三个消息实体 – 登录请求、登录回应以及用户上线通知消息;但是这些消息的解析以及逻辑处理都在一个handler里面执行,只是定义了几个消息体,就需要进行if else的判断,如果使用这种方式,当我们的消息体数量增加后,从而造成了if else泛滥。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf byteBuf = (ByteBuf) msg;
byteBuf.markReaderIndex();
if (MAGIC_NUMBER != byteBuf.readInt()) {
return;
}
byteBuf.resetReaderIndex();
BasePacket packet = Byte2PacketDecoder.getInstance().decode(byteBuf);
if (packet instanceof LoginRequestMessage) {
LoginRequestMessage message = (LoginRequestMessage) packet;
log.info("用户:{}, ID:{} 链接服务器", message.getUsername(), message.getUserId());
LoginResponseMessage loginResponseMessage = new LoginResponseMessage();
loginResponseMessage.setSuccess(true);
ByteBuf loginResponse = Packet2ByteEncoder.getInstance().encode(ctx.alloc(), loginResponseMessage);
ctx.channel().writeAndFlush(loginResponse);
// 保存已登录的channel
ChannelGroup channelGroup = new DefaultChannelGroup(ctx.executor());
// 通知所有用户有用户上线
channelGroup.add(ctx.channel());
OnlineMessage responseMessage = new OnlineMessage(message.getUserId());
ByteBuf response = Packet2ByteEncoder.getInstance().encode(ctx.alloc(), responseMessage);
channelGroup.writeAndFlush(response);
} else {
log.info("未知消息: {}", packet);
}
}
回顾下我们之前讲过的ChannelHandler和pipeline,每一个Inbound或者outbound在处理完当前逻辑后,可以通过对应的channelRead()和write()把事件传递给下一个handler进行处理;比如,在pipeline的每一个channelHandler中, ctx是上下文信息,msg是从上一个handler中已经被处理过的ByteBuf,如果当前的handler在pipeline中排行第一,则它的工作就是对ByteBuf进行校验或者解码,从而传播到下面的handler进行逻辑处理;每一个handler职责单一,处理单一的一种消息类型
netty中的编解码器
在netty中,已经内置了编解码器统一的类型
编码器 MessageToByteEncoder
对于编码器,netty提供了 MessageToByteEncoder ,这个类继承了ChannelOutboundHandlerAdapter类,属于outbound类型,我们只需要继承这个类,然后实现其中的encode(ChannelHandlerContext ctx, BasePacket msg, ByteBuf out) 方法即可,具体实现如下
protected void encode(ChannelHandlerContext ctx, BasePacket msg, ByteBuf out) throws Exception {
Packet2ByteEncoder.getInstance().encode(out, msg);
}
从该编码器中可以看到,事件传播过来时,并没有ByteBuf,而是到了此处才开始进行编码,所以我们在构建返回的消息类型时,只需要关注业务逻辑处理,从而直接返回对应的实体即可,编码工作由MessageToByteEncoder进行实现
解码器 ByteToMessageDecoder
相对于编码器,netty同样提供了 ByteToMessageDecoder 这个解码器,属于Inbound类型,继承了ChannelInboundHandlerAdapter类,实现其中的decode(ChannelHandlerContext ctx, ByteBuf in, List out) 方法,具体实现如下
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
out.add(Byte2PacketDecoder.getInstance().decode(in));
}
netty中的策略处理器
在上一节中,我们在接收到消息时,需要自己去判断具体的消息类型,使用if…else的方式进行判断,在消息越来越多的时候,if…else也会越来越长,这不是一个好的编码方式,例如
if (packet instanceof LoginRequestMessage) {
...
} else if (packet instanceof TextMessageRequestMessage) {
...
} else if (packet instanceof ImageMessageRequestMessage) {
...
} else if (packet instanceof GroupRequestMessage) {
...
} else if (packet instanceof XxxRequestMessage) {
...
} else {
...
}
这样的编码方式不仅会把代码拉得很长,每一个都需要判断,而且这也是一个我们在实现业务逻辑的时候不太想去关注的一个问题,因此netty实现了一个SimpleChannelInboundHandler,它实现了消息类型的判断以及把对应事件进行传播到处理对应消息类型的handler中,我们只需要在对应的handler中实现自己的业务逻辑即可。实现代码
public class NettyServerLoginHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage message) {
}
}
public class NettyServerUnknownHandler extends SimpleChannelInboundHandler<BasePacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, BasePacket msg) {
}
}
具体的demo
基于上述三种类型的处理器,我们重新实现一遍上一节的登录以及提醒用户上线消息的代码
服务端
@Slf4j
public class NettyServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
serverBootstrap.group(bossGroup, workerGroup)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new PacketDecoder());
ch.pipeline().addLast(new NettyServerLoginHandler());
ch.pipeline().addLast(new NettyServerUnknownHandler());
ch.pipeline().addLast(new PacketEncoder());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
log.error("启动服务器失败", e);
} finally {
// 优雅关闭
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
我们看到,在decoder和encoder这两个类中间对应着两个不同类型的handler,看下具体实现
NettyServerLoginHandler.java
@Slf4j
public class NettyServerLoginHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage message) {
log.info("用户:{}, ID:{} 链接服务器", message.getUsername(), message.getUserId());
LoginResponseMessage loginResponseMessage = new LoginResponseMessage();
loginResponseMessage.setSuccess(true);
ctx.channel().writeAndFlush(loginResponseMessage);
// 保存channel自定义属性
ctx.channel().attr(AttributeKey.newInstance("login")).set(message.getUserId());
// 保存已登录的channel
ChannelGroup channelGroup = new DefaultChannelGroup(ctx.executor());
channelGroup.add(ctx.channel());
OnlineMessage responseMessage = new OnlineMessage(message.getUserId());
// 通知所有用户有用户上线
channelGroup.writeAndFlush(responseMessage);
}
}
NettyServerUnknownHandler.java
@Slf4j
public class NettyServerUnknownHandler extends SimpleChannelInboundHandler<BasePacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, BasePacket msg) {
log.info("未知消息,消息体: {}", msg);
}
}
这两个handler都集成了SimpleChannelInboundHandler类,以及实现了其中的 channelRead0() 方法,该方法中只需要实现对应的业务逻辑即可,大大简化了我们的开发以及减少了不必要的if else
由此可见,服务端的流程为
客户端
NettyClient.java
@Slf4j
public class NettyClient {
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
try {
bootstrap.group(nioEventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new PacketDecoder());
ch.pipeline().addLast(new NettyClientLoginHandler());
ch.pipeline().addLast(new NettyClientOnlineHandler());
ch.pipeline().addLast(new NettyClientUnknowHandler());
ch.pipeline().addLast(new PacketEncoder());
}
}).connect("127.0.0.1", 8888).addListener(
future -> {
if (future.isSuccess()) {
log.info("链接服务器成功,开始发送登录消息");
LoginRequestMessage message = new LoginRequestMessage();
message.setUserId(1);
message.setUsername("test1");
message.setPassword("password1");
((ChannelFuture) future).channel().writeAndFlush(message);
} else {
System.out.println("链接服务器失败");
System.exit(0);
}
}
).channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
nioEventLoopGroup.shutdownGracefully();
}
}
}
NettyClientLoginHandler.java
@Slf4j
public class NettyClientLoginHandler extends SimpleChannelInboundHandler<LoginResponseMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginResponseMessage msg) throws Exception {
if (!msg.isSuccess()) {
log.info("登录失败,失败原因:{}", msg.getErrMsg());
}
log.info("登录成功");
ctx.channel().attr(AttributeKey.newInstance("login")).set(true);
}
}
NettyClientOnlineHandler.java
@Slf4j
public class NettyClientOnlineHandler extends SimpleChannelInboundHandler<OnlineMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, OnlineMessage msg) throws Exception {
log.info("userId=[{}]上线了", msg.getUserId());
}
}
NettyClientUnknowHandler.java
@Slf4j
public class NettyClientUnknowHandler extends SimpleChannelInboundHandler<BasePacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, BasePacket msg) throws Exception {
log.info("未知消息, 消息: [{}]", msg);
}
}
客户端同样简化了逻辑判断代码,只需要关注具体逻辑实现即可,客户端流程