第三部分:网络协议
WebSocket是一个先进的网络协议,被开发用来用来提高网络的性能和web应用的响应率,我们将介绍Netty对WebSocket这两个特性的支持,同时我们也会举一个简单的实例来说明讲解这两个WebSocket的特性
在第十二章节中,你将学会如何使用WebSocket实现数据双向传输的功能,我们会写一个聊天室的方式讲解这个数据双向传输的问题,我们这个聊天室的实例是这样的:多个浏览器客户端可是实时的相互通信,你也会学会如何将普通的HTTP协议切换升级成WebSocket协议,当然我们需要提前检测客户端是否支持WebSocket
我们也会在第三部分的最后介绍Netty对User Dategram Protocol(UDP)协议的支持,我们将会构建一个广播机制的服务器,并且会监控客户端的状态,以致客户端可以适用更多的实际的使用
第十二章:WebSocket
本章内容包括:
1)实时网络的概念
2)WebSocket协议
3)使用Netty构建基于WebSocket的聊天室服务端
如果你比较了解近期的Web应用的发展的话,你肯定会接触过“实时web”这个概念,如果你有实时系统有过实战的经验的话,你可能会在质疑“实时web”这个意味着什么
所以我们一开始就需要澄清这个概念,这并不是我们称之为“全实时品质服务”,我们可以称之为“近实时服务”,在这种场景下,后台的计算结果必须在指定的时间内给以传播,这个指定的时间比较给以保证,但是请求响应的这种HTTP协议设计并不是适合这种场景,如果运用到这种场景会有很多问题,因为你不知道什么时候去请求,截止到目前为止,事实证明目前还没有任何方案被设计出来,还没有任何方案被认为是令人满意的
尽管现在还有很多学术的讨论关于“timed web services”这个词汇的定义,现在被普遍接受的概念目前为止还没有出现,所以目前为止,我们只能从维基百科中接受一个并不是权威的对real-time web的定义:
一个近实时的web是需要网络web系统使用某种技术或者某种策略使使用者能够最快速度的获取信息,而不是需要使用者或者客户端使用他们的软件去主动调用或者检测某个数据源是否发生了变更
从技术上简单地陈述一下,也许一个成熟的实时web也许还没有真正的到来,但是在这个理念会加速大家对这个技术的期望,有了这种技术你可以几乎瞬发地获取到信息,这个章节我们讨论的WebSocket协议是近实时技术研究方向上前进的很扎实的一步
12.1 Introducing WebSocket
WebSocket协议是被设计来用来解决web应用中数据双向传输的一个实际的解决方案,这种协议可以是服务器和客户端在任何事件可以传输信息,因此,需要它们可以异步地处理信息
Netty对WebSocket的支持包括了对WebSocket的很多主要实现,所以你可以直接在你的项目中直接采用Netty提供的WebSocket服务是很直接方便的,与Netty设计的理念一样,你可以充分地使用WebSocket协议在不需要关心它内部的实现细节上,我们将通过使用WebSocket协议创建一个实时的web版的聊天系统
12.2 Our example WebSocket application
我们的示例应用将通过使用WebSocket协议来实现一个基于浏览器的聊天应用来说明实时通讯的功能,这种实例很类似于你在FaceBook上遇到的那种文本消息的交互,我们也会将我们的示例支持多个用户可以同时使用,进行相互通信
图12.1说明了我们示例的主要逻辑
1)客户端发送信息
2)发送的信息广播到所有其他链接的客户端中
这就是一个聊天室如何工作的基本逻辑:每个人可以其他的每个人进行交谈,在我们的示例中我们只会实现我们的服务端的代码,客户端则是通过浏览器的一些功能去简单实现的,在接下来的几页内容里,你会看见Netty的WebSocket是如何简单地构建这个示例的
12.3 Adding WebSocket support
升级握手机制被用来将标准的HTTP协议或者HTTPS协议切换成WebSocket协议,当然一个应用会以HTTP/S开始在需要升级的场景下升级为WebSocket应用,这种升级可以是由某个具体的URL指定触发的
我们的应用就会使用这样的升级机制,如果一个URL请求是以/ws为结尾的,我们将协议升级成WebSocket协议,否则我们默认使用最基本的HTTP/S协议,当连接成功升级之后,所有的数据将使用WebSocket协议进行传播,图12.2说明了服务器的逻辑,服务器使用的是Netty,具体是通过一系列的ChannelHandler实现的,在下一个小节中,我们将具体来描述这些组件,我们会解释这些处理HTTP和WebSocket协议的技术
12.3.1 Handling HTTP requests
首先我们将会实现用来处理HTTP请求的组件,这个组件服务于网页端,用来提供聊天室的入口,并且用来展示由每个连接客户端发送信息的展示,代码清单12.1展示了HttpRequestHandler的具体实现,这个类继承于SimpleChannelInboundHandler用来处理FullHttpRequest信息,请注意代码实现中,方法channelRead0的实现是如何处理URI中以/ws为结尾的请求的
如果一个URI请求中有“/ws”的时候,HttpRequestHandler将会在FullHttpRequest上调用retain方法,再通过fireChannelRead(msg)方法使其转向到下一个ChannelInboundHandler中,调用retain是有必要的,因为在channelRead方法完成之后,它将会对FullHttpRequest调用release方法来释放资源
如果一个客户端发送一个HTTP1.1的消息头,期待一个"100-continue",那么HttpRequestHandler将会发送一个100-continue的响应,在所有的消息头被设置后,HttpRequestHandler将会写一个HttpResponse返回给客户端,返回的并不是一个完整的FullHttpResponse,因为它只是响应的第一个部分,所以在这里并没有调用writeAndFlush方法,这个方法将会在结尾调用
如果加密和压缩都不需要的情况下,那么我们可以在DefaultFileRegion中存储index.html的内容,这样可以获取最大的效率,这样可以利用零拷贝技术来获取最高的传输效率,因为这个理由,你可以了解在一个ChannelPipeline中是否有SslHandler,如果有,你就有可选方案可以使用ChunkedNioFile
HttpRequestHandler写一个LastHttpContent来标记一个响应的结束,如果不需要keepalive,那么HttpRequestHandler将会在最后写入结束的时候的ChannelFuture增加一个ChannelFutureListener来关闭连接,在这里你就可以调用writeAndFlush方法来将值钱所有写入的信息刷入
目前为止,这块代码代表了聊天室的第一个模块,它将管理纯净的HTTP请求和响应,下一个小节,我们将处理WebSocket协议的帧数据,这个数据将承载着真实的聊天信息
12.3.2 Handling WebSocket frames
RFC的WebSocket协议由IETF发布的,规范了六种类型的帧数据,Netty为每一种帧数据提供了一个POJO的实现,表12.1列出了这些帧数据的类型并且描述他们的使用说明
我们的聊天室将使用如下的数据帧类型
1)CloseWebSocketFrame
2)PingWebSocketFrame
3)PongWebSocketFrame
4)TextWebSocketFrame
事实上TextWebSocketFrame类型的数据是我们真正需要处理的,为了符合RFC的WebSocket,Netty提供了WebSocketServerProtocolHandler来管理其他类型的帧数据
下面的代码清单展示用来处理TextWebSocketFrame的ChannelInboundHandler,这个也可以用来追踪在ChannelGroup中的存活的WebSocket连接
TextWebSocketFrameHandler只负责很小一块的功能,当WebSocket与新的客户端成功握手之后,它将会通知所有的连接着的所有客户端,通过将其写入ChannelGroup的所有Channel中,然后它将新增的Channel写入ChannelGroup中
如果一个TextWebSocketFrame被接收,那么它将对TextWebSocketFrame对象调用retain方法,然后使用writeAndFlush方法将其传输到ChannelGroup中以致于所有的连接的WebSocket的Channel都能接收到这个讯息
在之前,调用retain方法是有必要的,是因为当channelRead0的方法返回的时候TextWebSocketFrame这个对象的计数引用会递减一,因为所有的操作都是异步的,writeAndFlush可能会在有一定的延迟的时间内完成,所以我们不允许获取一个不合法的对象引用,所以调用retain方法
因为Netty内部已经给我们的WebSocket的实现完成了大部分的工作,唯一剩下来的事情就是初始化好ChannelPipeline,因为每一个新的Channel都在这创建的,因为这个原因,我们需要一个ChannelInitializer
12.3.3 Initializing the ChannelPipeline
因为我们之前已经学习过,在ChannelPipeline中安装ChannelHandler时,你只需要继承ChannelInitializer类,然后实现initChannel方法,下面的代码清单展示了ChatServerInitializer的具体实际代码
initChannel方法的调用可以设置ChannelPipeline中注册的Channel,通过这种方法可以安装所需的所有ChannelHandler,在表12.2给出了部分摘要,每一个都描述了他们自己负责的功能
Netty的WebSocketServerProtocolHandler处理所有已经授权的WebSocket帧类型的数据,并且同时升级握手,如果握手成功,指定的一些ChannelHandler需要增加到管道中,如果没有需要,那么这些handler将不会再从管道中移除
在协议升级之前管道的状态如图12.3所示,这张图代表了又ChatServerInitializer初始化之后的管道的状态
当协议升级之后,WebSocketServerProtocolHandler将会以WebSocketFrameDecoder取代HttpRequestDecoder,以WebSocketFrameEncoder取代HttpResponseEncoder
,为了最大化性能,它会在这个时候移除任何不需要的多余的Handler,这些Handler可能WebSocket使用不到,这些Handler包括如12.3所示的HttpObjectAggregator和HttpRequestHandler
图12.4展示了当操作升级完成后管道的状态,注意目前为止Netty支持四种类型版本的WebSocket,每一个版本都有他们自己的实现,选择正确的WebSocketFrameDecoder和WebSocketFrameEncoder这个行为是自动完成的,这取决于你客户端也就是浏览器支持哪一种版本的webSocket
12.3.4 Bootstrapping
这个聊天室的最后一个模块就是启动服务器初始化所有的ChatServerInitializer,这些动作将被ChatServer处理,如下面展示的一样
应用的所有代码已经完成了,现在我们开始测试
12.4
Testing the application
第十二章节的所有代码示例已经全部给出了,现在你需要构建运行测试用例,我们可以使用如下的Maven命令来构建和启动项目
这个项目的pom.xml配置了在端口9999来启动项目,如果你想要使用其他的端口,你可以不需要编辑改变量,你可以在命令中重新覆盖该变量
下面是控制台打印的构建和启动的日记
在你的浏览器中输入http://localhost:9999来进入你的应用,图12.5展示了Chrome浏览器的UI
图12.5展示了两个链接的客户端,第一个连接是使用浏览器页面头部提供的输入接口,第二个连接是通过浏览器的底部的控制台连接的,你会发现无论从这两个客户端的发送的信息,发送的信息都能够在其他的客户端中正常的展示
这就是一个完整的示例,这个示例很好地展示了在一个浏览器中WebSocket如何构建一个实时通讯的
12.4.1 What about encryption?
在真实的生产环境中,你将很快被要求将加密器加入到你的应用中,对于Netty应用来说,这是小事一桩,你只需要将SslHandler加入到ChannelPipeline中去然后配置一下就可以了,下面的代码清单展示了我们是如何实现这个功能的,我们只需要创建一个SecureChatServerInitializer,使其继承与ChatServerInitializer就可以了
最后一步就是让ChatServer采用我们刚刚创建的SecureChatServerInitializer这样就可以使SslHandler加入到管道中去,我们使用SecureChatServer实现了这个效果
这样就可以使整个通讯都支持SSL/TLS的加密了,与之前一样,我们依旧使用Maven去构建一下
现在你又可以在http://localhost:9999看到SecureChatServer了
12.5 Summary
在这个章节中,我们使用了Netty实现的WebSocket写了一个网页版的实时数据系统,我们讲解了WebSocket支持的数据类型,也讨论了我们遇到的一些限制,尽管并不是每一个场景都可以使用WebSocket的,但是有一点是毫无疑问的,对于web开发来说,WebSocket技术是这个方面的一个很重要的一大突破