webSocket demo (webSocket/SpringBoot/myBatis)
记录 WebSocket 学习过程
WebSocket
一种由HTML5 提供的 在单个 TCP 连接上进行的全双工通讯协议。将客户端和服务端之间的数据交换变得简单,允许服务端主动向客户端推送数据。
在 WebSocket API 中,浏览器和服务器只需要完成一次握手即可,两者之间可创建持久性的连接,并进行双向数据传输。
默认使用 80 端口
优势
- 较少的控制开销:减少了以前HTTP每次请求的请求头长问题,节省了宽带资源
- 更强的实时性:服务端随时可以主动发送数据给客户端,减少了HTTP的客户端请求后再返回响应
- 二进制支持:WebSocket 定义了 二进制 帧,相对HTTP,可以更轻松地处理二进制内容
- 可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议
协议
- WebSocket 是独立的、创建在 TCP 上的协议
- WebSocket 通过 HTTP/1.1 协议的 101 状态码进行握手
创建
// 需要先做验证,判断浏览器是否支持
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 代码
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
@ServerEndpoint(value = "/websocket", encoders = {JsonCommandEncoder.class})
@Component
public class WebSocketServlet {
/** 线程安全的,带lock锁,JUC中的 */
private static CopyOnWriteArraySet<WebSocketServlet> 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> T getEndpointInstance(Class<T> 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);
}
}
解码器
/**
* 在 websocket 中直接发送 obj 会有问题 - No encoder specified for object of class
* 需要对 obj 创建解码类,实现 websocket 中的 Encoder.Text<>
* */
public class JsonCommandEncoder implements Encoder.Text<JsonCommand> {
/**
* 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() {
}
}
配置类
@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 连接用户
/**
* 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();
}
}
}
项目地址:https://gitee.com/fjigww/websocket_Demo
数据库连接修改后,可 test 下 DaoTest直接添加随机数据