热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

SpringBoot+WebSocket+Netty实现消息推送的示例代码

这篇文章主要介绍了SpringBoot+WebSocket+Netty实现消息推送的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

上一篇文章讲了Netty的理论基础,这一篇讲一下Netty在项目中的应用场景之一:消息推送功能,可以满足给所有用户推送,也可以满足给指定某一个用户推送消息,创建的是SpringBoot项目,后台服务端使用Netty技术,前端页面使用WebSocket技术。

大概实现思路:

  • 前端使用webSocket与服务端创建连接的时候,将用户ID传给服务端
  • 服务端将用户ID与channel关联起来存储,同时将channel放入到channel组中
  • 如果需要给所有用户发送消息,直接执行channel组的writeAndFlush()方法
  • 如果需要给指定用户发送消息,根据用户ID查询到对应的channel,然后执行writeAndFlush()方法
  • 前端获取到服务端推送的消息之后,将消息内容展示到文本域中

下面是具体的代码实现,基本上每一步操作都配有注释说明,配合注释看应该还是比较容易理解的。

第零步:引入Netty的依赖,和一个工具包(只用到了json工具,可用其他json工具代替)


 io.netty
 netty-all 
 4.1.33.Final



 cn.hutool
 hutool-all
 5.2.3


第一步:在NettyConfig中定义一个channel组,管理所有的channel,再定义一个map,管理用户与channel的对应关系。

package com.sixj.nettypush.config;

import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author sixiaojie
 * @date 2020-03-28-15:07
 */
public class NettyConfig {
  /**
   * 定义一个channel组,管理所有的channel
   * GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
   */
  private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

  /**
   * 存放用户与Chanel的对应信息,用于给指定用户发送消息
   */
  private static ConcurrentHashMap userChannelMap = new ConcurrentHashMap<>();

  private NettyConfig() {}
  
  /**
   * 获取channel组
   * @return
   */
  public static ChannelGroup getChannelGroup() {
    return channelGroup;
  }

  /**
   * 获取用户channel map
   * @return
   */
  public static ConcurrentHashMap getUserChannelMap(){
    return userChannelMap;
  }
}

第二步:创建NettyServer,定义两个EventLoopGroup,bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作,需要说明的是,需要开启一个新的线程来执行netty server,要不然会阻塞主线程,到时候就无法调用项目的其他controller接口了。

package com.sixj.nettypush.websocket;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;

/**
 * @author sixiaojie
 * @date 2020-03-28-13:44
 */

@Component
public class NettyServer{
  private static final Logger log = LoggerFactory.getLogger(NettyServer.class);
  /**
   * webSocket协议名
   */
  private static final String WEBSOCKET_PROTOCOL = "WebSocket";

  /**
   * 端口号
   */
  @Value("${webSocket.netty.port:58080}")
  private int port;

  /**
   * webSocket路径
   */
  @Value("${webSocket.netty.path:/webSocket}")
  private String webSocketPath;

  @Autowired
  private WebSocketHandler webSocketHandler;

  private EventLoopGroup bossGroup;
  private EventLoopGroup workGroup;

