websocket
1. WebSocket介绍
-
WebSocket 是一种网络通信协议。RFC6455 定义了它的通信标准。
-
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
-
HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。
-
这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。
-
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步 AJAX 请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。
2. websocket协议
-
本协议有两部分:握手和数据传输。
-
握手是基于http协议的。
- 客户端(浏览器)实现
3.1 websocket对象
实现 WebSockets 的 Web 浏览器将通过 WebSocket 对象公开所有必需的客户端功能(主要指支持 Html5 的浏览器)。
以下 API 用于创建 WebSocket 对象:
var ws = new WebSocket(url);
参数url格式说明: ws://ip地址:端口号/资源名称
3.2 websocket事件
WebSocket 对象的相关事件
事件 | 事件处理程序 | 描述 |
---|---|---|
onopen | websocket对象.onopen | 连接建立时触发 |
onmessage | websocket对象.onmessage | 客户端接收服务端数据时触发 |
onerror | websocket对象.onerror | 通信发生错误时触发 |
onclose | websocket对象.onclose | 连接关闭时触发 |
3.3 WebSocket方法
WebSocket 对象的相关方法:
方法 | 描述 |
---|---|
send | 使用连接时发送消息 |
服务器实现
Java WebSocket应用由一系列的WebSocketEndpoint组成。Endpoint 是一个java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口, 就像Servlet之与http请求一样。
我们可以通过两种方式定义Endpoint:
· 第一种是编程式, 即继承类 javax.websocket.Endpoint并实现其方法。
· 第二种是注解式, 即定义一个POJO, 并添加 @ServerEndpoint相关注解。
实现流程
服务端如何接收数据
通过为 Session 添加 MessageHandler 消息处理器来接收消息,当采用注解方式定义Endpoint时,我们还可以通过 @OnMessage 注解指定接收消息的方法。
服务端如何推送数据
发送消息则由RemoteEndpoint完成,其实例由Session维护,根据使用情况,我们可以通过
Session.getBasicRemote获取同步消息发送的实例,然后调用其sendXxx()方法就可以发送消息,可以通过
Session.getAsyncRemote获取异步消息发送实例。
实现一个简单的聊天室功能
步骤:
1.首先导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
private Long id;
@TableField(value = "from_user_id")
private User fromUser;
@TableField(value = "to_user_id")
private User toUser;
private String content;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime updateTime;
private int messageType;
@TableField(exist = false)
private String toName;
@TableField(exist = false)
private String fromName;
@TableField(exist = false)
private String message;
//添加好友码
public static final int ADD_FRIEND = 2;
//好友列表消息码
public static final int FRIEND_LIST_TYPE = 3;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T>{
/**
* 状态码
*/
private Integer code;
/**
* 提示信息,如果有错误时,前端可以获取该字段进行提示
*/
private boolean flag;
private String message;
private T data; //数据
public static <T> Result<T> success(T object) {
Result<T> r = new Result<T>();
r.data = object;
r.code = 1;
r.flag = true;
return r;
}
public static <T> Result<T> error(String message) {
Result r = new Result();
r.message = message;
r.code = 0;
r.flag = false;
return r;
}
}
@PostMapping("/login")
public Result<User> login(@RequestBody User user ,HttpSession session){
String password = user.getPassword();
String username = user.getUsername();
log.info("用户登录操作");
//MD5加密
password = DigestUtils.md5DigestAsHex(password.getBytes());
//根据用户名查找数据库
LambdaQueryWrapper<User> wrapper =new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername,username);
User one = userService.getOne(wrapper);
if (one == null){
return Result.error("用户不存在,请先注册");
}if (! one.getPassword().equals(password)){
return Result.error("用户名或者密码有误");
}
// 登录成功,将用户的ID存储到WebSocket连接的Session中
session.setAttribute("userId", one.getId()); // 假设用户ID为one.getId()
String sessionId = session.getId();
return Result.success(one);
}
@GetMapping("/getUsername")
private String getUsername(HttpSession session) {
// 从 HttpSession 中获取用户信息
User user = (User) session.getAttribute("user");
if (user != null) {
return user.getUsername();
}
return null;
}
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
@Bean
//注入ServerEndpointExporter bean.对象,自动注册使用了@ServerEndpoint
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 定义一个 WebSocket 入口,客户端需要连接到它才能接收推送消息
registry.addEndpoint("/websocket").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用推送的消息代理(即使用 STOMP 实现 WebSocket 的代理)
config.enableSimpleBroker("/topic");
// 开启基于用户的 WebSocket 会话
config.setUserDestinationPrefix("/user");
}
}
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {
/**
* 获取session对象
* @param sec
* @param request
* @param response
*/
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//获取HttpSession.对象
HttpSession httpsession = (HttpSession) request.getHttpSession();
//将httpSession存储到配置对象
sec.getUserProperties().put(HttpSession.class.getName(), httpsession);
}
}
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)
@Component
@Slf4j
public class ChatEndpoint {
private Session session;
private static HttpSession httpSession;
//用来存储每一个客户端对象对应的ChatEndpoint对象
private static final Map<String,Session> onlineUsers = new ConcurrentHashMap<>();
@OnOpen
public void onopen(Session session, EndpointConfig config) {
//将局部的session对象赋值给成员session
this.session = session;
//获取Httpsession对象 ,键值对集合,得到键获取值
this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String user = (String) this.httpSession.getAttribute("user");
onlineUsers.put(user,session);
//广播消息,获取在线的所有好友
String message = MessageUtils.getMessage(true, null, getFriendsName());
broadcastAllUsers(message);
}
/**
* 获取所有在线的好友信息,名称
* @return
*/
public Set getFriendsName(){
Set<String> set = onlineUsers.keySet();
return set;
}
/**
* 发给所有人的广播
* @param message
*/
private void broadcastAllUsers(String message){
// 拿到所有的用户的chatEndpoint对象 //存储用户的session信息
Set<Map.Entry<String,Session>> entries = onlineUsers.entrySet();
//遍历map集合
for (Map.Entry<String, Session> entry : entries) {
//获取所有用户对应的session对象 //拥有getBasicRemote发送消息的方法
Session session =entry.getValue();
//发送消息
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@OnMessage
public void onMessage(String message, @PathParam("username") String username){
try {
log.info("服务端收到用户username={}的消息:{}", username, message);
//将消息转换成message对象
Message msg = JSON.parseObject(message,Message.class);
//获取接收方的用户名
String toName = msg.getToName();
//获取消息数据
String message1 = msg.getMessage();
//获取接收方的用户的session对象
Session session = onlineUsers.get(toName);
if (session != null) {
// 获取当前登录的用户 从session中获取
String user = (String) httpSession.getAttribute("user"); //user代表的是发送方
String message2 = MessageUtils.getMessage(false, user, message1);
session.getBasicRemote().sendText(message2);
}else {
log.info("未找到用户username{}的session",toName);
}
if (msg.ADD_FRIEND ==2 && session != null){
//获取发送发
String user = (String) httpSession.getAttribute("user");
//获取消息
String message2 = MessageUtils.getMessage(false, user, message1);
//发送
session.getBasicRemote().sendText(message2);
//将好友添加到相应的列表中,例如用Map存储好友列表
Map<String, List<String>> friendLists = (Map<String, List<String>>) httpSession.getAttribute("friendLists");
List<String> friendList=friendLists.get(user);
friendList.add(toName);
friendLists.put(user,friendList);
httpSession.setAttribute("friendLists", friendLists);
//发送好友列表
Message friendListMessage = new Message();
friendListMessage.setMessageType(3);
friendListMessage.setFromName("System");
friendListMessage.setToName(user);
friendListMessage.setMessage(JSON.toJSONString(friendList));
ChatEndpoint.send(friendListMessage,getFriendsName());
}
else {
log.info("未找到用户username{}的session",toName);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void send(Message friendListMessage, Set friendsName) {
try {
String toName = friendListMessage.getToName();
String message = friendListMessage.getMessage();
Session session = onlineUsers.get(toName);
if (session != null) {
String json = JSON.toJSONString(message);
session.getBasicRemote().sendText(json);
} else {
log.info("未找到用户{}的session", toName);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@OnClose
public void onclose(Session session) {
//提出session中的记录
String user = (String) this.httpSession.getAttribute("user");
onlineUsers.remove(user);
//通知所有用户,此账号下线。
String message = MessageUtils.getMessage(true, null, getFriendsName());
broadcastAllUsers(message);
}