------------------2019-4-17---------------
优化界面后 移动端运行如图:
网页端运行如图,主要分辨率调成手机端适应的了,网页的不缩放的话有点大。
------------------2019-4-17---------------
P:我之前测试阶段把整个springCloud的配置一起打包了…其实只用打包一个service的jar包即可。下面的附件有点大个…实际上里面有用的就一个service-hi,其他都是springCloud的配置
在(一)中提到在本地运行成功但是在Linux下运行失败的原因已经找到了主要是跨域的问题导致websocket连接失败。
简单解决方法:
wsUrl = "ws://localhost:8763/websocket/" + channel + "/" + name;
改成获取本地路径的,这种方式修改不适用反向代理等方式,因为他获取的是地址,反向代理后地址不一样还是会跨域
wsUrl = "ws://" + document.domain + ":8763/websocket/" + channel + "/" + name;
wsUrl = "ws://" + document.domain + ":8763/websocket/" + channel + "/" + name;
然后用几个方法来接收后端的消息
ws.onclose = function () {
console.log('链接关闭');
reconnect();
};
ws.onerror = function () {
console.log('发生异常了');
reconnect();
};
ws.onopen = function () {
console.log('链接开启');
//心跳检测重置
heartCheck.start();
};
ws.onmessage = function (event) {
if (event.data != '心跳检测') {
setMessageInnerHTML(event.data);
}
//拿到任何消息都说明当前连接是正常的 后台收到消息后要发送消息确保收到消息
heartCheck.start();
}
这里的错误及失败需要通过重连机制来保证重连
打开连接时开启心跳检测机制,然后在收到消息后重置心跳检测机制的事件。
重启及心跳机制如下:
function reconnect() {
if (lockReconnect) {
return;
}
lockReconnect = true;
//没连接上会一直重连,设置延迟避免请求过多
tt && clearTimeout(tt);
tt = setTimeout(function () {
createWebSocket(channel, name);
lockReconnect = false;
}, 4000);
}
//用来保持一致连接的状态
var heartCheck = {
timeout: 30000, // 心跳检测时长
timeoutObj: null, // 定时变量
serverTimeoutObj: null,
start: function () {
var self = this;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(function () {
ws.send("HeartBeat");
self.serverTimeoutObj = setTimeout(function () {
ws.close();//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
}, self.timeout)
}, this.timeout)
}
}
心跳检测是因为我在使用的时候发现websocket一段时间不用就会自动断开,网上也有很多心跳的资料参考,但是资料中都没有提一个关键的地方onmessage 里面一定要重置心跳检测的时间,并且后台在收到心跳的消息后要发回消息,这样才能成功。
心跳机制在实际应用上我觉得还是比较重要的。
后台需要建立一个专有的控制器来接收websocket的连接请求,并需要注入一个Config的Bean。
package com.cjdjyf.servicehi.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author :cjd
* @description:
* @create 2019-04-12 11:57
**/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
package com.cjdjyf.servicehi.websocket;
import com.cjdjyf.servicehi.redis.Subscriber;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author :cjd
* @description: websocket连接类
* @create 2019-04-12 11:57
**/
@ServerEndpoint(value = "/websocket/{channel}/{name}")
@Component
public class MyWebSocket {
private Session session;
private Thread thread;
private Subscriber subscriber;
private static ConcurrentHashMap map = new ConcurrentHashMap<>();
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(@PathParam(value = "channel") String channel, @PathParam(value = "name") String name,
Session session) {
this.session = session;
Subscriber before = map.get(name);
//如果第一次连接
if (before == null) {
subscriber = new Subscriber(channel, name, this);
thread = new Thread(this.subscriber);
thread.start();
map.put(name, subscriber);
} else {
//多次连接 如果频道和之前的不一样则订阅新的频道并取消之前订阅的频道
if (!before.getChannel().equals(channel)) {
before.unsubscribe();
subscriber = new Subscriber(channel, name, this);
thread = new Thread(this.subscriber);
thread.start();
map.put(name, subscriber);
}
}
/* if (set.add(name)) {
this.subscriber = new Subscriber(channel, name, this);
thread = new Thread(this.subscriber);
thread.start();
}*/
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
//退出后要取消之前订阅的频道并从map中移除
if (thread != null && thread.isAlive()) {
map.remove(subscriber.getName());
subscriber.unsubscribe();
thread.interrupt();
}
}
@OnMessage
public void onMessage(String message) {
try {
sendMessage("心跳检测");
} catch (IOException e) {
e.printStackTrace();
}
}
@OnError
public void onError(Session session, Throwable error) {
}
public void sendMessage(String message) throws IOException {
session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
}
对(一)中的订阅者也作出了略微的修改
package com.cjdjyf.servicehi.redis;
import com.cjdjyf.servicehi.websocket.MyWebSocket;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import javax.websocket.Session;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author :cjd
* @description: 订阅者
* @create 2019-04-11 16:08
**/
public class Subscriber extends JedisPubSub implements Runnable {
private String channel;
private MyWebSocket webSocket;
private String name;
private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public Subscriber(String channel, String name, MyWebSocket webSocket) {
this.channel = channel;
this.name = name;
this.webSocket = webSocket;
}
@Override
public void onMessage(String channel, String message) { //收到消息会调用
try {
webSocket.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onSubscribe(String channel, int subscribedChannels) { //订阅了频道会调用
RedisUtil.publish(channel, format.format(new Date()) + " " + name + "加入聊天室" + channel);
}
@Override
public void onUnsubscribe(String channel, int subscribedChannels) { //取消订阅 会调用
RedisUtil.publish(channel, format.format(new Date()) + " " + name + "退出聊天室" + channel);
}
public void run() {
//开一个线程去订阅,阻塞
Jedis jedis = null;
try {
jedis = RedisUtil.getJedis();
jedis.subscribe(this, channel);
} catch (Exception e) {
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
public MyWebSocket getWebSocket() {
return webSocket;
}
public void setWebSocket(MyWebSocket webSocket) {
this.webSocket = webSocket;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
整体上,我的做法和网上的一些资料有些不同。
实现上主要依赖了Redis的发布/订阅。
本实现是由Redis精准地将消息发送给订阅了频道的WebSocket,再由各自的WebSocket发送消息给各自的前端接收。
还有一种只用WebSocket来实现,是直接将消息发送给自己的WebSocket,再由自己的消息处理中调用别的WebSocket的推送消息方法。
看代码应该可以发现一些不同
我的发送消息给前端代码是集中在订阅者,由Redis的publish触发订阅了该频道的订阅者的事件onMessage然后发送给各自保存下来的webSocket(或session)发送给各自的前端。
@Override
public void onMessage(String channel, String message) { //收到消息会调用
try {
webSocket.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
而网上的一般都是用Map存下webSocket然后通过迭代器循环发送给各自连接的前端。
项目求简单被我放在了之前的SpringBoot下,然后用maven打包成jar包,拖到Linux后
nohup java -jar xxx.jar >xxx.txt &
来进行后台运行
建立脚本
start.sh
#!/bin/bash
nohup java -jar eureka-server-exec.jar >eureka.txt &
nohup java -jar config-server-exec.jar >config.txt &
nohup java -jar service-hi-exec.jar >hi.txt &
stop.sh
#!/bin/bash
PID=$(ps -ef | grep eureka-server-exec.jar | grep -v grep | awk '{ print $2 }')
if [ -z "$PID" ]
then
echo Application is already stopped
else
echo kill $PID
kill $PID
fi
PID=$(ps -ef | grep config-server-exec.jar | grep -v grep | awk '{ print $2 }')
if [ -z "$PID" ]
then
echo Application is already stopped
else
echo kill $PID
kill $PID
fi
PID=$(ps -ef | grep service-hi-exec.jar | grep -v grep | awk '{ print $2 }')
if [ -z "$PID" ]
then
echo Application is already stopped
else
echo kill $PID
kill $PID
fi
授予文件读写权,然后防火墙开端口,就可以在外网访问了。
后续准备优化一下界面及用redis存一下聊天记录。