微信小程序WebSocket与Netty

1 篇文章 0 订阅
1 篇文章 0 订阅

微信小程序与WebSocket

应用场景:微信小程序通过WebSocket实现与后端的即时通讯

项目场景:如果需要在微信小程序中实现好友添加类似的消息提醒功能,前端(微信小程序)可以通过WebSocket实时的接收好友添加信息,后端使用netty&消息队列实现。
实现效果:(当收到好友请求时,收件箱出现未处理消息提示)
当收到好友请求时,收件箱出现未处理消息提示


问题描述

在做小程序项目时,遇到一个功能问题:如何实现小程序间的好友添加功能(可能鸡肋了一些。。-_-||
我们先总结下好友添加的流程,以AB作为好友添加功能的对象:整个流程分为两个模块

A发送添加B为好友->B实时显示A的好友添加信息
B点击处理信息->用户间关系更新

功能实现

① 好友添加

好友添加对于后端来说是消息传递问题,如何实现实时的消息传递?前端常使用的工具是websocket,只不过这里要换到微信小程序上实现,后端使用Netty和消息队列进行消息的转发。

第一步 前端websocket实现

websocket:
用户登录的时候建立连接
接收发送message

在用户登录的时候,获取openid,建立通信
微信小程序在打开的时候会调用onlaunch函数,在此处获取用户的openid&建立通信

  onLaunch: function () {
    var that = this;
    //先获取到用户的openid
    this.getOpenid();
  }

getOpenid:后端获取用户openid的具体实现可以参照

https://editor.csdn.net/md/?articleId=127756088

getOpenid: function () {
    var that = this;
    wx.login({
      success(res) {
        if (res.code) { // wx.login授权成功后
          //发起网络请求
          wx.request({
            url: '后端获取用户openid的URL',
            method: "POST",
            success(res) {
              that.globalData.openid = res.data.data;
              wx.request({
                url: '通过openid再次发送请求获取到微信用户的具体信息',
                method: 'GET',
                success(resLogin) {
                  if(resLogin.data.code==200){
                  	  // 这里可以根据自己的后端返回数据进行改变 
	                  that.globalData.userRelation = resLogin.data.data; //获取用户的好友列表
	                  that.globalData.userinfo = resLogin.data.info; //获取用户的具体信息
	                  // 定义 消息
	                  var message = {};
	                  message.did = that.globalData.userinfo.id; //获取用户的id(数据库中的)
	                  // 前后端定义好的消息的类型
	                  /**
	                   * 1:连接
	                   * 7:好友添加
	                   */
	                  // 建立连接
	                  that.openSocket();
	                  message.type = 1;
	                  message = JSON.stringify(message)
	                  // 给服务器发送消息
	                  that.sendMessage(message);
                }else { // 获取用户的好友列表失败
                }
              }
              })
            },
            fail(data) { // 获取openid失败
              wx.showToast({
                title: '获取openid失败',
                icon: 'fail',
                duration: 2000
              })
            }
          })
        } else { // 登录失败
          wx.showToast({
            title: '登录失败',
            icon: 'fail',
            duration: 2000
          })
        }
      }
    })
  },

openSocket():建立连接

都是微信小程序中定义好的关于websocket的使用函数
包括监听服务器推送的消息以及连接成功、连接断开函数
简单认为调用了wx.connectSocket后,其余的函数实时的检测整个信道的通讯状态

openSocket() {
    //打开时的动作
    wx.onSocketOpen(() => {
      console.log('WebSocket 已连接')
      this.globalData.socketStatus = 'connected'; //更新全局的socketStatus状态
      this.login();
    })
    //断开时的动作
    wx.onSocketClose(() => {
      console.log('WebSocket 已断开')
      this.globalData.socketStatus = 'closed' //更新全局的socketStatus状态
    })
    //报错时的动作
    wx.onSocketError(error => {
      console.error('socket error:', error)
    })
    // 监听服务器推送的消息
    wx.onSocketMessage(message => {
      //把JSONStr转为JSON
      message = message.data;
      if (typeof message != 'object') {
        message = message.replace(/\ufeff/g, ""); //重点 转码
        var jj = JSON.parse(message);
        message = jj;
      }
      console.log("【websocket监听到消息】内容如下:");
      console.log(message);
      if (message.type == 7) {
      	// 7:好友添加
      	// 自定义消息处理
      }
      if (message.type == 6) {
      	// 6:xxx
      	// 自定义消息处理
      }
      if (message.type == 5) {
      	// 5:xxx
      	// 自定义消息处理
      }
    })
    // 打开信道
    wx.connectSocket({
      url: "wss://" + "自己的后端Netty的URL例如 域名:端口号", 
    })
  },

在微信小程序使用websocket的时候要注意,一定需要再微信小程序中注册对应的wss://url,否则请求会爆错

sendMessage:

sendMessage(message) {
    if (this.globalData.socketStatus === 'connected') { // 首先确认是连接状态
      //自定义的发给后台识别的参数 
      console.log("发送的消息", message)
      wx.sendSocketMessage({
        data: message
      })
    }
  }

第二步 后端Netty&RabbitMQ实现

这里只做单服务器的消息处理,集群服务器情况未考虑
后端的文件目录结构如下:

Netty服务器目录结构
(后端内容较多,后期会把Netty的内容上传到github上
主要思路:
Netty和业务后端是作为两个服务器,Netty可以看做是用户和服务器之间的桥梁,考虑以下场景

  1. 用户A点击好友添加模块,搜索并向B发送添加请求。

业务端:
① 对于好友请求,首先进行业务操作。 比如保存到数据库中
② 处理完后,将消息添加到消息队列中

业务服务器

    /**
     * 前端的是微信名称
     * 发送好友请求的时候
     * @param addMsg
     * @return
     */
    @PostMapping("/add")
    public Response addUser(@RequestBody AddMsg addMsg){
    	......
        /*
        	...自己的一系列的业务处理内容
		*/
        //发送消息  websocket去处理
        String jsonStr = JSONUtil.toJsonStr(addMsg);
        rabbitTemplate.convertAndSend("ws_exchange","",jsonStr);
        return Response.success();
    }
  1. 此时后端向消息队列中(Netty中一直在监听队列)发送message,告诉Netty:“A要添加B为好友啦”。

① 消息队列接收到这个消息时,获取到B的Channel,向B的channel发送message
② 此时B的websocket就会接收到message,前端进行之后的一系列处理

但是

Netty服务器消息队列处理消息
在处理消息之前,我们需要知道我们拿到了消息,也根据消息的内容知道消息时从谁来的到谁去的,但是我们怎么知道对方的channel或者说我们怎么找到对方的websocket,向对方的websocket发送消息?—Channel注册
关于Channel注册:
在第一节的时候会发现最开始建立websocket连接的时候,会发送一个连接请求,这个操作就是将用户对应的websocket的channel注册到Netty中,可以看做告诉Netty你来了,给你个学号。

这个的实现是个比较固定的流程了(Netty服务器):
① ChannelGroup 对象(存储 key:value — 用户:websocket)

public class ChannelGroup {
    /*
    * key:设备id   这里直接用openid代替设备id  没有用户下线问题
    * channel:设备id的连接对象
    * */
    public static Map<String, Channel>  channelMap = new HashMap<>();

    /*
    * 添加channel到容器中
    * did 用户id
    * channel
    * */
    public static void addChannel(String did, Channel channel){
        channelMap.put(did,channel);
    }

    public static Channel getChannel(String did){
        return channelMap.get(did);
    }

    public static void removeChannel(String did){
        channelMap.remove(did);
    }

    public static void removeChannel(Channel channel){
        if(channelMap.containsValue(channel)){
            Set<Map.Entry<String, Channel>> entries = channelMap.entrySet();
            for(Map.Entry<String, Channel> ent :entries){
                if(ent.getValue()==channel){
                    channelMap.remove(ent.getKey());
                    break;
                }
            }
        }
    }
}

② 建立处理链

@Component
public class StartWebServerSocket implements CommandLineRunner {

    @Value("${netty.port}")
    private Integer port;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RabbitTemplate rabbitTemplate;
    /*
    * springboot初始化成功之后调用
    * */
    @Override
    public void run(String... args) throws Exception {
        //创建两个线程池  (线程模型使用的是主从模型)
        EventLoopGroup master = new NioEventLoopGroup();//主线程负责连接
        EventLoopGroup slave = new NioEventLoopGroup();//从线程池主要负责读写

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(master,slave);//设置主从线程模型
            serverBootstrap.channel(NioServerSocketChannel.class);//设置服务器

            //设置处理链
            serverBootstrap.childHandler(new ChannelInitializer() {
                @Override
                protected void initChannel(Channel channel) throws Exception {
                    ChannelPipeline pipeline = channel.pipeline();

                    //http
                    pipeline.addLast(new HttpServerCodec());
                    //http
                    pipeline.addLast(new HttpObjectAggregator(1024*10));
                    //添加websocket的编解码器
                    pipeline.addLast(new WebSocketServerProtocolHandler("/"));//哪个路径使用 websocket的编解码器

                    pipeline.addLast(new WebSocketHandler());

                    pipeline.addLast(new ConnHandler(redisTemplate)); //连接请求

                    pipeline.addLast(new HeartHandler()); // 心跳请求

                    pipeline.addLast(new AddHandler(rabbitTemplate)); // 好友添加

                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(port);
            channelFuture.sync();//阻塞住
            System.out.println("成功启动server");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

③ message处理中心

@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
        String text = textWebSocketFrame.text();
        NettyMsg nettyMsg = JSONUtil.toBean(text, NettyMsg.class);

        Integer type = nettyMsg.getType();
        if(type.equals(1)){
            //表示是“连接”的消息类型
            nettyMsg = JSONUtil.toBean(text, ConnMsg.class);
        }else if(type.equals(2)){
            //心跳
            //channelHandlerContext.channel().writeAndFlush(new TextWebSocketFrame("嗯!"));
            nettyMsg = JSONUtil.toBean(text, HeartMsg.class);
        }else if(type.equals(3)){
            //添加好友
            nettyMsg = JSONUtil.toBean(text, AddMsg.class);
        }
        //往下传递
        channelHandlerContext.fireChannelRead(nettyMsg);
    }
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("新客户端连接");
    }
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端断开连接");
        ChannelGroup.removeChannel(ctx.channel());
    }
}

④ 处理连接请求 ConnHandler
(redisTemplate在这里没什么用)

public class ConnHandler extends SimpleChannelInboundHandler<ConnMsg> {
    private StringRedisTemplate redisTemplate;

    //利用构造器注入  因为这个类不是spring自己的bean  是我们自己new出来的 所以需要从外面来传一个
    public ConnHandler(StringRedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ConnMsg connMsg) throws Exception {
        System.out.println("发送连接消息"+connMsg.toString());
        // 注册channel 以用户在数据库中的id作为key value为channelHandlerContext.channel()
        ChannelGroup.addChannel(connMsg.getDid(),channelHandlerContext.channel());
        TextWebSocketFrame resp = new TextWebSocketFrame(JSONUtil.toJsonStr(connMsg));
        // 返回给用户 告诉他:好的,我们俩之间建立连接成功了!
        channelHandlerContext.writeAndFlush(resp);
    }
}

在前期准备工作完成之后,我们就可以开始各种message的处理代码了

处理消息

@Component
public class WsQueueListener {
    @RabbitHandler
    @RabbitListener(queues = "ws_queue_${netty.port}")
    public void addRequestMsg(String jsonStr){
        /*
        * 同意还是拒绝  还是新的好友添加请求
        * 前端利用值不同  进行不同的页面显示
        * */
        AddMsg addMsg = JSONUtil.toBean(jsonStr, AddMsg.class);
        Integer tid = addMsg.getTid();
        Channel channel = ChannelGroup.getChannel(tid.toString());
        System.out.println("拿到了发送好友请求的消息"+addMsg);
        if (channel != null) {
            //转成json 放进去数据帧里面
            System.out.println("channel!=null");
            String toJsonStr = JSONUtil.toJsonStr(addMsg);
            TextWebSocketFrame resp = new TextWebSocketFrame(toJsonStr);
            //好友添加消息发送过去
            channel.writeAndFlush(resp);
        }
    }
}

实时上如果对其他业务数据库什么的没什么要做的,可以直接通过websocket发送message,Netty找到对应用户的信道,发送给对方的websocket。rabbitMQ可以看做一个公共的信箱,有需要的人(业务服务器)从里面读取信封记载完信息之后,放回去给下一个人(Netty)处理。
3. B接收到消息之后,接下来的操作包括同意、拒绝好友请求,都可以通过3,4的两步进行实时的响应(或者经过业务的controller之后放入消息队列中,再通过第5步)

tips: 当某个用户下线了之后,是不能实时的接收好友请求消息的,所以在用户再次上线时获取关于未处理消息的列表直接进行显示。

pom.xml

   	  <dependency>
           <groupId>io.netty</groupId>
           <artifactId>netty-all</artifactId>
           <version>4.1.77.Final</version>
       </dependency>
       <dependency>
           <groupId>cn.hutool</groupId>
           <artifactId>hutool-all</artifactId>
           <version>5.1.0</version>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-amqp</artifactId>
	  </dependency>

只需要搞清楚整个前后端的通讯逻辑,整个流程就比较清晰了 (๑•̀ㅂ•́)و✧

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值