WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据。HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是WebSocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已。WebSocket的握手基于HTTP中的协议升级机制,当服务端收到这个HTTP的协议升级请求后,如果支持WebSocket协议则返回HTTP状态码101,WebSocket的握手便成功了,之后客户端与服务端会使用之前HTTP请求所使用的TCP连接来相互发送消息。它和 HTTP 最大不同是:WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动地向对方发送或接收数据。
1、使用@ServerEndpoint注解(该注解由java提供)监听一个WebSocket请求路径,这里监听了客户端的连接端口/reverse
,并定义了如何处理客户端发来的消息。
@ServerEndpoint("/reverse")
public class ReverseWebSocketEndpoint {
@OnMessage
public void handleMessage(Session session, String message) throws IOException {
session.getBasicRemote().sendText("Reversed: " + new StringBuilder(message).reverse());
}
/**
* 连接开启
*/
@OnOpen
...
/**
* 连接关闭
*/
@OnClose
...
/**
* 连接异常
*/
@OnError
...
}
注意:在springboot工程中使用websocket时,@EnableWebSocket***要在启动类上使用此注解。
WebSocket 首次连接时,默认使用了 OriginHandshakeInterceptor 拦截,它主要做域名拦截,我们可以通过继承它做其他连接处理。这个类中的方法,只会在连接时执行一次,数据传输过程中会走通道拦截。
@Component
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 可做到对输入和输出数据的拦截处理。
@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 做匹配处理。
@MessageMapping("/stomp")
@RestController
public class WebSocketController {
// @SendTo("/topic/message") 广播发送给 /topic/message 订阅客户端
// @SendToUser("1/message") 发送给指定订阅用户
@Autowired
public WebSocketService webSocketService;
@MessageMapping("/send/{pathParam}")
public void sendBroadcast(@Headers Map headers, @Header String simpSessionId, TextMessage
textMessage, @DestinationVariable String pathParam) {
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);
}
}
}
业务层 WebSocketService 实现了登录、断开、群发和定点发送消息的功能。
@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);
}
}
public class TextMessage {
// 为了避免客户端传空,引起解析报错,需在字段上添加@Nullable。
@Nullable
// 用户id
private String userId;
@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;
}
}
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 使用MessageHandler处理器处理"websocket"过来的请求
registry.addHandler(new MessageHandler(), "/websocket")
.addInterceptors(new HttpSessionHandshakeInterceptor())
// 允许跨域访问
.setAllowedOrigins("*");
}
}
1、实现WebSocketHandler接口来处理握手、连接、关闭、接收信息、发送信息的处理类。
void afterConnectionEstablished(WebSocketSession session) throws Exception;
void handleMessage(WebSocketSession session, WebSocketMessage> message) throws Exception;
void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;
void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;
boolean supportsPartialMessages();
2、继承AbstractWebSocketHandler抽象类,根据不同业务重写信息处理。
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception;
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception;
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception;
3、继承Spring写好的文本处理类TextWebSocketHandler。对建立连接、接收/发送消息、异常等情况进行处理。
// 关闭连接
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception;
// 三次握手成功
public void afterConnectionEstablished(WebSocketSession session) throws Exception;
// 处理客户发送的信息
public void handleMessage(WebSocketSession session, WebSocketMessage> message) throws Exception;
// 实现案例
public class MessageHandler extends TextWebSocketHandler{
Logger log = LoggerFactory.getLogger(MessageHandler.class);
//用来保存连接进来session
private List sessions = new CopyOnWriteArrayList<>();
/**
* 关闭连接进入这个方法处理,将session从 list中删除
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session);
log.info("{} 连接已经关闭,现从list中删除 ,状态信息{}", session, status);
}
/**
* 三次握手成功,进入这个方法处理,将session 加入list 中
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
log.info("用户{}连接成功.... ",session);
}
/**
* 处理客户发送的信息,将客户发送的信息转给其他用户
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage> message) throws Exception {
log.info("reveice client msg: {}",message.getPayload());
session.sendMessage(new TextMessage("i reveice client msg...."+System.nanoTime()));
for(WebSocketSession wss : sessions)
if(!wss.getId().equals(session.getId()))
wss.sendMessage(message);
}
}
WebSocket在握手之后便直接基于TCP进行消息通信,但WebSocket只是TCP上面非常轻的一层,它仅仅将TCP的字节流转换成消息流(文本或二进制),至于怎么解析这些消息的内容完全依赖于应用本身。因此为了协助client与server进行消息格式的协商,WebSocket在握手的时候保留了一个子协议字段。STOMP是一种简单的面向文本的消息传递协议,最初是为脚本语言(如Ruby,Python和Perl)创建的,用于连接到企业消息代理。它旨在解决常用消息传递模式的一个子集。STOMP可以用于任何可靠的双向流媒体网络协议,如TCP和WebSocket。虽然STOMP是面向文本的协议,但消息的有效载荷可以是文本或二进制。Spring框架支持通过Spring-messaging和spring-websocket模块在WebSocket上使用STOMP。
Spring的spring-messaging模块支持STOMP协议,包含了消息处理的关键抽象。下面是一个简单的消息处理示意图:
Message:消息。里面带有header和payload。
MessageHandler:处理client消息的实体。
MessageChannel:解耦消息发送者与消息接收者的实体。client可以发送消息到channel,而不用管这条消息最终被谁处理。
Broker:存放消息的中间件。client可以订阅broker中的消息。
// 同HTTP在TCP套接字上添加 请求-响应 模型层一样,STOMP在WebSocket之上提供了一个基于帧的线路格式层,用来定义消息语义。
// SEND:STOMP命令,表明会发送一些内容
SEND
// destination:头信息,用来表示消息发送到哪里
destination:/app/marco
// content-length:头信息,用来表示负载内容的大小
content-length:20
// 空行
// 帧内容(负载)内容
{\"message\":\"hello word!\"}
1)在spring中启用STOMP通讯不用我们自己去写原生态的帧,spring的消息功能是基于代理模式构建。在实现WebSocketMessageBrokerConfigurer
接口的websocket配置类上使用@EnableWebSocketMessageBroker
(表明启用WebSocket消息中间件,以及WebSocket消息处理)。
public interface WebSocketMessageBrokerConfigurer {
// 添加这个Endpoint,这样在网页中就可以通过websocket连接上服务,也就是我们配置websocket的服务地址,并且可以指定是否使用socketjs
void registerStompEndpoints(StompEndpointRegistry var1);
// 配置发送与接收的消息参数,可以指定消息字节大小,缓存大小,发送超时时间
void configureWebSocketTransport(WebSocketTransportRegistration var1);
// 设置输入消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间
void configureClientInboundChannel(ChannelRegistration var1);
// 设置输出消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间
void configureClientOutboundChannel(ChannelRegistration var1);
// 添加自定义的消息转换器,spring 提供多种默认的消息转换器,返回false,不会添加消息转换器,返回true,会添加默认的消息转换器,当然也可以把自己写的消息转换器添加到转换链中
boolean configureMessageConverters(List var1);
// 配置消息代理,哪种路径的消息会进行代理处理
void configureMessageBroker(MessageBrokerRegistry var1);
// 自定义控制器方法的参数类型,有兴趣可以百度google HandlerMethodArgumentResolver这个的用法
void addArgumentResolvers(List var1);
// 自定义控制器方法返回值类型,有兴趣可以百度google HandlerMethodReturnValueHandler这个的用法
void addReturnValueHandlers(List var1);
}
2)配置类的一种实现方式。
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{
@Autowired
private MyChannelInterceptor myChannelInterceptor;
/**
* 添加Endpoint,在网页中就可以通过websocket连接上服务,
* 也就是我们配置websocket的服务地址,并且可以指定是否使用socketjs
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/**
* 1. 将 /stomp/websocketJs路径注册为STOMP的端点,
* 用户连接了这个端点后就可以进行websocket通讯。
* 2. setAllowedOrigins("*")表示可以跨域。
* 3. withSockJS()表示支持socktJS访问(在浏览器并不是直接使用原生的的WebSocket协议,而是使用更高级的SockJS和StompJS。
* 为了应对许多浏览器不支持WebSocket协议的问题,Spring对备选协议SockJS提供了支持。)
* 4. 添加自定义拦截器。
*/
registry.addEndpoint("/stomp/websocketJS")
.setInterceptors(new WebSocketHandshakeInterceptor())
.setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 定义了两个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
registry.enableSimpleBroker("/topic", "/queue");
// 定义了服务端接收地址的前缀(也是全局使用的订阅前缀),也即客户端给服务端发消息的地址前缀
registry.setApplicationDestinationPrefixes("/app");
// 使用客户端一对一通信的时候的前缀(默认就是/user)通常与@SendToUser搭配使用
registry.setUserDestinationPrefix("/user");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(myChannelInterceptor);
}
}
3、如果我们业务关心用户的数量、在线数量、连接状况等数据,我们也可以通过ChannelRegistration对象的setInterceptors方法添加监听,这里先展示一个完整的实现类,监听接口在后面会介绍,代码中的WebSocketHandShakeInterceptor拦截器,是上一个例子已经实现的,用于存储httpsession,在WebSocketChannelInterceptor拦截器中可以做一些在线人数统计等操作,后面会介绍。
@MessageMapping("/sendTest")
接收客户端发送的消息,当客户端发送消息的目的地为/app/sendTest时,交给该注解所在的方法处理消息,其中/app是在WebSocketConfig配置类的configureMessageBroker方法中添加。
registry.setApplicationDestinationPrefixes("/app");
若没有添加@SendTo注解且该方法有返回值,则返回的目的地地址为/topic/sendTest,经过消息代理,客户端需要订阅了这个主题才能收到返回消息。
@SubscribeMapping("/subscribeTest")
接收客户端发送的订阅,当客户端订阅的目的地为/app/subscribeTest时,交给该注解所在的方法处理订阅,其中/app为客户端请求前缀。
若没有添加@SendTo注解且该方法有返回值,则返回的目的地地址为/app/sendTest,不经过消息代理,客户端需要订阅了这个主题才能收到返回消息。通过@SubscribeMapping 、SimpMessagingTemplate (服务器主动推送)的形式所订阅的地址结果(如果点击关闭WebSocket连接后将无法获取订阅消息)
@SendTo("/topic/subscribeTest")
返回消息的目的地地址为/topic/subscribeTest,经过消息代理,客户端需要订阅了这个主题才能收到返回消息。