背景
- 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; }