  /**
   * 启动
   * @throws InterruptedException
   */
  private void start() throws InterruptedException {
    bossGroup = new NioEventLoopGroup();
    workGroup = new NioEventLoopGroup();
    ServerBootstrap bootstrap = new ServerBootstrap();
    // bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作
    bootstrap.group(bossGroup,workGroup);
    // 设置NIO类型的channel
    bootstrap.channel(NioServerSocketChannel.class);
    // 设置监听端口
    bootstrap.localAddress(new InetSocketAddress(port));
    // 连接到达时会创建一个通道
    bootstrap.childHandler(new ChannelInitializer() {

      @Override
      protected void initChannel(SocketChannel ch) throws Exception {
        // 流水线管理通道中的处理程序(Handler),用来处理业务
        // webSocket协议本身是基于http协议的,所以这边也要使用http编解码器
        ch.pipeline().addLast(new HttpServerCodec());
        ch.pipeline().addLast(new ObjectEncoder());
        // 以块的方式来写的处理器
        ch.pipeline().addLast(new ChunkedWriteHandler());
        /*
        说明:
        1、http数据在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合
        2、这就是为什么,当浏览器发送大量数据时,就会发送多次http请求
         */
        ch.pipeline().addLast(new HttpObjectAggregator(8192));
        /*
        说明:
        1、对应webSocket,它的数据是以帧(frame)的形式传递
        2、浏览器请求时 ws://localhost:58080/xxx 表示请求的uri
        3、核心功能是将http协议升级为ws协议,保持长连接
        */
        ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));
        // 自定义的handler,处理业务逻辑
        ch.pipeline().addLast(webSocketHandler);

      }
    });
    // 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
    ChannelFuture channelFuture = bootstrap.bind().sync();
    log.info("Server started and listen on:{}",channelFuture.channel().localAddress());
    // 对关闭通道进行监听
    channelFuture.channel().closeFuture().sync();
  }

  /**
   * 释放资源
   * @throws InterruptedException
   */
  @PreDestroy
  public void destroy() throws InterruptedException {
    if(bossGroup != null){
      bossGroup.shutdownGracefully().sync();
    }
    if(workGroup != null){
      workGroup.shutdownGracefully().sync();
    }
  }
  @PostConstruct()
  public void init() {
    //需要开启一个新的线程来执行netty server 服务器
    new Thread(() -> {
      try {
        start();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }).start();
  }
}

第三步: 具体实现业务的WebSocketHandler,具体实现逻辑看注释

package com.sixj.nettypush.websocket;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.sixj.nettypush.config.NettyConfig;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;


/**
 * TextWebSocketFrame类型, 表示一个文本帧
 * @author sixiaojie
 * @date 2020-03-28-13:47
 */
@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler {
  private static final Logger log = LoggerFactory.getLogger(NettyServer.class);

  /**
   * 一旦连接,第一个被执行
   * @param ctx
   * @throws Exception
   */
  @Override
  public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    log.info("handlerAdded 被调用"+ctx.channel().id().asLongText());
    // 添加到channelGroup 通道组
    NettyConfig.getChannelGroup().add(ctx.channel());
  }

  /**
   * 读取数据
   */
  @Override
  protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
    log.info("服务器收到消息:{}",msg.text());

    // 获取用户ID,关联channel
    JSONObject jsOnObject= JSONUtil.parseObj(msg.text());
    String uid = jsonObject.getStr("uid");
    NettyConfig.getUserChannelMap().put(uid,ctx.channel());

    // 将用户ID作为自定义属性加入到channel中,方便随时channel中获取用户ID
    AttributeKey key = AttributeKey.valueOf("userId");
    ctx.channel().attr(key).setIfAbsent(uid);

    // 回复消息
    ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器连接成功!"));
  }

  @Override
  public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
    log.info("handlerRemoved 被调用"+ctx.channel().id().asLongText());
    // 删除通道
    NettyConfig.getChannelGroup().remove(ctx.channel());
    removeUserId(ctx);
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    log.info("异常:{}",cause.getMessage());
    // 删除通道
    NettyConfig.getChannelGroup().remove(ctx.channel());
    removeUserId(ctx);
    ctx.close();
  }

  /**
   * 删除用户与channel的对应关系
   * @param ctx
   */
  private void removeUserId(ChannelHandlerContext ctx){
    AttributeKey key = AttributeKey.valueOf("userId");
    String userId = ctx.channel().attr(key).get();
    NettyConfig.getUserChannelMap().remove(userId);
  }
}

**第四步:**具体消息推送的接口
public interface PushService {
  /**
   * 推送给指定用户
   * @param userId
   * @param msg
   */
  void pushMsgToOne(String userId,String msg);

  /**
   * 推送给所有用户
   * @param msg
   */
  void pushMsgToAll(String msg);
}

接口实现类:

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author sixiaojie
 * @date 2020-03-30-20:10
 */
@Service
public class PushServiceImpl implements PushService {

