很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
首选需要配置一个客户端的实体类
package com.example.websocket.entity;
import lombok.Data;
import javax.websocket.Session;
import java.time.LocalDateTime;
/**
* 客户端实体类
*
* @author abdul
* @since 2024/08/29 19:50
*/
@Data
public class ClientInfoEntity {
/**
* 客户端唯一标识
*/
private String token;
/**
* 客户端连接的session
*/
private Session session;
/**
* 连接存活时间
*/
private LocalDateTime existTime;
}
创建 ChatEndpoint2 该类负责监听客户端的连接、断开连接、接收消息、发送消息等操作
package com.example.websocket.util;
import cn.hutool.core.util.ObjectUtil;
import com.example.common.easyExcel.exception.ExcelException;
import com.example.websocket.config.GetHttpSessionConfig;
import com.example.websocket.entity.ClientInfoEntity;
import com.example.websocket.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.CrossOrigin;
import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 该类负责监听客户端的连接、断开连接、接收消息、发送消息等操作。
*
* @author abdul
* @since 2024/08/29 19:50
*/
@Slf4j
@Component
@CrossOrigin(origins = "*")
@ServerEndpoint(value = "/webSocket/{token}", configurator = GetHttpSessionConfig.class)
public class ChatEndpoint2 {
//key:客户端连接唯一标识(token)
//value:ClientInfoEntity
public static final Map<String, ClientInfoEntity> uavWebSocketInfoMap = new ConcurrentHashMap<String, ClientInfoEntity>();
private static final int EXIST_TIME_HOUR = 6;
/**
* 连接建立成功调用的方法
*
* @param session 第一个参数必须是session
* @param sec
* @param token 代表客户端的唯一标识
*/
@OnOpen
public void onOpen(Session session, EndpointConfig sec, @PathParam("token") String token) {
if (uavWebSocketInfoMap.containsKey(token)) {
throw new ServiceException("token已建立连接");
}
//把成功建立连接的会话在实体类中保存
ClientInfoEntity entity = new ClientInfoEntity();
entity.setToken(token);
entity.setSession(session);
//默认连接6个小时
entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));
uavWebSocketInfoMap.put(token, entity);
//之所以获取http session 是为了获取获取httpsession中的数据 (用户名 /账号/信息)
// sendMessageFromConsole();
log.info("WebSocket 连接建立成功: " + token);
}
/**
* 当断开连接时调用该方法
*
* @param session
*/
@OnClose
public void onClose(Session session, @PathParam("token") String token) {
// 找到关闭会话对应的用户 ID 并从 uavWebSocketInfoMap 中移除
if (ObjectUtil.isNotEmpty(token) && uavWebSocketInfoMap.containsKey(token)) {
uavWebSocketInfoMap.remove(token);
log.info("WebSocket 连接关闭成功: " + token);
}
}
/**
* 接受消息
* 这是接收和处理来自用户的消息的地方。我们需要在这里处理消息逻辑,可能包括广播消息给所有连接的用户。
*
*/
@OnMessage
public void onMessage( @PathParam("token") String token, String message) throws IOException {
ClientInfoEntity entity = uavWebSocketInfoMap.get(token);
if (entity==null){
throw new ExcelException("token:"+token+"连接断开");
}
//业务逻辑
//只要接受到客户端的消息就进行续命(时间)
entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));
if (entity.getSession().isOpen()) {
entity.getSession().getBasicRemote().sendText(message);
}
}
/**
* 处理WebSocket中发生的任何异常。可以记录这些错误或尝试恢复。
*/
@OnError
public void one rror(Throwable error) {
log.error("报错信息:" + error.getMessage());
error.printStackTrace();
}
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy:MM:dd hh:mm:ss");
/**
* 发生消息定时器
*/
@PostConstruct
@Scheduled(cron = "0/1 * * * * ? ")
public void refreshDate() {
//开启定时任务,1秒一次向前台发送当前时间
//当没有客户端连接时阻塞等待
System.out.println(new Date());
if (!uavWebSocketInfoMap.isEmpty()) {
//超过存活时间进行删除
Iterator<Map.Entry<String, ClientInfoEntity>> iterator = uavWebSocketInfoMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, ClientInfoEntity> entry = iterator.next();
if (entry.getValue().getExistTime().compareTo(LocalDateTime.now()) <= 0) {
log.info("WebSocket " + entry.getKey() + " 已到存活时间,自动断开连接");
try {
entry.getValue().getSession().close();
} catch (IOException e) {
log.error("WebSocket 连接关闭失败: " + entry.getKey() + " - " + e.getMessage());
}
//过期则进行移除
iterator.remove();
}
}
sendMessage(FORMAT.format(new Date()));
}
}
/**
* 群发信息的方法
*
* @param message 消息
*/
public void sendMessage(String message) {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
+ "发送全体消息:" + message);
//循环客户端map发送消息
uavWebSocketInfoMap.values().forEach(item -> {
//向每个用户发送文本信息。这里getAsyncRemote()解释一下,向用户发送文本信息有两种方式,
// 一种是getBasicRemote,一种是getAsyncRemote
//区别:getAsyncRemote是异步的,不会阻塞,而getBasicRemote是同步的,会阻塞,由于同步特性,第二行的消息必须等待第一行的发送完成才能进行。
// 而第一行的剩余部分消息要等第二行发送完才能继续发送,所以在第二行会抛出IllegalStateException异常。所以如果要使用getBasicRemote()同步发送消息
// 则避免尽量一次发送全部消息,使用部分消息来发送,可以看到下面sendMessageToTarget方法内就用的getBasicRemote,因为这个方法是根据用户id来私发的,所以不是全部一起发送。
item.getSession().getAsyncRemote().sendText(message);
});
}
}
创建 GetHttpSessionConfig 类,主要用于WebSocket的握手配置
package com.example.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import java.util.Map;
import java.util.UUID;
/**
* 主要用于WebSocket的握手配置
* @author abdul
* @since 2024/08/29 19:55
*/
@Configuration
@EnableWebSocket
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {
/**
* 注意: 每一个客户端发起握手,端点就有一个新的实列,那么引用的这个配置也是新的实列,这里sec的用户属性也不同就不会产生冲突。
* 修改握手机制 就是第一次http发送过来的握手
* @param sec 服务器websocket端点的配置
* @param request
* @param response
*/
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
// 将从握手的请求中获取httpsession
HttpSession httpSession =(HttpSession) request.getHttpSession();
/**
* 一般会在请求头中添加token 解析出来id作为键值对
*/
Map<String, Object> properties = sec.getUserProperties();
/**
* 一个客户端和和服务器发起一次请求交互 就有一个唯一session
* 设置唯一标识:为每个客户端生成一个唯一的UUID作为连接标识,并将其存储在UserProperties中,便于后续跟踪与管理
*/
// properties.put(HttpSession.class.getName(),httpSession);
String sessionKey = UUID.randomUUID().toString().replaceAll("-", "");
properties.put("Connected",sessionKey);
}
}
创建controller方法
package com.example.websocket.controller;
//import com.example.easyexcel.feign.RemoteServiceClient;
import com.example.websocket.entity.ClientInfoEntity;
import com.example.websocket.entity.Message;
import com.example.websocket.service.MessageService;
import com.example.websocket.util.ChatEndpoint2;
import com.example.websocket.util.ClientEndpoint1;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.websocket.ContainerProvider;
import javax.websocket.DeploymentException;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
import java.io.IOException;
import java.net.URI;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
/**
* @version 1.0
* @Author Abdul
* @Date 2024/10/14 11:22
* @注释
*/
@RestController
@RequestMapping("/websocket")
public class ServerController {
@Resource
private ChatEndpoint2 server;
private Session session;
@Resource
private MessageService service;
/**
* 客户端获取连接
*/
@GetMapping("/conn")
public Session conn(String token) {
try {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
// 创建连接的阻塞锁
final CountDownLatch latch = new CountDownLatch(1);
// 连接到服务器
session = container.connectToServer(ClientEndpoint1.class, URI.create("ws://localhost:9000/webSocket/"+token));
// 等待连接关闭
latch.await();
} catch (DeploymentException | IOException | InterruptedException e) {
}
return session;
}
/**
* 服务端 发送信息调用的接口
*/
@PostMapping("/server/send")
public String SendMessage(@Param("token") String token, Message message) throws IOException {
String serverMessage = "服务端:" + message.getText();
message.setSender("服务端");
save(message);
server.onMessage(token, serverMessage);
return "success";
}
/**
* 客户端 发送信息调用的接口
*/
@GetMapping("/client/send")
public String SendClientMessage(Message message,String token) throws IOException {
try {
ClientInfoEntity clientInfoEntity = ChatEndpoint2.uavWebSocketInfoMap.get(token);
Session session1 = clientInfoEntity.getSession();
String clientMessage = "客户端:" + message.getText();
message.setSender("客户端");
save(message);
session1.getBasicRemote().sendText(clientMessage);
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
//将对话记录保存到数据库
public void save(Message message) {
message.setCreateTime(new Date());
service.save(message);
}
}
首先客户端获取连接 需要传入自己唯一token的参数
可以看见客户端成功连接上了服务端
接着客户端向服务端发送信息:
服务端接着对客户端进行发送信息响应
至此 客户端 服务端实现双向实时通信 并把对话信息保存到数据库中
标签:websocket,SpringBoot,public,token,example,import,客户端,链接,webSocket From: https://blog.csdn.net/No_speak/article/details/143159255