Netty websocket

Network protocols

WebSocket是一种高级网络协议,旨在提高Web应用程序的性能和响应能力。 我们将通过编写示例应用程序来探索Netty对它们的支持。

在第12章中,您将学习如何使用WebSocket实现双向数据传输,方法是构建一个聊天室服务器,其中多个浏览器客户端可以实时通信。 您还将看到如何通过检测客户端是否支持它,从应用程序中的HTTP切换到WebSocket协议。

我们将在第13章中总结第3部分,研究Netty对用户数据报协议(UDP)的支持。在这里,您将构建一个广播服务器和监视器客户端,可以适应许多实际用途。

本章讲介绍:

  • real-time web 的概念
  • WebSocket 协议
  • 使用Netty 创建一个基于 WebSocket 的聊天室服务端程序

如果您关注网络技术的最新发展,您很可能会遇到实时网络短语,如果您有工程领域的实时应用程序经验,您可能会对这个术语的含义持怀疑态度。

因此,我们首先要澄清的是,这不是所谓的硬实时服务质量(QoS),其中保证了在指定时间间隔内交付计算结果。 仅仅HTTP的请求/响应设计使得这个问题非常严重,因为过去设计的方法都没有提供令人满意的解决方案。

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

The real-time web is a network web using technologies and practices that
enable users to receive information as soon as it is published by its
authors, rather than requiring that they or their software check a source
periodically for updates.
实时网络是一种使用技术和实践的网络网络,使用户能够在作者发布信息后立即接收信息,而不是要求他们或他们的软件定期检查信息源以进行更新。

简而言之,一个成熟的实时网络可能不会即将到来,但其背后的想法正在推动对几乎即时访问信息的不断增长的期望。 我们将在本章中讨论的WebSocket协议是朝着这个方向的良好支持的步骤。

12.1 Introducing WebSocket

WebSocket协议是从头开始设计的,旨在为Web上的双向数据传输问题提供实用的解决方案,允许客户端和服务器随时传输消息,从而要求它们异步处理消息接收。 (最新的浏览器支持WebSocket作为HTML5的客户端API。)

Netty对WebSocket的支持包括所有正在使用的主要实现,因此在您的下一个应用程序中采用它非常简单。 与Netty一样,您可以完全使用协议,而无需担心其内部实现细节。 我们将通过创建基于WebSocket的实时聊天应用程序来证明这一点。

12.2 Our example WebSocket application

我们的示例应用程序将通过使用WebSocket协议实现基于浏览器的聊天应用程序来演示实时功能,例如您可能在Facebook的文本消息功能中遇到过。 我们将通过允许多个用户同时相互通信来进一步发展。
图12.1说明了应用程序逻辑:

  1. 一个客户端发送一条消息。
  2. 这条消息广播到所有已经建立连接的其他客户端。

这就是您期望聊天室工作的方式:每个人都可以与其他人交谈。 在我们的示例中,我们将仅实现服务器端,客户端是通过网页访问聊天室的浏览器。 正如您将在接下来的几页中看到的那样,WebSocket使编写此服务器变得简单。

Netty websocket_第1张图片

12.3 Adding WebSocket support

称为升级握手的机制用于从标准HTTP或HTTPS协议切换到WebSocket。 因此,使用WebSocket的应用程序将始终以HTTP / S开头,然后执行升级。 当恰好发生这种情况时,应用程序是特定的; 它可能是在启动时或者在请求特定URL时。

我们的应用程序采用以下约定:如果请求的URL以/ ws结尾,我们将协议升级到WebSocket。 否则,服务器将使用基本HTTP / S. 连接升级后,所有数据都将使用WebSocket传输。 图12.2说明了服务器逻辑,它一如Netty,将由一组ChannelHandler实现。 在我们解释用于处理HTTP和WebSocket协议的技术时,我们将在下一节中对它们进行描述。

12.3.1 Handling HTTP requests

首先,我们将实现处理HTTP请求的组件。 此组件将提供访问聊天室的页面,并显示已连接客户端发送的消息。 代码清单12.1包含了这个HttpRequestHandler的代码,它为SimpleHttpRequest消息扩展了SimpleChannelInboundHandler。 请注意channelRead0() 的实现如何转发URI / ws的任何请求。

Netty websocket_第2张图片