  @Override
  public void pushMsgToOne(String userId, String msg){
    ConcurrentHashMap userChannelMap = NettyConfig.getUserChannelMap();
    Channel channel = userChannelMap.get(userId);
    channel.writeAndFlush(new TextWebSocketFrame(msg));
  }
  @Override
  public void pushMsgToAll(String msg){
    NettyConfig.getChannelGroup().writeAndFlush(new TextWebSocketFrame(msg));
  }
}

controller:

package com.sixj.nettypush.controller;

import com.sixj.nettypush.service.PushService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author sixiaojie
 * @date 2020-03-30-20:08
 */
@RestController
@RequestMapping("/push")
public class PushController {

  @Autowired
  private PushService pushService;

  /**
   * 推送给所有用户
   * @param msg
   */
  @PostMapping("/pushAll")
  public void pushToAll(@RequestParam("msg") String msg){
    pushService.pushMsgToAll(msg);
  }
  /**
   * 推送给指定用户
   * @param userId
   * @param msg
   */
  @PostMapping("/pushOne")
  public void pushMsgToOne(@RequestParam("userId") String userId,@RequestParam("msg") String msg){
    pushService.pushMsgToOne(userId,msg);
  }
}

第五步:前端html页面




  
  



  
    
    
  

目前为止,所有代码已经写完了,测试一下

首先运行这个html文件,会看到服务端给前端返回的消息“服务器连接成功了!”,后端日志会打印服务器收到消息:{"uid":"123456"}

然后使用postman测试推送的接口

测试成功,打完收工

到此这篇关于SpringBoot+WebSocket+Netty实现消息推送的示例代码的文章就介绍到这了,更多相关SpringBoot+WebSocket+Netty消息推送内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持! 


推荐阅读
  • Netty源代码分析服务器端启动ServerBootstrap初始化
    本文主要分析了Netty源代码中服务器端启动的过程,包括ServerBootstrap的初始化和相关参数的设置。通过分析NioEventLoopGroup、NioServerSocketChannel、ChannelOption.SO_BACKLOG等关键组件和选项的作用,深入理解Netty服务器端的启动过程。同时,还介绍了LoggingHandler的作用和使用方法,帮助读者更好地理解Netty源代码。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文内容皆为作者原创,如需转载,请注明出处:https:www.cnblogs.comxuexianqip13045462.html1.自定义分页器的拷贝及使用当我们需要使用 ... [详细]
  • 我正在使用sql-serverkafka-connect和debezium监视sqlserver数据库,但是当我发布并运行我的wo ... [详细]
  • 使用nodejs爬取b站番剧数据,计算最佳追番推荐
    本文介绍了如何使用nodejs爬取b站番剧数据,并通过计算得出最佳追番推荐。通过调用相关接口获取番剧数据和评分数据,以及使用相应的算法进行计算。该方法可以帮助用户找到适合自己的番剧进行观看。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 本文介绍了高校天文共享平台的开发过程中的思考和规划。该平台旨在为高校学生提供天象预报、科普知识、观测活动、图片分享等功能。文章分析了项目的技术栈选择、网站前端布局、业务流程、数据库结构等方面,并总结了项目存在的问题,如前后端未分离、代码混乱等。作者表示希望通过记录和规划,能够理清思路,进一步完善该平台。 ... [详细]
  • 使用正则表达式爬取36Kr网站首页新闻的操作步骤和代码示例
    本文介绍了使用正则表达式来爬取36Kr网站首页所有新闻的操作步骤和代码示例。通过访问网站、查找关键词、编写代码等步骤,可以获取到网站首页的新闻数据。代码示例使用Python编写,并使用正则表达式来提取所需的数据。详细的操作步骤和代码示例可以参考本文内容。 ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
  • PRML读书会第十四章 Combining Models(committees,Boosting,AdaBoost,决策树,条件混合模型)...
    主讲人网神(新浪微博:豆角茄子麻酱凉面)网神(66707180)18:57:18大家好,今天我们讲一下第14章combiningmodel ... [详细]
  • 本文摘要配置目的:寄存器配置用于更改路由器启动过程。配置目的:寄存器配置用于更改路由器启动过程。启动位由4位16进制寄存器组成格式:0xA ... [详细]
author-avatar
mobiledu2502870133
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有