微信小程序与WebSocket
应用场景:微信小程序通过WebSocket实现与后端的即时通讯
项目场景:如果需要在微信小程序中实现好友添加类似的消息提醒功能,前端(微信小程序)可以通过WebSocket实时的接收好友添加信息,后端使用netty&消息队列实现。
实现效果:(当收到好友请求时,收件箱出现未处理消息提示)
问题描述
在做小程序项目时,遇到一个功能问题:如何实现小程序间的好友添加功能(可能鸡肋了一些。。-_-||
我们先总结下好友添加的流程,以A和B作为好友添加功能的对象:整个流程分为两个模块
① 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的内容上传到github上
主要思路
:
Netty和业务后端是作为两个服务器,Netty可以看做是用户和服务器之间的桥梁,考虑以下场景
- 用户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();
}
- 此时后端向消息队列中(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>
只需要搞清楚整个前后端的通讯逻辑,整个流程就比较清晰了 (๑•̀ㅂ•́)و✧