// Extends SimpleChannelInboundHandler to handle FullHttpRequest messages
public class HttpRequestHandler
    extends 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 {
         // If a WebSocket upgrade is requested, increments the reference count(retain) and passes it to the next ChannelInboundHandler
         if(wsUri.equalsIgnoreCase(request.getUri())) {
             ctx.fireChannelRead(request.retain());
         } else {
           // Handlers 100 Continue requests in conformity with HTTP 1.1
           if(HttpHeaders.is100ContinueExpected(request)) {
               send100Continue(ctx);
           }
           // Reads index.html
           RandomAccessFile file = new RandomAccessFile(INDEX, "r");
           HttpResponse response = new DefaultHttpResponse(
               request.getProtocolVersion(), HttpResponseStatus.OK);
           response.headers().set(
               HttpHeaders.Names.CONTENT_TYPE,
               "text/plain; charset=UTF-8");
           boolean keepAlive = HttpHeaders.isKeepAlive(request);
           // If keepalive is requested, adds the required headers
           if(keepAlive) {                                        
              response.headers().set(
                  HttpHeaders.Names.CONTENT_LENGTH, file.length());
              response.headers().set(HttpHeaders.Names.CONNECTION,
                  HttpHeaders.Values.KEEP_ALIVE);
           }

           // Writes the HttpResponse to the client
           ctx.write(response);
           // Writes index.html to the client
           if (ctx.pipeline().get(SslHandler.class) == null) {
               ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
           } else {
              ctx.write(new ChunkedNioFile(file.getChannel()));
           }
           // Writes and flushes the LastHttpContent to the client
           ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
           if(!keepAlive) {
              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();
    }
}
                                       

如果HTTP请求引用URI / ws,则HttpRequestHandler在FullHttpRequest上调用 retain() 并通过调用 fireChannelRead(msg) 将其转发到下一个 ChannelInboundHandler . 需要调用retain(),因为在 channelRead() 完成后,它将调用 FullHttpRequest 上的 release() 来释放其资源。(请参阅第6章中对SimpleChannelInboundHandler的讨论。)

如果客户端发送HTTP 1.1标头Expect:100-continue,则HttpRequestHandler发送100 Continue响应。 在设置标头后,HttpRequestHandler将HttpResponse d写回客户端。 这不是FullHttpResponse,因为它只是响应的第一部分。 此外,此处不调 writeAndFlush()。
这是在最后完成的。

如果既不需要加密也不需要压缩,则可以通过将index.html e的内容存储在DefaultFileRegion中来实现最高效率。 这将利用零拷贝来执行传输。 因此,您需要检查ChannelPipeline中是否存在SslHandler。 或者,您使用ChunkedNioFile。

HttpRequestHandler写一个LastHttpContent来标记响应的结束。 如果未请求keepalive,则HttpRequestHandler将ChannelFutureListener添加到上次写入的 ChannelFuture 并关闭连接。 这是您调用 writeAndFlush() 来刷新所有以前写入的消息的地方。

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

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

12.3.2 Handling WebSocket frames

由IETF发布的WebSocket RFC定义了六个帧; Netty为每个人提供POJO实施。 表12.1列出了帧类型并描述了它们的用法。

Frame type 描述
BinaryWebSocketFrame 包含 binary data
TextWebSocketFrame 包含 text data
ContinuationWebSocketFrame 包含 text 或者 binary 数据,他属于前一个 BinaryWebSocketFrame 或者 TextWebSocketFrame
CloseWebSocketFrame 表示CLOSE请求,包含关闭状态代码和短语
PingWebSocketFrame 请求传输PongWebSocketFrame
PongWebSocketFrame 作为响应发送给一个 PingWebSocketFrame

Netty websocket_第3张图片

我们的聊天程序将会使用下面这些 frame 类型:

  • CloseWebSocketFrame
  • PingWebSocketFrame
  • PongWebSocketFrame
  • TextWebSocketFrame

TextWebSocketFrame是我们实际需要处理的唯一一个。 根据WebSocket RFC,Netty提供了一个WebSocketServerProtocolHandler来管理其他的。

下面的清单显示了TextWebSocketFrames的ChannelInboundHandler,它还将跟踪其ChannelGroup中的所有活动WebSocket连接。

// Extends SimpleChannelInboundHandler and handle TextWebSocketFramemessages
pyblic class TextWebSocketFrameHandler
    extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    private final ChannelGroup group;
  
    public TextWebSocketFrameHandler(ChannelGroup group) {
        this.group = group;
    }

    // Overrides userEventTriggered() to handle custome events
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx,
        Object evt) throws Exception {
        if(evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE)     
        {
        ctx.pipeline().remove(HttpRequestHandler.class);
        // Notifies all connected WebSocket clients that new Client has connected
        group.writeAndFlush(new TextWebSocketFrame(
            "Client" + ctx.channel() + " joined"));
        group.add(ctx.channel());
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx,
        TextWebSocketFrame msg) throws Exception {
        // Increments the reference count of the message and writes it to all connected clients in the ChannelGroup
        group.writeAndFlush(msg.retain());
    }
}          
            

