(一)WebSocket简介与应用场景
[1]介绍:和Socket一样,都是用于进行长连接通讯。JavaWeb传统通信方式都是请求-响应的方式进行,即浏览器必须发起请求后,才能收到服务器的响应数据。而WebSocket实现了服务器主动向浏览器发送数据,无需浏览器请求的效果。
[2]应用场景:
-
web版的在线聊天室
-
实时广播消息到浏览器
(二)WebSocket整合入SpringBoot
[1]准备工作
(1)导入WebSocket的SpringBoot依赖
org.springframework.boot spring-boot-starter-websocket
(2)导入JQuery,前端发起WebSocket连接需要JQuery
[2]配置WebSocket
和其他框架整合入SpringBoot一样,再config包中创建一个,MyWebSocketConfig的,并使用@Configuration注解修饰,在该类中用@Bean装配ServerEndpointExporter对象
@Configuration
public class MyWebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Bean
public ServerEndpointExporter getServerEndpointExporter (){
return new ServerEndpointExporter();
}
}
[3]创建WebSocket服务节点ServerEndpoint类与WebSocket连接对象类
在component包下创建ChatWebSocketServer类该类封装了整个WebSocket的通信逻辑和回调函数,作为一个组件存在,且同样交给SpringIOC容器来管理,因此使用@Component注解修饰,该组件作为单例存在。
ChatWebSocketServer的设计如下
@Component
@ServerEndpoint("/chatServer/{userId}")
public class ChatWebSocket {
private static int onlineCount = 0;
public static ConcurrentHashMap webSocketConnMap = new ConcurrentHashMap<>();
@onOpen
public void onOpen(Session session,@PathParam("userId") String userId) {
//建立了ws新连接
WebSocketConnection webSocketConnection=new WebSocketConnection();
webSocketConnection.setUserId(userId);
webSocketConnection.setWsSession(session);
if(webSocketConnMap.containsKey(userId)){
//如果Map中有该key,则先清除掉
webSocketConnMap.remove(userId);
webSocketConnMap.put(userId,webSocketConnection);
}else{
//没有则直接加入
webSocketConnMap.put(userId,webSocketConnection);
//在线数加1
addonlineCount();
}
System.out.println("用户连接:"+userId+",当前在线人数为:" + getonlineCount());
try {
webSocketConnection.sendMessage("连接成功");
} catch (IOException e) {
System.err.print("用户:"+userId+",网络异常!!!!!!");
}
}
@onClose
public void onClose(@PathParam("userId")String userId) {
if(webSocketConnMap.containsKey(userId)){
webSocketConnMap.remove(userId);
//从set中删除
subonlineCount();
}
System.out.println("用户退出:"+userId+",当前在线人数为:" + getonlineCount());
}
@onMessage
public void onMessage(String message, @PathParam("userId")String userId) {
System.out.println("用户消息:"+userId+",报文:"+message);
//可以群发消息,目前仅发送给接收者ID
if(!StringUtils.isEmpty(message)){
try {
//解析发送的报文
JSonObject jsonObject =new JSonObject(message);
//追加发送人(防止串改)
jsonObject.put("fromUserId",userId);
String toUserId=jsonObject.getString("toUserId");
//传送给对应toUserId用户的websocket连接
if(!StringUtils.isEmpty(toUserId)&&webSocketConnMap.containsKey(toUserId)){
webSocketConnMap.get(toUserId).sendMessage(jsonObject.toString());
}else{
System.err.println("请求的userId:"+toUserId+"不在该服务器上");
//否则不在这个服务器上,发送到mysql或者redis
}
//也给发送人自己发送一份,能看到自己的发送记录
webSocketConnMap.get(userId).sendMessage(jsonObject.toString());
}catch (Exception e){
e.printStackTrace();
}
}
}
@onError
public void onError(@PathParam("userId")String userId, Throwable error) {
System.err.println("用户错误:"+userId+",原因:"+error.getMessage());
error.printStackTrace();
}
public static synchronized int getonlineCount() {
return onlineCount;
}
public static synchronized void addonlineCount() {
onlineCount++;
}
public static synchronized void subonlineCount() {
onlineCount--;
}
}
ServerPoint服务点类有4个关键的生命周期函数,使用ws特有的注解进行修饰:
(1)@OnOpen:前端建立ws连接时调用
在该方法中,主要将建立连接时的两大参数获取到,其一是建立该ws连接的用户id,其二是该ws连接的Session会话对象。前者将用于作为凭证去Map中寻找对应的服务节点对象,后者将用于发送消息给前端。因此再该类中将这两个参数构造为WebSocketConnection对象,放入Map中存储,以便以后的使用。
(2)@OnMessage:当收到前端发送的消息时调用
在该方法中,可以获取到两个参数,其一是消息发送者的id即此处为userId,其二是发送内容String类型,在该方法中具体的操作取决于具体的业务需求。此处可以通过userId在Map中获得对应的WebSocketConnetion连接对象,用连接对象中提供的方法将业务逻辑中处理后的信息返回给客户端。在本演示项目中,模拟的是两人聊天。因此OnMessage中做了判断处理,只对接受方ID发送信息,也对发送本人也留了一份。
(3)@OnClose:当连接意外断开或是前端发起了断开连接后调用
在该方法中,可以获取到消息发送的ID即此处为userId。该函数的主要目的就是将该userId对应的WebSocketConnection连接对象从Map中移除。
(4)@OnError:当发生错误时调用
在该方法中,可以获取到消息发送的ID即此处为userId。该函数的主要目的就是将错误日志输出和记录。
在pojo包下创建WebSocketConnection类,该类作为WS连接的实体类,包含两个字段,其一是userId,其二是ws的Session对象。并提供了一个方法sendMessage,直接调用该方法就可将消息发回给前端,在前端的ws回调中进行处理
WebSocketConnection的设计如下
import javax.websocket.*;
import java.io.IOException;
public class WebSocketConnection {
private String userId;//用户ID
private Session wsSession;//ws会话对象
//省略getter setter和构造
public void sendMessage(String message) throws IOException {
wsSession.getBasicRemote().sendText(message);
}
}
[4]WebSocket服务节点ServerEndpoint类注入其他bean的特殊处理
由于Socket特性,长连接通信中,socket对象本身是多实例(即每建立一个连接就会产生一个Socket对象),且WebSocket线程是独立于Spirng主线程异步存在,新建立的Socket对象不是由Spring创建而是由异步线程创建,因此自动注入无效。
正确的方法应该使用静态工具类BeanUtil的方式来获取Bean,需要实现ApplicationContextAware接口,拿到ApplicationContext对象来静态getBean()【详见SpringBoot高级用法】
(三)前端使用WebSocket
在前端页面使用websocket前先导入jQuery
[1]指定ws的连接url
ws的连接格式为ws://localhost:8080/serverEndpointxx的形式
-
若为http则使用ws://作为协议头
-
若为https则使用wss://作为协议头【此模式下仅可使用域名,不能使用ip】
在本项目中的ws连接为:
var socketUrl="ws://localhost:8080/websocket/chatServer/"+$("#userId").val();
其中$("#userId").val()代表建立ws连接的用户Id,真实情况应该是从session中获取
[2]构建WebSocket对象并绑定监听函数
var socket=new WebSocket()传入刚刚ws的url
监听函数同样为那4个,此处以匿名函数的方式进行绑定
socket = new WebSocket(socketUrl);
socket.onopen = function() {
console.log("websocket已打开");
//socket.send("这是来自客户端的消息" + location.href + new Date());
};
//获得消息事件
socket.onmessage = function(msg) {
console.log(msg.data);
//发现消息进入 开始处理前端触发逻辑
$("#chatBox").append(""+msg.data+"");
};
//关闭事件
socket.onclose = function() {
console.log("websocket已关闭");
};
//发生了错误事件
socket.onerror = function() {
console.log("websocket发生了错误");
}
其中最重要的就是onmessage函数,该函数携带msg参数,可以获得后端发回的数据,然后在该函数
中进行前端的逻辑。
需要注意:在创建WebSocket对象的时候,就已经建立好ws连接了,不用手动去连。
[3]使用WebSocket对象发送消息和断开连接
分别为以下两个函数
socket.send('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
socket.close();
完整的前端代码展示
聊天室页面 开两个这个页面,分别用两个ID作为发送者和接收者,发送信息即可测试【发送者ID(也是当前登录者)】:
【接收者ID】:
【发送内容】:
【操作】:开启socket
【操作】:发送消息
【接收到的内容】:...
效果
可能出现的问题:
1:因为配置了AOP切面而导致启动项目时WebSocket报错:it is not annotated with @ServerEndpoint无法启动。
解决办法:AOP切面配置切点时排出掉WebSocket自定组件(这里是ChatWebSocketServer )所在的包路径。



