- 相关:
- 【笔记】JavaWeb - Tomcat - https://blog.csdn.net/LawssssCat/article/details/103486432
- spring websocket - https://zhuanlan.zhihu.com/p/402460034
- 参考:
- 【视频】 https://www.bilibili.com/video/BV1dJ411N7Um?p=48
- 基于Tomcat实现的WebSocket实例 - https://www.cnblogs.com/3b2414/p/14062319.html ⭐️
- Tomcat实现WebSocket原理 - https://zhuanlan.zhihu.com/p/512027616
文章目录
WebSocket
WebSocket是HTML5新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说: 服务器可以在任意时刻发送消息给浏览器。(⚠️这就很厉害了!因为以前的HTTP协议中,服务器都是无法主动发送信息的!!因为HTTP协议是一个请求-响应协议,请求必须先由浏览器发送给服务器,服务器才有机会作出响应把数据发送给浏览器)
传统HTTP协议的问题
如果服务器无法主动发送请求,这样一来,要在浏览器中搞一个实时聊天,或者在线多人游戏就没法实现。
如果硬要实现:
解决方法 | 问题 |
---|---|
借助Flash这个(略带遗憾的)插件 | 浏览器中的flash被乔布斯爷爷干没了 |
在浏览器中用js进行定时器轮询 | 1. 实时性不够; 2. 大量的请求将大大增加服务器压力 |
Comet Comet本质上也是轮询,但是在没有消息的情况下,服务器先拖一段时间,等到有消息了再回复。 | 这个机制暂时地解决了实时性问题,但是带来了新问题: 1. 服务器需要开启大量的线程数: 以多线程模式运行的服务器会让大部分线程大部分时间都处于挂起状态,极大地浪费服务器资源。 2. 连接不可控: 一个HTTP连接在长时间没有数据传输的情况下,链路上的任何一个网关都可能关闭这个连接,而网关是我们不可控的,这就要求Comet连接必须定期发送一些ping数据表示连接“正常工作” |
WebSocket
综上几种机制都是治标不治本,所以HTML5推出了WebSocket标准,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发送消息给对方。
WebSocket不是全新的协议,而是利用了HTTP协议来建立连接。下图展示了WebSocket是如何建立连接的:
-
首先,WebSocket连接还是由浏览器发起,但是请求协议、请求头有所不同
- 请求协议由http变为了ws。(💡但是协议的封装、解析还是走的http那一套)
- 请求头中有键值对
Connection: Upgrade
、Upgrade: websocket
💡 提示
还有一些特殊的请求头标识:
- Sec-WebSocket-Key 用于标识这个连接是一个BASE64编码的密文,要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答
- Sec-WebSocket-Accept 服务端与客户端一致的密钥计算出来的信息
- Sec-WebSocket-Version 指定了WebSocket的协议版本
-
服务器响应:
- 响应的状态不再是200,而是101
- 响应头有键值对:
Connection: upgrade
、Upgrade: websocket
Tomcat 的 WebSocket
Tomcat的7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356),而在7.0.5版本之前(7.0.2之后)则采用自定义API,即WebSocketServlet实现。
EndPoint
接口设计
Java WebSocket应用由一系列的WebSocketEndpoint组成。Endpoint是一个java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口,就像Servlet与http请求一样。
我们可以通过两种方式定义Endpoint:
- 第一种是编程式,即继承类
javax.websocket.Endpoint
并实现其方法 - 第二种是注解式,即定义一个POJO,并添加
@ServerEndpoint
相关注解
生命周期
(无论是编程式定义、还是注解式定义)都要使用Endpoint接口。Endpoint接口实例在WebSocket握手时创建(open),并在客户端与服务端链接过程中有效,最后在链接关闭时结束(close)。
在Endpoint接口中明确定义了与其生命周期相关的方法,规范实现者确保生命周期的各个阶段调用实例的相关方法。声明周期方法如下:
方法 | 含义描述 | 注解 |
---|---|---|
onOpen | 当开启一个新的会话时调用,该方法是客户端与服务端握手成功后调用的方法 | @OnOpen |
onClose | 当会话关闭时调用 | @OnClose |
onError | 当连接过程中异常时调用 | @OnError |
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package javax.websocket;
public abstract class Endpoint {
/**
💡 当会话(session)开始时触发该事件
* Event that is triggered when a new session starts.
*
* @param session The new session.
* @param config The configuration with which the Endpoint was
* configured.
*/
public abstract void onOpen(Session session, EndpointConfig config);
/**
💡 当会话(session)关闭时触发该事件
* Event that is triggered when a session has closed.
*
* @param session The session
* @param closeReason Why the session was closed
*/
public void onClose(Session session, CloseReason closeReason) {
// NO-OP by default
}
/**
💡 当一个协议错误发生时触发该事件
* Event that is triggered when a protocol error occurs.
*
* @param session The session.
* @param throwable The exception.
*/
public void onError(Session session, Throwable throwable) {
// NO-OP by default
}
}
MessageHandler
接收消息
通过为Session添加MessageHandler消息处理器来接收消息:
- 当采用注解方式定义EndPoint时,我们可以通过@OnMessage注解指定接收消息的方法
发送消息
发送消息时则是由RemoteEndPoint完成,其实例由Session维护,根据使用情况,我们可以:
- 通过
Session.getBasicRemote
获得同步消息发送的实例,然后调用其sendXXX()
方法发送消息 - 通过
Session.getAsyncRemote
获取异步消息发送的实例
Tomcat处理WebSocket请求流程 ⭐️
从原理上,看Tomcat是如何处理WebSocket请求的
Tomcat用ProtocolHandler组件屏蔽应用层协议的差异,ProtocolHandler两个关键组件:Endpoint和Processor。
💡 提示
这里的Endpoint跟WebSocket中的Endpoint完全是两回事,连接器中的Endpoint组件用来处理I/O通信。WebSocket本质是个应用层协议,不能用HttpProcessor处理WebSocket请求,而要用专门Processor,在Tomcat就是UpgradeProcessor。
因为Tomcat是将HTTP协议升级成WebSocket协议的,因为WebSocket是通过HTTP协议握手的,当WebSocket握手请求到来时,HttpProtocolHandler首先接收到这个请求,在处理这个HTTP请求时,Tomcat通过一个特殊的Filter判断该当前HTTP请求是否是一个WebSocket Upgrade请求(即包含Upgrade: websocket的HTTP头信息),如果是,则在HTTP响应里添加WebSocket相关的响应头信息,并进行协议升级。
就是用UpgradeProtocolHandler替换当前的HttpProtocolHandler,相应的,把当前Socket的Processor替换成UpgradeProcessor,同时Tomcat会创建WebSocket Session实例和Endpoint实例,并跟当前的WebSocket连接一一对应起来。这个WebSocket连接不会立即关闭,并且在请求处理中,不再使用原有的HttpProcessor,而是用专门的UpgradeProcessor,UpgradeProcessor最终会调用相应的Endpoint实例来处理请求
Tomcat对WebSocket请求的处理没有经过Servlet容器,而是通过UpgradeProcessor组件直接把请求发到ServerEndpoint实例,并且Tomcat的WebSocket实现不需要关注具体I/O模型的细节,从而实现了与具体I/O方式的解耦。
案例: 聊天室(浏览器、WebSocket)
todo https://www.bilibili.com/video/BV1dJ411N7Um?p=53
# 实现流程(时序图)
# 消息格式
客户端 --> 服务端: {"fromName": "fuu", "toName": "boo", "content": "约?"}
服务端 --> 客户端:
1. 如果type为user,则说明返回的是用户列表:
{"type": "user", "toName": "", "fromName": "","data": "fuu,boo,caa"}
2. 如果type为message,则说明返回的是消息内容:
{"type": "message", "toName": "boo", "fromName": "fuu","data": "约!"}