TextWebSocketFrameHandler只有很少的职责。 当与新客户端的WebSocket握手成功完成时,它通过写入ChannelGroup中的所有Channel来通知所有连接的客户端,然后将新Channel添加到ChannelGroup.

如果收到TextWebSocketFrame,它会调用retain()并使用writeAndFlush()将其传输到ChannelGroup,以便所有连接的WebSocket Channel都接收它。

和以前一样,调用 retain() 是必需的,因为当 channelRead0() 返回时TextWebSocketFrame 的引用计数将减少。 由于所有操作都是异步的,因此writeAndFlush() 可能会在以后完成,并且它不能访问已变为无效的引用。

由于Netty在内部处理大部分剩余功能,因此现在唯一要做的就是为每个创建的新Channel初始化ChannelPipeline。 为此,我们需要一个ChannelInitializer。

12.3.3 Initializing the ChannelPipeline

如您所知,要在 ChannelPipeline 中安装 ChannelHandler,您需要扩展 ChannelInitializer并实现 initChannel() 。 以下清单显示了生成的ChatServerInitializer的代码。

// Extends ChannelInitializer
public class ChatServerIntializer extends ChannelIntializer<Channel> {
    private final ChannelGroup group;

    public ChatServerIntializer(ChannelGroup group) {
        this.group = group;
    }

    // Adds all needed ChannelHandlers to the ChannelPipeline
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new ChunkedWriteHandler());
        pipeline.addLast(new HttpObjectAggregator(64 * 1024));
        pipeline.addLast(new HttpRequestHandler("/ws"));
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
        pipeline.addLast(new TextWebSocketFrameHandler(group));
    }
}            

对 initChannel() 的调用通过安装所有必需的 ChannelHandler 来设置新注册 Channel 的ChannelPipeline。 表12.2总结了这些内容及其各自的职责。

ChannelHandler Responsibility
HttpServerCodec 将字节解码为HttpRequest,HttpContent,和LastHttpContent。 编码HttpRequest,HttpContent和LastHttpContent到字节。
ChunkedWriteHandler 写入文件的内容。
HttpObjectAggregator 将HttpMessage及其后续HttpContent聚合到单个FullHttpRequest或FullHttpResponse中(取决于它是否用于处理请求或响应)。 安装此选项后,管道中的下一个ChannelHandler将仅接收完整的HTTP请求。
HttpRequestHandler 处理FullHttpRequest(未发送到 /ws URI)。
WebSocketServerProtocolHandler 根据WebSocket规范的要求,处理WebSocket升级握手,PingWebSocketFrames,PongWebSocketFrames和CloseWebSocketFrames。
TextWebSocketFrameHandler 处理TextWebSocketFrames和握手完成事件

Netty websocket_第4张图片

Netty websocket_第5张图片
Netty 的 WebSocketServerProtocolHandler 处理所有强制 WebSocket 帧类型和升级握手本身。 如果握手成功,则将所需的ChannelHandler添加到管道中,并删除不再需要的那些。

Netty websocket_第6张图片

升级前的管道状态如图12.3所示。 这表示ChatServerInitializer初始化后的ChannelPipeline。

升级完成后,WebSocketServerProtocolHandler将替换带有WebSocketFrameDecoder的HttpRequestDecoder和带有WebSocketFrameEncoder的HttpResponseEncoder。 为了最大限度地提高性能,它将删除WebSocket连接不需要的任何ChannelHandler。 这些将包括图12.3中所示的HttpObjectAggregator和HttpRequestHandler。

图12.4显示了这些操作完成后的ChannelPipeline。 请注意,Netty目前支持四种版本的WebSocket协议,每种版本都有自己的实现类。 根据客户端(此处为浏览器)支持的内容,自动执行正确版本的WebSocketFrameDecoder和WebSocketFrameEncoder的选择。

Netty websocket_第7张图片

12.3.4 Bootstrapping

图片的最后一部分是引导服务器并安装ChatServerInitializer的代码。 这将由ChatServer类处理,如此处所示。

public class ChatServer {
    // Creates DefaultChannelGroup that will hold all connected WebSocket channels
    private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
    private final EventLoopGroup group = new NioEventLoopGroup();
    private Channel channel;

    public ChannelFuture start(InetSocketAddress address) {
        // Bootstraps the server
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(group)
            .channel(NioServerSocketChannel.class)
            .childHandler(createInitializer(channelGroup));
        ChannelFuture future = bootstrap.bind(address);
        future.syncUninterruptibly();
        channel = future.channel();
        return future;
   }


   // Creates the ChatServerInitializer
   protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
       return new ChatServerInitializer(group);
   }

   // Handles server shutdown and releases all resources
   public void destroy() {
       if(channel != null) {
           channel.close();
       }
       channelGroup.close();
       group.shutdownGracefully();
   }
  
   public static void main(String[] args) throws Exception {
       if (args.length != 1) {
           System.err.println("Please give port as argument.");
           System.exit(1);
       }
       int port = Integer.praseInt(args[0]);
       final ChatServer endpoint = new ChatServer();
       ChannelFuture future = endpoint.start( new InetSocketAddress(port));
       Runtime.getRuntime().addShutdownHook(new Thread() {
           @Override
           public void run() {
               endpoint.destroy();
           }
       });
       future.channel().closeFuture().syncUninterruptibly();
   }
 }                                   

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

12.4 Testing the application

chapter12目录中的示例代码包含构建和运行服务器所需的一切。 (如果您尚未设置包括Apache Maven在内的开发环境,请参阅第2章中的说明。)

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

mvn -PChatServer clean package exec:exec

项目文件pom.xml配置为在端口9999上启动服务器。要使用其他端口,您可以编辑文件中的值或使用System属性覆盖它:

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

以下清单显示了命令的主要输出(已删除非必要行)。

Netty websocket_第8张图片

Netty websocket_第9张图片

您可以通过将浏览器指向http:// localhost:9999来访问该应用程序。 图12.5显示了Chrome浏览器中的UI。

该图显示了两个连接的客户端。 第一个是使用顶部的界面连接。 第二个客户端通过底部的Chrome浏览器命令行连接。 您会注意到两个客户端都发送了消息,并且每条消息都显示在两个客户端上。

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

12.4.1 What about encryption?

在现实生活中,您很快就会被要求为此服务器添加加密。 使用Netty,只需将SslHandler添加到ChannelPipeline并进行配置即可。 以下清单显示了如何通过扩展ChatServerInitializer来创建SecureChatServerInitializer来完成此操作。

// Adding encryption to the ChannelPipeline
// Extends ChatServerInitializer to add encryption
public class SecureChatServerInitializer extends ChatServerInitializer {
    private final SslContext context;

    public SecureChatServerInitializer(ChannelGroup group,
        SslContext context) {
        super(group);
        this.context = context;
   }

   @Override
   protected void initChannel(Channel ch) throws Exception {
       // Calls the parent's initChannel()
       super.initChannel(ch);
       SSLEngine engine = context.newEngine(ch.alloc());
       // Adds the SslHandler to the ChannelPipeline
       ch.pipeline().addFirst(new SslHandler(engine));   
  }
}     

最后一步是调整ChatServer以使用SecureChatServerInitializer,以便在管道中安装SslHandler。 这给了我们这里显示的SecureChatServer。

// Adding encryption to the ChatServer
// SecureChatServer extends ChatServer to support encryption
public class SecureChatServer extends ChatServer {
    private final SslContext context;

    public SecureChatServer(SslContext context) {
        this.context = context;
    }

    @Override
    protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
        // Returns the previously created SecureChatServerInitializer to enable encryption
        return new SecureChatServerInitializer(group, context);
   }

   public static void main(String[] args) throws Exception {
       if(args.length != 1) {
           System.err.println("Please give port as argument");
           System.exit(1);
       }
       int port = Integer.parseInt(args[0]);
       SelfSignedCertificate cert = new SelfSignedCertificate();
       SslContext context = SslContext.newServerContext(
       cert.certificate(), cert.privateKey());

       final SecureChatServer endpoint = new SecureChatServer(context);
       ChannelFuture future = endpoint.start(new InetSocketAddress(port));
       Runtime.getRuntime().addShutdownHook(new Thread() {
           @Override
           public void run() {
               endpoint.destroy();
           }
       });
       future.channel().closeFuture().syncUninterruptibly();
    }
 }                                  

这就是为所有通信启用SSL / TLS加密所需的全部内容。 和以前一样,您可以使用Apache Maven来运行应用程序。 它还将检索任何所需的依赖项。

Netty websocket_第10张图片
现在,您可以从其HTTPS URL访问SecureChatServer:https://localhost:9999。

12.5 Summary

在本章中,您学习了如何使用Netty的WebSocket实现来管理Web应用程序中的实时数据。 我们介绍了支持的数据类型,并讨论了您可能遇到的限制。 虽然在所有情况下都可能无法使用WebSocket,但应该清楚它代表了一个重要的进步网络技术。

你可能感兴趣的:(netty)