目录

WebSocket

本章主要内容

  • 实时 Web 的概念
  • WebSocket 协议
  • 使用 Netty 构建一个基于 WebSocket 的聊天室服务器

如果你有跟进 Web 工作的最新进展,你很可能就遇到过“实时 Web”这个短语,而如果你 在工程领域中有实时应用程序的实战经验,那么你可能有点怀疑这个术语到底意味着什么。

因此, 让我们首先澄清, 这里并不是指所谓的硬实时服务质量(QoS), 硬实时服务质 量是保证计算结果将在指定的时间间隔内被递交。 仅 HTTP 的请求/响应模式设计就使得其 很难被支持,从过去所设计的各种方案中都没有提供一种能够提供令人满意的解决方案的事 实中便可见一斑。

虽然已经有了一些关于正式定义实时Web服务 语义的学术讨论,但是被普遍接受的定义似 乎还未出现。因此现在我们将采纳下面来自维基百科的非权威性描述:

实时 Web 利用工作和实践,使用户在信息的作者发布信息之后就能够立即收到信 息,而不需要他们或者他们的软件周期性地检查信息源以获取更新。

服务端发送消息给客户端,客户端能立即受到而不需要去轮询

简而言之,虽然全面的实时Web可能并不会马上到来, 但是它背后的想法却助长了对于几乎瞬时获得信息的期望。我们将在本章中讨论的WebSocket 协议便是在这个方向上迈出的坚实 的一步。

WebSocket简介

WebSocket 协议是完全重新设计的协议, 旨在为 Web 上的双向数据传输问题提供一个切实可行的解决方案, 使得客户端和服务器之间可以在任意时刻传输消息, 因此, 这也就要求 它们异步地处理消息回执。(作为 HTML5 客户端 API 的一部分,大部分最新的浏览器都已经 支持了 WebSocket。)

Netty 对于 WebSocket 的支持包含了所有正在使用中的主要实现,因此在你的下一个应 用程序中采用它将是简单直接的。和往常使用 Netty 一样,你可以完全使用该协议,而无需 关心它内部的实现细节。 我们将通过创建一个基于 WebSocket 的实时聊天应用程序来演示 这一点。

http中长连接和websocket的长连接的区别

什么是http协议

HTTP是一个应用层协议,无状态的,端口号为80。主要的版本有1.0/1.1/2.0.

  • HTTP/1.* 一次请求-响应,建立一个连接,用完关闭;
  • HTTP/1.1 串行化单线程处理,可以同时在同一个tcp链接上发送多个请求,但是只有响应是有顺序的,只有上一个请求完成后,下一个才能响应。一旦有任务处理超时等,后续任务只能被阻塞(线头阻塞);
  • HTTP/2 并行执行。某任务耗时严重,不会影响到任务正常执行

什么是websocket

Websocket是html5提出的一个协议规范,是为解决客户端与服务端实时通信。本质上是一个基于tcp,先通过HTTP/HTTPS协议发起一条特殊的http请求进行握手后创建一个用于交换数据的TCP连接。

WebSocket优势: 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,双方可以在任意时刻,相互推送信息。同时,服务器与客户端之间交换的头信息很小。

什么是长连接、短连接

  • 短连接:

连接->传输数据->关闭连接

HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。

也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。

  • 长连接、

连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。

长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。

http和websocket的长连接区别

HTTP1.1通过使用Connection:keep-alive进行长连接,HTTP 1.1默认进行持久连接。在一次 TCP 连接中可以完成多个 HTTP 请求,但是对每个请求仍然要单独发 header,Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。这种长连接是一种“伪链接”

websocket的长连接,是一个真的全双工。长连接第一次tcp链路建立之后,后续数据可以双方都进行发送,不需要发送请求头。

keep-alive双方并没有建立正真的连接会话,服务端可以在任何一次请求完成后关闭。WebSocket 它本身就规定了是正真的、双工的长连接,两边都必须要维持住连接的状态。

我们的WebSocket示例应用程序

为了让示例应用程序展示它的实时功能,我们 将通过使用 WebSocket 协议来实现一个基于浏 览器的聊天应用程序,就像你可能在 Facebook 的文本消息功能中见到过的那样。我们将通过使 得多个用户之间可以同时进行相互通信,从而更进一步。

图 12-1 说明了该应用程序的逻辑:

https://gitee.com/lienhui68/picStore/raw/master/null/20200913075644.png

  • 客户端发送一个消息;
  • 该消息将被广播到所有其他连接的客户端。

这正如你可能会预期的一个聊天室应当的工作方式:所有的人都可以和其他的人聊天。在示 例中,我们将只实现服务器端,而客户端则是通过 Web 页面访问该聊天室的浏览器。正如同你 将在接下来的几页中所看到的,WebSocket 简化了编写这样的服务器的过程。

添加WebSocket支持

在从标准的HTTP或者HTTPS协议切换到WebSocket时,将会使用一种称为升级握手 的机 制。因此,使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确切时刻特定于应用程序;它可能会发生在启动时,也可能会发生在请求了某个特定的 URL之后。

