消息推送常见方式
1. 轮询(短轮询)
浏览器以指定的时间间隔向服务器发出HTTP请求,服务器实时返回数据给浏览器
使用场景:使用定时任务每秒给后端发送HTTP请求
劣势:服务器没有新的数据更新会返回空的数据,下一次发请求并且更新数据才会请求到,展示会有延迟,而且每秒钟处理请求,会对服务器造成很大的压力
2. 长轮询
浏览器发出ajax请求,服务器端接收到请求后,会阻塞请求直到有数据或者超时才返回
3. SSE(server-sent event) : 服务器发送事件
SSE在服务器和客户端之间打开一个单向通道
服务端响应的不再是一次性的数据包,而是text/event-stream类型的数据流信息
服务器有数据变更时将数据流式传输到客户端
4. WebSocket
WebScoket是一种在基于TCP连接上进行全双工通信的协议
全双工(Full Duplex):允许数据在两个方向上同时传输。
半双工(Half Duplex):允许数据在两个方向上传输,但是同一个时间段内只允许一个方向上传输。
WebScoket API
客户端 [浏览器]API
1. WebSocket对象创建
let ws = new WebSocket(URL);
2. WebSocket对象相关事件
3. WebSocket对象提供的方法
服务端的API
简介
Tomcat的7.0.5 版本开始支持WebSocket,并且实现了Java WebSocket规范。
Java WebSocket应用由一系列的Endpoint组成。Endpoint是一个iava对象,代表Websocket链接的一端,对于服务端我们可以视为处理具体WebSocket消息的接口。
我们可以通过两种方式定义Endpoint:
第一种是编程式,即继承类javax.websocket.Endpoint并实现其方法
第二种是注解式,即定义一个POJO,并添加 @ServerEndpoint相关注解
Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法,规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期方法如下:
//Endpoint是一个iava对象,代表Websocket链接的一端,对于服务端我们可以视为处理具体WebSocket消息的接口。
//注解使用@ServerEndpoint("/路径")
@ServerEndpoint("/chat")
@Component//交给spring容器管理
public class ChatEndpoint {
@OnOpen
//连接建立时被调用
public void onePen(Session session, EndpointConfig config){
}
@OnMessage
//接收到客户端发送的数据时被调用
public void onMessage(String message){
}
@OnClose
//连接关闭时被调用
public void onClose(Session session){
}
}
服务端接受客户端发送的数据
编程式
通过定义一个类去实现 MessageHgndler接口 消息处理器来接收消息
注解式
在定义Endpoint时,通过@OnMessage注解指定接收消息的方法
服务器推送数据给客户端
发送消息则由 RemoteEndpoint 完成,其实例由 Session 维护。
发送消息有2种方式发送消息
通过session.getBasicRemote 获取同步消息发送的实例,然后调用其 sendXxx()方法发送消息
通过session.getAsyncRemote 获取异步消息发送实例,然后调用其sendXxx()方法发送消息
在线聊天室实现
流程图
消息格式
客户端-->服务端
{"toName":"张三","message":“你好”}
服务端-->客户端
1.系统消息格式:
{"'system":true,"fromName":null,"message":["李四","王五"]}
2.推送给某一个用户的消息格式:
{"system":false,"fromName":"张三",message":"你好"}
代码实现
依赖:
<!-- websocket的坐标 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.79</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
Configuration配置
package com.example.webscoket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
//注入ServerEndpointExporter, 自动扫描使用@@ServerEndpoint注解的
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
配置类,用于获取HHpSession对象
package com.example.webscoket.component;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
//配置类,用于获取HHpSession对象
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
//将httpsession对象存储到配置对象中
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
工具类
package com.example.webscoket.utils;
import com.alibaba.fastjson.JSON;
import com.example.webscoket.ws.pojo.ResultMessage;
public class MessageUtils {
public static String getMessage(boolean isSystemMessage,String fromName,Object message) {
ResultMessage result = new ResultMessage();
result.setSystem(isSystemMessage);
result.setMessage(message);
if (fromName!=null){
result.setMessage(fromName);
}
return JSON.toJSONString(result);
}
}
package com.example.webscoket.ws.pojo;
import lombok.Data;
@Data
public class Message {
private String toName;
private String message;
}
package com.example.webscoket.ws.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 封装json格式消息的工具类
*
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultMessage {
private Boolean System;
private Object Message;
private String FromName;
public Boolean getSystem() {
return System;
}
public void setSystem(Boolean system) {
System = system;
}
public Object getMessage() {
return Message;
}
public void setMessage(Object message) {
Message = message;
}
public String getFromName() {
return FromName;
}
public void setFromName(String fromName) {
FromName = fromName;
}
}
注解形式使用@ServerEndpoint("/路径")
在 @ServerEndpoint 注解中引入配置器configurator
@ServerEndpoint(value ="/chat",configurator = GetHttpSessionConfigurator.class)
package com.example.webscoket.ws; import com.alibaba.fastjson.JSON; import com.example.webscoket.config.GetHttpSessionConfigurator; import com.example.webscoket.utils.MessageUtils; import com.example.webscoket.ws.pojo.Message; import com.example.webscoket.ws.pojo.ResultMessage; import org.springframework.stereotype.Component; import javax.servlet.http.HttpSession; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; //Endpoint是一个iava对象,代表Websocket链接的一端,对于服务端我们可以视为处理具体WebSocket消息的接口。 //注解使用@ServerEndpoint("/路径") //在 @ServerEndpoint 注解中引入配置器@ServerEndpoint(value ="/chat",configurator = GetHttpSessionConfigurator.class) @ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class) @Component//交给spring容器管理 public class ChatEndpoint { private static final Map<String,Session>onLineUsers=new ConcurrentHashMap<>();//用来保存session private HttpSession httpSession; /** * 建立websocket连接后,被调用 * @param session * @param config */ @OnOpen public void onePen(Session session, EndpointConfig config){//config==sec //1.将session进行保存 this.httpSession= (HttpSession) config.getUserProperties().get(HttpSession.class.getName()); String user = (String) this.httpSession.getAttribute("user"); onLineUsers.put(user,session);//存入Map中 //2.广播消息.需要将登录的所有的用户推送给所有的用户 String message = MessageUtils.getMessage(true, null, getFriends()); broadcastAllUsers(message); } //返回map集合中的所有key对象,并封装返回为Set集合 public Set getFriends(){ Set<String> set = onLineUsers.keySet(); System.out.printf("set======"+set); return set; } //遍历存入的map取消息 private void broadcastAllUsers(String message){ //遍历map集合 Set<Map.Entry<String, Session>> entries = onLineUsers.entrySet();//entrySet实现了Set接口,里面存放的是键值对。一个K对应一个V。 for (Map.Entry<String, Session> entry : entries) { //获取到所有用户对应的session对象 Session session = entry.getValue(); System.out.println(entry.getKey()+","+entry.getValue()); //发送消息 try { session.getBasicRemote().sendText(message);//同步消息 } catch (Exception e) { //记录日志 } } } /** * 浏览器发送消息到服务端,该方法被调用 * * 张三 ---> 李四 * @param message */ @OnMessage public void onMessage(String message){ //将消息推送给指定的用户 Message msg = JSON.parseObject(message, Message.class);//转换为java对象 //获取 消息接收方的用户名 String toName = msg.getToName(); String mess = msg.getMessage(); //获取消息接收方用户对象的session对象 Session session = onLineUsers.get(toName); //当前登录者的session对象 String user = (String) this.httpSession.getAttribute("user"); String msg1 = MessageUtils.getMessage(false, user, mess); try { session.getBasicRemote().sendText(msg1); } catch (Exception e) { //记录日志 } } /** * 断开 websocket 连接时被调用 * @param session */ @OnClose //连接关闭时被调用 public void onClose(Session session){ //1.从onlineUser中剔除当前用户的session对象 String user = (String) this.httpSession.getAttribute("user"); onLineUsers.remove(user); //2.通知其他所有的用户,当前用户下线了 String message = MessageUtils.getMessage(true, null, getFriends()); broadcastAllUsers(message); } }
实体
package com.example.webscoket.entity;
import lombok.Data;
@Data
public class User {
private String userId;
private String username;
private String password;
}
package com.example.webscoket.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
private Boolean flag;
private String message;
public Boolean getFlag() {
return flag;
}
public void setFlag(Boolean flag) {
this.flag = flag;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
controller
package com.example.webscoket.controller;
import com.example.webscoket.entity.Result;
import com.example.webscoket.entity.User;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
@RestController
@RequestMapping("/user")
public class UserController {
public Result login(@RequestBody User user, HttpSession session){
Result result = new Result();
if (user!=null &&"123".equals(user.getPassword())){
result.setFlag(true);
result.setMessage("登陆成功");
session.setAttribute("user",user.getUsername());
}else {
result.setFlag(false);
result.setMessage("登陆失败");
}
return result;
}
}