netty之微信-实现客户端与服务端收发消息(十一)

实战:实现客户端与服务端收发消息

这一小节,我们来实现客户端与服务端收发消息,我们要实现的具体功能是:在控制台输入一条消息之后按回车,校验完客户端的登录状态之后,把消息发送到服务端,服务端收到消息之后打印并且向客户端发送一条消息,客户端收到之后打印。

收发消息对象

首先,我们来定义一下客户端与服务端的收发消息对象,我们把客户端发送至服务端的消息对象定义为 MessageRequestPacket

@Data
public class MessageRequestPacket extends Packet {

    private String message;

    @Override
    public Byte getCommand() {
        return MESSAGE_REQUEST;
    }
}

指令为 MESSAGE_REQUEST = 3

我们把服务端发送至客户端的消息对象定义为 MessageResponsePacket

@Data
public class MessageResponsePacket extends Packet {

    private String message;

    @Override
    public Byte getCommand() {

        return MESSAGE_RESPONSE;
    }
}

指令为 MESSAGE_RESPONSE = 4

至此,我们的指令已经有如下四种

public interface Command {

    Byte LOGIN_REQUEST = 1;

    Byte LOGIN_RESPONSE = 2;

    Byte MESSAGE_REQUEST = 3;

    Byte MESSAGE_RESPONSE = 4;
}

判断客户端是否登录成功

前面一小节,我们在文末出了一道思考题:如何判断客户端是否已经登录?

客户端启动流程这一章节,我们有提到可以给客户端连接,也就是 Channel 绑定属性,通过 channel.attr(xxx).set(xx) 的方式,那么我们是否可以在登录成功之后,给 Channel 绑定一个登录成功的标志位,然后判断是否登录成功的时候取出这个标志位就可以了呢?答案是肯定的

我们先来定义一下是否登录成功的标志位

public interface Attributes {
    AttributeKey<Boolean> LOGIN = AttributeKey.newInstance("login");
}

然后,我们在客户端登录成功之后,给客户端绑定登录成功的标志位

ClientHandler.java

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    // ...
        if (loginResponsePacket.isSuccess()) {
            LoginUtil.markAsLogin(ctx.channel());
            System.out.println(new Date() + ": 客户端登录成功");
        } else {
            System.out.println(new Date() + ": 客户端登录失败,原因:" + loginResponsePacket.getReason());
        }
    // ...
}

这里,我们省去了非关键代码部分

public class LoginUtil {
    public static void markAsLogin(Channel channel) {
        channel.attr(Attributes.LOGIN).set(true);
    }

    public static boolean hasLogin(Channel channel) {
        Attribute<Boolean> loginAttr = channel.attr(Attributes.LOGIN);

        return loginAttr.get() != null;
    }
}

如上所示,我们抽取出 LoginUtil 用于设置登录标志位以及判断是否有标志位,如果有标志位,不管标志位的值是什么,都表示已经成功登录过,接下来,我们来实现控制台输入消息并发送至服务端。

控制台输入消息并发送

客户端启动这小节中,我们已经学到了客户端的启动流程,现在,我们在客户端连接上服务端之后启动控制台线程,从控制台获取消息,然后发送至服务端

NettyClient.java


private static void connect(Bootstrap bootstrap, String host, int port, int retry) {
    bootstrap.connect(host, port).addListener(future -> {
        if (future.isSuccess()) {
            Channel channel = ((ChannelFuture) future).channel();
            // 连接成功之后,启动控制台线程
            startConsoleThread(channel);
        } 
        // ...
    });
}

private static void startConsoleThread(Channel channel) {
    new Thread(() -> {
        while (!Thread.interrupted()) {
            if (LoginUtil.hasLogin(channel)) {
                System.out.println("输入消息发送至服务端: ");
                Scanner sc = new Scanner(System.in);
                String line = sc.nextLine();
                
                MessageRequestPacket packet = new MessageRequestPacket();
                packet.setMessage(line);
                ByteBuf byteBuf = PacketCodeC.INSTANCE.encode(channel.alloc(), packet);
                channel.writeAndFlush(byteBuf);
            }
        }
    }).start();
}

这里,我们省略了非关键代码,连接成功之后,我们调用 startConsoleThread() 开始启动控制台线程,然后在控制台线程中,判断只要当前 channel 是登录状态,就允许控制台输入消息。

从控制台获取消息之后,将消息封装成消息对象,然后将消息编码成 ByteBuf,最后通过 writeAndFlush() 将消息写到服务端,这个过程相信大家在学习了上小节的内容之后,应该不会太陌生。接下来,我们来看一下服务端收到消息之后是如何来处理的。

服务端收发消息处理

ServerHandler.java

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf requestByteBuf = (ByteBuf) msg;

    Packet packet = PacketCodeC.INSTANCE.decode(requestByteBuf);

    if (packet instanceof LoginRequestPacket) {
        // 处理登录..
    } else if (packet instanceof MessageRequestPacket) {
        // 处理消息
        MessageRequestPacket messageRequestPacket = ((MessageRequestPacket) packet);
        System.out.println(new Date() + ": 收到客户端消息: " + messageRequestPacket.getMessage());

        MessageResponsePacket messageResponsePacket = new MessageResponsePacket();
        messageResponsePacket.setMessage("服务端回复【" + messageRequestPacket.getMessage() + "】");
        ByteBuf responseByteBuf = PacketCodeC.INSTANCE.encode(ctx.alloc(), messageResponsePacket);
        ctx.channel().writeAndFlush(responseByteBuf);
    }
}

服务端在收到消息之后,仍然是回调到 channelRead() 方法,解码之后用一个 else 分支进入消息处理的流程。

首先,服务端将收到的消息打印到控制台,然后封装一个消息响应对象 MessageResponsePacket,接下来还是老样子,先编码成 ByteBuf,然后调用 writeAndFlush() 将数据写到客户端,最后,我们再来看一下客户端收到消息的逻辑。

客户端收消息处理

ClientHandler.java

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf byteBuf = (ByteBuf) msg;

    Packet packet = PacketCodeC.INSTANCE.decode(byteBuf);

    if (packet instanceof LoginResponsePacket) {
        // 登录逻辑...
    } else if (packet instanceof MessageResponsePacket) {
        MessageResponsePacket messageResponsePacket = (MessageResponsePacket) packet;
        System.out.println(new Date() + ": 收到服务端的消息: " + messageResponsePacket.getMessage());
    }
}

客户端在收到消息之后,回调到 channelRead() 方法,仍然用一个 else 逻辑进入到消息处理的逻辑,这里我们仅仅是简单地打印出消息,最后,我们再来看一下服务端和客户端的运行效果

完整的代码参考 github, 分别启动 NettyServer.java 与 NettyClient.java 即可看到效果。

控制台输出

客户端

 

image.png

 

 

服务端

 

image.png

 

 

总结

在本小节中

  1. 我们定义了收发消息的 Java 对象进行消息的收发。
  2. 然后我们学到了 channel 的 attr() 的实际用法:可以通过给 channel 绑定属性来设置某些状态,获取某些状态,不需要额外的 map 来维持。
  3. 接着,我们学习了如何在控制台获取消息并且发送至服务端。
  4. 最后,我们实现了服务端回消息,客户端响应的逻辑,可以看到,这里的部分实际上和前面一小节的登录流程有点类似。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值