本片以使用为主,概念请移动百度
不做过多解释,备注的很详细
参考:https://www.zifangsky.cn/1364.html
分布式WebSocket一般可以通过以下两种方案来实现:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.49version>
dependency>
spring:
# rabbitmq 配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: yang
password: yang
publisher-confirms: true #支持发布确认
publisher-returns: true #支持发布返回
listener:
simple:
acknowledge-mode: manual #采用手动应答
concurrency: 1 #指定最小的消费者数量
max-concurrency: 3 #指定最大的消费者数量
retry:
enabled: true #是否支持重试
logging:
#打印sql
level:
com.it.cloud.modules: debug
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* RabbitMQ配置
*
* @author 司马缸砸缸了
* @date 2019/7/29 13:44
* @description
*/
@Configuration
public class RabbitConfig {
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 定制化amqp模版 可根据需要定制多个
*
*
* 此处为模版类定义 Jackson消息转换器
* ConfirmCallback接口用于实现消息发送到RabbitMQ交换器后接收ack回调 即消息发送到exchange ack
* ReturnCallback接口用于实现消息发送到RabbitMQ 交换器,但无相应队列与交换器绑定时的回调 即消息发送不到任何一个队列中 ack
*
* @return the amqp template
*/
// @Primary
@Bean
public AmqpTemplate amqpTemplate() {
Logger log = LoggerFactory.getLogger(RabbitTemplate.class);
// 使用jackson 消息转换器, 传输对象时屏蔽掉,防止二次json转换,多了一个"
// rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
rabbitTemplate.setEncoding("UTF-8");
// 消息发送失败返回到队列中,yml需要配置 publisher-returns: true
// 当mandatory标志位设置为true时,如果exchange根据自身类型和消息routingKey无法找到一个合适的queue存储消息,
// 那么broker会调用basic.return方法将消息返还给生产者;
// 当mandatory设置为false时,出现上述情况broker会直接将消息丢弃;
// 通俗的讲,mandatory标志告诉broker代理服务器至少将消息route到一个队列中,否则就将消息return给发送者
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
String correlationId = message.getMessageProperties().getCorrelationId();
log.debug("消息:{} 发送失败, 应答码:{} 原因:{} 交换机: {} 路由键: {}", correlationId, replyCode, replyText, exchange, routingKey);
});
// 消息确认,yml需要配置 publisher-confirms: true
// 1.消费者确认 2.exchange没有路由到queue
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.debug("消息发送到exchange成功,id: {}", correlationData.getId());
} else {
log.debug("消息发送到exchange失败,原因: {}", cause);
}
});
return rabbitTemplate;
}
/* ----------------------------------------------------------------------------Direct exchange test--------------------------------------------------------------------------- */
/**
* 声明Direct交换机 支持持久化.
*
* @return the exchange
*/
@Bean("directExchange")
public Exchange directExchange() {
return ExchangeBuilder.directExchange("DIRECT_EXCHANGE").durable(true).build();
}
/**
* 声明一个队列 支持持久化.
*
* @return the queue
*/
@Bean("directQueue")
public Queue directQueue() {
return QueueBuilder.durable("DIRECT_QUEUE").build();
}
/**
* 通过绑定键 将指定队列绑定到一个指定的交换机 .
*
* @param queue the queue
* @param exchange the exchange
* @return the binding
*/
@Bean
public Binding directBinding(@Qualifier("directQueue") Queue queue,
@Qualifier("directExchange") Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("DIRECT_ROUTING_KEY").noargs();
}
/* ----------------------------------------------------------------------------Topic exchange test--------------------------------------------------------------------------- */
/**
* 声明 topic 交换机.
*
* @return the exchange
*/
@Bean("topicExchange")
public TopicExchange topicExchange() {
return (TopicExchange) ExchangeBuilder.topicExchange("TOPIC_EXCHANGE").durable(true).build();
}
/**
* Fanout queue A.
*
* @return the queue
*/
@Bean("topicQueueA")
public Queue topicQueueA() {
return QueueBuilder.durable("TOPIC_QUEUE_A").build();
}
/**
* 绑定队列A 到Topic 交换机.
*
* @param queue the queue
* @param topicExchange the topic exchange
* @return the binding
*/
@Bean
public Binding topicBinding(@Qualifier("topicQueueA") Queue queue,
@Qualifier("topicExchange") TopicExchange topicExchange) {
return BindingBuilder.bind(queue).to(topicExchange).with("TOPIC.ROUTE.KEY.*");
}
/* ----------------------------------------------------------------------------Fanout exchange test--------------------------------------------------------------------------- */
/**
* 声明 fanout 交换机.
*
* @return the exchange
*/
@Bean("fanoutExchange")
public FanoutExchange fanoutExchange() {
return (FanoutExchange) ExchangeBuilder.fanoutExchange("FANOUT_EXCHANGE").durable(true).build();
}
/**
* Fanout queue A.
*
* @return the queue
*/
@Bean("fanoutQueueA")
public Queue fanoutQueueA() {
return QueueBuilder.durable("FANOUT_QUEUE_A").build();
}
/**
* Fanout queue B .
*
* @return the queue
*/
@Bean("fanoutQueueB")
public Queue fanoutQueueB() {
return QueueBuilder.durable("FANOUT_QUEUE_B").build();
}
/**
* 绑定队列A 到Fanout 交换机.
*
* @param queue the queue
* @param fanoutExchange the fanout exchange
* @return the binding
*/
@Bean
public Binding bindingA(@Qualifier("fanoutQueueA") Queue queue,
@Qualifier("fanoutExchange") FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queue).to(fanoutExchange);
}
/**
* 绑定队列B 到Fanout 交换机.
*
* @param queue the queue
* @param fanoutExchange the fanout exchange
* @return the binding
*/
@Bean
public Binding bindingB(@Qualifier("fanoutQueueB") Queue queue,
@Qualifier("fanoutExchange") FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queue).to(fanoutExchange);
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* 开启WebSocket支持
*
* @author 司马缸砸缸了
* @date 2019/7/29 13:44
* @description
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketServer.java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
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.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* websocket服务端
*
* @author 司马缸砸缸了
* @date 2019/7/29 13:46
*/
@ServerEndpoint("/websocket/{userId}")
@Component
@Slf4j
public class WebSocketServer {
// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
// concurrent包的线程安全Map,用来存放每个客户端对应的MyWebSocket对象。
private static ConcurrentHashMap<String, WebSocketServer> websocketMap = new ConcurrentHashMap<>();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
// 接收sid
private String userId = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
websocketMap.put(userId, this);
log.info("websocketMap->" + JSON.toJSONString(websocketMap));
// webSocketSet.add(this); //加入set中
addOnlineCount(); // 在线数加1
log.info("有新窗口开始监听:" + userId + ",当前在线连接数为" + getOnlineCount());
this.userId = userId;
try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("websocket IO异常");
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if (websocketMap.get(this.userId) != null) {
websocketMap.remove(this.userId);
// webSocketSet.remove(this); //从set中删除
subOnlineCount(); // 在线数减1
log.info("有一连接关闭!当前在线连接数为" + getOnlineCount());
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到来自窗口" + userId + "的信息:" + message);
if (StringUtils.isNotBlank(message)) {
JSONArray list = JSONArray.parseArray(message);
for (int i = 0; i < list.size(); i++) {
try {
// 解析发送的报文
JSONObject object = list.getJSONObject(i);
String toUserId = object.getString("toUserId");
String contentText = object.getString("contentText");
object.put("fromUserId", this.userId);
// 传送给对应用户的websocket
if (StringUtils.isNotBlank(toUserId) && StringUtils.isNotBlank(contentText)) {
WebSocketServer socketx = websocketMap.get(toUserId);
// 需要进行转换,userId
if (socketx != null) {
socketx.sendMessage(JSON.toJSONString(object));
// 此处可以放置相关业务代码,例如存储到数据库
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 单发自定义消息
*/
public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
log.info("推送消息到窗口" + userId + ",推送内容:" + message);
// 可以通过SpringContextUtils得到bean,进行数据库操作
WebSocketServer webSocketServer = websocketMap.get(userId);
if (webSocketServer != null) {
webSocketServer.sendMessage(message);
}
}
/**
* 群发自定义消息
*/
public static void sendInfoAll(String message) throws IOException {
log.info("推送消息到所有窗口,推送内容:" + message);
for (Map.Entry<String, WebSocketServer> entry : websocketMap.entrySet()) {
WebSocketServer item = entry.getValue();
try {
item.sendMessage(message);
} catch (IOException e) {
continue;
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
public static ConcurrentHashMap<String, WebSocketServer> getSessionmap() {
return WebSocketServer.websocketMap;
}
}
SocketController.java
import cn.hutool.json.JSONUtil;
import com.it.cloud.common.base.Result;
import com.it.cloud.modules.rabbitmq.producer.MqService;
import com.it.cloud.modules.websocket.WebSocketServer;
import com.it.cloud.modules.websocket.dto.SocketMessageDTO;
import org.apache.commons.lang.SerializationUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.io.IOException;
/**
* wbsocket 推送
*
* @author 司马缸砸缸了
* @date 2019/7/29 13:44
* @description
*/
@RestController
@RequestMapping("/socket")
public class SocketController {
@Autowired
private MqService mqService;
// 跳转页面
/* @GetMapping("/{userId}")
public ModelAndView socket(@PathVariable String userId) {
ModelAndView mav=new ModelAndView("/websocket");
mav.addObject("userId", userId);
return mav;
}*/
//推送数据接口
@ResponseBody
@RequestMapping("/push/{userId}")
public Result pushToWeb(@PathVariable String userId, String message) {
SocketMessageDTO dto = new SocketMessageDTO();
dto.setUserId(userId);
dto.setMessage(message);
// 发送到消息队列,广播模式
mqService.fanout(JSONUtil.toJsonStr(dto));
/*try {
WebSocketServer.sendInfo(message,userId);
} catch (IOException e) {
e.printStackTrace();
return Result.error(userId+"#"+e.getMessage());
}*/
return Result.ok(userId);
}
}
SocketMessageDTO.java
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author yangyang7_kzx
* @date 2019/7/29 17:35
* @description socket消息
*/
@ApiModel(value = "Socket消息体", description = "Socket消息体")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SocketMessageDTO implements Serializable {
@ApiModelProperty(value = "socket userId")
private String userId;
@ApiModelProperty(value = "消息")
private String message;
}
MqService.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.UUID;
/**
* 消息发送服务
*
* @author 司马缸砸缸了
* @date 2019/7/29 13:44
* @description
*/
@Slf4j
@Service
public class MqService {
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 测试广播模式.
*
* @param message
* @return the response entity
*/
public void fanout(String message) {
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("FANOUT_EXCHANGE", "", message, correlationData);
}
/**
* 测试Direct模式.
*
* @param p the p
* @return the response entity
*/
// public void direct(String p) {
// CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// rabbitTemplate.convertAndSend("DIRECT_EXCHANGE", "DIRECT_ROUTING_KEY", p, correlationData);
// }
}
MqService.java
import cn.hutool.json.JSONUtil;
import com.it.cloud.common.base.Result;
import com.it.cloud.modules.websocket.WebSocketServer;
import com.it.cloud.modules.websocket.dto.SocketMessageDTO;
import com.rabbitmq.client.Channel;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 消息监听器
*
* @author 司马缸砸缸了
* @date 2019/7/29 13:44
* @description
*/
@Component
public class MqConsumer {
private static final Logger log = LoggerFactory.getLogger(MqConsumer.class);
/**
* FANOUT广播队列监听一.
*
* @param message the message
* @param channel the channel
* @throws IOException the io exception 这里异常需要处理
*/
@RabbitListener(queues = {"FANOUT_QUEUE_A"})
public void on(Message message, Channel channel) throws IOException {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
String responsJson = new String(message.getBody());
log.debug("consumer FANOUT_QUEUE_A : " + responsJson);
//发送给浏览器
if (StringUtils.isNotBlank(responsJson)) {
SocketMessageDTO dto = JSONUtil.toBean(responsJson, SocketMessageDTO.class);
String userId = dto.getUserId();
String msg = dto.getMessage();
if (WebSocketServer.getSessionmap().get(userId) != null) {
try {
//推送消息到页面
WebSocketServer.sendInfo(msg, userId);
} catch (IOException e) {
e.printStackTrace();
log.error("socket send error,userId=" + userId + "#" + e.getMessage());
}
}
}
}
/**
* FANOUT广播队列监听二.
*
* @param message the message
* @param channel the channel
* @throws IOException the io exception 这里异常需要处理
*/
@RabbitListener(queues = {"FANOUT_QUEUE_B"})
public void t(Message message, Channel channel) throws IOException {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
log.debug("FANOUT_QUEUE_B " + new String(message.getBody()));
}
/**
* DIRECT模式.
*
* @param message the message
* @param channel the channel
* @throws IOException the io exception 这里异常需要处理
*/
@RabbitListener(queues = {"DIRECT_QUEUE"})
public void message(Message message, Channel channel) throws IOException {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
log.debug("DIRECT " + new String(message.getBody()));
}
}
<script>
export default {
data () {
return {
path: 'ws://localhost:8081/websocket/20',
socket: ''
}
},
mounted () {
// 初始化
this.init()
},
methods: {
init: function () {
if (typeof WebSocket === 'undefined') {
alert('您的浏览器不支持socket')
} else {
// 实例化socket
this.socket = new WebSocket(this.path)
// 监听socket连接
this.socket.onopen = this.open
// 监听socket错误信息
this.socket.onerror = this.error
// 监听socket消息
this.socket.onmessage = this.getMessage
}
},
open: function () {
console.log('socket连接成功')
},
error: function () {
console.log('连接错误')
},
getMessage: function (msg) {
console.log(msg.data)
},
send: function () {
this.socket.send('哈哈')
},
close: function () {
console.log('socket已经关闭')
}
},
destroyed () {
// 销毁监听
this.socket.onclose = this.close
}
}
script>
浏览器访问:http://localhost:8081/socket/push/20?message=aaaaaaaaaaa
之后在前端控制台Console中查看消息
稍后更新,需要请联系
开源项目,持续不断更新中,喜欢请 Star~
IT-CLOUD :IT服务管理平台,集成基础服务,中间件服务,监控告警服务等