实战:Netty 实现客户端登录
本小节,我们来实现客户端登录到服务端的过程
登录流程
从上图中我们可以看到,客户端连接上服务端之后
- 客户端会构建一个登录请求对象,然后通过编码把请求对象编码为 ByteBuf,写到服务端。
- 服务端接受到 ByteBuf 之后,首先通过解码把 ByteBuf 解码为登录请求响应,然后进行校验。
- 服务端校验通过之后,构造一个登录响应对象,依然经过编码,然后再写回到客户端。
- 客户端接收到服务端的之后,解码 ByteBuf,拿到登录响应响应,判断是否登陆成功
逻辑处理器
接下来,我们分别实现一下上述四个过程,开始之前,我们先来回顾一下客户端与服务端的启动流程,客户端启动的时候,我们会在引导类 Bootstrap 中配置客户端的处理逻辑,本小节中,我们给客户端配置的逻辑处理器叫做 ClientHandler
public class ClientHandler extends ChannelInboundHandlerAdapter {
}
然后,客户端启动的时候,我们给 Bootstrap 配置上这个逻辑处理器
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new ClientHandler());
}
});
这样,在客户端侧,Netty 中 IO 事件相关的回调就能够回调到我们的 ClientHandler。
同样,我们给服务端引导类 ServerBootstrap 也配置一个逻辑处理器 ServerHandler
public class ServerHandler extends ChannelInboundHandlerAdapter {
}
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new ServerHandler());
}
}
这样,在服务端侧,Netty 中 IO 事件相关的回调就能够回调到我们的 ServerHandler。
接下来,我们就围绕这两个 Handler 来编写我们的处理逻辑。
客户端发送登录请求
客户端处理登录请求
我们实现在客户端连接上服务端之后,立即登录。在连接上服务端之后,Netty 会回调到 ClientHandler 的 channelActive() 方法,我们在这个方法体里面编写相应的逻辑
ClientHandler.java
public void channelActive(ChannelHandlerContext ctx) {
System.out.println(new Date() + ": 客户端开始登录");
// 创建登录对象
LoginRequestPacket loginRequestPacket = new LoginRequestPacket();
loginRequestPacket.setUserId(UUID.randomUUID().toString());
loginRequestPacket.setUsername("flash");
loginRequestPacket.setPassword("pwd");
// 编码
ByteBuf buffer = PacketCodeC.INSTANCE.encode(ctx.alloc(), loginRequestPacket);
// 写数据
ctx.channel().writeAndFlush(buffer);
}
这里,我们按照前面所描述的三个步骤来分别实现,在编码的环节,我们把 PacketCodeC 变成单例模式,然后把 ByteBuf 分配器抽取出一个参数,这里第一个实参 ctx.alloc() 获取的就是与当前连接相关的 ByteBuf 分配器,建议这样来使用。
写数据的时候,我们通过 ctx.channel() 获取到当前连接(Netty 对连接的抽象为 Channel,后面小节会分析),然后调用 writeAndFlush() 就能把二进制数据写到服务端。这样,客户端发送登录请求的逻辑就完成了,接下来,我们来看一下,服务端接受到这个数据之后是如何来处理的。
服务端处理登录请求
ServerHandler.java
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf requestByteBuf = (ByteBuf) msg;
// 解码
Packet packet = PacketCodeC.INSTANCE.decode(requestByteBuf);
// 判断是否是登录请求数据包
if (packet instanceof LoginRequestPacket) {
LoginRequestPacket loginRequestPacket = (LoginRequestPacket) packet;
// 登录校验
if (valid(loginRequestPacket)) {
// 校验成功
} else {
// 校验失败
}
}
}
private boolean valid(LoginRequestPacket loginRequestPacket) {
return true;
}
我们向服务端引导类 ServerBootstrap 中添加了逻辑处理器 ServerHandler 之后,Netty 在收到数据之后,会回调 channelRead() 方法,这里的第二个参数 msg,在我们这个场景中,可以直接强转为 ByteBuf,为什么 Netty 不直接把这个参数类型定义为 ByteBuf ?我们在后续的小节会分析到。
拿到 ByteBuf 之后,首先要做的事情就是解码,解码出 java 数据包对象,然后判断如果是登录请求数据包 LoginRequestPacket,就进行登录逻辑的处理,这里,我们假设所有的登录都是成功的,valid() 方法返回 true。 服务端校验通过之后,接下来就需要向客户端发送登录响应,我们继续编写服务端的逻辑。
服务端发送登录响应
服务端处理登录响应
ServerHandler.java
LoginResponsePacket loginResponsePacket = new LoginResponsePacket();
loginResponsePacket.setVersion(packet.getVersion());
if (valid(loginRequestPacket)) {
loginResponsePacket.setSuccess(true);
} else {
loginResponsePacket.setReason("账号密码校验失败");
loginResponsePacket.setSuccess(false);
}
// 编码
ByteBuf responseByteBuf = PacketCodeC.INSTANCE.encode(ctx.alloc(), loginResponsePacket);
ctx.channel().writeAndFlush(responseByteBuf);
这段逻辑仍然是在服务端逻辑处理器 ClientHandler 的 channelRead() 方法里,我们构造一个登录响应包 LoginResponsePacket,然后在校验成功和失败的时候分别设置标志位,接下来,调用编码器把 Java 对象编码成 ByteBuf,调用 writeAndFlush() 写到客户端,至此,服务端的登录逻辑编写完成,接下来,我们还有最后一步,客户端处理登录响应。
客户端处理登录响应
ClientHandler.java
客户端接收服务端数据的处理逻辑也是在 ClientHandler 的 channelRead() 方法
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf byteBuf = (ByteBuf) msg;
Packet packet = PacketCodeC.INSTANCE.decode(byteBuf);
if (packet instanceof LoginResponsePacket) {
LoginResponsePacket loginResponsePacket = (LoginResponsePacket) packet;
if (loginResponsePacket.isSuccess()) {
System.out.println(new Date() + ": 客户端登录成功");
} else {
System.out.println(new Date() + ": 客户端登录失败,原因:" + loginResponsePacket.getReason());
}
}
}
客户端拿到数据之后,调用 PacketCodeC 进行解码操作,如果类型是登录响应数据包,我们这里逻辑比较简单,在控制台打印出一条消息。
至此,客户端整个登录流程到这里就结束了,这里为了给大家演示,我们的客户端和服务端的处理逻辑较为简单,但是相信大家应该已经掌握了使用 Netty 来做服务端与客户端交互的基本思路,基于这个思路,再运用到实际项目中,并不是难事。
最后,我们再来看一下效果,下面分别是客户端与服务端的控制台输出,完整的代码参考 GitHub, 分别启动 NettyServer.java 与 NettyClient.java 即可看到效果。
服务端
客户端
总结
本小节,我们们梳理了一下客户端登录的基本流程,然后结合上一小节的编解码逻辑,我们使用 Netty 实现了完整的客户端登录流程。