Netty实战(十三)WebSocket协议(一)

简介: WebSocket 协议是完全重新设计的协议,旨在为 Web 上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息,因此,这也就要求它们异步地处理消息回执。

@TOC

一、什么是WebSocket 协议

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

二、简单的 WebSocket 程序示例

2.1 程序逻辑

我们先设计一个基于浏览器的聊天程序来更好的理解WebSocket ,它的逻辑如下:

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

像这样:
1.png

2.2 添加 WebSocket 支持

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

我们先这样设定:如果被请求的 URL 以/ws 结尾,那么我们把该协议升级为 WebSocket;否则,服务器将使用基本的 HTTP/S。在连接已经升级完成之后,所有数据都将会使用 WebSocket 进行传输。

2.png

2.3 处理 HTTP 请求

上一节我们说使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。所以我们应该先实现处理 HTTP 请求的组件。让它提供用于访问聊天室并显示由连接的客户端发送的消息的网页。

import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile;

import java.io.File;
import java.io.RandomAccessFile;
import java.net.URISyntaxException;
import java.net.URL;

/**
 * Author: lhd
 * Data: 2023/6/12
 * Annotate: http请求处理
 */
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
   
    //扩展 SimpleChannelInboundHandler 以处理 FullHttpRequest 消息

    private final String wsUri;
    private static final File INDEX;
    static {
   
   
        URL location = HttpRequestHandler.class
                .getProtectionDomain()
                .getCodeSource().getLocation();
        try {
   
   
            String path = location.toURI() + "index.html";
            path = !path.contains("file:") ? path : path.substring(5);
            INDEX = new File(path);
        } catch (URISyntaxException e) {
   
   
            throw new IllegalStateException("Unable to locate index.html", e);
        }
    }
    public HttpRequestHandler(String wsUri) {
   
   
        this.wsUri = wsUri;
    }
    @Override
    public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
   
   
        //如果请求了 WebSocket协议升级,则增加引用计数(调用 retain()方法),并将它传递给下一个ChannelInboundHandler
        if (wsUri.equalsIgnoreCase(request.uri())) {
   
   
            ctx.fireChannelRead(request.retain());
        } else {
   
   
            //处理 100 Continue   请求以符合 HTTP 1.1 规范
            if (HttpUtil.is100ContinueExpected(request)) {
   
   
                send100Continue(ctx);
            }
            //读取 index.html
            RandomAccessFile file = new RandomAccessFile(INDEX, "r");
            HttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
            response.headers().set(HttpHeaderNames .CONTENT_TYPE, "text/plain; charset=UTF-8");
            boolean keepAlive = HttpUtil.isKeepAlive(request);
            //如果请求了keep-alive,则添加所需要的 HTTP头信息
            if (keepAlive) {
   
   
                response.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.length());
                response.headers().set(HttpHeaderNames .CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            }
            //将 HttpResponse写到客户端
            ctx.write(response);
            if (ctx.pipeline().get(SslHandler.class) == null) {
   
   
                //将 index.html 写到客户端
                ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
            } else {
   
   
                ctx.write(new ChunkedNioFile(file.getChannel()));
            }
            //写 LastHttpContent 并冲刷至客户端
            ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            if (!keepAlive) {
   
   
                //如果没有请求 keep-alive,则在写操作完成后关闭 Channel
                future.addListener(ChannelFutureListener.CLOSE);
            }
        }
    }
    private static void send100Continue(ChannelHandlerContext ctx) {
   
   
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
            ctx.writeAndFlush(response);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
   
   
        cause.printStackTrace();
        ctx.close();
    }
}

我们简单的说一下上面代码的流程:

(1) 如果该 HTTP 请求指向了地址为/ws 的 URI,那么 HttpRequestHandler 将调用 FullHttpRequest 对象上的 retain()方法,并通过调用 fireChannelRead(msg)方法将它转发给下一个 ChannelInboundHandler 。之所以需要调用 retain()方法,是因为调用 channelRead()方法完成之后,它将调用 FullHttpRequest 对象上的 release()方法以释放它的资源。

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

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

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

这部分代码纯粹的是 HTTP 请求和响应,下面我们来处理 实际传输聊天内容的 WebSocket 帧。

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

2.4 处理 WebSocket 帧

