Websocket——化神篇
WebSocket机制
WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:
WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样;
WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。
和socket区别
Socket其实并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
当两台主机通信时,必须通过Socket连接,Socket则利用TCP/IP协议建立TCP连接。TCP连接则更依靠于底层的IP协议,IP协议的连接则依赖于链路层等更低层次。
WebSocket则是一个典型的应用层协议。
区别
Socket是传输控制层协议,WebSocket是应用层协议。
前世今生
众所周知,Web 应用的交互过程通常是客户端通过浏览器发出一个请求,服务器端接收请求后进行处理并返回结果给客户端,客户端浏览器将信息呈现,这种机制对于信息变化不是特别频繁的应用尚可,但对于实时要求高、海量并发的应用来说显得捉襟见肘,尤其在当前业界移动互联网蓬勃发展的趋势下,高并发与用户实时响应是 Web 应用经常面临的问题,比如金融证券的实时信息,Web 导航应用中的地理位置获取,社交网络的实时消息推送等。
消息推送常用方式
轮询方式
1.轮询是一种客户端与服务器之间实时通信的技术手段。客户端定期发送请求来查询服务器是否有新数据或事件,并将响应返 回给客户端。如果服务器有新的数据或事件,则将其返回给客户端;如果没有,则返回一个空响应。客户端收到响应后,可 以处理数据或事件,并根据需要继续发送下一个请求。
2.长轮询是一种改进的轮询技术,其主要目的是降低轮询过程中的资源消耗和延迟。长轮询的基本原理是客户端发送一个 HTTP请求给服务器,并保持连接打开,直到服务器有新的数据或事件时才返回响应给客户端。在这期间,服务器会一直保持连接打开,直到超时或有新数据或事件(HTTP1.1版本就是这个性质长连接)
SSE(服务器发送事件)
SSE在服务器和客户端之间打开一个单向通道
服务端响应的不再是一次性的数据包,而是text/event-stream类型的数据流信息
服务器有数据变更时将数据流式传输到客户端
WebSocket
客户端API
服务端 API
Endpoint示例
@ServerEndpoint("/chat")
@Component
public class ChatEndpoint {
@OnOpen
//连接建立时被调用
public void onOpen(Session session, EndpointConfig config){}
@OnMessage
//接收到客户端发送的数据时被调用
public void onMessage(String message){}
@OnClose
//连接关闭时被调用
public void onClose(Session session){}
}
ChatEndpoint类通过实现WebSocket协议,用于处理客户端的连接、消息传递和关闭事件。
在线聊天室实现
流程分析
消息格式
引入坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
编写配置类
@Configuration
public class WebsocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
通过在配置类中定义一个 ServerEndpointExporter 的 @Bean 方法,Spring 会自动创建一个 ServerEndpointExporter 实例,并将其加入到 Spring 容器中。Spring框架会在启动时通过这个实例自动扫描项目中所有使用@ServerEndpoint注解的类,并将它们注册为WebSocket端点。
编写配置类,用于获取 HttpSession 对象
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//获取HttpSession对象
HttpSession httpSession = (HttpSession) request.getHttpSession();
//将httpSession对象保存起来
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
GetHttpSessionConfig类用于在WebSocket握手过程中获取HTTP会话(HttpSession)对象,并将其保存到用户属性中,以便在WebSocket会话中使用。
ChatEndpoint类
package com.itheima.ws;
import com.alibaba.fastjson.JSON;
import com.itheima.config.GetHttpSessionConfig;
import com.itheima.utils.MessageUtils;
import com.itheima.ws.pojo.Message;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @version v1.0
* @ClassName: ChatEndpoint
* @Description: TODO(一句话描述该类的功能)
* @Author: 黑马程序员
*/
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {
private static final Map<String,Session> onlineUsers = new ConcurrentHashMap<>();
private HttpSession httpSession;
/**
* 建立websocket连接后,被调用
* @param session
*/
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
//1,将session进行保存
this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String user = (String) this.httpSession.getAttribute("user");
onlineUsers.put(user,session);
//2,广播消息。需要将登陆的所有的用户推送给所有的用户
String message = MessageUtils.getMessage(true,null,getFriends());
broadcastAllUsers(message);
}
public Set getFriends() {
Set<String> set = onlineUsers.keySet();
return set;
}
private void broadcastAllUsers(String message) {
try {
//遍历map集合
Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();
for (Map.Entry<String, Session> entry : entries) {
//获取到所有用户对应的session对象
Session session = entry.getValue();
//发送消息
session.getBasicRemote().sendText(message);
}
} catch (Exception e) {
//记录日志
}
}
/**
* 浏览器发送消息到服务端,该方法被调用
*
* 张三 --> 李四
* @param message
*/
@OnMessage
public void onMessage(String message) {
try {
//将消息推送给指定的用户
Message msg = JSON.parseObject(message, Message.class);
//获取 消息接收方的用户名
String toName = msg.getToName();
String mess = msg.getMessage();
//获取消息接收方用户对象的session对象
Session session = onlineUsers.get(toName);
String user = (String) this.httpSession.getAttribute("user");
String msg1 = MessageUtils.getMessage(false, user, mess);
session.getBasicRemote().sendText(msg1);
} catch (Exception e) {
//记录日志
}
}
/**
* 断开 websocket 连接时被调用
* @param session
*/
@OnClose
public void onClose(Session session) {
//1,从onlineUsers中剔除当前用户的session对象
String user = (String) this.httpSession.getAttribute("user");
onlineUsers.remove(user);
//2,通知其他所有的用户,当前用户下线了
String message = MessageUtils.getMessage(true,null,getFriends());
broadcastAllUsers(message);
}
}
@ServerEndpoint(value = “/chat”, configurator = GetHttpSessionConfig.class) 这行代码声明了一个 WebSocket 端点,客户端可以通过 /chat 路径与之建立连接,并且在握手阶段使用 GetHttpSessionConfig 类来进行自定义配置。
该类是Spring Boot应用中的一个WebSocket端点,它用于处理聊天功能。主要功能包括:
- 在用户建立WebSocket连接时保存用户信息和会话对象。
- 当用户发送消息时,将消息转发给指定的接收用户。
- 当用户断开连接时,从在线用户列表中移除用户并通知其他用户。
- 使用GetHttpSessionConfig配置器来获取HTTP会话中的用户信息。
userController
package com.itheima.controller;
import com.itheima.pojo.Result;
import com.itheima.pojo.User;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
@RestController
@RequestMapping("user")
public class UserController {
/**
* 登陆
* @param user 提交的用户数据,包含用户名和密码
* @param session
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody User user, HttpSession session) {
Result result = new Result();
if(user != null && "123".equals(user.getPassword())) {
result.setFlag(true);
//将数据存储到session对象中
session.setAttribute("user",user.getUsername());
} else {
result.setFlag(false);
result.setMessage("登陆失败");
}
return result;
}
/**
* 获取用户名
* @param session
* @return
*/
@GetMapping("/getUsername")
public String getUsername(HttpSession session) {
String username = (String) session.getAttribute("user");
return username;
}
}
Result
package com.itheima.pojo;
import lombok.Data;
/**
* @version v1.0
* @ClassName: Result
* @Description: 用来封装http请求的响应数据
* @Author: 黑马程序员
*/
@Data
public class Result {
private boolean flag;
private String message;
}
User
package com.itheima.pojo;
import lombok.Data;
/**
* @version v1.0
* @ClassName: User
* @Description: 接收登录请求的数据
* @Author: 黑马程序员
*/
@Data
public class User {
private String userId;
private String username;
private String password;
}
MessageUtils
package com.itheima.utils;
import com.alibaba.fastjson.JSON;
import com.itheima.ws.pojo.ResultMessage;
/**
* @version v1.0
* @ClassName: MessageUtils
* @Description: 封装json格式消息的工具类
* @Author: 黑马程序员
*/
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.setFromName(fromName);
}
return JSON.toJSONString(result);
}
}
Message
package com.itheima.ws.pojo;
import lombok.Data;
/**
* @version v1.0
* @ClassName: Message
* @Description: 用于封装浏览器发送给服务端的消息数据
* @Author: 黑马程序员
*/
@Data
public class Message {
private String toName;
private String message;
}
ResultMessage
package com.itheima.ws.pojo;
import lombok.Data;
/**
* @version v1.0
* @ClassName: ResultMessage
* @Description: 用来封装服务端给浏览器发送的消息数据
* @Author: 黑马程序员
*/
@Data
public class ResultMessage {
private boolean isSystem;
private String fromName;
private Object message;//如果是系统消息是数组
}