1、WebSocket 简介
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
2.前提
单机的消息推送实现十分简单,通过websocket中转就可以了,但是在分布式环境下不支持session共享因为服务器不同,所以可以采用 rabbitMQ+webSocket实现分布式消息推送。
项目基于之前的项目改造,首先要集成RabbitMQ,WebSokcet,主要的实现思想是:
客户端通过websocket注册到各自的服务器上,服务器绑定到同一个同一个MQ队列上,这样每次客户端发消息,先是投放到MQ中,再由绑定到队列上的消费者去发送socket信息,进而实现跨服务器的消息推送。
3.代码实现
这里贴一下主要的代码
开启WebSocket支持:
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketServer:
@ServerEndpoint("/websocket/{from}")
@Component
public class WebSocketServer {
private static Logger logger = LoggerFactory.getLogger(WebSocketServer.class);
private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<String, WebSocketServer>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//from
private static String from = "";
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session,@PathParam("from") String from) {
this.session = session;
webSocketMap.put(from, this); //加入set中
addOnlineCount(); //在线数加1
logger.info("有新窗口开始监听:"+from+",当前在线人数为" + getOnlineCount());
this.from=from;
try {
sendMessage("连接成功");
} catch (IOException e) {
logger.error("websocket IO异常");
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketMap.remove(this); //从set中删除
subOnlineCount(); //在线数减1
logger.info("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
logger.info("收到来自窗口"+from+"的信息:"+message);
JSONObject obj = new JSONObject();
obj.put("cmd", "heartcheck");//业务类型
obj.put("msgTxt", "服务端心跳响应 ");//消息内容
obj.put("msgDate", DateUtils.getCurrentDateTime());//时间
session.getAsyncRemote().sendText(obj.toJSONString());
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
logger.error("发生错误");
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 群发自定义消息
* */
public static void sendMqMessage(String message) throws IOException {
JSONObject jsonObject = new JSONObject();
logger.info("推送消息到窗口"+from+",推送内容:"+message);
ChatMsg chatMsg = JSONObject.parseObject(message, ChatMsg.class);
if(chatMsg != null && chatMsg.getTo() != null && webSocketMap.containsKey(chatMsg.getTo())){
webSocketMap.get(chatMsg.getTo()).sendMessage(message);
}else{
logger.error("用户"+chatMsg.getTo()+",不在线!");
}
}
/**
* 群发自定义消息
* */
public static void sendInfo(String message,@PathParam("from") String from) throws IOException {
JSONObject jsonObject = new JSONObject();
logger.info("推送消息到窗口"+from+",推送内容:"+message);
// for (WebSocketServer item : webSocketMap) {
// try {
// String date = format.format(new Date());
// String mes = message+ " (" + date + ")";
// jsonObject.put("mes",mes);
// jsonObject.put("sender",from);
// //这里可以设定只推送给这个sid的,为null则全部推送
// if(from==null) {
// item.sendMessage(jsonObject.toString());
// }else if(item.from.equals(from)){
// item.sendMessage(jsonObject.toString());
// }
// } 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--;
}
}
创建消息队列:
@Configuration
public class FanoutRabbitConfig {
public static final String DEFAULT_BOOK_QUEUE = "dev.book.fanout.a.queue";
@Bean
public Queue queueMessageA() {
// 第一个是 QUEUE 的名字,第二个是消息是否需要持久化处理
return new Queue(DEFAULT_BOOK_QUEUE, true);
}
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange("fanoutExchange");
}
@Bean
Binding bindingExchangeMessage(Queue queueMessageA, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queueMessageA).to(fanoutExchange);
}
}
消费者:
@Component
public class BookHandler {
private static final Logger logger = LoggerFactory.getLogger(BookHandler.class);
@RabbitListener(queues = {FanoutRabbitConfig.DEFAULT_BOOK_QUEUE})
public void listenerAutoAck(String text, Message message, Channel channel) {
// TODO 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
final long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
logger.info("[消费者一监听的消息] - [{}]", text);
new WebSocketServer().sendMqMessage(text);
// TODO 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
try {
// TODO 处理失败,重新压入MQ
channel.basicRecover();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
消息类:
public class ChatMsg {
private String from;//发送的username
private String to;//接收者
private String content;//内容
private Date date;//时间
private String fromNickname;//昵称
public String getFromNickname() {
return fromNickname;
}
public void setFromNickname(String fromNickname) {
this.fromNickname = fromNickname;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}
发送消息controller:
@Controller
@RequestMapping("/testmq")
public class MqTestController {
private static Logger logger = LoggerFactory.getLogger(MqTestController.class);
@Autowired
private RabbitTemplate rabbitTemplate; //rabbitTemplate是springboot 提供的默认实现
@RequestMapping(value="/send")
@ResponseBody
public void defaultMessage(String message, String from, String to) {
ChatMsg chatMsg = new ChatMsg();
chatMsg.setFrom(from);
chatMsg.setTo(to);
chatMsg.setContent(message);
chatMsg.setDate(new Date());
rabbitTemplate.convertAndSend("fanoutExchange", "", JSONObject.toJSONString(chatMsg));
}
}
pom依赖:
<!-- rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- Websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置文件:
#rabbitmq 配置
spring:
rabbitmq:
host: 192.0.0.171
port: 5672
username: admin
password: 123456
#虚拟主机
virtual-host: /
listener:
simple:
#手动ACK
acknowledge-mode: manual
4.测试
首先打两个jar包,使用两个不同端口运行起来,打开两个浏览器窗口进行测试。
经过简单测试没有问题。
欢迎关注个人公众号