背景
- HTTP 协议有一个缺陷:通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息
- WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端
- 举例来说,我们想要查询当前的排队情况,只能是页面轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此WebSocket 就是这样发明的。
SpringBoot的WebSocket
引入MAVEN依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocketConfig
启用websocket支持很简单,直接一个配置类搞定。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketServer
websocket和socket类似,有客户端和服务端,客户端就是pc、app等,服务端就是我们后端了。因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller,直接使用注解
@ServerEndpoint(value = “/websocket/{appNo}”)和@Component启用即可,然后在里面实现@OnOpen,@OnClose, @OnMessage,@OnError等方法即可。
package com.dongzhengafc.facesign.websocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
/**
* @Author: TheBigBlue
* @Description: 向app端实时推送业务状态信息
* @Date: 2019/7/16
**/
//由于是websocket 所以原本是@RestController的http形式
//直接替换成@ServerEndpoint即可,作用是一样的 就是指定一个地址
//表示定义一个websocket的Server端
@Component
@ServerEndpoint(value = "/websocket/{appNo}")
public class WebSocketController {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketController.class);
/**
* @Author: TheBigBlue
* @Description: 加入连接
* @Date: 2019/7/16
* @Param appNo: 申请单号
* @Param relTyp: 关系人类型
* @Param session:
* @Return:
**/
@OnOpen
public void onOpen(@PathParam("appNo") String appNo, Session session) {
LOGGER.info("[" + appNo + "]加入连接!");
WebSocketUtil.addSession(appNo, session);
}
/**
* @Author: TheBigBlue
* @Description: 断开连接
* @Date: 2019/7/16
* @Param appNo:
* @Param relTyp:
* @Param session:
* @Return:
**/
@OnClose
public void onClose(@PathParam("appNo") String appNo, Session session) {
LOGGER.info("[" + appNo + "]断开连接!");
WebSocketUtil.remoteSession(appNo);
}
/**
* @Author: TheBigBlue
* @Description: 发送消息
* @Date: 2019/7/16
* @Param appNo: 申请单号
* @Param relTyp: 关系人类型
* @Param message: 消息
* @Return:
**/
@OnMessage
public void OnMessage(@PathParam("appNo") String appNo, String message) {
String messageInfo = "服务器对[" + appNo + "]发送消息:" + message;
LOGGER.info(messageInfo);
Session session = WebSocketUtil.ONLINE_SESSION.get(appNo);
if("heart".equalsIgnoreCase(message)){
LOGGER.info("客户端向服务端发送心跳");
//向客户端发送心跳连接成功
message = "success";
}
//发送普通信息
WebSocketUtil.sendMessage(session, message);
}
@OnError
public void onError(Session session, Throwable throwable) {
LOGGER.error(session.getId() + "异常:", throwable);
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
throwable.printStackTrace();
}
}
package com.dongzhengafc.facesign.websocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.websocket.RemoteEndpoint.Async;
import javax.websocket.Session;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
/**
* @Author: TheBigBlue
* @Description:
* @Date: 2019/7/16
**/
public class WebSocketUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketUtil.class);
/**
* @Author: TheBigBlue
* @Description: 使用map进行存储在线的session
* @Date: 2019/7/16
**/
public static final Map<String, Session> ONLINE_SESSION = new ConcurrentHashMap<>();
/**
* @Author: TheBigBlue
* @Description: 添加Session
* @Date: 2019/7/16
* @Param userKey:
* @Param session:
* @Return:
**/
public static void addSession(String userKey, Session session) {
ONLINE_SESSION.put(userKey, session);
}
public static void remoteSession(String userKey) {
ONLINE_SESSION.remove(userKey);
}
/**
* @Author: TheBigBlue
* @Description: 向某个用户发送消息
* @Date: 2019/7/16
* @Param session:
* @Param message:
* @Return:
**/
public static Boolean sendMessage(Session session, String message) {
if (session == null) {
return false;
}
// getAsyncRemote()和getBasicRemote()异步与同步
Async async = session.getAsyncRemote();
//发送消息
Future<Void> future = async.sendText(message);
boolean done = future.isDone();
LOGGER.info("服务器发送消息给客户端" + session.getId() + "的消息:" + message + ",状态为:" + done);
return done;
}
}
推送消息
推送消息,可以自己写接口调用,或者前端发起,或者通过第三方工具连接。
- 自己写接口调用
package com.dongzhengafc.facesign.websocket;
import com.dongzhengafc.facesign.base.api.JsonResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: TheBigBlue
* @Description: 向客户端推送业务状态信息
* @Date: 2019/7/16
**/
@RestController
@RequestMapping("/socket")
public class WebSocketPushController {
@Autowired
private WebSocketController webSocketController;
/**
* @Author: TheBigBlue
* @Description:
* @Date: 2019/7/16
* @Param appNo: 发送的用户名
* @Param relTyp: 发送的用户名
* @Param message: 发送的信息
* @Return:
**/
@RequestMapping("/push")
public JsonResponse pushToWeb(String appNo, String message) {
webSocketController.OnMessage(appNo, message);
return JsonResponse.success();
}
}
- 前端请求连接,发送信息。
<script>
var socket;
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else{
console.log("您的浏览器支持WebSocket");
//实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接
//等同于socket = new WebSocket("ws://localhost:8083/checkcentersys/websocket/20");
socket = new WebSocket("${basePath}websocket/${cid}".replace("http","ws"));
//打开事件
socket.onopen = function() {
console.log("Socket 已打开");
//socket.send("这是来自客户端的消息" + location.href + new Date());
};
//获得消息事件
socket.onmessage = function(msg) {
console.log(msg.data);
//发现消息进入 开始处理前端触发逻辑
};
//关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
//此时可以尝试刷新页面
}
//离开页面时,关闭socket
//jquery1.8中已经被废弃,3.0中已经移除
// $(window).unload(function(){
// socket.close();
//});
}
</script>
- 第三方工具连接:http://www.websocket-test.com/
相关问题
1. 打war包部署tomcat报错
Application startup failed
java.lang.IllegalStateException: Failed to register @ServerEndpoint class:
- 原因:SpringBoot Run As 可以快速启动项目,且能够即时刷新。其原因是SpringBoot拥有一个内置的Tomcat,此Tomcat的版本可在pom.xml中指定。每次我们使用SpringBoot Run As启动项目时,我们的web容器即就是这个内置的Tomcat。此刻web容器连同项目本身都是由Spring进行代理。而当我们将项目打成war包,部署在服务器上的某个Tomcat下时。此刻我们的项目将会交由这个Tomcat去管理。因为外部Tomcat的优先级高于Spring内置Tomcat。问题就在这里。当我们在IDE内使用 SpringBoot Run As去启动时,Spring会帮我们找到内置Tomcat lib中的javax.websocket包加载使用。所以项目正常运行。而当我们将打好的war包放在外部Tomcat上进行启动时。Tomcat管理器根据之前的Javax.websocket包的路径找不到对应的ServerEndpoint类资源文件,因此自然会注册失败。
- 解决:pom.xml 引入依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
- 部署发现问题仍然存在,这是因为当我们使用外部Tomcat时,项目的管理权将会由Spring交接至Tomcat。 而Tomcat7及后续版本是对websocket直接支持的,且我们所使用的jar包也是tomcat提供的。 但是我们在WebSocketConfig中将ServerEndpointExporter指定给Spring管理。而部署后ServerEndpoint是需要Tomcat直接管理才能生效的。所以此时即就是此包的管理权交接失败,那肯定不能成功了。最后我们需要将WebSocketConfig中的bean配置注释掉。然后再打包上传部署测试。一切正常!
//@Configuration
//public class WebSocketConfig {
//
// @Bean
// public ServerEndpointExporter serverEndpointExporter() {
// return new ServerEndpointExporter();
// }
//
//}
2. Websocket在1分钟后自动断开连接报错EOFException
- 这是因为websocket长连接有默认的超时时间(1分钟,由proxy_read_timeout决定),就是超过一定的时间没有发送任何消息,连接会自动断开。解决办法就是让浏览器每隔一定时间(要小于超时时间)发送一个心跳。
- 或者部署到服务器后,nginx 代理默认配置了访问超时时间为90s,我们可以修改这个值。nginx 通过在客户端和后端服务器之间建立起一条隧道来支持WebSocket。为了使nginx可以将来自客户端的Upgrade请求发送给后端服务器,Upgrade和Connection的头信息必须被显式的设置,一旦我们完成以上设置,nginx就可以处理WebSocket连接了。注意,必须要有proxy_set_header Host h o s t : host:host:server_port; 这个配置,否则会报403错误。
location /web/count {
proxy_pass http://tomcat-server;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}