Netty服务端(采集服务)发送消息(指令)到客户端(设备、上位机)
一般情况下,客户端只会主动向服务端发送心跳和实时数据,其他消息,如指令等需要服务端主动发送到上位机
实施方案:
-
服务端和客户端第一次建立连接的时候,将Channel保存到ConcurrentHashMap或者Redis中(保证线程安全);
-
服务端发送消息到客户端时,通过设备标识从ConcurrentHashMap或者Redis中获取目标客户端的Channel;
-
判断Channel是否存活,存活则发送消息到客户端,非存活从ConcurrentHashMap或者Redis中删除Channel;
-
服务端将消息发送到客户端后,服务端能正常接收到客户端回写的消息即可。
本文以将Channel保存到ConcurrentHashMap中为例
新建一个ChannelMap类,在客户端第一次连接时保存 Channel连接。后续服务端向客户端发送消息时,先从此Map中找到对应的客户端Channel,再向Channel中写入消息发送至客户端。
/**
* 使用静态内部类来实现懒加载的单例模式,因为JVM在加载静态内部类时会保证线程安全
*/
public class ChannelMap {
private static class ChannelMapHolder {
// ChannelMap存储客户端的ID标识与通道的对应关系
private static final ConcurrentHashMap<String, Channel> INSTANCE = new ConcurrentHashMap<>();
}
public static ConcurrentHashMap<String, Channel> getChannelMap() {
return ChannelMapHolder.INSTANCE;
}
public static Channel getChannel(String id) {
return getChannelMap().get(id);
}
}
服务端收到客户端心跳时,将Channel加入channelMap中
public class NettyServerHandler extends SimpleChannelInboundHandler<Message> {
private static final Logger log = LoggerFactory.getLogger(NettyServerHandler.class);
/**
* 客户端连接服务器时触发
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
log.info("有新的连接:[{}]", ctx.channel().id().asLongText());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Message msg) {
// 获取消息实例中的消息体
String content = msg.getContent();
// 对不同消息类型进行处理
MessageEnum type = MessageEnum.getStructureEnum(msg);
switch (type) {
case CONNECT:
// 将通道加入ChannelMap
ChannelMap.getChannelMap().put(msg.getId(), ctx.channel());
// 将客户端ID作为自定义属性加入到channel中,方便随时channel中获取用户ID
ctx.channel().attr(AttributeKey.valueOf("id")).setIfAbsent(msg.getId());
// TODO 对心跳消息的处理
case STATE:
// TODO 对设备状态消息的处理
default:
log.info("[{}]消息内容:[{}]", type.content, content);
}
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
log.info("{}设备已下线", ctx.channel().id().asLongText());
// map中移除channel
removeId(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 打印异常
log.info("异常:{}", cause.getMessage());
// map中移除channel
removeId(ctx);
// 关闭连接
ctx.close();
}
private void removeId(ChannelHandlerContext ctx) {
// 获取channel中id
String id = ctx.channel().attr(AttributeKey.valueOf("id")).get();
// map移除channel
ChannelMap.getChannelMap().remove(id);
}
}
新建一个服务端发送消息的业务类,并通过客户端id在map中获取到channel通道,将消息转化成JSON后,通过writeAndFlush
发送至客户端
@Service
public class PushMsgServiceImpl implements PushMsgService {
@Override
public void push(Message msg) {
// 客户端ID
Channel channel = ChannelMap.getChannel(msg.getId());
if (ObjectUtils.isNotEmpty(channel) && channel.isActive()) {
try {
channel.writeAndFlush(msg);
} catch(Exception e) {
// TODO 对于消息发送失败的处理
}
}
throw new BizException(“客户端已断开连接,消息发送失败”);
}
}
writeAndFlush
()参数是自定义编码的泛型对象实例。如自定义的Message
消息解析类
public class MessageEncodeHandler extends MessageToByteEncoder<Message> {
private static String delimiter;
public MessageEncodeHandler(String delimiter) {
this.delimiter = delimiter;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf out) throws Exception {
out.writeBytes((message.toJsonString() + delimiter).getBytes(CharsetUtil.UTF_8));
}
}
当客户端与服务端建立连接并交换通信时,整个流程如下:
-
客户端与服务端建立连接:
- 客户端与服务端建立TCP连接,客户端连接到服务端的Netty服务器。
- 在服务端,有一个Netty服务器应用程序,它监听并接受客户端连接请求。
-
客户端发送连接消息:
- 客户端首次连接到服务端时,通常会发送一个特定类型的消息,例如连接消息或握手消息。
- 在服务端,Netty服务器的
NettyServerHandler
接收并处理连接消息。这个处理程序会将客户端的Channel
与客户端的唯一标识(如设备ID)相关联,并将Channel
添加到ChannelMap
中,以便以后可以通过设备ID查找到对应的Channel
。
-
客户端发送心跳和数据消息:
- 一旦连接建立,客户端可以定期发送心跳消息以保持连接的活跃性。
- 此外,客户端可以发送实时数据消息或其他类型的消息,服务端的
NettyServerHandler
会处理这些消息。
-
服务端向客户端发送消息:
- 当服务端需要向客户端发送消息,例如指令或通知,它会调用
PushMsgService
的pushMessageToClient
方法。 - 这个方法会从
ChannelMap
中获取客户端的Channel
。 - 如果
Channel
存在且处于活动状态,服务端可以使用writeAndFlush
方法将消息写入Channel
,这会将消息发送到客户端。
- 当服务端需要向客户端发送消息,例如指令或通知,它会调用
-
客户端接收消息:
- 客户端的Netty应用程序会处理从服务端接收到的消息,根据消息类型执行相应的操作,如处理指令、显示通知等。
-
客户端维持连接:
- 客户端可以定期发送心跳消息,以确保连接保持活跃。
- 如果客户端或服务端检测到连接异常,可以采取适当的措施,如重新连接或关闭连接。
-
客户端断开连接:
- 当客户端主动断开连接或发生异常时,客户端的Netty应用程序通常会关闭
Channel
,并在服务端的NettyServerHandler
中将客户端从ChannelMap
中移除。
- 当客户端主动断开连接或发生异常时,客户端的Netty应用程序通常会关闭