WebSocket概念
早期,很多网站为了实现推送技术,所用的技术都是轮询(也叫短轮询)。轮询是指由浏览器每隔一段时间向服务器发出 HTTP 请求,然后服务器返回最新的数据给客户端
这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而 HTTP 请求与响应可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源
在这种情况下,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯
- WebSocket 与 HTTP 和 HTTPS 使用相同的 TCP 端口,可以绕过大多数防火墙的限制
- Websocket 使用 ws 或 wss 的统一资源标志符(URI),其中 wss 表示使用了 TLS 的 Websocket
WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455,后由 RFC 7936 补充规范。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输
WebSocket优点
- 较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小
- 更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少
- 保持连接状态:与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息
- 更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容
- 可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议
由于 WebSocket 拥有上述的优点,所以它被广泛地应用在即时通讯/IM、实时音视频、在线教育和游戏等领域
注解实现WebSocket
依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
监听类
/**
* 监听Ws地址
*/
@ServerEndpoint("/myWs")
@Component
@Slf4j
public class WsServerEndpoint {
static Map<String,Session> sessionMap = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session){
sessionMap.put(session.getId(),session);
log.info("websocket is open: "+ session.getId());
}
@OnClose
public void onClose(Session session){
sessionMap.remove(session.getId());
log.info("websocket is close");
}
@OnMessage
public String onMessage(String text){
log.info("接收到消息:"+text);
return "已收到消息";
}
@Scheduled(fixedRate = 2000)
public void sendMsg() throws IOException {
for(String key : sessionMap.keySet()){
sessionMap.get(key).getBasicRemote().sendText("111");
}
}
}
配置类
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
启动类
@SpringBootApplication
@EnableScheduling
public class WebsocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebsocketApplication.class, args);
}
}
前端页面
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
</body>
<script>
let ws = new WebSocket("ws://localhost:8080/myWs")
ws.onopen = function (){
ws.send("hello")
}
ws.onmessage = function (message){
console.log(message.data)
}
</script>
</html>
Spring抽象类,接口实现WebSocket
依赖引入
同上
握手拦截器
@Slf4j
@Component
public class MyWsInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
log.info(request.getRemoteAddress().toString()+"开始握手");
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
log.info(request.getRemoteAddress().toString()+"完成握手");
super.afterHandshake(request, response, wsHandler, ex);
}
}
WebSocket拦截器
@Component
public class MyWsHandler extends AbstractWebSocketHandler {
private static Map<String,SessionBean> sessionBeanMap;
private static AtomicInteger clientIdMaker;
static {
sessionBeanMap= new ConcurrentHashMap<>();
clientIdMaker = new AtomicInteger(0);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
SessionBean sessionBean = new SessionBean(session,clientIdMaker.getAndIncrement());
sessionBeanMap.put(session.getId(),sessionBean);
super.afterConnectionEstablished(session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
System.out.println(sessionBeanMap.get(session.getId()).getClientId() + ":" + message.getPayload());
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
super.handleTransportError(session, exception);
if(session.isOpen()){
session.close();
}
sessionBeanMap.remove(session.getId());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
super.afterConnectionClosed(session, status);
sessionBeanMap.remove(session.getId());
System.out.println(sessionBeanMap.get(session.getId()).getClientId() + "关闭连接");
}
@Scheduled(fixedRate = 2000)
public void sendMsg() throws IOException {
for(String key : sessionBeanMap.keySet()){
sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage("111"));
}
}
}
配置类
@Configuration
@EnableWebSocket
public class MyWsConfig implements WebSocketConfigurer {
@Resource
private MyWsHandler myWsHandler;
@Resource
private MyWsInterceptor myWsInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myWsHandler,"/myWs1").addInterceptors(myWsInterceptor)
.setAllowedOriginPatterns("*");
}
}
前端页面以及启动类
同上,但是前端页面需要修改WebSocket链接路径