我们的应用程序将采用下面的约定:如果被请求的 URL 以 /ws 结尾,那么我们将会把该协 议升级为 WebSocket;否则,服务器将使用基本的 HTTP/S。在连接已经升级完成之后,所有数 据都将会使用 WebSocket 进行传输。图 12-2 说明了该服务器逻辑,一如在 Netty 中一样,它由一 组 ChannelHandler 实现。我们将会在下一节中,解释用于处理 HTTP 以及 WebSocket 协议的 工作时,描述它们。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913080750.png

处理Http请求

首先,我们将实现该处理 HTTP 请求的组件。这个组件将提供用于访问聊天室并显示由连接 的客户端发送的消息的网页。代码清单 12-1 给出了这个 HttpRequestHandler 对应的代码, 其扩展了 SimpleChannelInboundHandler 以处理 FullHttpRequest 消息。需要注意的是, channelRead0() 方法的实现是如何转发任何目标 URI 为 /ws 的请求的。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913081152.png

如果该 HTTP 请求指向了地址为 /ws 的 URI,那么 HttpRequestHandler 将调用 FullHttpRequest 对象上的 retain() 方法,并通过调用 fireChannelRead(msg) 方法将它转发给下一 个 ChannelInboundHandler 。之所以需要调用 retain() 方法,是因为调用 channelRead() 方法完成之后,它将调用 FullHttpRequest 对象上的 release() 方法以释放它的资源,fireChannelRead(msg) 是异步的,可能出现read方法执行完fireChannelRead还没有执行的情况,所以需要引用计数加1。(参见 我们在第 6 章中对于 SimpleChannelInboundHandler 的讨论。)

如果客户端发送了 HTTP 1.1 的 HTTP 头信息 Expect: 100-continue ,那么 Http- RequestHandler 将会发送一个 100 Continue 响应。在该 HTTP 头信息被设置之后, Http- RequestHandler 将会写回一个 HttpResponse 给客户端。 这不是一个 FullHttpResponse ,因为它只是响应的第一个部分。此外,这里也不会调用 writeAndFlush() 方法, 在结束的时候才会调用。

如果不需要加密和压缩,那么可以通过将 index.html 的内容存储到 DefaultFile- Region 中来达到最佳效率。 这将会利用零拷贝特性来进行内容的传输。为此,你可以检查一下, 是否有 SslHandler 存在于在 ChannelPipeline 中,如果有的话需要拷贝到用户空间执行,此时可以使用 ChunkedNioFile 。

HttpRequestHandler 将写一个 LastHttpContent 来标记响应的结束。如果没有请 求 keep-alive ,那么 HttpRequestHandler 将会添加一个 ChannelFutureListener 到最后一次写出动作的 ChannelFuture ,并关闭该连接。在这里,你将调用 writeAndFlush() 方法以冲刷所有之前写入的消息。

这部分代码代表了聊天服务器的第一个部分,它管理纯粹的 HTTP 请求和响应。接下来,我 们将处理传输实际聊天消息的 WebSocket 帧。

WEBSOCKET 帧

WebSocket 以帧的方式传输数据,每一帧代表消息的一部分。一个完整的消息可能会包含许多帧。

处理WebSocket帧

由 IETF 发布的 WebSocket RFC,定义了 6 种帧,Netty 为它们每种都提供了一个 POJO 实现。 表 12-1 列出了这些帧类型,并描述了它们的用法。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913081700.png

我们的聊天应用程序将使用下面几种帧类型:

  • CloseWebSocketFrame;
  • PingWebSocketFrame;
  • PongWebSocketFrame;
  • TextWebSocketFrame

TextWebSocketFrame 是我们唯一真正需要处理的帧类型。为了符合 WebSocket RFC, Netty 提供了 WebSocketServerProtocolHandler 来处理其他类型的帧。

代码清单 12-2 展示了我们用于处理 TextWebSocketFrame 的 ChannelInboundHandler , 其还将在它的 ChannelGroup 中跟踪所有活动的 WebSocket 连接。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913082311.png

TextWebSocketFrameHandler 只有一组非常少量的责任。当和新客户端的 WebSocket 握手成功完成之后 ,它将通过把通知消息写到 ChannelGroup 中的所有 Channel 来通知所 有已经连接的客户端,然后它将把这个新 Channel 加入到该 ChannelGroup 中 。

如果接收到了 TextWebSocketFrame 消息 , TextWebSocketFrameHandler 将调用 TextWebSocketFrame 消息上的 retain() 方法,并使用 writeAndFlush() 方法来将它传 输给 ChannelGroup ,以便所有已经连接的 WebSocket Channel 都将接收到它。

和之前一样,对于 retain() 方法的调用是必需的,因为当 channelRead0() 方法返回时, TextWebSocketFrame 的引用计数将会被减少。由于所有的操作都是异步的,因此, writeAndFlush() 方法可能会在 channelRead0() 方法返回之后完成,而且它绝对不能访问一个已经失 效的引用。

