1、注意事项
(1)微信小程序的websocket连接服务端websocket服务,开发时可用ws+ip+端口,发布时只能用wss+域名。
(2)小程序其他业务访问后台开发商可用http+IP+端口,发布时只能用https+域名
(3)小程序要在微信小程序公众平台配置合法域名发布后才能正常访问。
2、后台代码
(1)后台类说明:
- websocket启动监听交互类。
- 存储当前已连接用户及用户操作类。
(2)导入jar包(maven导入)
<!-- 添加websocket依赖jar包 -->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.3.0</version>
</dependency>
(3)操作类:WsPool
package com.thinkgem.jeesite.modules.mjd.listener;
import com.thinkgem.jeesite.common.utils.StringUtils;
import org.java_websocket.WebSocket;
import java.util.*;
public class WsPool {
private static final Map<WebSocket, String> wsUserMap = new HashMap<WebSocket, String>();
/**
* 通过websocket连接获取其对应的用户
*/
public static String getUserByWs(WebSocket conn) {
return wsUserMap.get(conn);
}
/**
* 根据userName获取WebSocket,这是一个list,此处取第一个
* 因为有可能多个websocket对应一个userName(但一般是只有一个,因为在close方法中,我们将失效的websocket连接去除了)
*/
public static WebSocket getWsByUser(String userName) {
Set<WebSocket> keySet = wsUserMap.keySet();
synchronized (keySet) {
for (WebSocket conn : keySet) {
String cuser = wsUserMap.get(conn);
if (cuser.equals(userName)) {
return conn;
}
}
}
return null;
}
/**
* 向连接池中添加连接
*/
public static void addUser(String userName, WebSocket conn) {
wsUserMap.put(conn, userName); // 添加连接
}
/**
* 获取所有连接池中的用户,因为set是不允许重复的,所以可以得到无重复的user数组
*/
public static Collection<String> getOnlineUser() {
List<String> setUsers = new ArrayList<String>();
Collection<String> setUser = wsUserMap.values();
for (String u : setUser) {
setUsers.add(u);
}
return setUsers;
}
/**
* 移除连接池中的连接
*/
public static boolean removeUser(WebSocket conn) {
if (wsUserMap.containsKey(conn)) {
wsUserMap.remove(conn); // 移除连接
return true;
} else {
return false;
}
}
/**
* 向特定的用户发送数据
*/
public static void sendMessageToUser(WebSocket conn, String message) {
if (null != conn && null != wsUserMap.get(conn)) {
conn.send(message);
}
}
/**
* 向所有用户名中包含某个特征得用户发送消息
*/
public static void sendMessageToSpecialUser(String message,String special) {
Set<WebSocket> keySet = wsUserMap.keySet();
if (special == null) {
special = "";
}
synchronized (keySet) {
for (WebSocket conn:keySet) {
String user = wsUserMap.get(conn);
try {
if (user != null) {
String [] cus = user.split("_");
if (!StringUtils.isNullOrEmpty(cus[0])) {
String cusDot = "," + cus[0] + ",";
if (cusDot.contains(","+special+",")) {
conn.send(message);
}
}else {
conn.send(message);
}
}
}catch (Exception e) {
e.printStackTrace();
//wsUserMap.remove(conn);
}
}
}
}
/**
* 向所有的用户发送消息
*/
public static void sendMessageToAll(String message) {
Set<WebSocket> keySet = wsUserMap.keySet();
synchronized (keySet) {
for (WebSocket conn : keySet) {
String user = wsUserMap.get(conn);
if (user != null) {
conn.send(message);
}
}
}
}
}
(4)websocket启动监听交互类 MyWebScoket :
package com.thinkgem.jeesite.modules.mjd.listener;
import com.alibaba.fastjson.JSONObject;
import org.java_websocket.WebSocket;
import org.java_websocket.WebSocketImpl;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Map;
public class MyWebScoket extends WebSocketServer {
public MyWebScoket() throws UnknownHostException {
super();
}
public MyWebScoket(int port) {
super(new InetSocketAddress(port));
}
public MyWebScoket(InetSocketAddress address) {
super(address);
}
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
// ws连接的时候触发的代码,onOpen中我们不做任何操作
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
//断开连接时候触发代码
userLeave(conn);
System.out.println("有用户已离线==========="+reason);
}
@Override
public void onMessage(WebSocket conn, String message) {
//有用户连接进来
Map<String, String> obj = (Map<String,String>) JSONObject.parse(message);
System.out.println(message);
//添加用户到线程池
String username = obj.get("name");
userJoin(conn, username);
/*int i=0;*/
/*WsPool.sendMessageToUser(c,i+"");*/
/*while(true){
try {
Thread.sleep(3*1000);
WsPool.sendMessageToUser(c,i+"");
i++;
System.out.println(i+"");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}*/
}
@Override
public void onError(WebSocket conn, Exception ex) {
//错误时候触发的代码
System.out.println("on error");
ex.printStackTrace();
}
/**
* 去除掉失效的websocket链接
*/
private void userLeave(WebSocket conn){
WsPool.removeUser(conn);
}
/**
* 将websocket加入用户池
* @param conn
* @param userName
*/
private void userJoin(WebSocket conn,String userName){
WsPool.addUser(userName, conn);
}
/**
* 启动websocket
*/
public static void startWebsocketInstantMsg() {
WebSocketImpl.DEBUG = false;
MyWebScoket s;
s = new MyWebScoket(4909);
s.start();
System.out.println("websocket启动成功");
}
}
(5)字符串操作类 StringUtils :
package com.thinkgem.jeesite.common.utils;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.LocaleResolver;
import com.google.common.collect.Lists;
/**
* 字符串工具类, 继承org.apache.commons.lang3.StringUtils类
*/
public class StringUtils extends org.apache.commons.lang3.StringUtils {
private static final char SEPARATOR = '_';
private static final String CHARSET_NAME = "UTF-8";
/**
* 转换为字节数组
* @param str
* @return
*/
public static byte[] getBytes(String str){
if (str != null){
try {
return str.getBytes(CHARSET_NAME);
} catch (UnsupportedEncodingException e) {
return null;
}
}else{
return null;
}
}
/**
* 转换为Boolean类型
* 'true', 'on', 'y', 't', 'yes' or '1' (case insensitive) will return true. Otherwise, false is returned.
*/
public static Boolean toBoolean(final Object val){
if (val == null){
return false;
}
return BooleanUtils.toBoolean(val.toString()) || "1".equals(val.toString());
}
/**
* 转换为字节数组
* @param str
* @return
*/
public static String toString(byte[] bytes){
try {
return new String(bytes, CHARSET_NAME);
} catch (UnsupportedEncodingException e) {
return EMPTY;
}
}
/**
* 如果对象为空,则使用defaultVal值
* see: ObjectUtils.toString(obj, defaultVal)
* @param obj
* @param defaultVal
* @return
*/
public static String toString(final Object obj, final String defaultVal) {
return obj == null ? defaultVal : obj.toString();
}
/**
* 是否包含字符串
* @param str 验证字符串
* @param strs 字符串组
* @return 包含返回true
*/
public static boolean inString(String str, String... strs){
if (str != null){
for (String s : strs){
if (str.equals(trim(s))){
return true;
}
}
}
return false;
}
/**
* 替换掉HTML标签方法
*/
public static String replaceHtml(String html) {
if (isBlank(html)){
return "";
}
String regEx = "<.+?>";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(html);
String s = m.replaceAll("");
return s;
}
/**
* 替换为手机识别的HTML,去掉样式及属性,保留回车。
* @param html
* @return
*/
public static String replaceMobileHtml(String html){
if (html == null){
return "";
}
return html.replaceAll("<([a-z]+?)\\s+?.*?>", "<$1>");
}
/**
* 替换为手机识别的HTML,去掉样式及属性,保留回车。
* @param txt
* @return
*/
public static String toHtml(String txt){
if (txt == null){
return "";
}
return replace(replace(Encodes.escapeHtml(txt), "\n", "<br/>"), "\t", " ");
}
/**
* 缩略字符串(不区分中英文字符)
* @param str 目标字符串
* @param length 截取长度
* @return
*/
public static String abbr(String str, int length) {
if (str == null) {
return "";
}
try {
StringBuilder sb = new StringBuilder();
int currentLength = 0;
for (char c : replaceHtml(StringEscapeUtils.unescapeHtml4(str)).toCharArray()) {
currentLength += String.valueOf(c).getBytes("GBK").length;
if (currentLength <= length - 3) {
sb.append(c);
} else {
sb.append("...");
break;
}
}
return sb.toString();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "";
}
public static String abbr2(String param, int length) {
if (param == null) {
return "";
}
StringBuffer result = new StringBuffer();
int n = 0;
char temp;
boolean isCode = false; // 是不是HTML代码
boolean isHTML = false; // 是不是HTML特殊字符,如
for (int i = 0; i < param.length(); i++) {
temp = param.charAt(i);
if (temp == '<') {
isCode = true;
} else if (temp == '&') {
isHTML = true;
} else if (temp == '>' && isCode) {
n = n - 1;
isCode = false;
} else if (temp == ';' && isHTML) {
isHTML = false;
}
try {
if (!isCode && !isHTML) {
n += String.valueOf(temp).getBytes("GBK").length;
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (n <= length - 3) {
result.append(temp);
} else {
result.append("...");
break;
}
}
// 取出截取字符串中的HTML标记
String temp_result = result.toString().replaceAll("(>)[^<>]*(<?)",
"$1$2");
// 去掉不需要结素标记的HTML标记
temp_result = temp_result
.replaceAll(
"</?(AREA|BASE|BASEFONT|BODY|BR|COL|COLGROUP|DD|DT|FRAME|HEAD|HR|HTML|IMG|INPUT|ISINDEX|LI|LINK|META|OPTION|P|PARAM|TBODY|TD|TFOOT|TH|THEAD|TR|area|base|basefont|body|br|col|colgroup|dd|dt|frame|head|hr|html|img|input|isindex|li|link|meta|option|p|param|tbody|td|tfoot|th|thead|tr)[^<>]*/?>",
"");
// 去掉成对的HTML标记
temp_result = temp_result.replaceAll("<([a-zA-Z]+)[^<>]*>(.*?)</\\1>",
"$2");
// 用正则表达式取出标记
Pattern p = Pattern.compile("<([a-zA-Z]+)[^<>]*>");
Matcher m = p.matcher(temp_result);
List<String> endHTML = Lists.newArrayList();
while (m.find()) {
endHTML.add(m.group(1));
}
// 补全不成对的HTML标记
for (int i = endHTML.size() - 1; i >= 0; i--) {
result.append("</");
result.append(endHTML.get(i));
result.append(">");
}
return result.toString();
}
/**
* 转换为Double类型
*/
public static Double toDouble(Object val){
if (val == null){
return 0D;
}
try {
return Double.valueOf(trim(val.toString()));
} catch (Exception e) {
return 0D;
}
}
/**
* 转换为Float类型
*/
public static Float toFloat(Object val){
return toDouble(val).floatValue();
}
/**
* 转换为Long类型
*/
public static Long toLong(Object val){
return toDouble(val).longValue();
}
/**
* 转换为Integer类型
*/
public static Integer toInteger(Object val){
return toLong(val).intValue();
}
/**
* 获得i18n字符串
*/
public static String getMessage(String code, Object[] args) {
LocaleResolver localLocaleResolver = (LocaleResolver) SpringContextHolder.getBean(LocaleResolver.class);
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
Locale localLocale = localLocaleResolver.resolveLocale(request);
return SpringContextHolder.getApplicationContext().getMessage(code, args, localLocale);
}
/**
* 获得用户远程地址
*/
public static String getRemoteAddr(HttpServletRequest request){
String remoteAddr = request.getHeader("X-Real-IP");
if (isNotBlank(remoteAddr)) {
remoteAddr = request.getHeader("X-Forwarded-For");
}else if (isNotBlank(remoteAddr)) {
remoteAddr = request.getHeader("Proxy-Client-IP");
}else if (isNotBlank(remoteAddr)) {
remoteAddr = request.getHeader("WL-Proxy-Client-IP");
}
return remoteAddr != null ? remoteAddr : request.getRemoteAddr();
}
/**
* 驼峰命名法工具
* @return
* toCamelCase("hello_world") == "helloWorld"
* toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCamelCase(String s) {
if (s == null) {
return null;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == SEPARATOR) {
upperCase = true;
} else if (upperCase) {
sb.append(Character.toUpperCase(c));
upperCase = false;
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* 驼峰命名法工具
* @return
* toCamelCase("hello_world") == "helloWorld"
* toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCapitalizeCamelCase(String s) {
if (s == null) {
return null;
}
s = toCamelCase(s);
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
/**
* 驼峰命名法工具
* @return
* toCamelCase("hello_world") == "helloWorld"
* toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toUnderScoreCase(String s) {
if (s == null) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean nextUpperCase = true;
if (i < (s.length() - 1)) {
nextUpperCase = Character.isUpperCase(s.charAt(i + 1));
}
if ((i > 0) && Character.isUpperCase(c)) {
if (!upperCase || !nextUpperCase) {
sb.append(SEPARATOR);
}
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
/**
* 转换为JS获取对象值,生成三目运算返回结果
* @param objectString 对象串
* 例如:row.user.id
* 返回:!row?'':!row.user?'':!row.user.id?'':row.user.id
*/
public static String jsGetVal(String objectString){
StringBuilder result = new StringBuilder();
StringBuilder val = new StringBuilder();
String[] vals = split(objectString, ".");
for (int i=0; i<vals.length; i++){
val.append("." + vals[i]);
result.append("!"+(val.substring(1))+"?'':");
}
result.append(val.substring(1));
return result.toString();
}
public static boolean isNullOrEmpty(String value) {
if (value == null || value.length() == 0) {
return true;
}
return false;
}
public static boolean isNullOrEmpty(Object value) {
if (value == null) {
return true;
}
if (value.toString().length() == 0) {
return true;
}
return false;
}
}
(6)启动服务端 websocket 服务
//启动 websocket 服务
MyWebScoket.startWebsocketInstantMsg();
3、小程序端代码
进入小程序就连接服务端的websocket。需要在app.js中的添加代码:
//app.js
App({
onLaunch: function () {
var that = this;
// 展示本地存储能力
var logs = wx.getStorageSync('logs') || []
logs.unshift(Date.now())
wx.setStorageSync('logs', logs)
// 登录
wx.login({
success: res => {
//连接服务器
console.log(that.globalData.socketStatus);
if (that.globalData.socketStatus == 'closed') {
console.log('启用websocket');
that.openSocket();
}
}
})
// 获取用户信息
wx.getSetting({
success: res => {
if (res.authSetting['scope.userInfo']) {
// 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
wx.getUserInfo({
success: res => {
// 可以将 res 发送给后台解码出 unionId
this.globalData.userInfo = res.userInfo
console.log(res);
// 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
// 所以此处加入 callback 以防止这种情况
if (this.userInfoReadyCallback) {
this.userInfoReadyCallback(res)
}
}
})
}
}
})
},
//开启websocket连接
openSocket() {
//打开时执行
wx.onSocketOpen(() => {
console.log('WebSocket 已连接')
this.globalData.socketStatus = 'connected';//全局记录socket状态
wx.setStorage({
key: "socketStatus",
data: 'connected'
})
this.sendMessage();//调用发消息方法
})
//断开时的执行
wx.onSocketClose(() => {
console.log('WebSocket 已断开');
this.globalData.socketStatus = 'closed';
clearInterval(this.globalData.i);
wx.setStorage({
key: "socketStatus",
data: 'closed'
})
})
//报错时执行
wx.onSocketError(error => {
console.error('socket error:', error);
})
// 监听服务器推送到微信小程序的消息
wx.onSocketMessage(message => {
console.log(message.data); //websocket监听到服务端发的消息
})
// 连接Socket
wx.connectSocket({
url: 'wss://www.swandian.com/wss',
})
},
//关闭Socke
closeSocket() {
if (this.globalData.socketStatus == 'connected') {
wx.closeSocket({
success: () => {
this.globalData.socketStatus = 'closed'
}
})
}
},
//发送消息
sendMessage() {
if (this.globalData.socketStatus === 'connected') {
//自定义的发给后台识别的参数 ,我这里发送的是name
wx.sendSocketMessage({
data: "{\"name\":\"" + getApp().globalData.openid + "\"}"
})
}
},
// 定时发送消息方法,为了保持websocket的连接(因为服务器设置了当websocket一分钟内无消息自动断开连接)
// sendCode: function (e) {
// var that = this;
// var i = setInterval(function () {
// that.sendMessage();
// }, 55000)
// },
//全局变量
globalData: {
openid: “”,
socketStatus: 'closed',//websocket连接状态
socketMessage: '',//socket消息内容
code: null,
}
})
4、发布及小程序端wss访问代理转发
- Tomcat部署好后台项目
- 申请域名,申请SSL证书,下载证书文件。
- nginx 配置代理转发