一. WebSocket概述
- 在后端服务器与服务器之间,HTTP请求是可以相互发送的,但是在浏览器与服务器之间,HTTP请求只能从浏览器发起,方向固定,不能从服务器往浏览器主动去发起HTTP请求。
问题:服务器无法直接向浏览器发送请求(HTTP协议的缺陷)
常见替代方案:轮询,浏览器给服务器不断地发送请求询问支付结果。
轮询缺陷:
- 浪费带宽(因为浏览器会不断地给服务器发送请求)
- 实时性差(并不是服务器知道结果以后,浏览器那边就能立刻知道,要等到浏览器下一次去询问的时候才能知道结果)
- 服务器压力大(特别是像促销之类的活动,服务器会收到大量的询问订单结果的请求)
1.1 WebSocket协议
- 2008年提出,2011年成为标准
- 最早是HTML5新增的协议,现在基本所有的语言都支持
- 作用:可以在浏览器和服务器之间建立一个全双工的通信通道(就是浏览器既可以往服务器发请求,服务器也可以往浏览器发请求,这就叫一个全双工的通信协议)
1.2 WebSocket实现浏览器与服务器的通信流程 / 过程
1. 浏览器发起HTTP请求,请求建立WebSocket连接
- 在浏览器发起的HTTP请求当中,URL是以ws:开头的,而不是之前见到的http或者https开头的。ws是WebSocket的缩写。
2. 服务器响应同意协议更改
- 服务器响应同意协议更改,会响应一个101的状态码,切换协议的意思
3. 相互发送数据
- 绿色箭头代表的是浏览器向服务器发送的消息,红色箭头代表的是服务器向浏览器发送的消息
1.3 WebSocket底层原理
- WebSocket协议建立在TCP协议基础上的,所以服务器端也容易实现,不同的语言都有支持
- TCP协议是全双工协议,HTTP协议基于它,但设计成了单向的
- WebSocket协议是对HTTP协议的一个补充,某一些HTTP做不到的场景WebSocket来补充它
- WebSocket没有同源限制(同一个IP,同一个端口叫同源,就是浏览器端的地址和服务器端的地址它的IP端口是一致的,这就叫同源)
- 什么是不同源,就是IP端口不一致的。
- 现在前后端分离项目经常就是不同源的,前端会起一个端口比如8081,后端会起一个端口比如9091,此时前端去访问后端就会存在这个不同源的一个限制,就是会存在一个跨域异常。
- 可以通过前端配代理去解决或者上到服务器上可以通过Nginx反向代理去解决这个跨域问题。
- 因为WebSocket没有同源限制,所以即使你的前端的源和后端的源不一致,也不影响WebSocket请求的发送。
二. Java实现WebSocket的两种方式
使用Spring封装
在Spring里面已经帮我们实现了WebSocket的基本功能,它完成了HTTP协议升级为WebSocket协议的这个过程,封装了通信,以及完成了很多的通信细节的功能。
我们并不是要去实现WebSocket这个功能的后端开发,我们要做的就是在Spring给我们提供的实现了WebSocket功能的基础之上去完成一些业务功能的开发,这些业务的功能可以有两种方式来实现:
- 一种是比较简单的,基于Java注解的方式
- 另一种是基于Spring提供的上层封装,它有很多应用级别的封装,我们用它去开发的话,会比较的快捷和方便。
3.1 基于Java注解实现WebSocket服务器端
LomBok依赖可以让我们更方便的去打印日志,同时它还提供一些注解可以快速的给实体类生成Getter、Setter方法。
package com.gch.java;import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/**监听WebSocket客户端地址/myWs谁来连接了WebSocket服务端*/
@ServerEndpoint("/myWs")
@Component // 声明为一个Bean
@Slf4j
public class WsServerEndpoint {// 有多个客户端来连接,应该使用线程安全的CurrentHashMappublic static Map<String,Session> sessionMap = new ConcurrentHashMap<>();// 当WebSocket客户端连接WebSocket服务端的时候,我们需要来声明一下我们的服务端去做哪些事情// 连接建立时执行的操作@OnOpen// 每一个WebSocket连接对于服务端来说都是一个sessionpublic void onOpen(Session session){sessionMap.put(session.getId(),session);log.info("websocket is open");}// 监听通信// 当WebSocket客户端浏览器给WebSocket服务器发消息的时候,我服务器做什么事情// 收到了客户端消息执行的操作@OnMessagepublic String onMessage(String text){log.info("收到了一条信息:" + text);return "已收到你的消息";}// 监听到连接关闭时执行的操作@OnClosepublic void onClose(Session session){sessionMap.remove(session.getId(),session);log.info("websocket is close");}// 每隔多少毫秒去执行一次定时任务给客户端发送xx@Scheduled(fixedRate = 2000)public void sendMsg() throws IOException {for(String key: sessionMap.keySet()){sessionMap.get(key).getBasicRemote().sendText("心跳");}}// 现在,Spring框架并不能扫描到我们的服务终端,我们还需要再加一个配置类// HTTP加上S之后它是一个更安全的传输
}
package com.gch.java;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** 配置类* 注入Spring WebSocket框架里的一个对象叫做ServerEndpointExporter*/
@Configuration
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter(){return new ServerEndpointExporter();}
}
package com.gch;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;@EnableScheduling // 开启定时任务
@SpringBootApplication
public class WebsocketQuickstartApplication {public static void main(String[] args) {SpringApplication.run(WebsocketQuickstartApplication.class, args);}}
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>ws client</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>
3.3 基于Spring框架实现WebSocket服务器端
Spring框架已经在应用层面去帮我们定义了一些接口和抽象类,我们只要实现这些接口和抽象类,就能够完成一个目录比较清晰的WebSocket的服务器端,并且它里面有很多封装的功能我们可以直接使用。