/**
* websocket消息订阅处理
*/
@Slf4j
@Component
@ServerEndpoint(value = "/socket/subscribe")
@ToString
public class SocketSubscribeEndPoint {
/**
* websocket连接数目
*/
private static AtomicInteger WEBSOCKET_CONNECTION_NUM = new AtomicInteger();
/**
* socket路径携带认证码参数名称
*/
private static final String TOKEN_KEY = "token";
@Getter
private Session session;
@Getter
private String username;
/**
* 订阅的topic主题集合
*/
@Getter
private List<String> socketTopics;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
@Getter
private static final CopyOnWriteArraySet<SocketSubscribeEndPoint> WEB_SOCKET_SET = new CopyOnWriteArraySet<>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
try {
// 如果Session携带token,验证token获取用户名
String token = session.getPathParameters().get(TOKEN_KEY);
if (StringUtils.isNotBlank(token)) {
UserAuthenService userAuthenService = SpringUtil.getBean(UserAuthenService.class);
this.username = userAuthenService.getUsername(token);
}
this.session = session;
// 连接数+1
int num = WEBSOCKET_CONNECTION_NUM.addAndGet(1);
log.info(username +"开开心心进入了直播间,当前人数:{}", num);
} catch (Exception e) {
log.error("websocket连接处理错误", e);
}
}
/**
* 连接成功后收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message) {
if(StringUtils.isBlank(message)){
log.info("客户端发送空字符串消息");
return;
}
Object obj = JSONObject.parse(message);
try {
// 多个topic
if (obj instanceof JSONArray) {
log.debug("接收客户端订阅消息:{}", message);
// 设置多个topic集合
this.socketTopics = JSONObject.parseArray(message, String.class);
//将此webSocket对象放入set集合
WEB_SOCKET_SET.add(this);
//回复客户端消息订阅成功
this.session.getAsyncRemote()
.sendText(JSONObject.toJSONString(
SubscribePushEntity.builder()
.type(SubscribePushEntity.SUB_RESULT_MESSAGE_TYPE)
.content(ResponseData.success("订阅成功"))
.build()));
log.info("订阅成功:{}", this);
return;
}
// 单个topic 不做处理 此场景可用多个topic兼容
if(obj instanceof JSONObject){
log.info("客户端发送JSON对象消息:{}",message);
// WEB_SOCKET_SET.add(this);
return;
}
log.info("客户端发送其它消息:{}",message);
} catch (Exception e) {
log.error("订阅websocket消息处理错误", e);
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
//移除当前连接用户所有订阅信息
WEB_SOCKET_SET.remove(this);
//在线数减1
int num = WEBSOCKET_CONNECTION_NUM.decrementAndGet();
log.info(username + "骂骂咧咧离开了直播间,当前人数:{}", num);
}
/**
* 发生错误时调用
*
* @param error 错误对象
*/
@OnError
public void onError(Throwable error) {
log.error("websocket处理发生错误", error);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public boolean equals(Object obj) {
if(!(obj instanceof SocketSubscribeEndPoint)){
return false;
}
SocketSubscribeEndPoint socketSubscribeEndPoint = (SocketSubscribeEndPoint)obj;
return this.session == socketSubscribeEndPoint.session;
}
}
/**
* 订阅消息回复消息协议实体类
*
*/
@Data
@Builder
@ToString
public class SubscribePushEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 消息类型:1、订阅过程消息、2、业务消息
*/
public static final int SUB_RESULT_MESSAGE_TYPE = 1;
public static final int BUSINESS_MESSAGE_TYPE = 2;
/**
* 回复消息类型
*/
private int type ;
/**
* 订阅的topic
*/
private String topic;
/**
* 消息内容
*/
private Object content;
}
/**
* 订阅消息处理逻辑
*/
@Service
@Slf4j
public class SubscribeServiceImpl implements SocketSubscribeService {
@Override
public void sendMessage(String topic, Object content) {
CopyOnWriteArraySet<SocketSubscribeEndPoint> socketSubscribeEndPoints = SocketSubscribeEndPoint.getWEB_SOCKET_SET();
for (SocketSubscribeEndPoint socketSubscribeEndPoint : socketSubscribeEndPoints) {
// 匹配topic 组装订阅消息回复消息协议实体类 发送消息
Pattern pattern = Pattern.compile("^"+topic.replace("+","[0-9a-zA-Z]+")+"$");
List<String> sendTopics = socketSubscribeEndPoint.getSocketTopics().stream().filter(s->pattern.matcher(s).matches()).collect(Collectors.toList());
if(!CollectionUtils.isEmpty(sendTopics) && sendTopics.size() > 0 ){
try {
socketSubscribeEndPoint.getSession()
.getAsyncRemote().sendText(
JSONObject.toJSONString(
SubscribePushEntity
.builder()
.topic(sendTopics.get(0))
.type(SubscribePushEntity.BUSINESS_MESSAGE_TYPE)
.content(content)
.build()));
}catch (Exception e){
log.error("发送socket消息失败",e);
}
}
}
}
}
/**
* kafka进行websocket消息分发,确保双机情况下客户端订阅任意一台服务器的socket都能收到消息
*/
@Slf4j
@Component
public class KafkaWebSocketPublisher {
@Value(value = "${kafka.websocket.topic}")
private String topic;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Autowired
private SocketSubscribeService socketSubscribeService;
private static final String TOPIC = "topic";
private static final String CONTENT = "content";
/**
* 服务器websocket消息发送到kafka
*
* @param websocketTopic
* @param content
*/
@Async
public void sendMessage(String websocketTopic, MessageContent content) {
JSONObject map = new JSONObject();
map.put(TOPIC, websocketTopic);
map.put(CONTENT, content);
kafkaTemplate.send(topic, map.toJSONString());
}
/**
* 监听kafka中的websocket消息 发送到对应的socket
* @param record
*/
public void sendWebsocket(ConsumerRecord<?, ?> record) {
JSONObject map = JSONObject.parseObject((String) record.value());
socketSubscribeService.sendMessage((String) map.get(TOPIC), map.get(CONTENT));
}
}
/**
* kafka监听消费
*/
@Slf4j
@Component
@AllArgsConstructor
public class KafkaConsumer {
private KafkaStrategyContext kafkaStrategyContext;
private KafkaWebSocketPublisher kafkaWebSocketPublisher;
private SocketTransferService socketTransferService;
private DailyExportThreadManager dailyExportThreadManager;
/**
* 消费监听
*/
@KafkaListener(topics = "#{'${kafka.topics}'.split(',')}")
public void onMessage1(List<ConsumerRecord<?, ?>> record) {
log.info("本次批量处理kafka消息数:{}", record.size());
kafkaStrategyContext.converterData(record);
}
/**
* websocket监听
*/
@KafkaListener(topics = "#{'${kafka.websocket.topic}'}", groupId = "#{'${kafka.websocket.consumer}'}")
public void websocket(List<ConsumerRecord<?, ?>> record) {
log.info("本次批量处理kafka消息数:{}", record.size());
for (ConsumerRecord<?, ?> consumerRecord : record) {
// 消费的哪个topic、partition的消息,打印出消息内容
log.info("kafka websocket消息:" + consumerRecord.topic() + "-" + consumerRecord.partition() + "-" + consumerRecord.value());
// 监听kafka中的websocket消息 发送到对应的socket
kafkaWebSocketPublisher.sendWebsocket(consumerRecord);
}
}
}
/**
* 用户订阅websocket消息格式,前端后台socket消息协议
*/
@Data
@Builder
@ToString
public class MessageContent {
@JsonFormat(pattern = "HH:mm:ss", timezone = "GMT+8")
private Date time;
/**
* 发送消息体
*/
private Object message;
/**
* 消息类型
*/
private String messageType;
}
// 实际业务处理,发送websocket消息
public class XXXServiceServiceImpl extends ServiceImpl<XXXMapper, XXXEntity> implements XXXService {
@Override
public void sendWebSocketMessage(String xxxMessage) {
kafkaWebSocketPublisher.sendMessage("xxxtopic", MessageContent.builder()
.time(new Date())
.messageType("xxxType")
.message(xxxMessage)
.build());
}
//前端
// websocket 初始化
websocketInit () {
try {
this.wsUrl =
process.env.NODE_ENV === 'development'
? process.env.VUE_APP_BASE_API.replace('http', 'ws') + '/api/socket/subscribe' // 本地开发
: 'ws://' + location.host + '/api/socket/subscribe' // 线上部署
this.ws = new WebSocket(this.wsUrl)
// 连接成功
this.ws.onopen = () => {
console.log(
'socket连接成功'
)
this.ws.send(JSON.stringify([`xxxtopic`]))
this.refreshTime = setInterval(() => {
this.ws.send(JSON.stringify([`xxxtopic`]))
}, 30 * 1000)
}
// 报错
this.ws.onerror = err => {
console.log(
'%c err',
err
)
// ws 重连
this.reconnect()
}
// 接收消息
this.ws.onmessage = msg => {
const message = JSON.parse(msg.data)
// console.log(message)
switch (+message.type) {
case 1:
break
case 2:
const obj = {}
// 替换
message.content.message.replace(
/([^(, ]+)=([^, )]+)/g,
(_, k, v) => (obj[k] = v)
)
const { progress, describe } = obj || {}
this.progress = progress * 100
this.describe = describe
}
}
} catch (error) {
console.log(error)
}
},
// 重连
reconnect () {
if (this.lockReconnect) return
this.lockReconnect = true
// 设置重连次数
if (this.reconnectNumber < 5) {
this.reconnectTimer = setTimeout(() => {
console.log(
'websocket重连......!'
)
this.websocketInit()
this.lockReconnect = false
this.reconnectNumber += 1
}, 10 * 1000)
} else {
clearTimeout(this.reconnectTimer)
}
}