本篇文章我们将基于Netty几个开箱即用的封装快速落地一个易于拓展与维护的客户端服务端通信示例,希望对你有所帮助。
基于Netty快速落地自定义协议消息通信
1.提出需求
我们需要使用Netty快速落地一套客户端和服务端系统通信程序,客户端会在与服务端建立连接后发送自定义协议的登录包,然后服务端完成校验之后返回自定义协议的登录处理结果:
2.服务端设计与实现
按照我们以往的处理器逻辑,对于服务端我们可能会编写一个处理器handler,其内部负责:
- 对收到的数据包解码。
- 根据数据包类型走不同的if-else逻辑。
- 回复相应的加密后的数据包。
这种做法将编码、解码、数据逻辑全部耦合在一个处理器上,违背了单一职责的设计,导致代码臃肿,后续的功能的拓展和维护都十分不便。
对此本文做法是针对不同数据包指定相应处理器,通过pipeline自带的责任链模式将这些处理器串联起来,并将编码和解码的handler单独抽离出来维护:
因为客户端会向服务端发送登录包,对应文件编码规则为:
- 第一个整形位,设置为登录包类型为1。
- 第二个整型为设置为登录包数据长度。
- 第三个字节数组设置为序列化后的数据包。
所以我们解码的逻辑为:
- 获取4个字节知晓类型。
- 获取4个字节解析长度。
- 读取对应长度的字节数组将其反序列化为对应类型的数据包。
而Netty也为我们解码的逻辑提供了一个类MessageToMessageDecoder,我们只需继承并重写其decode方法,将bytebuf解码后的结果传入out列表中即可传播到对应的处理器上:
对此我们给出解码器的处理器Handler的逻辑,可以看到该解码器会按照编码的要求进行解析:
public class ServerDecodeHandler extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
//获取消息类型
int type = msg.readInt();
if (type == 1) {
//获取实际消息长度
int length = msg.readInt();
//读取数据并反序列化
byte[] data = new byte[length];
msg.readBytes(data);
LoginPacket loginPacket = JSON.parseObject(data, LoginPacket.class);
out.add(loginPacket);
}
}
}
消息被解码器解码之后,就可以传播到对应业务处理器上,为了保证读取到不同的消息被不同业务处理器处理,Netty提供了一个开箱即用的读消息处理器,它会根据我们的指定的泛型为数据包进行匹配,只有与泛型类一致才会进行处理:
所以我们的认证处理器AuthHandler 继承SimpleChannelInboundHandler并指明泛型LoginPacket专门处理读取到的LoginPacket:
public class AuthHandler extends SimpleChannelInboundHandler<LoginPacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginPacket msg) throws Exception {
//如果用户名和密码一致则通过loginResp发送一个hello包,反之回复发送失败
if (msg.getUserName().equals("user") && msg.getPassword().equals("123456")) {
LoginRespPacket loginRespPacket = new LoginRespPacket();
loginRespPacket.setType(2);
loginRespPacket.setMessage("hello netty client");
ctx.writeAndFlush(loginRespPacket);
} else {
LoginRespPacket loginRespPacket = new LoginRespPacket();
loginRespPacket.setType(2);
loginRespPacket.setMessage("login failed");
ctx.writeAndFlush(loginRespPacket);
}
}
}
该处理器匹配消息包的逻辑我们可以通过源码进行简单介绍,当解码后的数据包通过pipeline传播来到AuthHandler 时,它会调用继承自SimpleChannelInboundHandler的channelRead方法并通过acceptInboundMessage查看这个消息类型和泛型是否一致,如果一致则会调用channelRead0最终回调到我们的channelRead0方法,而且相较于channelHandler,我们的SimpleChannelInboundHandler还会在finally语句自动按需检查并释放bytebuf内存:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
//判断当前消息类类型和指明的泛型是否匹配
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
//如果匹配则直接调用我们重写的channelRead0
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
//调用结束后还会检查按需释放bytebuf内存
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
认证处理器确定登录包正确,则发送loginResp响应,对应的数据包也需要按照类型、长度、序列化包字符串的格式进行编码,所以我们还需要编写一个编码器,同理我们还是使用Netty开箱即用的MessageToByteEncoder将编码后数据写到out这个bytebuf中:
public class ServerEncodeHandler extends MessageToByteEncoder<Packet> {
@Override
protected void encode(ChannelHandlerContext ctx, Packet msg, ByteBuf out) throws Exception { //如果是Resp类型,则依次写入类型、长度、序列化包到ByteBuf中
if (msg.getType() == 2) {
LoginRespPacket loginRespPacket = (LoginRespPacket) msg;
out.writeInt(loginRespPacket.getType());
byte[] bytes = JSON.toJSONBytes(loginRespPacket);
out.writeInt(bytes.length);
out.writeBytes(bytes);
}
}
}
3.客户端设计与实现
而客户端也和上文类型,我们先编写一个连接激活后发送登录包的处理器:
public class LoginHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
LoginPacket loginPacket = new LoginPacket();
loginPacket.setUserName("user");
loginPacket.setPassword("123456");
ctx.writeAndFlush(loginPacket).
}
}
然后就是编码器,同样是继承MessageToByteEncoder实现:
public class ClientEncodeHandler extends MessageToByteEncoder<Packet> {
@Override
protected void encode(ChannelHandlerContext ctx, Packet msg, ByteBuf out) throws Exception {
//按照类型、长度、序列化包进行编码
if (msg.getType() == 1) {
LoginPacket loginPacket = (LoginPacket) msg;
byte[] jsonBytes = JSON.toJSONBytes(loginPacket);
out.writeInt(msg.getType());
out.writeInt(jsonBytes.length);
out.writeBytes(jsonBytes);
}
}
}
收到包后,根据第一个整型字节匹配到LoginRespPacket,将其解码为LoginRespPacket:
public class ClientDecodeHandler extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
int type = msg.readInt();
//如果type为2则说明是loginResp,按照类型、长度、反序列化包处理器
if (type == 2) {
int length = msg.readInt();
byte[] data = new byte[length];
msg.readBytes(data);
LoginRespPacket loginRespPacket = JSON.parseObject(data, LoginRespPacket.class);
out.add(loginRespPacket);
}
}
}
最终传播到LoginRespHandler打印输出:
public class LoginRespHandler extends SimpleChannelInboundHandler<LoginRespPacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRespPacket msg) throws Exception {
System.out.println(JSONUtil.toJsonStr(msg));
}
}
4.最终成果验收
按照上述解耦的处理器完成开发之后,我们分别启动服务端和客户端,最终客户端就会得到如下输出:
{"message":"hello netty client","type":2}
由此基于Netty开箱即用的客户端服务端通信模型完成。