WebSocket 是 HTML5 开始提供的一种在 TCP 上进行的套接字全双工通讯协议,可以实现客户端与服务器端的异步通信,服务器的推送功能。WebSocket 和 Http 的区别如下所示。
WebSocket 是基于 TCP 通信,浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,首次的请求是使用的 HTTP 连接,连接之后浏览器和服务器之间就形成了一条快速通道。两者之间就可以通过 TCP 通道进行数据交互。
WebSocket 传输的数据格式比较轻量,可以发送纯文本,也可以直接发送二进制数据。和传统的 Http 长连接相比,性能开销小,通信高效。如图所示 和 http 一样它有有加密方式 ws(不加密) 和 wss(加密)。
WebSocket 是 HTML5 增加的通信协议,并不是所有的浏览器都支持,目前可支持的版本如下所示。
在Spring 中开发 WebSocket 框架主要基于 spring-websocket 和 spring-messaging 库。
在 WebSocket 中,主要包含如下信息。
在浏览器我们并不是直接使用原生的的 WebSocket 协议,而是使用更高级的 SockJS 和 StompJS。
为了应对许多浏览器不支持 WebSocket 协议的问题,Spring 对备选协议 SockJS 提供了支持。
SockJS 的请求格式如下:
http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
SockJS 是 WebSocket 的备选方案,但它同样是一种偏底层的协议,Spring 建议我们使用它们的高级协议 STOMP。
STOMP 是一种简单的面向文本的消息传递协议,其前身是 TTMP 协议(一个简单的基于文本的协议),专为消息中间件设计,如 ActiveMQ。它提供了一个可互操作的连接格式,允许 STOMP 客户端与任意 STOMP 消息代理(Broker)进行交互。虽然 STOMP 是一种面向文本的协议,但消息的有效负载可以是文本或二进制文件。
同 HTTP 在 TCP 套接字上添加请求-响应模型层一样,STOMP 在 WebSocket 之上提供了一个基于帧的线路格式层,用来定义消息语义。
STOMP 帧由命令,一个或多个头信息以及负载所组成。如下就是一个发送数据的 STOMP 帧:
SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@
WebSocket 的 javascript 开发使用 SockJS 和 Stomp。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html lang="en">
<head>
<title>WebSockettitle>
<script src="${pageContext.request.contextPath}/js/jquery.js">script>
<script src="${pageContext.request.contextPath}/js/sockjs.js">script>
<script src="${pageContext.request.contextPath}/js/stomp.js">script>
<script type="text/javascript">
$(document).ready(function () {
setConnected(false);
});
var stompClient = null;
// 连接
function connect() {
var userId = $("#userId").val();
var socket = new SockJS("/stomp");
stompClient = Stomp.over(socket);
stompClient.connect({'userId': userId}, function (frame) {
setConnected(true);
console.log('连接成功: ' + frame);
showGreeting("连接成功");
// 广播
stompClient.subscribe('/topic/message', function (greeting) {
console.log('subscribe: ' + greeting);
showGreeting(JSON.parse(greeting.body).content);
});
// 一对一通信
stompClient.subscribe('/user/' + userId + '/message', function (greeting) {
console.log('subscribe: ' + greeting);
showGreeting(JSON.parse(greeting.body).content);
});
}, function (error) {
console.log('connect error: ' + error);
alert(error);
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect(function () {
setConnected(false);
console.log("断开");
});
}
}
function send() {
if (stompClient.connected) {
var body = JSON.stringify({
'userId': $("#send_userId").val(),
'content': $("#send_content").val()
});
stompClient.send("/app/stomp/send", {'type': 'text'}, body);
console.log("send: " + body);
} else {
setConnected(false);
}
}
// 显示信息
function showGreeting(message) {
var response = document.getElementById('response');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(message));
response.appendChild(p);
}
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversationDiv").show();
} else {
$("#conversationDiv").hide();
}
$("#response").html('');
}
script>
head>
<body>
<div>
<div>
<label>登录label><br/>
<input type="text" id="userId" placeholder="输入账号" value="937447974"/>
<button id="connect" onclick="connect();">连接button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">断开button>
div>
<div id="conversationDiv">
<label>发送消息label>
<br/>
<input type="text" id="send_userId" placeholder="对方账号"/>
<input type="text" id="send_content" placeholder="输入信息"/>
<button onclick="send();">发送button>
<br/>
<label>控制台label><br/>
<p id="response">p>
div>
div>
body>
html>
可以看到客户端开发只需要建立连接,添加订阅,发送消息和断开连接等相关功能。
在 xml 中 配置 SpringMVC 的相关配置。WebSocket 配置使用 Java 的方式开发,这有利于模块独立化。
package com.websocket.config;
/**
* WebsocketConfig.java
* Websocket 配置
* Created by 阳君 on 2017/10/20.
* Copyright © 2017年 websocket. All rights reserved.
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
private InboundChannelInterceptor inboundChannelInterceptor;
// 连接站点配置
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp") // stomp 连接点
.addInterceptors(new AuthHandshakeInterceptor()) // 拦截器
.setAllowedOrigins("*")
.withSockJS();
}
// 消息传输参数配置
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
super.configureWebSocketTransport(registration);
registration.setSendTimeLimit(15 * 1000) // 超时时间
.setSendBufferSizeLimit(512 * 1024) // 缓存空间
.setMessageSizeLimit(128 * 1024); // 消息大小
}
// 输入通道配置
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
super.configureClientInboundChannel(registration);
registration.interceptors(this.inboundChannelInterceptor);// 设置拦截器
registration.taskExecutor() // 线程信息
.corePoolSize(400) // 核心线程池
.maxPoolSize(800) // 最多线程池数
.keepAliveSeconds(60); // 超过核心线程数后,空闲线程超时60秒则杀死
}
// 输出通道配置
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
super.configureClientOutboundChannel(registration);
}
// 配置消息转化器
@Override
public boolean configureMessageConverters(List messageConverters) {
return super.configureMessageConverters(messageConverters);
}
// 配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
super.configureMessageBroker(registry);
registry.enableSimpleBroker("/topic", "/user"); // 推送消息前缀
registry.setApplicationDestinationPrefixes("/app") // 应用请求前缀
.setUserDestinationPrefix("/user/"); // 推送用户前缀
}
}
WebSocketConfig 有丰富的配置,我们可以定义很多的操作。
WebSocket 首次连接时,会使用 Http 的连接方式,默认使用了 OriginHandshakeInterceptor 拦截,它主要做域名拦截,我们可以通过继承它做其他连接处理。
package com.websocket.config;
/**
* AuthHandshakeInterceptor.java
* 握手拦截器
* Created by 阳君 on 2017/10/21.
* Copyright © 2017年 websocket. All rights reserved.
*/
public class AuthHandshakeInterceptor extends OriginHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler
wsHandler, Map attributes) throws Exception {
System.out.println("beforeHandshake");
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
@Nullable Exception ex) {
System.out.println("afterHandshake");
super.afterHandshake(request, response, wsHandler, ex);
}
}
这个类中的方法,只会在连接时执行一次,数据传输过程中会走通道拦截。
WebSockt 是通过 Channel 传输数据,通过继承 ChannelInterceptorAdapter 可做到对输入和输出数据的拦截处理。
package com.websocket.config;
/**
* InboundChannelInterceptor.java
* 输入通道拦截器
* Created by 阳君 on 2017/10/21.
* Copyright © 2017年 websocket. All rights reserved.
*/
@Component
public class InboundChannelInterceptor extends ChannelInterceptorAdapter {
@Autowired
private WebSocketService webSocketService;
@Override
public Message> preSend(Message> message, MessageChannel channel) {
System.out.println("preSend:" + message.getHeaders());
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
StompCommand stompCommand = accessor.getCommand();
if (StompCommand.CONNECT.equals(stompCommand)) {
String userId = accessor.getFirstNativeHeader("userId");
String simpSessionId = accessor.getHeader("simpSessionId").toString();
this.webSocketService.connect(simpSessionId, userId);
} else if (StompCommand.DISCONNECT.equals(stompCommand)) {
this.webSocketService.disconnect(accessor.getHeader("simpSessionId").toString());
}
return super.preSend(message, channel);
}
@Override
public void postSend(Message> message, MessageChannel channel, boolean sent) {
System.out.println("postSend输入数据处理后");
super.postSend(message, channel, sent);
}
@Override
public void afterSendCompletion(Message> message, MessageChannel channel, boolean sent, @Nullable Exception ex) {
super.afterSendCompletion(message, channel, sent, ex);
System.out.println("afterSendCompletion");
}
}
输入通道的拦截可以自定义相关命令的实现,如连接和断开。
控制器写法和 SpringMVC 类似,主要是使用 @MessageMapping 做匹配处理。
package com.websocket.controller;
/**
* WebSocketController.java
*
* Created by 阳君 on 2017/10/20.
* Copyright © 2017年 websocket. All rights reserved.
*/
@MessageMapping("/stomp")
@RestController
public class WebSocketController {
// @SendTo("/topic/message") 广播发送给 /topic/message 订阅客户端
// @SendToUser("1/message") 发送给指定订阅用户
@Autowired
public WebSocketService webSocketService;
@MessageMapping("/send")
public void sendBroadcast(@Headers Map headers, @Header String simpSessionId, TextMessage
textMessage) {
TextMessage sendTM = new TextMessage(this.webSocketService.getPool().get(simpSessionId) + ": " + textMessage
.getContent());
String userId = textMessage.getUserId();
if (StringUtils.isEmpty(userId)) {
this.webSocketService.send(sendTM);
} else {
this.webSocketService.sendToUser(userId, sendTM);
}
}
}
@SendTo 和 @SendToUser 可以做返回数据处理,但实际开发中,多数会自定义返回操作。
业务层 WebSocketService 实现了登录、断开、群发和定点发送消息的功能。
package com.websocket.service;
/**
* WebSocketService.java
* 服务层
* Created by 阳君 on 2017/10/22.
* Copyright © 2017年 websocket. All rights reserved.
*/
@Service
public class WebSocketService {
private Map pool; // 连接池
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@PostConstruct
void init() {
this.pool = new HashMap();
}
public Map getPool() {
return pool;
}
/**
* 连接
*
* @param simpSessionId 会话id
* @param userId 用户id
*/
public void connect(String simpSessionId, String userId) {
if (!(StringUtils.isEmpty(simpSessionId) || StringUtils.isEmpty(userId))) {
this.pool.put(simpSessionId, userId);
TextMessage textMessage = new TextMessage(userId + "上线,当前在线人数:" + this.pool.size());
this.simpMessagingTemplate.convertAndSend("/topic/message", textMessage);
}
}
/**
* 断开连接
*
* @param simpSessionId 会话id
*/
public void disconnect(String simpSessionId) {
String userId = this.pool.get(simpSessionId);
if (userId != null) {
this.pool.remove(simpSessionId);
TextMessage textMessage = new TextMessage(userId + "离线,当前在线人数:" + this.pool.size());
this.send(textMessage);
}
}
/**
* 给所有用户发送消息
*
* @param payload 消息
* @throws MessagingException
*/
public void send(Object payload) throws MessagingException {
this.simpMessagingTemplate.convertAndSend("/topic/message", payload);
}
/**
* 给指定用户发消息
*
* @param userId 用户id
* @param payload 消息
* @throws MessagingException
*/
public void sendToUser(String userId, Object payload) throws MessagingException {
this.simpMessagingTemplate.convertAndSendToUser(userId, "/message", payload);
}
}
WebSocketService 是一个简单的实现,数据的回调使用了 SimpMessagingTemplate 。
传输的数据模型使用 TextMessage 完成。
package com.websocket.po;
import org.springframework.lang.Nullable;
public class TextMessage {
@Nullable
private String userId; // 用户id
@Nullable
private String content; // 内容
public TextMessage() {
}
public TextMessage(String content) {
this.content = content;
}
@Nullable
public String getUserId() {
return userId;
}
@Nullable
public String getContent() {
return content;
}
}
为了避免客户端传空,引起解析报错,需在字段上添加 @Nullable。
Java
Servlet-based WebSocket Support
Spring Integeration学习
理解JMS规范中的持久订阅和非持久订阅
WebSocket应用安全问题分析
WebSocket 教程
WebSocket+SockJs+STMOP
STOMP协议详解
时间 | 描述 |
---|---|
2017-10-26 | 博文完成 |
CSDN:http://blog.csdn.net/y550918116j
GitHub:https://github.com/937447974