写点什么

如何使用 Tomcat 实现 WebSocket 即时通讯服务服务端

  • 2022 年 5 月 10 日
  • 本文字数:4669 字

    阅读完需:约 15 分钟

本文分享自华为云社区《Tomcat支持WebSocket吗?》,作者: JavaEdge 。


HTTP 协议是“请求-响应”模式,浏览器必须先发请求给服务器,服务器才会响应该请求。即服务器不会主动发送数据给浏览器。


实时性要求高的应用,如在线游戏、股票实时报价和在线协同编辑等,浏览器需实时显示服务器的最新数据,因此出现 Ajax 和 Comet 技术:


  • Ajax 本质还是轮询

  • Comet 基于 HTTP 长连接做了一些 hack


但它们实时性不高,频繁请求也会给服务器巨大压力,也浪费网络流量和带宽。于是 HTML5 推出 WebSocket 标准,使得浏览器和服务器之间任一方都可主动发消息给对方,这样服务器有新数据时可主动推给浏览器。

WebSocket 原理

网络上的两个程序通过一个双向链路进行通信,这个双向链路的一端称为一个 Socket。一个 Socket 对应一个 IP 地址和端口号,应用程序通常通过 Socket 向网络发出或应答网络请求。Socket 不是协议,是对 TCP/IP 协议层抽象出来的 API。


WebSocket 跟 HTTP 协议一样,也是应用层协议。为兼容 HTTP 协议,它通过 HTTP 协议进行一次握手,握手后数据就直接从 TCP 层的 Socket 传输,与 HTTP 协议再无关。


这里的握手指应用协议层,不是 TCP 层,握手时,TCP 连接已建立。即 HTTP 请求里带有 websocket 的请求头,服务端回复也带有 websocket 的响应头。


浏览器发给服务端的请求会带上跟 WebSocket 有关的请求头,比如 Connection: Upgrade 和 Upgrade: websocket


若服务器支持 WebSocket,同样会在 HTTP 响应加上 WebSocket 相关的 HTTP 头部:

这样 WebSocket 连接就建立好了。


WebSocket 的数据传输以 frame 形式传输,将一条消息分为几个 frame,按先后顺序传输出去。为何这样设计?

  • 大数据的传输可以分片传输,无需考虑数据大小问题

  • 和 HTTP 的 chunk 一样,可边生成数据边传输,提高传输效率

Tomcat 如何支持 WebSocket

WebSocket 聊天室案例

浏览器端核心代码:

