文章目录
-
- 原理
- 作用
- [客户端 API](#客户端 API)
- [服务端 API](#服务端 API)
- 生命周期
- 常见注解
- SpringBoot示例
WebSocket是一种
通信协议
,它在客户端和服务器之间建立了一个双向通信的网络连接
。WebSocket是一种基于TCP连接上进行全双工通信
的协议
。
WebSocket允许客户端和服务器在单个TCP连接上
进行实时
双向通信,而不是传统的请求-响应模型
。WebSocket协议允许任意多的数据包
在任意方向上
发送,因此可以实现实时的
、双向的
、交互式的
数据传输。
原理
下图是使用轮询或WebSocket两种方式向服务器发起请求的简易流程。
上图的左边图是 轮询
的方式进行获取信息,可以看出在 一定时间间隔
的情况下客户端向服务端发起请求,如果有数据则返回数据,没有数据则返回空数据。
右边图则是使用WebSocket方式,在 一次握手
之后,两者就可以 创建持久性的连接
,如果服务端有数据的话,就可以实时返回数据。
下图是使用WebSocket的方式向服务器发起请求的详细流程。
该图解释了WebSocket连接原理。下面是上图的HTTP协议中的请求和响应数据。
请求数据:
- GET ws://localhost/chat HTTP/1.1
- Host: localhost
- Connection: Upgrade
- Upgrade: websocket
- Sec-WebSocket-Version: 13
- Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Extensions: permessage-deflate
响应数据:
- HTTP/1.1 101 Switching Protocols
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sec-WebSocket-Extensions: permessage-deflate
作用
- 实时数据传输:WebSocket允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要
完成一次握手
,两者之间就可以直接创建持久性的连接
,并进行双向数据传输
。 - 实时通知:WebSocket可以保持一个
长连接
,当服务器端有新的消息时,能够实时地推送到客户端。这使得诸如知乎的点赞通知、评论等实时交互功能得以实现,从而提供更好的用户体验。 - 数据收集:一些
次优级别的数据
,如行为日志、trace、异常执行栈收集等,可以通过WebSocket通道进行传输。这增加了信息的集中度,并能及时针对用户的行为进行合适的配置推送。
WebSocket通常用于 实时Web应用程序
,如聊天室、游戏、股票价格跟踪等,这些应用需要 实时交互
和 数据更新
。
但是在实际使用中仍需注意其安全性。例如,某些工具(如Fiddler、Charles等)能够 捕获WebSocket包
,但这并不意味着WebSocket本身不安全。在使用WebSocket时,应确保采取适当的安全措施,如加密和认证,以保护数据的传输和存储。
全双工和半双工是什么?
全双工(Full Duplex)和半双工(Half Duplex)是 通讯传输
中的两种不同模式,主要区别在于 数据在通信线路上的传输方向和方式
。
- 全双工允许数据在
两个方向上同时传输
,即通信线路可以同时存在双向信号传输
。这意味着在发送数据的同时,也能接收数据,两者是同步进行的。这种模式类似于我们平时打电话,说话的同时也能听到对方的声音。全双工方式下,通信系统的每一端都设置了发送器和接收器
,因此能控制数据同时在两个方向上传送。 - 半双工模式允许
数据在一个信号载体的两个方向上传输,但不能同时传输
。虽然数据可以沿两个方向传送,但在同一时刻,一个信道只允许单方向传送。若要改变传输方向,需由开关进行切换。半双工通信也被称为双向交替通信
,即发送和接收动作是交替进行的,不能同时进行。例如,一条窄窄的马路,同时只能有一辆车通过,当有两辆车对开时,只能一辆先过,另一辆再开。
全双工和半双工的主要区别在于数据传输的同步性和方向性。全双工能实现数据的瞬时同步双向传输,而半双工则只能实现双向交替传输。
客户端 API
javascript
let ws = new WebSocket(URL); // WebSocket对象创建
- 格式:协议://ip地址/访问路径
- 协议:协议名称为 ws
WebSocket对象相关事件:
事件 | 事件处理程序 | 描述 |
---|---|---|
open | ws.onopen | 连接建立时触发 |
message | ws.onmessage | 客户端接收到服务器发送的数据时触发 |
close | ws.onclose | 连接关闭时触发 |
WebSocket对象提供的方法:
方法名称 | 描述 |
---|---|
send() | 通过websocket对象调用该方法发送数据给服务端 |
前端客户端使用方式:
服务端 API
Tomcat的7.0.5 版本开始支持WebSocket,并且实现了Java WebSocket规范。Java WebSocket
应用由一系列的Endpoint
组成。Endpoint
是一个java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket
消息的接口。
我们可以通过两种方式定义 Endpoint
:
- 第一种是编程式,即继承类
javax.websocket.Endpoint
并实现其方法。 - 第二种是注解式,即定义一个POJO,并添加
@ServerEndpoint
相关注解。
Endpoint实例
在 WebSocket 握手
时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在 Endpoint接口
中明确定义了与其生命周期相关的方法, 规范实现者确保生命周期的各个阶段调用实例的相关方法。
生命周期方法如下:
方法 | 描述 | 注解 |
---|---|---|
onOpen() | 当开启一个新的会话时调用,该方法是客户端与服务端握手成功后调用的方法 | @OnOpen |
onClose() | 当会话关闭时调用 | @OnClose |
onError() | 当连接过程异常时调用 | @OnError |
服务端如何接收客户端发送的数据呢?
- 编程式:通过添加
MessageHandler
消息处理器来接收消息 - 注解式:在定义
Endpoint
时,通过@OnMessage
注解指定接收消息的方法
服务端如何推送数据给客户端呢?
发送消息则由 RemoteEndpoint
完成, 其实例由 Session
维护。发送消息有2种方式发送消息:
- 通过
session.getBasicRemote
获取同步消息发送的实例 , 然后调用其sendXxx()
方法发送消息。 - 通过
session.getAsyncRemote
获取异步消息发送实例,然后调用其sendXxx()
方法发送消息。
服务端使用方式:
生命周期
该图中右上角应为@OnClose。
常见注解
在 Java
的 WebSocket API
中,常见的注解包括以下几种:
@ServerEndpoint
:该类级别的注解,用于标记一个类并将其声明为WebSocket服务端点。这意味着此类会被部署为WebSocket服务器并在WebSocket实现的URI空间中可用。@OnOpen
:用于标识一个方法,当WebSocket连接建立时被调用。这个方法可以包含连接建立时需要执行的逻辑。@OnClose
:用于标识一个方法,当WebSocket连接关闭时被调用。此方法中可以执行与关闭连接相关的操作或资源释放。@OnMessage
:用于标识一个方法,在接收到客户端消息时被调用。此方法可以接收不同类型的参数,如String或ByteBuffer,用于处理从客户端接收到的消息。@OnError
:用于标识一个方法,在WebSocket通信过程中发生错误时被调用。此方法可以用于处理错误情况,如连接失败、消息解析错误等。
此外,还有一些其他的注解可以用于指定WebSocket的配置,例如:
encoders
:编码器属性,用于指定将消息从Java对象转换为WebSocket消息格式的编码器。decoders
:解码器属性,用于指定将WebSocket消息格式解码为Java对象的解码器。subprotocols
:子协议属性,用于指定WebSocket连接支持的子协议。configurator
:配置器属性,用于指定WebSocket端点的配置器,可以对端点进行更高级的配置。
以上列出的是基于 Java WebSocket API
的常见注解。如果使用其他编程语言或框架,注解可能会有所不同。
SpringBoot示例
这是一个简单示例,主要了解在SpringBoot中怎么使用WebSocket。
首先,需要在SpringBoot项目中引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
注册WebSocket
java
@Configuration
public class WebsocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
用来封装http请求的响应数据
java
@Data
public class Result {
private boolean flag;
private String message;
}
用于接收登录请求的数据
java
@Data
public class User {
private String userId;
private String username;
private String password;
}
用于封装浏览器发送给服务端的消息数据
java
@Data
public class Message {
private String toName;
private String message;
}
用来封装服务端给浏览器发送的消息数据
java
@Data
public class ResultMessage {
private boolean isSystem;
private String fromName;
private Object message; //如果是系统消息是数组
}
定义ChatEndpoint 类用于处理WebSocket中数据传输的逻辑,生命周期等功能。
java
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {
// 线程安全
private static final Map<String,Session> onlineUsers = new ConcurrentHashMap<>();
private HttpSession httpSession;
// 建立websocket连接后,被调用
@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) {
//记录日志
}
}
// 浏览器发送消息到服务端,该方法被调用
@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 连接时被调用
@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);
}
}
由于使用user name作为Map的key,所以要实时保存,用于消息的传递标识。如果使用数据库,建议还是使用userId作为key,保证唯一性。
java
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);
}
}
controller请求类
java
@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;
}
}
项目目录结构: