WebSocket 简介

WebSocket 协议是完全重新设计的协议,旨在为 Web 上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息

Netty 对于 WebSocket 的支持包含了所有正在使用钟的主要实现,我们将通过创建一个基于 WebSocket 的实时聊天应用程序来演示这一点

WebSocket 应用程序示例

我们将通过使用 WebSocket 协议来实现一个基于浏览器的聊天应用程序,使得多个用户之间可以同时进行相互通信

下图说明了该应用程序的逻辑:

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

所有人都在可以和其他人聊天,在示例中,我们只实现服务器,客户端则是通过 Web 页面访问该聊天室的浏览器

1. 添加 WebSocket 支持

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

我们的应用程序将采用如下约定:如果被请求的 URL 以 /ws 结尾,那么把该协议升级为 WebSocket,否则服务器将使用基本的 HTTP/S

下图解释了 Netty 如何处理 HTTP 以及 WebSocket 协议技术,它由一组 ChannelHandler 实现

2. 处理 HTTP 请求

首先,我们实现处理 HTTP 请求的组件,这个组件将提供用于访问聊天室并显示由连接的客户端发送的消息的网页

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
protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if (wsUri.equalsIgnoreCase(request.uri())) {
// 如果请求了 WebSocket 协议升级,则增加引用计数,并将它传递给下一个 ChannelInboundHandler
ctx.fireChannelRead(request.retain());
} else {
// 读取 index.html
RandomAccessFile file = new RandomAccessFile(INDEX, "r");
DefaultHttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
response.headers().set("CONTENT_TYPE", "text/html; charset=UTF-8");
// 将 HttpResponse 写到客户端
ctx.write(response);
// 将 index.html 写到客户端
if (ctx.pipeline().get(SslHandler.class) == null) {
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);
// 写操作完成后关闭 Channel
future.addListener(ChannelFutureListener.CLOSE);
}
}
}

3. 处理 WebSocket 帧

由 IETF 发布的 WebSocket RFC 定义了六种帧,Netty 为它们每种都提供了一个 POJO 实现。下表列出了这些帧类型,并描述了它们的用法

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

下述代码展示了用于处理 TextWebSocketFrame 的 ChannelInboundHandler,其还将在它的 ChannelGroup 中跟踪所有活动的 WebSocket 连接

public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    private final ChannelGroup group;

    public TextWebSocketFrameHandler(ChannelGroup group) {
this.group = group;
} @Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
// 如果该事件握手成功,则移除 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
protected void messageReceived(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 增加消息的引用计数,并将它写到 ChannelGroup 中所有已经连接的客户端
group.writeAndFlush(msg.retain());
}
}

4. 初始化 ChannelPipeline

为了将 ChannelHandler 安装到 ChannelPipeline 中,我们需要扩展 ChannelInitializer 并实现 initChannel() 方法

public class ChatServerInitializer extends ChannelInitializer<Channel> {

    private final ChannelGroup group;

    public ChatServerInitializer(ChannelGroup group) {
this.group = group;
} @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。Netty 的 WebSocketServerProtocolHandler 处理了所有委托管理的 WebSocket 帧类型以及升级握手本身。如果握手本身,那么所需的 ChannelHandler 将被添加到 ChannelPipeline 中,而那些不再需要的 ChannelHandler 则会被移除

5. 引导

最后一步是引导该服务器,并安装 ChatServerInitializer 的代码,这将由 ChatServer 类处理

public class ChatServer {

    private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
private final EventLoopGroup group = new NioEventLoopGroup();
private Channel channel; public ChannelFuture start(InetSocketAddress address) {
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;
} protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
return new ChatServerInitializer(group);
} public void destroy() {
if (channel != null) {
channel.close();
}
channelGroup.close();
group.shutdownGracefully();
} public static void main(String[] args) {
if (args.length != 1) {
System.err.println("Please give port as argument");
System.exit(1);
}
int port = Integer.parseInt(args[0]);
final ChatServer endpoint = new ChatServer();
ChannelFuture future = endpoint.start(new InetSocketAddress(port));
Runtime.getRuntime().addShutdownHook(new Thread(endpoint::destroy));
future.channel().closeFuture().syncUninterruptibly();
}
}

