WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455,后由 RFC 7936 补充规范。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocketAPI 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输
pom文件。
org.springframework.boot spring-boot-starter-parent 3.0.5
websocket依赖
org.springframework.boot spring-boot-starter-websocket
"vue": "3.2.45", "websocket": "^1.0.34"
package gzsf.epg.sever.webSocket.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * webSocket的配置类 * @author * @date 2023/10/7 */ @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
package gzsf.epg.sever.webSocket.websocket; import jakarta.websocket.Session; /** * @author * @date 2023/10/7 */ public class WebSocketClient { // 与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; //连接的uri private String uri; public Session getSession() { return session; } public void setSession(Session session) { this.session = session; } public String getUri() { return uri; } public void setUri(String uri) { this.uri = uri; } }
如下是后端发送消息的配置,如需发送文本类的数据需要配置这个
package gzsf.epg.sever.webSocket.util; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.json.JsonMapper; import gzsf.epg.sever.datacheck.vo.ExportVo; import jakarta.websocket.EncodeException; import jakarta.websocket.Encoder; import jakarta.websocket.EndpointConfig; public class ServerEncoder implements Encoder.Text{ @Override public void destroy() { // TODO Auto-generated method stub // 这里不重要 } @Override public void init(EndpointConfig arg0) { // TODO Auto-generated method stub // 这里也不重要 } /* * encode()方法里的参数和Text 里的T一致,如果你是Student,这里就是encode(Student student) */ @Override public String encode(ExportVo responseMessage) throws EncodeException { try { JsonMapper jsonMapper = new JsonMapper(); return jsonMapper.writeValueAsString(responseMessage); } catch ( JsonProcessingException e) { e.printStackTrace(); return null; } } }
package gzsf.epg.sever.webSocket.service; import gzsf.epg.sever.datacheck.vo.ExportVo; import gzsf.epg.sever.webSocket.util.ServerEncoder; import gzsf.epg.sever.webSocket.websocket.WebSocketClient; import jakarta.websocket.*; import jakarta.websocket.server.PathParam; import jakarta.websocket.server.ServerEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; /** * @author * @date 2023/10/7 */ @ServerEndpoint(value = "/websocket/{userName}", encoders = {ServerEncoder.class}) @Component public class WebSocketService { private static final Logger log = LoggerFactory.getLogger(WebSocketService.class); private static ApplicationContext applicationContext; public static void setApplicationContext(ApplicationContext context) { applicationContext = context; } private static ConcurrentHashMapwebSocketMap = new ConcurrentHashMap<>(); /** * 与某个客户端的连接会话,需要通过它来给客户端发送数据 */ private Session session; /** * 接收userName */ private String userName; /** * 客户端与服务端连接成功 * * @param session */ @OnOpen public void onOpen(Session session, @PathParam("userName") String userName) { this.session = session; //根据token获取用户名 this.userName = userName; WebSocketClient client = new WebSocketClient(); client.setSession(session); client.setUri(session.getRequestURI().toString()); webSocketMap.put(userName, client); log.info("连接成功!"+session.getRequestURI().toString()); log.info("连接成功!"+session); } /** * 客户端与服务端连接关闭 */ @OnClose public void onClose() { log.info("连接关闭!"); } /** * 客户端与服务端连接异常 * * @param error * @param session */ @OnError public void onError(Throwable error, Session session) { } /** * 客户端向服务端发送消息 * * @param message * @throws IOException */ @OnMessage public void onMsg(Session session, String message) throws IOException { log.info("消息测试!"); } public static void sendVo(String userName, ExportVo vo) { try { WebSocketClient webSocketClient = webSocketMap.get(userName); if (webSocketClient != null) { webSocketClient.getSession().getBasicRemote().sendObject(vo); } } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e.getMessage()); } catch (EncodeException e) { throw new RuntimeException(e.getMessage()); } } /** * 向指定客户端发送消息 * * @param message */ public static void sendMessage(String userName, String message) { try { log.info("准备发消息!"+userName); WebSocketClient webSocketClient = webSocketMap.get(userName); log.info("连接成功》》》》》》》》》"+webSocketMap); if (webSocketClient != null) { log.info("发送的消息是!"+message); webSocketClient.getSession().getBasicRemote().sendText(message); } } catch (IOException e) { e.printStackTrace(); log.info("失败"); throw new RuntimeException(e.getMessage()); } } }
修改启动类如下,服务类就能注入,否则在调用service的时候会空指针异常,具体原因可以去了解下 spring 默认管理
package gzsf.epg.sever; import gzsf.epg.common.core.utils.SpringContextUtils; import gzsf.epg.common.security.annotation.EnableCustomConfiguration; import gzsf.epg.common.security.annotation.EnableFeignBeanClients; import gzsf.epg.sever.webSocket.service.WebSocketService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Import; /** * @author * @date 2023/10/19 */ @EnableCustomConfiguration @EnableFeignBeanClients @SpringBootApplication @Import(SpringContextUtils.class) public class ServiceApplication { private static final Logger logger = LoggerFactory.getLogger(ServiceApplication.class); public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(ServiceApplication.class, args); //解决WebSocket不能注入的问题 WebSocketService.setApplicationContext(run); } }
连接地址里 admin为用户名,因为websocket是单独为每个用户创建长连接,发送的消息也是一对一发送,当然也可以改为一对多,全权在业务怎么用。前端可获取用户名传到后端,如下代码示例是写死的
注意:前端也需下载依赖
下面展示一些 内联代码片。
npm install vue-native-websocket
/** * 创建webSocket连接 */ function createWS() { webSocket.value = new WebSocket("ws://127.0.0.1:8080/server/websocket/admin"); //onopen事件监听 webSocket.value.addEventListener('open', e => { console.log('与服务端连接打开->', e) }, false) //onclose事件监听 webSocket.value.addEventListener('close', e => { console.log('与服务端连接关闭->', e) }, false) //onmessage事件监听 webSocket.value.addEventListener('message', e => { console.log('接收到服务端的消息->', e) }, false) //onerror事件监听 webSocket.value.addEventListener('error', e => { }, false) }
只需在需要开启长连接的时候调用createWS();即可;
在需要开启长连接处调用createWS();开启长连接即可,需注意用户名(该用户名就是唯一标识)传递。
前端发起建立长连接请求后,执行WebSocketService类中的onOpen方法,创建client并存放在map集合中以便执行完业务后发送反馈消息。
/** * 客户端与服务端连接成功 * * @param session */ @OnOpen public void onOpen(Session session, @PathParam("userName") String userName) { this.session = session; //根据token获取用户名 this.userName = userName; WebSocketClient client = new WebSocketClient(); client.setSession(session); client.setUri(session.getRequestURI().toString()); webSocketMap.put(userName, client); log.info("连接成功!"+session); }
后端接收到前端请求后可新建线程执行业务流程,先返回执行中状态给前端,防止前后端连接超时的报错。
@PostMapping("/importDx") public AjaxResult importDx(@RequestParam("xlsxFile") MultipartFile file) { this.onCallback(file); return AjaxResult.success(); } public void onCallback(MultipartFile file) { ExecutorService executor = Executors.newCachedThreadPool(); executor.execute(() -> { try { //服务类代码 } catch (IOException e) { throw new RuntimeException(e); } }); executor.shutdown(); }
业务执行完成后可调用,username为创建websocket的唯一标识。
/** * 调用sendMessage方法使用 */ WebSocketService.sendMessage(username, "执行成功");
只要是在长连接里面,想做什么全看业务,简单来说干什么都行!至此WebSocket使用结束