天天看点

Netty 实现 WebSocket 聊天功能

jdk 7+

maven 3.2.x

netty 4.x

eclipse 4.x

在我们的应用中,当 url 请求以“/ws”结束时,我们才升级协议为websocket。否则,服务器将使用基本的 http/s。一旦升级连接将使用的websocket 传输所有数据。

整个服务器逻辑如下:

Netty 实现 WebSocket 聊天功能

客户端/用户连接到服务器并加入聊天

http 请求页面或 websocket 升级握手

服务器处理所有客户端/用户

响应 uri “/”的请求,转到默认 html 页面

如果访问的是 uri“/ws” ,处理 websocket 升级握手

升级握手完成后 ,通过 websocket 发送聊天消息

让我们从处理 http 请求的实现开始。

扩展 simplechannelinboundhandler 用于处理 fullhttprequest信息

如果请求是 websocket 升级,递增引用计数器(保留)并且将它传递给在 channelpipeline 中的下个 channelinboundhandler

处理符合 http 1.1的 "100 continue" 请求

读取默认的 websocketchatclient.html 页面

判断 keepalive 是否在请求头里面

写 httpresponse 到客户端

写 index.html 到客户端,判断 sslhandler 是否在 channelpipeline 来决定是使用 defaultfileregion 还是 chunkedniofile

写并刷新 lasthttpcontent 到客户端,标记响应完成

如果 keepalive 没有要求,当写完成时,关闭 channel

httprequesthandler 做了下面几件事,

如果该 http 请求被发送到uri “/ws”,调用 fullhttprequest 上的 retain(),并通过调用 firechannelread(msg) 转发到下一个 channelinboundhandler。retain() 是必要的,因为 channelread() 完成后,它会调用 fullhttprequest 上的 release() 来释放其资源。 (请参考我们先前的 simplechannelinboundhandler 在第6章中讨论)

如果客户端发送的 http 1.1 头是“expect: 100-continue” ,将发送“100 continue”的响应。

在 头被设置后,写一个 httpresponse 返回给客户端。注意,这是不是 fullhttpresponse,唯一的反应的第一部分。此外,我们不使用 writeandflush() 在这里 - 这个是在最后完成。

如果没有加密也不压缩,要达到最大的效率可以是通过存储 index.html 的内容在一个 defaultfileregion 实现。这将利用零拷贝来执行传输。出于这个原因,我们检查,看看是否有一个 sslhandler 在 channelpipeline 中。另外,我们使用 chunkedniofile。

写 lasthttpcontent 来标记响应的结束,并终止它

如果不要求 keepalive ,添加 channelfuturelistener 到 channelfuture 对象的最后写入,并关闭连接。注意,这里我们调用 writeandflush() 来刷新所有以前写的信息。

websockets 在“帧”里面来发送数据,其中每一个都代表了一个消息的一部分。一个完整的消息可以利用了多个帧。 websocket "request for comments" (rfc) 定义了六中不同的 frame; netty 给他们每个都提供了一个 pojo 实现 ,而我们的程序只需要使用下面4个帧类型:

closewebsocketframe

pingwebsocketframe

pongwebsocketframe

textwebsocketframe

在这里我们只需要显示处理 textwebsocketframe,其他的会由 websocketserverprotocolhandler 自动处理。

下面代码展示了 channelinboundhandler 处理 textwebsocketframe,同时也将跟踪在 channelgroup 中所有活动的 websocket 连接

textwebsocketframehandler.java

覆盖了 handlerremoved() 事件处理方法。每当从服务端收到客户端断开时,客户端的 channel 自动从 channelgroup 列表中移除了,并通知列表中的其他客户端 channel

覆盖了 channelread0() 事件处理方法。每当从服务端读到客户端写入信息时,将信息转发给其他客户端的 channel。其中如果你使用的是 netty 5.x 版本时,需要把 channelread0() 重命名为messagereceived()

覆盖了 channelactive() 事件处理方法。服务端监听到客户端活动

覆盖了 channelinactive() 事件处理方法。服务端监听到客户端不活动

exceptioncaught() 事件处理方法是当出现 throwable 对象才会被调用,即当 netty 由于 io 错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来并且把关联的 channel 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。

上面显示了 textwebsocketframehandler 仅作了几件事:

当websocket 与新客户端已成功握手完成,通过写入信息到 channelgroup 中的 channel 来通知所有连接的客户端,然后添加新 channel 到 channelgroup

如果接收到 textwebsocketframe,调用 retain() ,并将其写、刷新到 channelgroup,使所有连接的 websocket channel 都能接收到它。和以前一样,retain() 是必需的,因为当 channelread0()返回时,textwebsocketframe 的引用计数将递减。由于所有操作都是异步的,writeandflush() 可能会在以后完成,我们不希望它来访问无效的引用。

由于 netty 处理了其余大部分功能,唯一剩下的我们现在要做的是初始化 channelpipeline 给每一个创建的新的 channel 。做到这一点,我们需要一个channelinitializer

1.扩展 channelinitializer

2.添加 channelhandler 到 channelpipeline

initchannel() 方法设置 channelpipeline 中所有新注册的 channel,安装所有需要的  channelhandler。

编写一个 main() 方法来启动服务端。

我们继续,剩下的就是绑定端口然后启动服务。这里我们在机器上绑定了机器所有网卡上的 8080 端口。当然现在你可以多次调用 bind() 方法(基于不同绑定地址)。

恭喜!你已经完成了基于 netty 聊天服务端程序。

在程序的 resources 目录下,我们创建一个 websocketchatclient.html 页面来作为客户端

逻辑比较简单,不累述。

Netty 实现 WebSocket 聊天功能