Netty 框架学习 —— 添加 WebSocket 支持的更多相关文章

  1. .Net Core 分布式微服务框架 - Jimu 添加 Swagger 支持

    系列文章 .Net Core 分布式微服务框架介绍 - Jimu .Net Core 分布式微服务框架 - Jimu 添加 Swagger 支持 一.前言 最近有空就优化 Jimu (一个基于.Net ...

  2. Netty 框架学习 —— 编解码器框架

    编解码器 每个网络应用程序都必须定义如何解析在两个节点之间来回传输的原始字节,以及如何将其和目标应用程序的数据格式做相互转换.这种转换逻辑由编解码器处理,编解码器由编码器和解码器组成,它们每种都可以将 ...

  3. Netty 框架学习 —— 引导

    概述 前面我们学习了 ChannelPipeline.ChannelHandler 和 EventLoop 之后,接下来的问题是:如何将它们组织起来,成为一个可实际运行的应用程序呢?答案是使用引导(B ...

  4. Netty 框架学习 —— 基于 Netty 的 HTTP/HTTPS 应用程序

    通过 SSL/TLS 保护应用程序 SSL 和 TLS 安全协议层叠在其他协议之上,用以实现数据安全.为了支持 SSL/TLS,Java 提供了 javax.net.ssl 包,它的 SSLConte ...

  5. Netty 框架学习 —— 预置的 ChannelHandler 和编解码器

    Netty 为许多提供了许多预置的编解码器和处理器,几乎可以开箱即用,减少了在烦琐事务上话费的时间和精力 空闲的连接和超时 检测空闲连接以及超时对于释放资源来说至关重要,Netty 特地为它提供了几个 ...

  6. 为libevent添加websocket支持(上)

    在跨平台网络基础库中,libevent与asio近年来使用比较广泛.asio对boost的依赖太大,个人认为发展前途堪忧,尤其asio对http没有很好的支持也是缺点之一. libevent对http ...

  7. Netty 框架学习 —— 第一个 Netty 应用

    概述 在本文,我们将编写一个基于 Netty 实现的客户端和服务端应用程序,相信通过学习该示例,一定能更全面的理解 Netty API 该图展示的是多个客户端同时连接到一台服务器.客户端建立一个连接后 ...

  8. Netty 框架学习 —— Netty 组件与设计

    Channel.EventLoop 和 ChannelFuture 这一节将对 Channel.EventLoop 和 ChannelFuture 类进行讨论,它们组合在一起,可以被认为是 Netty ...

  9. Netty 框架学习 —— ByteBuf

    概述 网络数据的基本单位总是字节,Java NIO 提供了 ByteBuffer 作为它的字节容器,但这个类的使用过于复杂.Netty 的 ByteBuf 具有卓越的功能性和灵活性,可以作为 Byte ...

随机推荐

  1. MergingSort

    递归排序的两种实现 <script type="text/javascript"> //归并排序(递归实现) //思想:堆排序利用了完全二叉树的性质,但是比较麻烦 // ...

  2. 变体 variety 计算机学科中的改变类型;输入法的 类型

    变体_百度百科 中文为改变原来的体式.或者计算机学科中的改变类型. 变体 variety 输入法的 类型

  3. 编译安装rsyslog

    安装gcc-c++ 615 yum -y install gcc c++ 616 yum -y install gcc-c++ 安装libestr.libee wget http://libestr. ...

  4. 043.Python线程基本介绍

    一 线程的基本概念 1.1 进程和线程 进程是资源分配的最小单位 线程是计算机中调度的最小单位 进程池: 开启过多的进程并不一走提高你的效率, 如果cp负载任务过多,平均单个任务执行的效率就会低,反而 ...

  5. node.js module初步理解-(转载)

    在开发一个复杂的应用程序的时候,我们需要把各个功能拆分.封装到不同的文件,在需要的时候引用该文件.没人会写一个几万行代码的文件,这样在可读性.复用性和维护性上都很差,几乎所有的编程语言都有自己的模块组 ...

  6. 要想在for语句中直接定义一个变量

    要想在for语句中直接  定义一个变量  (如下的代码) 1 for(uint16_t i=0;i<10;i++); 2 if( GPIO_ReadInputDataBit(GPIOA, GPI ...

  7. 血缘关系分析工具SQLFLOW--实践指南

    SQLFlow 是用于追溯数据血缘关系的工具,它自诞生以来以帮助成千上万的工程师即用户解决了困扰许久的数据血缘梳理工作. 数据库中视图(View)的数据来自表(Table)或其他视图,视图中字段(Co ...

  8. DDD兴起的原因以及与微服务的关系

    DDD为什么能火起来? 我们先不讨论DDD的定义, 先梳理一下DDD火起来的背景, 根据我学习的套路, 永远是为什么为先,再是解决什么问题,是什么东西, 最后如何使用.我们都知道这些年随着设备以及技术 ...

  9. Python+Selenium学习笔记12 - 窗口大小和滚动条

    涉及到的三个方法 set_window_size()  用于设置浏览器窗口的大小 e.gset_window_size(600,600) window.scrollTo() 用于设置浏览器窗口滚动条的 ...

  10. 开源项目核心商城(CoreShop)

    帮小伙伴推一下他的开源项目作者是@大灰灰 核心商城(CoreShop)Beta 支持可视化布局的.Net小程序商城 [![star](https://gitee.com/CoreUnion/CoreS ...