# websocket_Demo **Repository Path**: fjigww/websocket_Demo ## Basic Information - **Project Name**: websocket_Demo - **Description**: WebSocket 简单连接,基础小 Demo,SpringBoot下 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2019-04-02 - **Last Updated**: 2023-11-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## webSocket demo (webSocket/SpringBoot/myBatis) > **记录 WebSocket 学习过程** ### WebSocket > 一种由HTML5 提供的 在单个 TCP 连接上进行的全双工通讯协议。将客户端和服务端之间的数据交换变得简单,允许服务端主动向客户端推送数据。 > > 在 WebSocket API 中,浏览器和服务器只需要完成一次握手即可,两者之间可创建持久性的连接,并进行双向数据传输。 > > 默认使用 80 端口 #### 优势 - 较少的控制开销:减少了以前HTTP每次请求的请求头长问题,节省了宽带资源 - 更强的实时性:服务端随时可以主动发送数据给客户端,减少了HTTP的客户端请求后再返回响应 - 二进制支持:WebSocket 定义了 **二进制** 帧,相对HTTP,可以更轻松地处理二进制内容 - 可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议 #### 协议 - WebSocket 是独立的、创建在 TCP 上的协议 - WebSocket 通过 HTTP/1.1 协议的 101 状态码进行握手 #### 创建 ```js // 需要先做验证,判断浏览器是否支持 if("WebSocket" in window) {} // 创建:url,子协议(自定义协议) var ws = new WebSocket(url,[子协议]); ``` #### 属性 - ws.readyState - 0 - 连接尚未建立 - 1 - 表示连接已建立,可以通信 - 2 - 连接正在关闭 - 3 - 连接已关闭或连接不能打开 - ws.bufferedAmount - 表示只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出 UTF-8 文本字节数 #### 事件 - open - ws.onopen - 连接建立时触发 - message - ws.onmessage - 客户端接收服务端数据时触发 - error - ws.onerror - 通信发生错误时触发 - onclose - ws.onclose - 连接关闭时触发 #### 方法 - send() - ws.send() - 使用连接发送数据 - close() - ws.close() - 关闭连接 #### JS 代码 ```js var ws; function onopen() { var url = $("#url").val(); ws = new WebSocket(url); ws.onopen = function (ev) { } ws.onmessage = function (ev) { $("#msg").text(ev.data); } ws.onclose = function () { $("#msg").text(""); } ws.onerror = function (ev) { } } function wsclose() { ws.close(); } ``` #### Java 代码 1、WebSocket 是非单例的,初始化时注入 bean 成功,后新用户添加,创建新的对象时,不会再注入,所以不能简单的属性注入/构建注入等 - 带注入属性 **静态**,@Autowired 在 **set()** 方法上 - 手动注入 ApplicationContext 应用上下文获取 bean 2、sendObject() 发送数据时需要解码器,发送的数据为 JsonCommand,需要创建 JsonCommandEncoder 类 3、WebSocket 的容器管理,需要配置config, 注入 bean ServerEndpointExporter,会寻找 @ServerEndpoint 注解,注入 bean ```java @ServerEndpoint(value = "/websocket", encoders = {JsonCommandEncoder.class}) @Component public class WebSocketServlet { /** 线程安全的,带lock锁,JUC中的 */ private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet<>(); private Session session = null; /** * spring bean 默认单例,初始化时注入一次,而 websocket 非单例的,初始化时能注入成功, * 后当有新用户加入,将创建新的 websocket 对象,不会再注入,即获取到的 service/dao 将时 null 的 * * 1.以下是一种可行方法,静态的类,通过 set 注入 service,set 方法不能为静态 * */ private static IUserService userService; @Autowired public void setUserService(IUserService userService) { WebSocketServlet.userService = userService; } /** * 2.这是一种 websocket 注入 bean 的方法 实现 ApplicationContextAware 继承ServerEndpointConfig.Configurator * 通过手动注入 applicationContext 应用上下文获取 bean * ApplicationContextAware 让 bean 获取到 spring 容器,操作其中的实例,其本身并没有特殊功能,实现继承后传入 ApplicationContext * */ private static volatile BeanFactory context; @Override public T getEndpointInstance(Class clazz) throws InstantiationException { return context.getBean(clazz); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { WebSocketServletBean.context = applicationContext; } /** * 方法2注入 bean 的获取 * */ public IUserService getUserService() throws InstantiationException { return this.getEndpointInstance(IUserService.class); } /** * 开启连接 * */ @OnOpen public void onOpen(Session session) { this.session = session; webSocketSet.add(this); System.out.println("OnOpen:" + session.getId()); try { sendGroupMessage(session.getId() + ":onOpen"); sendSingleObject(); } catch (EncodeException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** * 断开连接 * */ @OnClose public void onClose() { System.out.println("OnClose:" + session.getId()); webSocketSet.remove(this); try { sendGroupMessage(session.getId() + ":onColse"); } catch (IOException e) { e.printStackTrace(); } } /** * 处理消息 * */ @OnMessage public void onMessage(String message) { System.out.println("OnMessage:" + session.getId()); System.out.println("message:" + message); try { sendGroupMessage(message); } catch (IOException e) { try { sendSingleMessage(message + "-->发送失败"); } catch (IOException e1) { e1.printStackTrace(); } e.printStackTrace(); } } /** * 出现错误 * */ @OnError public void onError(Throwable error) { System.out.println("OnError:" + error); error.printStackTrace(); } private void sendSingleMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } private void sendGroupMessage(String message) throws IOException { if (!StringUtils.isEmpty(message)) { for (WebSocketServlet wss : webSocketSet) { if (wss.equals(this)) { continue; } wss.session.getBasicRemote().sendText(message); } } } /** * 直接发送会有问题 - No encoder specified for object of class * 需要创建编码器 - JsonCommandEncoder(内部解释) * */ private void sendSingleObject() throws IOException, EncodeException { JsonCommand jsonCommand = userService.selectUser(); this.session.getBasicRemote().sendObject(jsonCommand); } /** * 在切点调用成功后执行 * */ public void sendGroupObject() throws IOException, EncodeException { JsonCommand jsonCommand = userService.selectUser(); for (WebSocketServlet wss : webSocketSet) { wss.session.getBasicRemote().sendObject(jsonCommand); } } @Test public void wssession() throws IOException, EncodeException { // websocket session发送消息 // 1.getAsyncRemote 异步远程,非阻塞式的 RemoteEndpoint.Async asyncRemote = this.session.getAsyncRemote(); asyncRemote.sendText(""); // 可发送回调信息 SendHandler asyncRemote.sendObject(new Object()); // 2.getBasicRemote 同步远程,阻塞式的 RemoteEndpoint.Basic basicRemote = this.session.getBasicRemote(); basicRemote.sendText(""); // 是否消息的最后一部分 basicRemote.sendText("", true); basicRemote.sendObject(new Object()); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } WebSocketServlet that = (WebSocketServlet) o; return Objects.equals(session, that.session); } @Override public int hashCode() { return Objects.hash(session); } } ``` **解码器** ```java /** * 在 websocket 中直接发送 obj 会有问题 - No encoder specified for object of class * 需要对 obj 创建解码类,实现 websocket 中的 Encoder.Text<> * */ public class JsonCommandEncoder implements Encoder.Text { /** * The Encoder interface defines how developers can provide a way to convert their * custom objects into web socket messages. The Encoder interface contains * subinterfaces that allow encoding algorithms to encode custom objects to: * text, binary data, character stream and write to an output stream. * * Encoder 接口定义了如何提供一种方法将定制对象转换为 websocket 消息 * 可自定义对象编码为文本、二进制数据、字符流、写入输出流 * Text、TextStream、Binary、BinaryStream * */ @Override public String encode(JsonCommand object) throws EncodeException { return JSON.toJSONString(object); } @Override public void init(EndpointConfig endpointConfig) { } @Override public void destroy() { } } ``` **配置类** ```java @Configuration public class WebSocketconfig { /** * 关闭 servlet 容器对 websocket端点的扫描 * When this class is used, by declaring it in Spring configuration, it should be * possible to turn off a Servlet container's scan for WebSocket endpoints. * 检测带 ServerEndpoint 注解的 bean 并注入 * Also detects beans annotated with ServerEndpoint and registers them as well * */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } } ``` #### 针对功能 WebSocket 连接成功后,获取 User 用户信息,增加切面类,当用户类有变动, UserDao 的某些方法执行,即推送新数据给 webSocket 连接用户 ```java /** * spring-boot 默认开启切面自动代理,不需要手动注入 * */ @Component @Aspect public class WebSocketAspect { @Autowired private WebSocketServlet webSocketServlet; @Pointcut("execution(* com.example.websocket_demo.dao.UserDao.delete(..))" + "|| execution(* com.example.websocket_demo.dao.UserDao.insert(..))" + "|| execution(* com.example.websocket_demo.dao.UserDao.update(..))") public void userInfoChange() {} @AfterReturning(pointcut = "userInfoChange()", returning = "ret") public void send(Object ret) throws IOException, EncodeException { // 执行的insert/update/delete方法成功后,返回值为1的,才执行 Integer num = 1; if (num.equals(ret)) { webSocketServlet.sendGroupObject(); } } } ```