var Chat = {};Chat.socket = null;Chat.connect = (function(host) {
//判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { // 若支持,则创建WebSocket JS类 Chat.socket = new WebSocket(host); } else if ('MozWebSocket' in window) { Chat.socket = new MozWebSocket(host); } else { Console.log('WebSocket is not supported by this browser.'); return; }
// 再实现几个回调方法 // 回调函数,当和服务器的WebSocket连接建立起来后,浏览器会回调这个方法 Chat.socket.onopen = function () { Console.log('Info: WebSocket connection opened.'); document.getElementById('chat').onkeydown = function(event) { if (event.keyCode == 13) { Chat.sendMessage(); } }; };
// 回调函数,当和服务器的WebSocket连接关闭后,浏览器会回调这个方法 Chat.socket.onclose = function () { document.getElementById('chat').onkeydown = null; Console.log('Info: WebSocket closed.'); };
// 回调函数,当服务器有新消息发送到浏览器,浏览器会回调这个方法 Chat.socket.onmessage = function (message) { Console.log(message.data); };});
复制代码

服务器端 Tomcat 实现代码:Tomcat 端的实现类加上**@ServerEndpoint**注解,value 是 URL 路径

@ServerEndpoint(value = "/websocket/chat")public class ChatEndpoint {
private static final String GUEST_PREFIX = "Guest"; // 记录当前有多少个用户加入到了聊天室,它是static全局变量。为了多线程安全使用原子变量AtomicInteger private static final AtomicInteger connectionIds = new AtomicInteger(0); //每个用户用一个CharAnnotation实例来维护,请你注意它是一个全局的static变量,所以用到了线程安全的CopyOnWriteArraySet private static final Set<ChatEndpoint> connections = new CopyOnWriteArraySet<>();
private final String nickname; private Session session;
public ChatEndpoint() { nickname = GUEST_PREFIX + connectionIds.getAndIncrement(); }
//新连接到达时,Tomcat会创建一个Session,并回调这个函数 @OnOpen public void start(Session session) { this.session = session; connections.add(this); String message = String.format("* %s %s", nickname, "has joined."); broadcast(message); }
//浏览器关闭连接时,Tomcat会回调这个函数 @OnClose public void end() { connections.remove(this); String message = String.format("* %s %s", nickname, "has disconnected."); broadcast(message); }
//浏览器发送消息到服务器时,Tomcat会回调这个函数 @OnMessage public void incoming(String message) { // Never trust the client String filteredMessage = String.format("%s: %s", nickname, HTMLFilter.filter(message.toString())); broadcast(filteredMessage); }
// WebSocket连接出错时,Tomcat会回调这个函数 @OnError public void onError(Throwable t) throws Throwable { log.error("Chat Error: " + t.toString(), t); }
// 向聊天室中的每个用户广播消息 private static void broadcast(String msg) { for (ChatAnnotation client : connections) { try { synchronized (client) { client.session.getBasicRemote().sendText(msg); } } catch (IOException e) { ... } } }}
复制代码

根据 Java WebSocket 规范的规定,Java WebSocket 应用程序由一系列的 WebSocket Endpoint 组成。Endpoint 是一个 Java 对象,代表 WebSocket 连接的一端,就好像处理 HTTP 请求的 Servlet 一样,你可以把它看作是处理 WebSocket 消息的接口。跟 Servlet 不同的地方在于,Tomcat 会给每一个 WebSocket 连接创建一个 Endpoint 实例。可以通过两种方式

定义和实现 Endpoint

编程式

编写一个 Java 类继承 javax.websocket.Endpoint,并实现它的 onOpen、onClose 和 onError 方法。这些方法跟 Endpoint 的生命周期有关,Tomcat 负责管理 Endpoint 的生命周期并调用这些方法。并且当浏览器连接到一个 Endpoint 时,Tomcat 会给这个连接创建一个唯一的 Session(javax.websocket.Session)。Session 在 WebSocket 连接握手成功之后创建,并在连接关闭时销毁。当触发 Endpoint 各个生命周期事件时,Tomcat 会将当前 Session 作为参数传给 Endpoint 的回调方法,因此一个 Endpoint 实例对应一个 Session,我们通过在 Session 中添加 MessageHandler 消息处理器来接收消息,MessageHandler 中定义了 onMessage 方法。在这里 Session 的本质是对 Socket 的封装,Endpoint 通过它与浏览器通信。

注解式

实现一个业务类并给它添加 WebSocket 相关的注解。

@ServerEndpoint(value = "/websocket/chat")
复制代码

注解,它表明当前业务类 ChatEndpoint 是个实现了 WebSocket 规范的 Endpoint,并且注解的 value 值表明 ChatEndpoint 映射的 URL 是/websocket/chat。ChatEndpoint 类中有 @OnOpen、@OnClose、@OnError 和在 @OnMessage 注解的方法,见名知义。


我们只需关心具体的 Endpoint 实现,比如聊天室,为向所有人群发消息,ChatEndpoint 在内部使用了一个全局静态的集合 CopyOnWriteArraySet 维护所有 ChatEndpoint 实例,因为每一个 ChatEndpoint 实例对应一个 WebSocket 连接,即代表了一个加入聊天室的用户。当某个 ChatEndpoint 实例收到来自浏览器的消息时,这个 ChatEndpoint 会向集合中其他 ChatEndpoint 实例背后的 WebSocket 连接推送消息。

  • Tomcat 主要做了哪些事情呢?

Endpoint 加载和 WebSocket 请求处理。

WebSocket 加载

Tomcat 的 WebSocket 加载是通过 SCI,ServletContainerInitializer,是 Servlet 3.0 规范中定义的用来接收 Web 应用启动事件的接口。


为什么要监听 Servlet 容器的启动事件呢?这样就有机会在 Web 应用启动时做一些初始化工作,比如 WebSocket 需要扫描和加载 Endpoint 类。


将实现 ServletContainerInitializer 接口的类增加 HandlesTypes 注解,并且在注解内指定的一系列类和接口集合。比如 Tomcat 为了扫描和加载 Endpoint 而定义的 SCI 类如下:

定义好 SCI,Tomcat 在启动阶段扫描类时,会将 HandlesTypes 注解指定的类都扫描出来,作为 SCI 的 onStartup 参数,并调用 SCI#onStartup。WsSci#HandlesTypes 注解定义了 ServerEndpoint.class、ServerApplicationConfig.class 和 Endpoint.class,因此在 Tomcat 的启动阶段会将这些类的类实例(不是对象实例)传递给 WsSci#onStartup。


  • WsSci 的 onStartup 方法做了什么呢?

构造一个 WebSocketContainer 实例,你可以把 WebSocketContainer 理解成一个专门处理 WebSocket 请求的 Endpoint 容器。即 Tomcat 会把扫描到的 Endpoint 子类和添加了注解 @ServerEndpoint 的类注册到这个容器,并且该容器还维护了 URL 到 Endpoint 的映射关系,这样通过请求 URL 就能找到具体的 Endpoint 来处理 WebSocket 请求。

WebSocket 请求处理

  • Tomcat 连接器的组件图


  • 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 技术实现了 Tomcat 与浏览器的双向通信,Tomcat 可以主动向浏览器推送数据,可以用来实现对数据实时性要求比较高的应用。这需要浏览器和 Web 服务器同时支持 WebSocket 标准,Tomcat 启动时通过 SCI 技术来扫描和加载 WebSocket 的处理类 ServerEndpoint,并且建立起了 URL 到 ServerEndpoint 的映射关系。


当第一个 WebSocket 请求到达时,Tomcat 将 HTTP 协议升级成 WebSocket 协议,并将该 Socket 连接的 Processor 替换成 UpgradeProcessor。这个 Socket 不会立即关闭,对接下来的请求,Tomcat 通过 UpgradeProcessor 直接调用相应的 ServerEndpoint 来处理。


还可以通过 Spring 来实现 WebSocket 应用。


点击关注,第一时间了解华为云新鲜技术~​

发布于: 刚刚阅读数: 2
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
如何使用Tomcat实现WebSocket即时通讯服务服务端_html5_华为云开发者社区_InfoQ写作社区