微信小程序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>

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

  • 35
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
为什么需要websocket? 传统的实时交互的游戏,或服务器主动发送消息的行为(如推送服务),如果想做在微信上,可能你会使用轮询的方式进行,不过这太消耗资源,大量的请求也加重了服务器的负担,而且延迟问题比较严重。如果是自己开发的app,为了解决这些问题,很多团队会自建socket,使用tcp长链接、自定协议的方式与服务器进行相对实时的数据交互。有能力的团队,采用这种方式自然没什么大问题。不过小团队可能就要花费很多时间去调试,要解决很多难题,这个在成本上就划不来。 H5引入了webSocket来解决网页端的长链接问题,而微信小程序也支持websocket。这是一个非常重要的特性,所以本系列的文章会专门拿出一篇来讨论websocketwebSocket本质上也是TCP连接,它提供全双工的数据传输。一方面可以避免轮询带来的连接频繁建立与断开的性能损耗,另一方面数据可以是比较实时的进行双向传输(因为是长链接),而且WebSocket允许跨域通信(这里有个潜在的跨域安全的问题,得靠服务端来解决)。目前除IE外的浏览器已经对webSocket支持得很好了,微信小程序再推一把之后,它会变得更加流行。 我们来设计一个新的demo,一个比较有趣的小游戏,多人版扫雷,准确地讲,多人版挖黄金。 游戏规则是这样的:把雷换成金子,挖到金子加一分,每人轮流一次(A挖完轮到B,B挖完A才能再点击),点中金子就算你的,也不会炸,游戏继续,直到把场上所有的金子都挖完游戏才结束。跟扫雷一样,数字也是表示周边有几个金子,然后用户根据场上已经翻出来的数字来猜哪一格可能有金子。 这种交互的游戏难点在于,用户的点击操作都要传到服务器上,而且服务器要实时的推送到其它玩家的应用上。另外用户自己也要接收对方操作时实时传过来的数据,这样才不至于重复点中同一个格子。简单讲,就是你要上报操作给服务器,而服务器也要实时给你推消息。为了简化整个模型,我们规定玩家必须轮流来点击,玩家A点完后,才能轮到玩家B,玩家B操作完,玩家A才能点。 我们分几步来实现这个功能。 一、实现思路 1、第一步,我们要先生成扫雷的地图场景 这个算法比较简单,简述一下。随机取某行某列就可以定位一个格子,标记成金子(-1表示金子)。mimeCnt表示要生成的金子的数量,用同样的方式循环标记mimeCnt个随机格子。生成完后,再用一个循环去扫描这些-1的格子,把它周边的格子都加1,当然必须是非金子的格子才加1。代码放在这里。 其中increaseArround用来把这格金子周边的格子都加1,实现也比较简单: 执行genMimeArr(),随机生成结果如下: -1表示金子。看了下貌似没什么问题。接下去,我们就要接入webSocket了。 (这个是js版本的,其实生成地图场景的工作是在后台生成,这个js版本只是一个演示,不过算法是一样的。) 2、我们需要一个支持webSocket服务端 本例子中,我们使用python的tornado框架来实现(tornado提供了tornado.websocket模块)。当然读者也可以使用socket.io,专为webSocket设计的js语言的服务端,用起来非常简单,它也对不支持webSocket的浏览器提供了兼容(flash或comet实现)。 笔者本人比较喜欢使用tornado,做了几年后台开发,使用最多的框架之一的就是它,NIO模型,而且非常轻量级,同样的rps,java可能需要700-800M的内存,tornado只要30-40M,所以在一台4G内存的机子上可以跑上百个tornado服务,而java,对不起,只能跑3个虚拟机。微服务的时代,这一点对小公司很重要。当然如果读者本人对java比较熟悉的话,也可以选择netty框架尝试一下。 webSocket用tornado的另一个好处是,它可以在同一个服务(端口)上同时支持webSocket及http两种协议。tornado的官方demo代码中展示了怎么实现同时使用两种协议。在本游戏中,可以这么用:用户进入首页,用http协议去拉取当前的房间号及数据。因为首页是打开最多的,进了首页的用户不一定会玩游戏。所以首页还没必要建立webSocket链接,webSocket链接主要用来解决频繁请求及推送的操作。首页只有一个请求操作。选了房间号后,进去下一个游戏页面再开始建立webSocket链接。 3、客户端 使用微信小程序开发工具,直接连接是会报域名安全错误的,因为工具内部做了限制,对安全域名才会允许连接。所以同样的,这里我们也继续改下工具的源码,把相关的行改掉就行修改方式如下: 找到asdebug.js的这一行,把它改成: if(false)即可。
微信小程序可以使用 WebSocket 协议来实现实时通信。WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它允许客户端和服务器之间进行实时数据传输。 要在微信小程序中使用 WebSocket,需要先创建一个 WebSocket 实例,然后通过该实例来发送和接收数据。以下是一个简单的示例代码: ```javascript // 创建 WebSocket 实例 var ws = wx.connectSocket({ url: 'wss://example.com/ws', }) // 监听 WebSocket 连接打开事件 ws.onOpen(function () { console.log('WebSocket 连接已打开') // 发送数据 ws.send('Hello, WebSocket!') }) // 监听 WebSocket 接收到服务器数据事件 ws.onMessage(function (data) { console.log('接收到服务器数据:', data) }) // 监听 WebSocket 连接关闭事件 ws.onClose(function () { console.log('WebSocket 连接已关闭') }) // 监听 WebSocket 连接错误事件 ws.onError(function (error) { console.error('WebSocket 错误:', error) }) ``` 在上面的示例中,我们创建了一个 WebSocket 实例并指定了要连接的服务器地址。然后,我们监听了 WebSocket 连接打开、接收数据、关闭和错误事件,并在事件发生时输出相关信息。最后,我们通过 `ws.send` 方法向服务器发送了一条消息。 需要注意的是,微信小程序中的 WebSocket 实现是基于原生 WebSocket 实现的,但也有一些差异,例如微信小程序中的 `WebSocket.send` 方法只支持发送字符串类型的数据。另外,微信小程序中的 WebSocket 实现还有一些性能和稳定性上的限制,因此在使用过程中需要注意避免过度使用和滥用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值