netty之微信-Netty 实现客户端登录(十)

实战:Netty 实现客户端登录

本小节,我们来实现客户端登录到服务端的过程

登录流程

 

image.png

 

 

从上图中我们可以看到,客户端连接上服务端之后

  1. 客户端会构建一个登录请求对象,然后通过编码把请求对象编码为 ByteBuf,写到服务端。
  2. 服务端接受到 ByteBuf 之后,首先通过解码把 ByteBuf 解码为登录请求响应,然后进行校验。
  3. 服务端校验通过之后,构造一个登录响应对象,依然经过编码,然后再写回到客户端。
  4. 客户端接收到服务端的之后,解码 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 即可看到效果。

服务端

 

image.png

 

 

客户端

 

image.png

 

 

总结

本小节,我们们梳理了一下客户端登录的基本流程,然后结合上一小节的编解码逻辑,我们使用 Netty 实现了完整的客户端登录流程。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是使用Netty-Mqtt-Client实现Mqtt客户端发布消息和订阅消息的核心Java代码,带注释说明: ```java import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.*; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttSubscribeMessage; import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; import io.netty.handler.codec.mqtt.MqttMessageBuilders.*; // 创建一个Mqtt客户端类 public class MqttClient { private final String clientId; // 客户端ID private final String serverHost; // 服务器主机名 private final int serverPort; // 服务器端口号 private final String username; // 用户名 private final String password; // 密码 private final int keepAlive; // 心跳间隔时间 private EventLoopGroup group; // Netty线程组 private MqttClientInitializer initializer; // Netty客户端初始化器 private Channel channel; // Netty通道 // 构造方法,初始化Mqtt客户端配置 public MqttClient(String clientId, String serverHost, int serverPort, String username, String password, int keepAlive) { this.clientId = clientId; this.serverHost = serverHost; this.serverPort = serverPort; this.username = username; this.password = password; this.keepAlive = keepAlive; } // 连接服务器 public void connect() { group = new NioEventLoopGroup(); // 创建Netty线程组 initializer = new MqttClientInitializer(clientId, username, password, keepAlive); // 创建Netty客户端初始化器 Bootstrap bootstrap = new Bootstrap(); // 创建Netty客户端启动器 bootstrap.group(group) .channel(NioSocketChannel.class) .remoteAddress(serverHost, serverPort) .handler(initializer); try { ChannelFuture future = bootstrap.connect().sync(); // 连接服务器,同步等待连接完成 if (future.isSuccess()) { // 连接成功 channel = future.channel(); // 获取Netty通道 } } catch (InterruptedException e) { e.printStackTrace(); } } // 断开连接 public void disconnect() { if (channel != null && channel.isActive()) { channel.close(); // 关闭Netty通道 } if (group != null) { group.shutdownGracefully(); // 关闭Netty线程组 } } // 发布消息 public void publish(String topic, String message, MqttQoS qos) { MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, false, 0); MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topic, 0); ByteBuf payload = Unpooled.buffer(); payload.writeBytes(message.getBytes()); MqttPublishMessage publishMessage = new MqttPublishMessage(header, variableHeader, payload); channel.writeAndFlush(publishMessage); // 发送Mqtt PUBLISH消息 } // 订阅主题 public void subscribe(String topic, MqttQoS qos) { MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.SUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(1); MqttTopicSubscription topicSubscription = new MqttTopicSubscription(topic, qos); MqttSubscribePayload payload = new MqttSubscribePayload(Arrays.asList(topicSubscription)); MqttSubscribeMessage subscribeMessage = new MqttSubscribeMessage(header, variableHeader, payload); channel.writeAndFlush(subscribeMessage); // 发送Mqtt SUBSCRIBE消息 } // 取消订阅主题 public void unsubscribe(String topic) { MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(1); MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Arrays.asList(topic)); MqttUnsubscribeMessage unsubscribeMessage = new MqttUnsubscribeMessage(header, variableHeader, payload); channel.writeAndFlush(unsubscribeMessage); // 发送Mqtt UNSUBSCRIBE消息 } } ``` 以上代码中,我们创建了一个MqttClient类,该类通过Netty-Mqtt-Client实现了Mqtt客户端发布消息和订阅消息的功能。具体实现细节如下: - connect()方法:连接Mqtt服务器,其中我们通过Netty创建了一个NioEventLoopGroup线程组、一个MqttClientInitializer客户端初始化器和一个Bootstrap客户端启动器,并将它们配置好后发起连接请求; - disconnect()方法:断开Mqtt服务器连接,关闭Netty通道和线程组; - publish()方法:发布Mqtt消息,其中我们使用了MqttFixedHeader、MqttPublishVariableHeader、ByteBuf和MqttPublishMessage等Netty-Mqtt-Client提供的类来构建Mqtt PUBLISH消息,并通过Netty通道将其发送给服务器; - subscribe()方法:订阅Mqtt主题,其中我们使用了MqttFixedHeader、MqttMessageIdVariableHeader、MqttTopicSubscription、MqttSubscribePayload和MqttSubscribeMessage等Netty-Mqtt-Client提供的类来构建Mqtt SUBSCRIBE消息,并通过Netty通道将其发送给服务器; - unsubscribe()方法:取消订阅Mqtt主题,其中我们使用了MqttFixedHeader、MqttMessageIdVariableHeader、MqttUnsubscribePayload和MqttUnsubscribeMessage等Netty-Mqtt-Client提供的类来构建Mqtt UNSUBSCRIBE消息,并通过Netty通道将其发送给服务器。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值