Spring Websocket 实践操作
一、Websocket与http
webSocket是HTML5的一种新协议,它实现了服务端与客户端的全双工通信,建立在传输层,tcp协议之上,即浏览器与服务端需要先建立tcp协议,再发送webSocket连接建立请求。它是一个持久层的协议,相对于HTTP这种非持久的协议来说,因为http本身无状态,若要保持状态,则需要通过cache session进行,造成每次数据请求时会携带大量信息。
websocket好处:
-
一个Web客户端只建立一个TCP连接;
-
Websocket服务端可以推送(push)数据到web客户端;
-
有更加轻量级的头,减少数据传送量。通过协议promote的方式将http升级成websocket。
二、轮询请求后台服务器对比
(1)ajax轮询
ajax轮询就是让浏览器隔个几秒就发送一次请求,询问服务器是否有新的信息。
(2)long poll(长轮询)
long poll的原理跟ajax轮询差不多,都说采用轮询方式,但是采用的是阻塞模型。也就是说客户端发起请求,如果没消息,就一直不返回Response给客户端。知道有消息才返回,返回完之后,客户端再次建立连接。
上面两种方式是不断建立HTTP连接,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输鉴别信息,来告诉服务端你是谁。然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性。
(3)WebSocket
WebSocket解决了HTTP的被动性和反复解析HTTP协议消耗资源的问题。
-
解决HTTP的被动性的问题:当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。解决了上面同步有延迟的问题。
-
解决服务器上消耗资源的问题:其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(php等)来处理。简单地说,我们有一个非常快速的 接线员(Nginx) ,他负责把问题转交给相应的 客服(Handler) 。
由于Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。
目前唯一的问题是:不兼容低版本的IE。
三、Spring Websocket
背景:spring 让工作变得更加简单方便。由于使用原生的API太麻烦,而且不同的Web 容器拥有对于API的不同的实现,这个对于使用者来说特别不友好,spring 将这些不同点进行屏蔽,抽象出一套自己的称呼,比如WebsocketSession,将这一套东西转换为Tomcat 支持的WebSocket 或者Jboss 支持的Websocket 或者Jetty 支持的Websocket。
3.1使用@EnableWebSocket注解,开启对WebSocket的支持功能
@EnableWebSocket的源码如下:
@Retention(RetentionPolicy.RUNTIME) //定义注解在JVM运行时保留
@Target(ElementType.TYPE) //定义注解应用于类
@Documented
@Import(DelegatingWebSocketConfiguration.class) //引用DelegatingWebSocketConfiguration类,开启使用webSocket协议
public @interface EnableWebSocket {
}
@EnableWebSocket中引用的DelegatingWebSocketConfiguration类的的源码如下:
@Configuration
public class DelegatingWebSocketConfiguration extends WebSocketConfigurationSupport {
private final List<WebSocketConfigurer> configurers = new ArrayList<WebSocketConfigurer>();
@Autowired(required = false)
public void setConfigurers(List<WebSocketConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addAll(configurers);
}
}
//注册websocket
@Override
protected void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
for (WebSocketConfigurer configurer : this.configurers) {
configurer.registerWebSocketHandlers(registry);
}
}
}
3.2 简单的Websocket使用流程
3.2.1 后台实现
(1)修改pom.xml文件,引入Websocket依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>4.2.2.RELEASE</version>
</dependency>
(2)Websocket控制器
@EnableWebMvc
@EnableWebSocket
public class MyConfigurerAdapter extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
@Inject
private MySocketHandler mySocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//前台:可以使用websocket环境
registry.addHandler(mySocketHandler, "/my/console").setAllowedOrigins("*").addInterceptors(new HandshakeInterceptor());
//前台:不可以使用websocket环境,则使用sockjs进行模拟连接
registry.addHandler(mySocketHandler, "/sockjs/my/console").setAllowedOrigins("*").addInterceptors(new HandshakeInterceptor()).withSockJS();
}
}
(3)Websocket处理器
@Service
public class MySocketHandler implements WebSocketHandler {
private Map<String, WebSocketSession> mySession = new Hashtable<>();
@Inject
private MyService myService; //自己个人实现的类
private static Logger logger = Logger.getLogger(MySocketHandler.class);
//webscoket建立连接之后的处理行数
@Override
public void afterConnectionEstablished( WebSocketSession session) throws Exception {
logger.info("----------webscoket已连接");
}
//客户端发送服务器的消息时的处理函数,在这里收到消息之后可以发送消息给前端
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
logger.info("----------服务器已接收到消息");
//前端发送的消息只能是字符串格式(可以理解成前端的传参),后台需要解析字符串
String params = (String) message.getPayload();
ObjectMapper mapper = new ObjectMapper();
HashMap<String, Object> map = mapper.readValue(params, HashMap.class);
String name = (String) map.get("name");
String id = (String) map.get("id");
Map<String, Object> result = myServer.selectReportData(name,id);
TextMessage reply = new TextMessage(JSONObject.toJSONString(result)); //结果转成JSONObject字符串,放在TextMessage对象中
try{
session.sendMessage(reply); //把数据发送给前端
}catch(IOException e){
logger.debug("----------发送信息给前端出错");
}
}
//处理传输错误
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
logger.info("----------出现传输错误");
}
//websocket关闭连接
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
logger.info("----------关闭Websocket连接");
}
//支持部分消息,默认return false
@Override
public boolean supportsPartialMessages() {
logger.info("----------支持部分信息");
return false;
}
}
(4)拦截器
public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor {
// 握手前
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
//在访问接口时候需要带apiKey,如:XXXX链接?apiKey=xxx
String apiKey = ((ServletServerHttpRequest) request).getServletRequest().getParameter("apiKey");
attributes.put("apiKey", apiKey);
return super.beforeHandshake(request, response, wsHandler, attributes);
}
// 握手后
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
super.afterHandshake(request, response, wsHandler, ex);
}
}
3.2.1 前端的实现
-
使用原生的Websocket来处理
//点击按钮触发Websocket
$("#export_btn").click(function(){
var socket;
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else{
//实现化WebSocket对象,指定要连接的服务器地址与端口建立连接:ws://路径:端口/项目名/配置的websocket地址
socket = new WebSocket("ws://localhost:8080/myweb/my/console?apiKey=F5EB285989C74F4B");
var params = {
name: "karry",
id: "wdfse13352ssasd"
}
//打开事件
socket.onopen = function() {
console.log("Socket 已打开");
//发送请求参数
socket.send(JSON.stringify(params));
};
//获得消息事件
socket.onmessage = function(msg) {
console.log(msg.data);
//发现后台消息进入, 开始处理前端触发逻辑
};
//关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
//此时可以尝试刷新页面
}
}
})
-
使用SocketJs来处理
$(function () {
socket = new SockJS(""); //创建连接
socket.onopen = function("/my/console?apiKey=F5EB285989C74F4B") {
socket.send("你好世界")
};
socket.onmessage = function(e) {
//有消息传递到了,可以进行处理
console.log('message', e.data);
alert(e.data)
};
socket.onclose = function() {
console.log('close');
};
$("#send").click(function () {
if(socket !=undefined){
socket.send("message");
}else{
console.log("socket is null")
}
});
});