文章目录
- 为什么要用WebSocket?
- WebSocket的握手阶段
- Spring Boot中使用WebSocket
- 添加WebSocket依赖
- 服务器代码编写
- WebSocketSession如何获取用户信息?
- 创建管理类管理用户与会话
- 客户端代码
为什么要用WebSocket?
我们往往需要一些这样的场景,服务器给客户端推送消息
,如淘宝推送消息,网上聊天等,这些场景下客户端没有主动向服务器发请求,而是由服务器主动的向客户端发送消息,但是之前用的HTTP协议是一次请求一次响应
那该如何实现服务器主动的向客户端推送消息呢?
如果继续使用HTTP协议,可以基于轮询
的方式实现,也就是客户端每隔一段时间给服务器发请求,看看有没有要发送给我的消息,如果有就获取到消息,如果没有就等待
上述轮询存在一定问题:
- 消耗更多的系统资源,客户端要频繁的向服务器发请求,而这些请求大多数是没有响应的
- 获取消息不够及时,只有轮询的时候(下次请求的周期)才能够获取到消息
如果提高轮询频率,则将消耗更多的系统资源,如果降低轮询频率,那获取消息就不够及时
此时可以使用WebSocket
协议,WebSocket协议也是应用层协议,传输层也是基于TCP协议的,该协议可以实现服务器主动向客户端推送消息的功能
WebSocket的握手阶段
先了解一下报文格式中的几个重要信息:
- FIN:表示是否关闭websocket
opcode
操作码:描述了当前的websocket数据帧是起到了啥作用(0x1表示文本数据,0x2表示二进制数据)- MASK:是否开启掩码操作,掩码操作是为了避免缓冲区溢出
payload length
:载荷的长度payload data
:载荷真正携带的数据
WebSocket协议的握手过程
总结如下:
- 客户端向服务端发一个申请建立websocket连接的HTTP请求,该请求是基于HTTP协议的,这个HTTP请求的请求头包含了重要的Header头,如
Connection: upgrade
,Upgrade: websocket
,标识要进行协议升级,并升级的协议类型为websocket - 服务端收到该请求后,返回一个HTTP响应,
响应状态码为101
表示协议切换,并且响应也会包含重要的Header头Connection: upgrade
,Upgrade: websocket
- 客户端与服务端建立好全双工的websocket长连接,后续传输都是基于WebSocket协议
Spring Boot中使用WebSocket
添加WebSocket依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
服务器代码编写
主要的步骤分为以下两步:
- 创建一个类作为WebSocketHandler(处理WebSocket中各个通信流程)
- 把上述类注册到Spring中,配置路由(关联上哪个路径对应上述的handler)
创建一个类继承TextWebSocketHandler
,并添加类注解@Component将该类注册到Spring中,并重写:
afterConnectionEstablished
:该方法会在websocket连接成功后被调用handleTextMessage
:该方法是在websocket收到消息的时候自动调用handleTransportError
:该方法是在websocket连接出现异常的时候自动调用的afterConnectionClosed
:该方法是在websocket连接关闭后自动调用的
@Component
public class WebSocketAPI extends TextWebSocketHandler {
@Override //该方法会在websocket连接成功后被调用
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//WebSocketSession是websocket连接对应的会话
System.out.println("建立连接了");
}
@Override //该方法是在websocket收到消息的时候自动调用
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//message 为收到的消息
System.out.println("发送消息:"+message.toString());
//session是个会话,里面记录了通信双方,通过session对象调用send方法实现服务器推送消息
session.sendMessage(message);
message.getPayload(); //获取的message字符串
}
@Override //该方法是在websocket连接出现异常的时候自动调用的
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
//exception记录了异常信息
System.out.println("连接出现异常了");
}
@Override //该方法是在websocket连接关闭后自动调用的
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
//status 为关闭的状态
System.out.println("连接关闭了");
}
}
创建另一个类实现WebSocketConfigurer接口
,在类上添加@Configuration
,@EnableWebSocket
,并重写registerWebSocketHandlers
方法
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private WebSocketAPI webSocketAPI;
@Override //通过该方法,把创建好的Handler类注册到具体的路径上
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//当浏览器,websocket的请求路径是/test的时候,就会调用到webSocketTest中的方法
registry.addHandler(webSocketAPI,"/websocketMessage");
}
}
每个和服务端建立websocket连接的客户端,都会在服务器这边有与之对应的WebSocketSession对象,服务器要想给谁发消息,就必须使用谁的WebSocketSession对象调用sendMessage方法发送消息,服务器向客户端推送消息使用session.sendMessage(String message)
发送消息,session为每个服务器与客户端建立的websocket会话WebSocketSession
WebSocketSession如何获取用户信息?
我们通常需要获取websocket保存的用户会话的用户信息,那如何获取到连接用户的用户信息呢?
通过注册特定的HttpSession拦截器,就可以把用户给HttpSession中添加的Attribute键值对,往WebSocketSession中也添加一份
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private WebSocketAPI webSocketAPI;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketAPI,"/websocketMessage").
// 通过注册特定的HttpSession拦截器,就可以把
// 用户给HttpSession中添加的Attribute键值对,往WebSocketSession中也添加一份
addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
添加完后,可以使用WebSocketSession对象调用getAttributes().get("user")
方法获取用户在HttpSession中保存的用户信息
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
User user = (User)session.getAttributes().get("user");
System.out.println("建立连接");
}
创建管理类管理用户与会话
可以使用HashMap来维护用户与WebSocketSession的关系,key为用户id,value为WebSocketSession对象,只是此时使用线程安全的集合类ConcurrentHashMap
@Component
public class WebSocketSessionManage {
private ConcurrentHashMap<Integer, WebSocketSession> websocketSessions = new ConcurrentHashMap<>();
//用户连接,添加连接关系
public void addWebSocketSession(Integer userId,WebSocketSession webSocketSession){
//用户已经上线,防止多开
if(websocketSessions.containsKey(userId)){
return;
}
websocketSessions.put(userId,webSocketSession);
}
//用户掉线,删除连接关系
public void delWebSocketSession(Integer userId,WebSocketSession webSocketSession){
WebSocketSession get = websocketSessions.get(userId);
//只有关系中存在自己的连接信息,才删除
if(get == webSocketSession){
websocketSessions.remove(userId);
}
}
//根据用户id获取WebSocketSession
public WebSocketSession getWebSocketSession(Integer userId){
return websocketSessions.get(userId);
}
}
客户端代码
客户端使用WebSocket的实例调用send方法即可向服务器发送消息,发送的消息一般为json字符串,所以我们可以使用websocket.send(JSON.stringfy(js))
将js对象序列换为json字符串发送
<script>
//websocket传输消息
//创建websocket实例
let websocket = new WebSocket("ws://" + location.host + "/websocketMessage");
//绑定一些函数
websocket.onopen = function(){
console.log('建立连接')
}
websocket.onmessage = function(e){
//e.data为收到服务端推送的消息
//e.data为一个json字符串,可以使用JSON.prase(e.data)转换为js对象
let resp = JSON.prase(e.data);
console.log('收到消息:'+resp)
}
websocket.onerror = function(){
console.log('出现异常')
}
websocket.onclose = function(){
console.log('关闭连接')
}
//使用websocket实例调用send方法即可向服务器发送消息
//注意:参数为字符串,不能为js对象
//要发送json格式的数据,将json对象序列化为字符串,JSON.stringfy(json)
websocket.send("hehe");
</script>