WebSocket定义
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
WebSocket 是独立的、创建在 TCP 上的协议。
Websocket 通过HTTP/1.1 协议的101状态码进行握手。
为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(handshaking)。
集成注解方式
注解
-
@ServerEndpoint 注解是 tomcat 7 中新增加的一个注解,用于标识一个类为 WebSocket 服务端点
WebSocket 服务端点监听客户端的 WebSocket 连接,并将连接管理起来,供客户端和服务端进行实时通信
一个标注有 @ServerEndpoint 的类必须包含一个无参构造函数,并且可以有一个或多个注解为 @OnOpen、@OnClose、 @OnMessage、@OnError 的方法。
-
@OnOpen:当 WebSocket 连接建立时,会调用标注有 @onOpen 的方法
-
@OnClose:当 WebSocket 连接关闭时,会调用标注有 @OnClose 的方法
-
@OnMessage:当收到客户端发送的消息时,会调用标注有 @OnMessage 的方法
在 @OnMessage 方法中,可以通过参数文本、二进制、PongMessage 等方式来接收客户端发送的消息。
同样可以通过 Session 给客户端发送消息,以实现双向通信。
-
@OnError:当出现错误时,会调用标注有 @OnError 的方法
注意:标注 @ServerEndpoint 注解的类对象是多例的,即每个连接都会创建一个新的对象
@ServerEndpoint 注解的参数配置
value 参数:必选参数,用于指定 WebSocket 服务端点的 URI 地址
decoders 参数:数组类型,指定解码器
包含用于将 WebSocket 消息解码为 Java 对象的解码器(Decoder)的类
解码器帮助将原始消息转换为 Java 对象
encoders 参数:数组类型,指定编解码器
包含用于将 Java 对象编码为 WebSocket 消息的编码器(Encoder)的类。
编码器帮助将 Java 对象转换为可以发送到客户端的消息
subprotocols 参数:用于指定一个或多个 WebSocket 的子协议
configurator 参数:一个类,用于提供配置 WebSocket 端点的自定义配置器
这允许在 WebSocket 端点创建和配置过程中进行额外的设置
注解使用实例
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
核心处理类
import com.blackcrow.common.utils.UUIDUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* websocket 服务端点。注意:websocket对象是多例的
*/
@Slf4j
@Component
@ServerEndpoint(value = "/webSocket/{uid}", configurator = WebSocketServerConfigurator.class)
public class WebSocketServer {
//静态变量,用来记录当前在线连接数。应设计成线程安全的。
private static final AtomicInteger onlineNum = new AtomicInteger(0);
/**
存储客户端对象和web Socket对象可以整合到一起
*/
//concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
private static CopyOnWriteArraySet<Session> sessionPools = new CopyOnWriteArraySet<Session>();
// 用于存储每个用户客户端对象
public static Map<String, WebsocketServer> onlineUserMap = new ConcurrentHashMap<>();
// 用户id
private String userId;
// 会话
private Session session;
@OnOpen
public void onOpen(Session session, EndpointConfig config){
this.session = session;
this.userId = UUIDUtil.get4UUID();
log.info("收到来自窗口的连接,userId={}", this.userId);
onlineUserMap.put(this.userId, this);
sessionPools.add(session);
onlineNum.incrementAndGet();
log.info(uid + "加入webSocket!当前人数为" + onlineNum);
}
@OnMessage
public void onMessage(String message, Session session){
log.info("收到来自服务端[{}]的的信息: {}", this.userId, message);
}
@OnClose
public void onClose(Session session){
onlineUserMap.remove(this.userId);
this.session = session;
sessionPools.remove(session);
int cnt = onlineNum.decrementAndGet();
log.info("有连接关闭,当前连接数为:{}", cnt);
}
@OnError
public void onError(Session session, Throwable throwable){
log.error("发生错误");
throwable.printStackTrace();
}
/**
* 給session连接推送消息
* 由于getBasicRemote()的同步特性,并且它支持部分消息的发送即sendText(xxx,boolean isLast). isLast的值表示是否一次发 * 送消息中的部分消息,对于如下情况:
* session.getBasicRemote().sendText(message, false);
* session.getBasicRemote().sendBinary(data);
* session.getBasicRemote().sendText(message, true);
* 由于同步特性,第二行的消息必须等待第一行的发送完成才能进行,而第一行的剩余部分消息要等第二行发送完才能继续发送,所以 * 在第二行会抛出IllegalStateException异常。如果要使用getBasicRemote()同步发送消息,则避免尽量一次发送全部消息,使用 * 部分消息来发送。
*/
private void sendMessage(Object message) {
try {
this.session.getBasicRemote().sendObject(message);
} catch (Exception e) {
e.printStackTrace();
log.error("向客户端推送数据发生错误", e);
}
}
public static void sendMessage(Session session, String message) throws IOException {
if (session != null) {
synchronized (session){
//getAsyncRemote()和getBasicRemote()确实是异步与同步的区别,大部分情况下,推荐使用getAsyncRemote()
session.getAsyncRemote().sendText(message);
}
}
}
/**
* 向所有连接群发消息
*/
public static void sendMessageToAll(Object message){
for (WebsocketServer item : onlineUserMap.values()) {
try {
item.sendMessage(message);
} catch (Exception e) {
log.error("向客户端推送数据发生错误", e);
}
}
}
}
WebSocketConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
/**
* 注入一个ServerEndpointExporter,
* 该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketServerConfigurator
请求链接拦截器
自定义校验权限,设置自定义请求头
sec-websocket-protocol
ws和http区别之一,但是http可以自定义设置请求头,ws却不能
var aWebSocket = new WebSocket(url [, protocols]);
url
要连接的URL;这应该是WebSocket服务器将响应的URL。
protocols 可选
一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个WebSocket子协议(例如,您可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互)。如果不指定协议字符串,则假定为空字符串。
protocols对应的就是发起ws连接时, 携带在请求头中的Sec-WebSocket-Protocol属性, 服务端可以获取到此属性的值用于通信逻辑(即通信子协议,当然用来进行token认证也是完全没问题的)
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import java.util.List;
import java.util.Map;
/**
* 握手处理器
*/
public class WebSocketServerConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
// 获取客户端发送的HTTP请求头信息
Map<String, List<String>> headers = request.getHeaders();
// 获取token
String submitedToken = headers.get("sec-websocket-protocol")
// TODO verifyToken 校验token
// 检查某个特定的请求头是否存在或是否符合要求
List<String> customHeaderValues = headers.get("Custom-Header");
if (customHeaderValues == null || customHeaderValues.isEmpty() || !customHeaderValues.get(0).equals("ExpectedValue")) {
// 如果请求头不符合要求,则拒绝握手
throw new RuntimeException("Custom header is missing or invalid");
}
// 如果请求头符合要求,则继续握手过程
super.modifyHandshake(sec, request, response);
}
}