5.Websocket实现消息推送
1.目的
项目需要一个在线协同办公功能来进行消息实时推送,我采用SpringBoot结合Websocket来实现该功能。WebSocket 是一种在单个TCP连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据,可以在客户端和服务端之间建立持久的连接,实现实时的双向通信。
相对于传统的HTTP请求,WebSocket具有以下优势:
- 实时性:Websocket提供实时的双向通信能力,服务器可以主动推送消息给客户端,而不需要客户端主动发送请求。这使得Websocket适用于需要及时更新的实时场景。
- 低延迟:Websocket通过建立长连接,可以减少每个消息的传输开销,从而降低通信的延迟。
- 较少的带宽占用:相比于HTTP请求,Websocket使用更少的带宽,因为Websocket在建立连接后只需要较小的额外开销
- 跨域支持:ebsocket可以轻松支持跨域通信,因为它不受浏览器同源策略的限制
WebSocket使用场景:
- 实时聊天应用:Websocket能够提供实时的双向通信,使得实时聊天系统能够实时更新消息,并且可以实现在线用户状态的实时更新。
- 实时协作编辑:Websocket使得多个用户能够实时协作编辑同一个文档,每个用户的修改可以广播给其他用户(腾讯文档),实现实时的协同编辑功能。
- 实时推送服务:Websocket可以与服务器建立持久化连接,服务器可以主动推送实时的更新给客户端,例如实时股票行情推送、实时新闻推送等
消息推送其实还可以通过消息队列来完成,后续将通过消息队列完成该功能。
2.使用
导入依赖<!--websocket实现消息推送-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
代码实现:
@Configuration public class WebSocketConfig { //通过@Bean注解将实例注入到Spring容器中生成Bean对象 @Bean public ServerEndpointExporter serverEndpointExporter(){ //注册该Bean对象到Spring容器后,将会自动注册所有@ServerEndPoint注解声明的websocket endpoint(端点) return new ServerEndpointExporter(); } }
@Data public class Message { //发送者 private String from; //接收者 private String to; //消息 private String text; //工单编号 private String woCode; //发送时间:规定为这种格式 @JSONField(format = "yyyy-MM-dd HH:mm:ss") private Date date; }
@ServerEndpoint("/webSocket/{username}")
@Component
public class WebSocketServer {
//静态变量,记录当前在线连接数,将其设置为线程安全
private static AtomicInteger onlineNum = new AtomicInteger();
//concurrent包的线程安全Set,存放每个客户端对应的WebSocketServer对象
private static ConcurrentHashMap<String, Session>sessionPools = new ConcurrentHashMap<>();
private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
//发送消息
public void sendMessage(Session session, String message) throws IOException {
if(session != null){
synchronized (session){
System.out.println("发送数据:"+message);
session.getBasicRemote().sendText(message);
}
}
}
//给指定权限用户发送消息
public void sendInfo(String username, String message){
Session session = sessionPools.get(username);
try{
sendMessage(session, message);
}catch(Exception e){
e.printStackTrace();
}
}
//给具有调度员群发消息
public void broadcast(String message){
for (Session session : sessionPools.values()) {
try{
sendMessage(session, message);
}catch(Exception e){
e.printStackTrace();
continue;
}
}
}
//建立连接成功调用
@OnOpen
public void onOpen(Session session, @PathParam(value = "username") String username){
sessionPools.put(username, session);
addOnlineCount();
System.out.println(username+"加入WebSocket!当前人数为:"+ onlineNum);
//广播上线信息
Message msg = new Message();
msg.setDate(new Date());
msg.setTo("0");
msg.setText(username);
broadcast(JSON.toJSONString(msg, true));
}
//关闭连接时调用
@OnClose
public void onClose(@PathParam(value = "username") String username){
sessionPools.remove(username);
subOnlineCount();
System.out.println(username + "断开webSocket连接!当前人数为" + onlineNum);
// 广播下线消息
Message msg = new Message();
msg.setDate(new Date());
msg.setTo("-2");//下线时设置为-2
msg.setText(username);
broadcast(JSON.toJSONString(msg,true));
}
//收到客户端信息后,根据接收人的username把消息推下去或者群发
// to=-1群发消息
@OnMessage
public void onMessage(String message) throws IOException{
System.out.println("server get" + message);
Message msg= JSON.parseObject(message, Message.class);
msg.setDate(new Date());
UserInfoMapper userInfoMapper = SpringBeanUtil.getBean(UserInfoMapper.class);
UserInfo from = userInfoMapper.getUserByUserId(msg.getFrom());
UserInfo to = userInfoMapper.getUserByUserId(msg.getTo());
WorkOrderMapper workOrderMapper = SpringBeanUtil.getBean(WorkOrderMapper.class);
if (msg.getTo().equals("-1")) {//当to为-1时就群发
broadcast(JSON.toJSONString(msg,true));
} else {//指定发送
if (from.getRoleId().equals(Integer.valueOf(4))){//发布工单给指挥员
//获取工单ID,然后查询工单,将工单转换为JSONString的格式
WorkOrder workOrder = workOrderMapper.getDetailsByCode(Integer.valueOf(msg.getWoCode()));
String text = JSON.toJSONString(workOrder);
msg.setText(text);
sendInfo(msg.getTo(), JSON.toJSONString(msg,true));
//记录工单发布的人和时间
DispatchTask task = new DispatchTask();
task.setDispatcher(from.getUserId());
task.setWoCode(Integer.valueOf(msg.getWoCode()));
task.setStatus(1);
task.setCreateUser(from.getUserId());
task.setCreateTime(new Date());
IDispatchTaskService dispatchTaskService = SpringBeanUtil.getBean(IDispatchTaskService.class);
boolean save = dispatchTaskService.save(task);
if (save == false){
log.error("插入发布工单任务失败");
}
}else if (from.getRoleId().equals(Integer.valueOf(5))){//审批工单
sendInfo(msg.getTo(), JSON.toJSONString(msg,true));
//记录工单审批的人和时间
DirectTask directTask = new DirectTask();
directTask.setDirector(from.getUserId());
directTask.setWoCode(Integer.valueOf(msg.getWoCode()));
if (!msg.getText().equals("批准")){
directTask.setStatus(0);
}
directTask.setStatus(1);//批准
directTask.setCreateUser(from.getUserId());
directTask.setCreateTime(new Date());
IDirectTaskService directTaskService = SpringBeanUtil.getBean(IDirectTaskService.class);
boolean save = directTaskService.save(directTask);
if (save == false){
log.error("插入审批工单任务失败");
}
}else{
onClose(from.getUserId());
}
}
}
//错误时调用
@OnError
public void onError(Session session, Throwable throwable){
System.out.println("发生错误");
throwable.printStackTrace();
}
public static void addOnlineCount(){
onlineNum.incrementAndGet();
}
public static void subOnlineCount(){
onlineNum.decrementAndGet();
}
public static AtomicInteger getOnlineNumber(){
return onlineNum;
}
public static ConcurrentHashMap<String, Session>getSessionPools(){
return sessionPools;
}
}
注意事项:
- 定义为WebSocket的服务端点类需要通过@ServerPoint注解标识,并且需要配合@Component注解注入到Spring容器中生成Bean实例;
- 为将标有@ServerEndPoint的WebSocket服务注册到WebSocket服务中,需要通过@Bean注解标注一个返回值为ServerEndpointExporter的方法,这样就能解决WebSocket服务器注入问题。
WebSocket存在的问题:
- 较高的带宽消耗:虽然相对于HTTP,WebSocket拥有较小的宽带消耗,但是WebSocket在建立连接后会一直保持开启状态,导致持续的数据传输,还是可能会占用更多的带宽资源。
- 连接状态管理:由于WebSocket连接的持久性,需要在服务器端管理大量的连接状态,这可能对服务器产生一定的负担。
- 网络代理限制:某些网络环境或代理服务器可能会阻止或限制WebSocket连接,从而导致无法正常建立连接或通信受限。
- 旧版本兼容性:相对于传统的HTTP协议,WebSocket是一个较新的技术,因此在某些旧版本的浏览器和服务器可能不被完全支持。
- 安全性问题:WebSocket需要实时的双向通信,可能会引入潜在的安全风险,如跨站脚本攻击(XSS)或服务器资源过度利用等。
运行结果:
工单发布

工单审批

3.原理
HTTP与WebSocket的区别:
下面贴出参考链接的一张HTTP与WebSocket对比图:

- WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息,而HTTP是单向的;
- WebSocket是需要浏览器和服务器握手进行建立连接的,而http是浏览器发起向服务器的连接。
注意:虽然HTTP/2也具备服务器推送功能,但HTTP/2 只能推送静态资源,无法推送指定的信息。
实现原理:
与http协议一样,WebSocket协议也需要通过已建立的TCP连接来传输数据。具体实现上是通过http协议建立通道,然后在此基础上用真正的WebSocket协议进行通信,所以WebSocket协议和http协议是有一定的交叉关系。
1.WebSocket基于HTTP完成了一部分握手
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
相对于HTTP协议握手请求,WebSocket多了Upgrade: websocket、Connection: Upgrade。这些就是 WebSocket 的核心了,告诉 Apache 、 Nginx 等服务器:我发起的请求要用 WebSocket 协议。
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
- Sec-WebSocket-Key 是一个 Base64 encode 的值,是浏览器随机生成的,用于验证WebSocket。
- Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同 URL 下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
- Sec-WebSocket-Version 是告诉服务器所使用的 WebSocket Draft (协议版本);
然后服务器会返回下列东西,表示已经接受到请求, 成功建立 WebSocket 啦!
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
- Upgrade: websocket、Connection: Upgrade告诉客户端使用的WebSocket协议;
- Sec-WebSocket-Accept 这个是经过服务器确认,并且加密过后的 Sec-WebSocket-Key;
- Sec-WebSocket-Protocol 则是表示最终使用的协议;
WebSocket连接的过程:
- 首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;
- 然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;
- 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。
浙公网安备 33010602011771号