IETF 发布的 WebSocket RFC,定义了 6 种帧,Netty 为它们每种都提供了一个 POJO 实现。

类 型 描 述
BinaryWebSocketFrame 包含了二进制数据
TextWebSocketFrame 包含了文本数据
ContinuationWebSocketFrame 包含属于上一个BinaryWebSocketFrame或TextWebSocketFrame 的文本数据或者二进制数据
CloseWebSocketFrame 表示一个 CLOSE 请求,包含一个关闭的状态码和关闭的原因
PingWebSocketFrame 请求传输一个 PongWebSocketFrame
PongWebSocketFrame 作为一个对于 PingWebSocketFrame 的响应被发送

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

  • CloseWebSocketFrame;
  • PingWebSocketFrame;
  • PongWebSocketFrame;
  • TextWebSocketFrame。

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

下面我们来处理 TextWebSocketFrame 的 ChannelInboundHandler,再将它的 ChannelGroup 中跟踪所有活动的 WebSocket 连接:

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;

/**
 * Author: lhd
 * Data: 2023/6/12
 * Annotate: 处理文本帧
 */
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
   
    //扩展 SimpleChannelInboundHandler, 并处理 TextWebSocketFrame 消息
    private final ChannelGroup group;
    public TextWebSocketFrameHandler(ChannelGroup group) {
   
   
        this.group = group;
    }

    //重写 userEventTriggered() 方法以处理自定义事件
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
   
   
        if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
   
   
            //如果该事件表示握手成功,则从该Channelipeline中移除 HttpRequestHandler,因为将不会接收到任何 HTTP 消息了
            ctx.pipeline().remove(HttpRequestHandler.class);
            //通知所有已经连接的WebSocket 客户端新的客户端已经连接上了
            group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined"));
            //将新的 WebSocket Channel添加到 ChannelGroup 中,以便它可以接收到所有的消息
            group.add(ctx.channel());
        } else {
   
   
            super.userEventTriggered(ctx, evt);
        }
    }
    @Override
    public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
   
   
        //增加消息的引用计数,并将它写到 ChannelGroup 中所有已经连接的客户端
        group.writeAndFlush(msg.retain());
    }
}

简单的说一下:

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

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

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

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

目录
相关文章
|
3月前
|
网络协议 数据处理
WebSocket协议基础
WebSocket协议基础
34 0
|
3月前
|
XML 存储 网络协议
tcp支持浏览器websocket协议
tcp支持浏览器websocket协议
|
3月前
|
移动开发 缓存 网络协议
Websocket协议原理及Ws服务器代码实现
Websocket协议原理及Ws服务器代码实现
|
2月前
|
Java Unix Linux
【Netty技术专题】「原理分析系列」Netty强大特性之Native transports扩展开发实战
当涉及到网络通信和高性能的Java应用程序时,Netty是一个强大的框架。它提供了许多功能和组件,其中之一是JNI传输。JNI传输是Netty的一个特性,它为特定平台提供了高效的网络传输。 在本文中,我们将深入探讨Netty提供的特定平台的JNI传输功能,分析其优势和适用场景。我们将介绍每个特定平台的JNI传输,并讨论其性能、可靠性和可扩展性。通过了解这些特定平台的JNI传输,您将能够更好地选择和配置适合您应用程序需求的网络传输方式,以实现最佳的性能和可靠性。
57 7
【Netty技术专题】「原理分析系列」Netty强大特性之Native transports扩展开发实战
|
3月前
|
网络协议 C++ 数据格式
websocket协议介绍与基于reactor模型的websocket服务器实现
websocket协议介绍与基于reactor模型的websocket服务器实现
58 0
|
1月前
|
NoSQL Redis
Netty实战:模拟Redis的客户端
Netty实战:模拟Redis的客户端
14 0
|
2月前
|
前端开发 网络协议 Java
WebSocket理论和实战
WebSocket详解与实战操作
|
2月前
|
前端开发 网络协议 JavaScript
|
3月前
|
传感器 监控 网络协议
WebSocket 实战:构建高效的实时应用
WebSocket 实战:构建高效的实时应用
WebSocket 实战:构建高效的实时应用
|
3月前
|
网络协议 Linux C++
Linux C/C++ websocket协议与服务器实现
Linux C/C++ websocket协议与服务器实现
69 0