前言
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。它对目前主流浏览器(Chrome, Safari,FireFox,IE等)的主流版本兼容性较好;它可以轻松实现客户端和服务器端双向数据传输和通讯并且支持多种数据通信格式(文本和二进制流),适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户与服务频繁交互的情况下,如实时共享、多人协作等平台。
WebSocket规范
在 WebSocket中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,更好的节省服务器资源和带宽,并且能够更实时地进行通讯。浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
STOMP子协议
WebSocket是个规范,在实际的实现中有HTML5规范中的WebSocket API和WebSocket的子协议STOMP。STOMP(Simple Text Oriented Messaging Protocol)简单(流)文本定向消息协议。
STOMP是基于帧的协议,它的前身是TTMP协议(一个简单的基于文本的协议),专为消息中间件设计。是属于消息队列的一种协议, 和AMQP, JMS平级. 它的简单性恰巧可以用于定义websocket的消息体格式. STOMP协议很多MQ都已支持, 比如RabbitMq, ActiveMq。生产者(发送消息)、消息代理、消费者(订阅然后收到消息)。
今天我们就基于STOMP子协议实现一个集群聊和单聊于一体的Web在线聊天室。
代码设计实现
一、服务器部分实现
/**
* @author andychen https://blog.51cto.com/14815984
* @description:STOMP协议配置
*/
/**
* 开启使用Stomp协议来传输基于Broker的消息
* 控制器才支持使用@MessageMapping
*/
@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {
@Resource
private AppConfig config;
/**
* 注册STOMP协议终结点,并映射到指定的URL(客户端访问时需要)
* 并允许跨域访问
* 指定使用SocketJS协议
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(config.stomp_endpoint)
.setAllowedOrigins("*")
.withSockJS();
}
/**
* 注册消息代理
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
String[] prefixs = config.broker_prefixs.split(";");
registry.enableSimpleBroker(prefixs[0],prefixs[1]);
registry.setUserDestinationPrefix(config.stomp_queue_name);
}
}
/**
* @author andychen https://blog.51cto.com/14815984
* @description:WebMVC 配置
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private AppConfig config;
/**
* 配置Controller和View的映射
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController(config.mvc_path)
.setViewName(config.mvc_view);
}
}
/**
* @author andychen https://blog.51cto.com/14815984
* @description:应用配置类
*/
@Component
public class AppConfig {
@Value("${config.mvc_path}")
public String mvc_path;
@Value("${config.mvc_vew}")
public String mvc_view;
@Value("${config.stomp_endpoint}")
public String stomp_endpoint;
@Value("${config.stomp_broker_prefixs}")
public String broker_prefixs;
@Value("${config.stomp_queue_name}")
public String stomp_queue_name;
}
/**
* @author andychen https://blog.51cto.com/14815984
* @description:STOMP在线聊天室控制器类
*/
@Controller
public class StompController {
/**
* 定义消息发送模板
*/
@Autowired
private SimpMessagingTemplate sendTemplate;
/**
* 群聊:发给所有订阅了代理并加入群聊的用户
* @return
*/
@MessageMapping("/mass/request")
/**
* 先发送到用户订阅的代理队列中,Broker再转发到订阅的用户终端
*/
@SendTo("/mass/send")
public ChatMessage mass(ChatMessage message){
System.out.println("sender:"+message.getSender()+", message content:"
+message.getContent());
return message;
}
/**
* 一对一单聊
* @return 发送到单个请求的用户端
*/
@MessageMapping("/single/request")
public ChatMessage single(ChatMessage message){
System.out.println("sender:"+message.getSender()+", message content:"
+message.getContent()+", recevier:"+message.getRecevier());
this.sendTemplate.convertAndSendToUser(message.getRecevier(), "/single", message);
return message;
}
}
/**
* @author andychen https://blog.51cto.com/14815984
* @description:聊天消息实体
*/
public class ChatMessage {
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getRecevier() {
return recevier;
}
public void setRecevier(String recevier) {
this.recevier = recevier;
}
/**
* 发送者
*/
private String sender;
/**
* 消息内容
*/
private String content;
/**
* 接受者
*/
private String recevier;
}
二、Web和JS部分实现
html>
xmlns:th="http://www.thymeleaf.org">
charset="UTF-8">
name="aplus-terminal" content="1">
name="apple-mobile-web-app-title" content="">
name="apple-mobile-web-app-capable" content="yes">
name="apple-mobile-web-app-status-bar-style" content="black-translucent">
name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
name="format-detection" content="telephone=no, address=no">
STOMP在线聊天室
rel="stylesheet" th:href="@{/css/chatroom.css}" type="text/css"/>
class="window_frame">
style="font-weight: bold;">选择你的网名:
style="font-weight: bold;">群聊:
class="chatWindow">
class="chatRecord">
id="mass_div" class="mobile-page">
class="sendWindow">
type="button" id="btnSend" value="发送" class="send_btn"/>
class="window_frame">
style="font-weight: bold;">选择聊天的对象:
style="font-weight: bold;">单聊:
class="chatWindow">
class="chatRecord">
id="single_div" class="mobile-page">
class="sendWindow">
type="button" id="btnSend2" value="发送" class="send_btn"/>
ChatRoom = {
client:null
};
/**
* 选择发送人
*/
ChatRoom.selectSender = function () {
ChatRoom.subscribeSingle();
};
/**
* 发送群聊信息
*/
ChatRoom.sendMassMsg = function () {
let msg = {};
let content = $("#txtContent").val().trim();
let userName = $("#selectSender").val();
msg.sender = userName;
msg.content = content;
if("" === userName){
alert("请选择你的身份!")
return;
}
if("" === content){
alert("发送的消息内容不能为空!")
return;
}
ChatRoom.client.send("/mass/request", {}, JSON.stringify(msg));
$("#txtContent").val("")
}
/**
* 发送单聊信息
*/
ChatRoom.sendSingleMsg = function () {
let content = $("#txtContent2").val().trim();
let sender = $("#selectSender").val();
let recevier = $("#selectRecevier").val().trim();
let container = $("#single_div");
let msg = {
sender: sender,
content: content,
recevier: recevier
};
if("" === sender){
alert("请选择你的身份!");
return;
}
if("" === recevier){
alert("请选择消息接收人!");
return;
}
if("" === content){
alert("发送消息不能为空!");
return;
}
ChatRoom.client.send("/single/request",{}, JSON.stringify(msg));
container.append("" +
" " +
" "+content+"" +
" " +
" " +sender+
" ");
$("#txtContent2").val("");
};
/**
* 连接服务器
*/
ChatRoom.connect = function () {
try{
let socket = new SockJS('/chatendpoint1');//采用SockJS连接服务器endpoint:chatendpoint1
ChatRoom.client = Stomp.over(socket);//使用STOMP协议
ChatRoom.client.connect({}, function (info) {
console.log("服务器连接: "+info);
//订阅广播消息
ChatRoom.subscribeMass();
});
}catch (e) {
console.log("连接异常:"+e);
}
};
/**
* 服务器广播消息订阅
*/
ChatRoom.subscribeMass = function(){
ChatRoom.client.subscribe('/mass/send', function (data) {
let msgObj = JSON.parse(data.body);
//渲染消息
let container = $("#mass_div");
let userName = $("#selectSender").val();
if(msgObj.sender === userName){
container.append("" +
" " +
" "+msgObj.content+"" +
" " +
" " +userName+
" ");
}else{
container.append(" "+
msgObj.sender+
""+
" "+
" "+msgObj.content+""+
""+
"");
}
});
};
/**
* 服务器单播消息订阅
*/
ChatRoom.subscribeSingle = function(){
let userName = $("#selectSender").val();
if("" === userName){
alert("请选择你的身份");
return;
}
//alert("开始监听用户端:"+userName);
$("title").text('STOMP在线聊天室 - '+userName);
//订阅特定用户发送的消息
ChatRoom.client.subscribe("/squeue/"+userName+"/single", function (data) {
let msgObj = JSON.parse(data.body);
let container = $("#single_div");
container.append(" "+
msgObj.sender+
""+
" "+
" "+msgObj.content+""+
""+
"");
});
};
/**
* 断开连接
*/
ChatRoom.disconnect = function (server) {
if(null != ChatRoom.client){
ChatRoom.client.disconnect();
}
console.log(server);
console.log("断开与服务器连接!");
};
/**
* 窗口卸载之前事件
*/
window.onbeforeunload = function () {
ChatRoom.disconnect();
}
/**
* 页面加载完成后
*/
$(function () {
//建立连接
ChatRoom.connect();
//注册事件
$("#selectSender").change(function () {
ChatRoom.selectSender();
});
$("#btnSend").click(function () {
ChatRoom.sendMassMsg();
});
$("#btnSend2").click(function () {
ChatRoom.sendSingleMsg()
});
});
结果验证
群聊相关截图
单聊截图
总结
以上就是采用STOMP子协议实现WebSocket通讯的关键代码和过程。其实实现WebSocket的方法不止一种,除了STOMP外还有,原生的WebSocket协议实现方式、Netty的是实现方式等。后面我们将重点基于后两种实现WebSocket高实时性网络通讯,请继续关注!有任何关于这块的问题,请下方留言,一起讨论。