因为 Netty 在内部处理了大部分剩下的功能,所以现在剩下唯一需要做的事情就是为每个新创建 的 Channel 初始化其 ChannelPipeline 。为此,我们将需要一个 ChannelInitializer 。

初始化ChannelPipeline

正如你已经学习到的,为了将 ChannelHandler 安装到 ChannelPipeline 中,你扩展了 ChannelInitializer ,并实现了 initChannel() 方法。代码清单 12-3 展示了由此生成的 ChatServerInitializer 的代码。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913082604.png

对于 initChannel() 方法的调用,通过安装所有必需的 ChannelHandler 来设置该新注 册的 Channel 的 ChannelPipeline 。这些 ChannelHandler 以及它们各自的职责都被总结 在了表 12-2 中。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913082713.png

Netty的 WebSocketServerProtocolHandler处理了所有委托管理的 WebSocket 帧类型以 及升级握手本身。如果握手成功,那么所需的 ChannelHandler 将会被添加到 ChannelPipeline 中,而那些不再需要的 ChannelHandler 则将会被移除。

WebSocket 协议升级之前的 ChannelPipeline 的状态如图 12-3 所示。这代表了刚刚被 ChatServerInitializer 初始化之后的 ChannelPipeline 。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913082813.png

当 WebSocket 协议升级完成之后, WebSocketServerProtocolHandler 将会把 Http- RequestDecoder 替换为 WebSocketFrameDecoder ,把 HttpResponseEncoder 替换为 WebSocketFrameEncoder 。为了性能最大化,它将移除任何不再被 WebSocket 连接所需要的 ChannelHandler 。这也包括了图 12-3 所示的 HttpObjectAggregator 和 HttpRequest- Handler 。

图 12-4 展示了这些操作完成之后的 ChannelPipeline 。需要注意的是,Netty目前支持 4 个版本的WebSocket协议, 它们每个都具有自己的实现类。Netty将会根据客户端(这里指浏览 器)所支持的版本 , 自动地选择正确版本的 WebSocketFrameDecoder 和 WebSocketFrameEncoder 。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913082910.png

引导

这幅拼图最后的一部分是引导该服务器,并安装 ChatServerInitializer的代码。这将由 ChatServer 类处理,如代码清单 12-4 所示。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913083042.png

这也就完成了该应用程序本身。现在让我们来测试它吧。

测试应用程序

目录 chapter12 中的示例代码包含了你需要用来构建并运行该服务器的所有资源。(如果 你还没有设置好你的包括 Apache Maven 在内的开发环境,参见第 2 章中的操作说明。)

我们将使用下面的 Maven 命令来构建和启动服务器:

1
mvn -PChatServer clean package exec:exec

项目文件 pom.xml 被配置为在端口 9999 上启动服务器。如果要使用不同的端口,可以通 过编辑文件中对应的值,或者使用一个 System 属性来对它进行重写:

1
mvn -PChatServer -Dport=1111 clean package exec:exec

代码清单 12-5 展示了该命令主要的输出(无关紧要的行已经被删除了)。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913083155.png

你通过将自己的浏览器指向 http://localhost:9999 来访问该应用程序。图 12-5 展示了其在 Chrome 浏览器中的 UI。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913083215.png

图中展示了两个已经连接的客户端。第一个客户端是使用上面的界面连接的,第二个客户端 则是通过底部的 Chrome 浏览器的命令行工具连接的。你会注意到,两个客户端都发送了消息, 并且每个消息都显示在两个客户端中。

这是一个非常简单的演示,演示了 WebSocket 如何在浏览器中实现实时通信。

如何进行加密

在真实世界的场景中,你将很快就会被要求向该服务器添加加密。使用 Netty,这不过是将一 个 SslHandler 添加到 ChannelPipeline 中,并配置它的问题。代码清单 12-6 展示了如何通 过扩展我们的 ChatServerInitializer 来创建一个 SecureChatServerInitializer 以完 成这个需求。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913083327.png

最后一步是调整 ChatServer 以使用 SecureChatServerInitializer ,以便在 Channel- Pipeline 中安装 SslHandler 。这给了我们代码清单 12-7 中所展示的 SecureChatServer 。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913083404.png

这就是为所有的通信启用 SSL/TLS 加密需要做的全部。和之前一样,可以使用 Apache Maven 来运行该应用程序,如代码清单 12-8 所示。它还将检索任何所需的依赖。

https://gitee.com/lienhui68/picStore/raw/master/null/20200913083423.png

小结

在本章中,你学习了如何使用 Netty 的 WebSocket 实现来管理 Web 应用程序中的实时数据。 我们覆盖了其所支持的数据类型,并讨论了你可能会遇到的一些限制。尽管不可能在所有的情况 下都使用 WebSocket,但是仍然需要清晰地认识到,它代表了 Web 工作的一个重要进展。