消息推送
HTTP请求只能在客户端发起请求后,服务端再返回消息,而不能在客户端未发起请求时服务端主动推送消息给客户端
轮询方式
客户端定时向服务端发送ajax请求,服务器接收到请求后马上返回消息并关闭连接
优点:后端程序编写比较简单
缺点:TCP的建立和关闭操作浪费时间和宽带,请求中有大半是无用,浪费带宽和服务器资源
长轮询
客户端向服务器发送ajax请求,服务器接到请求后hold住连接,客户端直到收到新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求
优点:在无消息的情况下不会频繁的请求,耗费资源小
缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护
Websocket协议
WebSocket协议是基于TCP 的一种新的网络协议。它实现了浏览器与服务器的全双工(full-duplex)通信,即允许服务器主动发送信息给客户端。因此,在WebSocket中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。同时它兼容了HTTP协议,默认使用HTTP的80端口和HTTPS的443端口,同时使用HTTP header进行协议升级
优点:
1、使用的资源更少:其头部更小
2、实时性更强:服务端可以通过连接主动向客户端推送消息
3、有状态:开启链接之后可以不用每次都携带状态信息
缺点:少部分浏览器不支持
示例:社交聊天(微信、QQ)、弹幕、多玩家玩游戏、协同编辑、股票基金实时报价、体育实况更新、视屏会议/聊天、基于位置的应用、在线教育等高实时行的场景
WebSocket流程
1、浏览器、服务器建立TCP连接,三次握手
2、TCP连接成功后,浏览器通过HTTP协议向服务器传送WebSocket支持的版本号等信息
3、连接成功后,双方通过TCP通道进行数据传输,不需要HTTP协议
注意: WebSocket简历连接需要先通过一个HTTP请求进行和服务端协商更新协议
(此图片来源于网络)
Spring MVC中实现WebSocket
添加websocket库
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
通过继承TextWebSocketHandler等实现websocket逻辑
1、编写websocket类,继承TextWebSocketHandler
如果是文本数据传输的话,可以选择继承TextWebSocketHandler,如果是二进制数据传输的话,可以选择继承BinaryWebSocketHandler(这里以继承TextWebSocketHandler为例子)
2、重写TextWebSocketHandler中的方法
我们一般重写图中标红的方法
afterConnectionEstablished :建立连接之后调用此方法(可以用来提示此用户是否上线)
handleTextMessage : 接收到客户端信息后调用此方法 (可以主动给客户端发送消息)
afterConnectionClosed :关闭连接之后调用此方法(可以用来提示此用户已经下线)
示例:
java
@Service
public class EchoService extends TextWebSocketHandler {
private final Logger logger = LoggerFactory.getLogger("EchoService");
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.info("[websocket_extend]建立连接:" + session.getId());
Object httpSessionId = session.getAttributes().get("httpSessionId");
logger.info("[httpSessionId_websocket] :" + httpSessionId);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
session.sendMessage(message);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
logger.info("[websocket_extend]连接关闭:" + session.getId() + "status:" + status.getReason());
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
logger.info("[websocket_extend]出现错误:" + session.getId() + " " + exception.getMessage());
}
}
3、将此类注册为websocket节点(注意加上@EnableWebSocket注解)
新建一个类名为WebSocketConfig,实现WebSocketConfigurer接口,重写WebSocketConfigure接口中的registerWebSocketHandlers方法
less
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private EchoService echoService;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoService, "/echo/channel")
// 配置此可以通过websocketSession拿到HttpSession中的内容
.addInterceptors(new HttpSessionHandshakeInterceptor())
// 设置域白名单
.setAllowedOrigins("*");
}
}
为什么要设置Interceptors?
从下图可以看到,addInterceptors()的参数类型为HandshakeInterceptor,通过名字HandshakeInterceptor(握手拦截器)可以猜测到,其发生在websocket协议与服务器握手的途中,可以拦截到用于升级协议的http请求,通过此拦截器来对webSocketSession做一些处理(最常用的就是拦截到握手时的http协议,获取到HttpSession,将HttpSession中的内容放置于webSocketSession中(注意,HttpSession和WebSocketSession是完全不一样的东西,大家可以试一下打印HttpSession的sessionId和WebSocketSession的sessionId,做一下对比))
点进去可以发现,HandshakeInterceptor是一个接口,里面有两个方法需要实现,分别是beforeHandshake和afterHandshake
接下来可以看看,springframework中实现HandshakeInterceptor接口的类都有哪些?从下图可以看到,实现HandshakeInterceptor的类有HttpSessionHandshakeInterceptor和OriginHandhsakeInterceptor
我们先来看OriginHandshakeInterceptor(其中WebUtils.isSameOrigin是判断其是否同源):
kotlin
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
if (!WebUtils.isSameOrigin(request) && this.corsConfiguration.checkOrigin(request.getHeaders().getOrigin()) == null) {
response.setStatusCode(HttpStatus.FORBIDDEN);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Handshake request rejected, Origin header value " + request.getHeaders().getOrigin() + " not allowed");
}
return false;
} else {
return true;
}
}
ini
public static boolean isSameOrigin(HttpRequest request) {
HttpHeaders headers = request.getHeaders();
String origin = headers.getOrigin();
if (origin == null) {
return true;
} else {
String scheme;
String host;
int port;
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest)request;
HttpServletRequest servletRequest = servletServerHttpRequest.getServletRequest();
scheme = servletRequest.getScheme();
host = servletRequest.getServerName();
port = servletRequest.getServerPort();
} else {
URI uri = request.getURI();
scheme = uri.getScheme();
host = uri.getHost();
port = uri.getPort();
}
UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
return ObjectUtils.nullSafeEquals(scheme, originUrl.getScheme()) && ObjectUtils.nullSafeEquals(host, originUrl.getHost()) && getPort(scheme, port) == getPort(originUrl.getScheme(), originUrl.getPort());
}
}
可以看到,其本质上还是在看 请求升级的http协议 和 其头部的 origin字段中的 协议、域名、以及端口是否一致,设置此Interceptor,即限制了只有同源请求才能升级成webSocket协议
注意:如果没有指明使用的Interceptor,则默认使用OriginHandshakeInterceptor
接下来我们来看HttpSessionHandshakeInterceptor,我们主要关注beforeHandshake和getSession这两个方法
typescript
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
HttpSession session = this.getSession(request);
if (session != null) {
if (this.isCopyHttpSessionId()) {
attributes.put("HTTP.SESSION.ID", session.getId());
}
Enumeration<String> names = session.getAttributeNames();
while(true) {
String name;
do {
if (!names.hasMoreElements()) {
return true;
}
name = (String)names.nextElement();
} while(!this.isCopyAllAttributes() && !this.getAttributeNames().contains(name));
attributes.put(name, session.getAttribute(name));
}
} else {
return true;
}
}
@Nullable
private HttpSession getSession(ServerHttpRequest request) {
if (request instanceof ServletServerHttpRequest serverRequest) {
return serverRequest.getServletRequest().getSession(this.isCreateSession());
} else {
return null;
}
}
从上述源码中我们可以看到,其会从HttpServletRequest中拿到其HttpSession的内容,再把HttpSession的内容都放到WebSocketSeesion中,从而达到当开启WebSocketSession的时候,也能拿到之前存储在HttpSession中的信息
PS:BeforeHandshake()方法中的其中一个参数类型为ServerHttpRequest,在getSession方法中,其会先去判断ServletHttpRequest request是否为ServletServerHttpRequest,若是就将其强转为ServletServerHttpRequest,再通过getServletRequest()方法,拿到HttpServletRequest(其中ServletServerHttpRequest是ServletHttpRequest的子类)
思考一个问题:那如果我们使用redis来存储数据,websocket可以拿到数据并存储在WebSocketSession里吗?
当然可以!
根据上面的说法,其实我们只需要自己实现一个类,然后实现HandshakeInterceptor接口,参考HttpSessionHandshakeInterceptor中beforeHandshake的逻辑就行,不过是将从HttpSession里拿数据换成了从redis里拿数据(记得配置redis的依赖以及其相关的地址和端口和密码)
代码示例:
typescript
@Component
public class RedisInterceptor implements HandshakeInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
HttpServletRequest r = null;
if (request instanceof ServletServerHttpRequest serverHttpRequest) {
r = serverHttpRequest.getServletRequest();
} else {
return true;
}
String token = "";
Cookie[] cookies = r.getCookies();
for(Cookie cookie : cookies) {
if(cookie.getName().equals("Token")) {
token = cookie.getValue();
}
}
if(!StringUtils.hasLength(token)) {
return false;
}
String s = redisTemplate.opsForValue().get(token);
attributes.put("Token", s);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
}
然后把WebSocketConfig中的Interceptor换成我们自己写的RedisInterceptor就好了
typescript
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoService, "/echo/channel")
// 配置此可以通过websocketSession拿到HttpSession中的内容
// .addInterceptors(new HttpSessionHandshakeInterceptor())
.addInterceptors(redisInterceptor)
.setAllowedOrigins("*");
}
为什么要设置setAllowOrigins?
根据上面的分析,我们知道,由于websocket默认使用同源拦截策略,,。而setAllowOrigins()就是在设置服务器白名单(注意,这里的origin都在指"源",也就是协议+域名/ip+端口的形式,ex:"http://127.0.0.1:8080")
设置为"*",则表示允许所有的源
为什么WebSocket握手的Http拿不到原来Http的Cookie?
我在使用WebSocket的时候,发生了一个问题,就是WebSocket握手Http拿不到原来Http的Cookie,就像下图,可以看到,在升级协议之前,是可以拿到Cookie中的JSESSIONID的
但是握手Http却带不上Cookie字段,为啥呢?
找了许久,发现是前端页面链接websocket的时候使用的是localhost而不是127.0.0.1
从而导致Host变成localhost:8080
而Cookie的Domain为127.0.0.1导致触发了Cookie的同源策略,从而导致拿不到Cookie,这点大家要格外注意一下