SpringBoot + WebSocket实现简单消息推送
WebSocket是一种双工通信的网络协议,所谓双工就是浏览器可以给服务器发送消息,服务器也可以向浏览器发送消息,那么,有了HTTP协议,为什么还要使用它呢?假设我们需要每天定时需要服务器给客户端推送消息,那么HTTP协议就做不到主动的发送信息给客户端,而WebSocket则可以实现这一功能,它会自动监控浏览器并且做出自动回复。
WebSocket的特点:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
下面来通过一个简单案例来更好地认识WebSocket的原理以及使用:
下面的案例浏览器会发送消息‘hello’到服务器,服务器收到消息后会回复server,打印在控制台。
首先建立maven项目,导入相应的依赖:
<!-- maven项目packaging改为war类型时,必须要加这个插件 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 测试springboot的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试时加入这两个依赖,当修改代码的时候就不用每次修改后重启服务器了 -->
<!-- https://mvnrepository.com/artifact/org.springframework/springloaded -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- springboot 集成jsp必须要借助这两个依赖 -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-jasper -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!-- WebSocket相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
实现WebSocket需要实现WebSocketHandler这个接口,源码如下:
public interface 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();
}
如果需要接收二进制消息也可以扩展抽象类AbstractWebSocketHandler,这个类实现了WebSocketHandler 接口。它额外增加了以下几个方法:
public abstract class AbstractWebSocketHandler implements WebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
if (message instanceof TextMessage) {
handleTextMessage(session, (TextMessage) message);
}
else if (message instanceof BinaryMessage) {
handleBinaryMessage(session, (BinaryMessage) message);
}
else if (message instanceof PongMessage) {
handlePongMessage(session, (PongMessage) message);
}
else {
throw new IllegalStateException("Unexpected WebSocket message type: " + message);
}
}
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 {
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
handleTextMessage方法对文本消息进行处理,handleBinaryMessage接收二进制消息进行处理。也许有时候不需要接收二进制消息。那需要扩展TextWebSocketHandler类,它继承自AbstractWebSocketHandler类,在这个类中的handleBinaryMessage方法接收到二进制消息就会关闭通信。
public class TextWebSocketHandler extends AbstractWebSocketHandler {
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
try {
session.close(CloseStatus.NOT_ACCEPTABLE.withReason("Binary messages not supported"));
}
catch (IOException ex) {
// ignore
}
}
}
下面我们扩展AbstractWebSocketHandler类来实现WebSocket的使用:
package cn.shinelon.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
@Component
public class SocketHandler extends AbstractWebSocketHandler{
public Logger log=LoggerFactory.getLogger(getClass());
/**
* 建立连接之后
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("Connection established");
}
/**
* 处理文本消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
log.info("Received message: "+message.getPayload());
//模拟延时
Thread.sleep(2000);
//发送文本消息
session.sendMessage(new TextMessage("Server"));
}
/**
* 连接关闭之后
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("Connection closed Status:"+status);
}
}
另外还需要对WebSocket进行注册以及注入到Spring容器中:
package cn.shinelon.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
@Bean
public SocketHandler socketHander(){
return new SocketHandler();
}
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(socketHander(), "/hello").withSockJS();
}
}
下面是jsp页面代码,主要是里面的JS代码:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<script type="text/javascript" src="http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script>
<script type="text/javascript">
//var url='ws://'+window.location.host+'/webSocket/hello';
var url='hello';
//var socket=new WebSocket(url);
var socket=new SockJS(url);
socket.onopen=function(){
console.log('Opening');
sayHello();
};
//处理消息
socket.onmessage=function(e){
console.log('Received message:',e.data);
setTimeout(function(){sayHello()},2000);
};
//处理关闭事件
socket.onclose=function(){
console.log('Closing');
};
function sayHello(){
console.log('Sending hello');
//发送消息
socket.send('Hello');
}
</script>
</head>
<body>
this is springboot
</body>
</html>
解释一下上面的代码:现在的大多浏览器都支持WebSocket,比如Chrom,FireFox等等主流的浏览器,但是也有一些老版本的浏览器不支持,如果你的浏览器支持WebSocket,可以使用new WebSocket(url)来创建,如果不支持,就需要使用new SockJS(url),它走的是HTTP协议,不过需要导入库(http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js )。也就是说如果你的浏览器不支持WebSocket,你需要将:
var url='ws://'+window.location.host+'/webSocket/hello';
var socket=new WebSocket(url);
改为:
var url='hello';
var socket=new SockJS(url);
下面是SpringBoot的资源文件的配置:
application.properties:
spring.mvc.view.prefix=WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
最后,编写一个简单的控制类来启动项目看看效果:
@SpringBootApplication
@Controller
@ComponentScan("cn.shinelon")
public class IndexController {
@RequestMapping("/ws")
public String ws() {
return "index";
}
}
然后浏览器输入localhost:8080/ws,就会看到浏览器和服务器端不停地发送消息: