Netty实现websocket
项目中有一个需求,就是能够让服务端主动向客户端发送通知信息,大致过程如下
门柜设备开门成功 -》返回开门成功信息给服务端 -》服务端返回ACK确认 -》服务端同时通知小程序用户开锁成功信息…
由于需要服务端主动向客户端发送信息,这里可以使用websocket来实现(WebSocket相关文章链接),下面就来实现一个基于websocket的服务器的基本建立过程(这里使用的时Netty,其基于NIO,有许多优点,这是我另一篇关于Netty的博客链接)
思路:
- 先建立起Netty服务器的基本架构
- 设计一个GlobalChannel来全局管理服务端与客户端之间的channel通信
- 客户端在首次访问页面时发起对服务端请求,并建立起channel,通过userId与自己的channel绑定,服务端在通知时可以根据userId找到对应的channel识别用户
代码:
Netty服务器搭建
@Service("nettyWebsocketService")
public class NettyWebsocketServiceImpl implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(NettyWebsocketServiceImpl.class);
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private SystemConfig systemConfig;
@Autowired
private MessageQueueService messageQueueService;
public static void main(String[] args) {
new NettyWebsocketServiceImpl().run();
}
@PostConstruct
public void initNetty(){
// threadPoolTaskExecutor.execute(new NettyWebsocketServiceImpl());
threadPoolTaskExecutor.execute(() -> run());
}
public void run(){
if (SystemTypeEnum.Linux != SystemTypeEnum.getSystem()) {
logger.info("只能在Linux服务器中执行监听");
//return;
}
while(!ApplicationContextListener.isStrartUp) {
try {
logger.info("Spring还在加载中,等候一秒后再试。");
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.info("", e);
}
}
logger.info("===========================Netty端口启动========");
// Boss线程:由这个线程池提供的线程是boss种类的,用于创建、连接、绑定socket, (有点像门卫)然后把这些socket传给worker线程池。
// 在服务器端每个监听的socket都有一个boss线程来处理。在客户端,只有一个boss线程来处理所有的socket。
EventLoopGroup bossGroup = new NioEventLoopGroup();
// Worker线程:Worker线程执行所有的异步I/O,即处理操作
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
// ServerBootstrap 启动NIO服务的辅助启动类,负责初始话netty服务器,并且开始监听端口的socket请求
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup);
// 设置非阻塞,用它来建立新accept的连接,用于构造serversocketchannel的工厂类
b.channel(NioServerSocketChannel.class);
// ChildChannelHandler 对出入的数据进行的业务操作,其继承ChannelInitializer
b.childHandler(new ChildChannelHandler(messageQueueService));
logger.info("服务端开启等待客户端连接 ... ...");
int wssPort = 8888;
logger.info("端口号:" + wssPort);
Channel ch = b.bind(wssPort).sync().channel();
ch.closeFuture().sync();
} catch (Exception e) {
logger.info("", e);
}finally{
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
channel处理器:
public class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
private static final Logger logger = LoggerFactory.getLogger(GlobalChannel.class);
MessageQueueService messageQueueService;
public ChildChannelHandler(MessageQueueService messageQueueService) {
this.messageQueueService = messageQueueService;
}
@Override
protected void initChannel(SocketChannel e) throws Exception {
logger.info("收到SocketChannel,现在进行channel初始化.");
// 设置30秒没有读到数据,则触发一个READER_IDLE事件。
e.pipeline().addLast(new IdleStateHandler(30, 0, 0));
// HttpServerCodec:将请求和应答消息解码为HTTP消息
e.pipeline().addLast("http-codec",new HttpServerCodec());
// HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息
e.pipeline().addLast("aggregator",new HttpObjectAggregator(65536));
// ChunkedWriteHandler:向客户端发送HTML5文件
e.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
// 在管道中添加我们自己的接收数据实现方法
e.pipeline().addLast("handler",new MyWebSocketServerHandler(messageQueueService));
}
}
处理数据的方法类:
public class MyWebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
private static final Logger logger = LoggerFactory.getLogger(WebSocketServerHandshaker.class);
private MessageQueueService messageQueueService;
private WebSocketServerHandshaker handshaker;
public MyWebSocketServerHandler(MessageQueueService messageQueueService) {
this.messageQueueService = messageQueueService;
}
/**
* channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 添加
String remoteAddress = ctx.channel().remoteAddress().toString();
logger.info("客户端与服务端连接开启:" + remoteAddress);
//当发生活跃链接时,注册到group中!!
GlobalChannel.group.add(ctx.channel());
}
/**
* channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端关闭了通信通道并且不可以传输数据
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 移除
Channel channel = ctx.channel();
GlobalChannel.group.remove(channel);
Long weixinUserId = GlobalChannel.weixinUserMap.get(channel.id());
GlobalChannel.channelMap.remove(weixinUserId);
GlobalChannel.weixinUserMap.remove(channel.id());
logger.info("客户端与服务端连接关闭:" + ctx.channel().remoteAddress().toString());
}
/**
* channel 通道 Read 读取 Complete 完成 在通道读取完成后会在这个方法里通知,对应可以做刷新操作 ctx.flush()
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
//这里可以根据需要自定义处理方法...
//标记建立连接的用户,通过这个标记来实现识别通知!
if (request != null) {
NettyMessageTypeEnum messageTypeEnum = NettyMessageTypeEnum.valueOf(request.getMessageType());
switch (messageTypeEnum) {
case 标记用户:
Long weixinUserId = request.getData().getLongValue("userId");
GlobalChannel.channelMap.put(weixinUserId, ctx.channel().id());
GlobalChannel.weixinUserMap.put(ctx.channel().id(), weixinUserId);
break;
case 测试回复:
weixinUserId = request.getData().getLongValue("userId");
Boolean close = request.getData().getBoolean("close");
NettyResponse response = new NettyResponse();
response.setMessageType(NettyMessageTypeEnum.开锁成功.getType());
Map<String, Object> result = new HashMap<>();
result.put("当前时间:", new Date().toString());
response.setData(result);
response.setWeixinUserId(weixinUserId);
try {
messageQueueService.sendWebSocket(response);
if (close != null && close) {
Channel channel = GlobalChannel.getChannel(response);
channel.close();
logger.info("关闭ws:" + response.getWeixinUserId());
}
} catch (Exception e) {
e.printStackTrace();
}
break;
default:
break;
}
}
}
GlobalChannel设计:
public class GlobalChannel {
private static final Logger logger = LoggerFactory.getLogger(GlobalChannel.class);
public static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
public static Map<Long, ChannelId> channelMap = new HashMap<>();
public static Map<ChannelId, Long> weixinUserMap = new HashMap<>();
public static Channel getChannel(NettyResponse response) {
ChannelId channelId = channelMap.get(response.getWeixinUserId());
return group.find(channelId);
}
public static void sendMessage(NettyResponse response) {
ChannelId channelId = channelMap.get(response.getWeixinUserId());
if (channelId == null) {
logger.info("IotMessageQueueListener 向用户[" + response.getWeixinUserId() + "]发送消息:[" + response.getMessageType() + "]但是该用户未连接");
return;
}
Channel channel = group.find(channelId);
if (channel == null) {
logger.info("IotMessageQueueListener 向用户[" + response.getWeixinUserId() + "]发送消息:[" + response.getMessageType() + "]但是Channel不存在");
} else {
TextWebSocketFrame tws = new TextWebSocketFrame(response.toJSONString());
channel.writeAndFlush(tws);//发送
logger.info("IotMessageQueueListener 向用户[" + response.getWeixinUserId() + "]发送消息:[" + response.getMessageType() + "]SUCC");
}
return;
}
}
大致过程如上所示,这里主要的设计时GlobalChannel关于用户与channel建立关系的方式,这里通过静态的HashMap进行关系映射,用户在第一次访问服务器时(可通过前端来实现websocket的建立),已经标记在服务器中,服务器再通过客户端用户的id准确地发出通知,实现了又服务端主动向客户端发送信息的效果。
补充:
这里为了达到解耦,可以在服务端向客户端发送消息中间加一个MQ来实现,当需要发送消息时,由服务端发送消息到消息队列中(生产者),然后启动一个监听器,来监听队列中是否出现消息,然后进行处理发送个客户端(消费者)。