WebSocket
WebSocket 通过“Upgrade handshake(升级握手)”从标准的 HTTP 或HTTPS 协议转为 WebSocket。因此,使用 WebSocket 的应用程序将始终以 HTTP/S 开始,然后进行升级。在什么时候发生这种情况取决于具体的应用;它可以是在启动时,或当一个特定的 URL 被请求时。
在我们的应用中,当 URL 请求以“/ws”结束时,我们才升级协议为WebSocket。否则,服务器将使用基本的HTTP/S。一旦升级连接将使用的WebSocket 传输所有数据。
整个服务器逻辑如下:

- 客户端/用户连接到服务器并加入聊天
- HTTP 请求页面或 WebSocket 升级握手
- 服务器处理所有客户端/用户
- 响应 URI “/”的请求,转到默认 html 页面
- 如果访问的是 URI“/ws” ,处理 WebSocket 升级握手
- 升级握手完成后 ,通过 WebSocket 发送聊天消息
服务端
让我们从处理 HTTP 请求的实现开始。
处理 HTTP 请求
HttpRequestHandler.java
|
|
- 扩展
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()来释放其资源。如果客户端发送的 HTTP 1.1 头是“Expect: 100-continue” ,将发送“100 Continue”的响应。
在 头被设置后,写一个
HttpResponse返回给客户端。注意,这是不是FullHttpResponse,唯一的反应的第一部分。此外,我们不使用writeAndFlush()在这里 - 这个是在最后完成。如果没有加密也不压缩,要达到最大的效率可以是通过存储 index.html 的内容在一个
DefaultFileRegion实现。这将利用零拷贝来执行传输。出于这个原因,我们检查,看看是否有一个SslHandler在ChannelPipeline中。另外,我们使用 ChunkedNioFile。写
LastHttpContent来标记响应的结束,并终止它如果不要求
keepalive,添加 ChannelFutureListener 到ChannelFuture对象的最后写入,并关闭连接。注意,这里我们调用writeAndFlush()来刷新所有以前写的信息。
处理 WebSocket frame
WebSockets 在“帧”里面来发送数据,其中每一个都代表了一个消息的一部分。一个完整的消息可以利用了多
个帧。 WebSocket “Request for Comments” (RFC) 定义了六中不同的 frame; Netty 给他们每个都提供了一
个 POJO 实现 ,而我们的程序只需要使用下面4个帧类型:
- CloseWebSocketFrame
- PingWebSocketFrame
- PongWebSocketFrame
- TextWebSocketFrame
在这里我们只需要显示处理 TextWebSocketFrame,其他的会由 WebSocketServerProtocolHandler 自动处理
下面代码展示了 ChannelInboundHandler 处理 TextWebSocketFrame,同时也将跟踪在 ChannelGroup中所有活动的 WebSocket 连接
TextWebSocketFrameHandler.java
|
|
TextWebSocketFrameHandler继承自SimpleChannelInboundHandler,这个类实现了ChannelInboundHandler接口,ChannelInboundHandler提供了许多事件处理的接口方法,然后你可以覆盖这些方法。现在仅仅只需要继承SimpleChannelInboundHandler类而不是你自己去实现接口方法。- 覆盖了
handlerAdded()事件处理方法。每当从服务端收到新的客户端连接时,客户端的 Channel 存入ChannelGroup列表中,并通知列表中的其他客户端 Channel - 覆盖了
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
WebsocketChatServerInitializer.java
|
|
- 扩展 ChannelInitializer
- 添加 ChannelHandler到
ChannelPipeline.initChannel()方法设置 ChannelPipeline 中所有新注册的 Channel,安装所有需要的ChannelHandler。
WebsocketChatServer.java
编写一个 main() 方法来启动服务端。
|
|
- NioEventLoopGroup是用来处理I/O操作的多线程事件循环器,Netty 提供了许多不同的EventLoopGroup的实现用来处理不同的传输。在这个例子中我们实现了一个服务端的应用,因此会有2个 NioEventLoopGroup 会被使用。第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。如何知道多少个线程已经被使用,如何映射到已经创建的 Channel上都需要依赖于 EventLoopGroup 的实现,并且可以通过构造函数来配置他们的关系。
- ServerBootstrap是一个启动 NIO 服务的辅助启动类。你可以在这个服务中直接使用 Channel,但是这会是一个复杂的处理过程,在很多情况下你并不需要这样做。
- 这里我们指定使用NioServerSocketChannel类来举例说明一个新的 Channel 如何接收进来的连接。
- 这里的事件处理类经常会被用来处理一个最近的已经接收的 Channel。SimpleChatServerInitializer 继承自ChannelInitializer是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel。也许你想通过增加一些处理类比如 SimpleChatServerHandler 来配置一个新的 Channel 或者其对应的ChannelPipeline来实现你的网络程序。当你的程序变的复杂时,可能你会增加更多的处理类到 pipline 上,然后提取这些匿名类到最顶层的类上。
- 你可以设置这里指定的 Channel 实现的配置参数。我们正在写一个TCP/IP 的服务端,因此我们被允许设置 socket 的参数选项比如
tcpNoDelay和keepAlive。请参考ChannelOption和详细的ChannelConfig实现的接口文档以此可以对ChannelOption 的有一个大概的认识。 - option() 是提供给NioServerSocketChannel用来接收进来的连接。
childOption()是提供给由父管道ServerChannel接收到的连接,在这个例子中也是 NioServerSocketChannel。 - 我们继续,剩下的就是绑定端口然后启动服务。这里我们在机器上绑定了机器所有网卡上的 8080 端口。当然现在你可以多次调用
bind()方法(基于不同绑定地址)。
恭喜!你已经完成了基于 Netty 聊天服务端程序。
客户端
在程序的 resources 目录下,我们创建一个 WebsocketChatClient.html 页面来作为客户端
WebsocketChatClient.html
|
|
逻辑比较简单,不累述。
先运行 WebsocketChatServer,再打开多个浏览器页面实现多个 客户端访问 http://localhost:8080

查看源码:
https://github.com/kakajing/netty4-demos/tree/master/src/main/java/com/netty/demo/websocketchat
