如果你有跟进Web技术的最新进展,你很可能就遇到过“实时Web”这个短语,这里并不是指所谓的硬实时服务质量(QoS),硬实时服务质量是保证计算结果将在指定的时间间隔内被递交。仅HTTP的请求/响应模式设计就使得其很难被支持。

实时Web利用技术和实践,使用户在信息的作者发布信息之后就能够立即收到信息,而不需要他们或者他们的软件周期性地检查信息源以及获取更新。

1、WebSocket简介

WebSocket协议是完全重新设计的协议,旨在为Web上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输信息,因此,这也就要求他们异步地处理消息回执(作为HTML5客户端API的一部分,大部分最新的浏览器都已经支持了WebSocket)

Netty对于WebSocket的支持包含了所有正在使用中的主要实现,因此在你的下一个应用程序中采用它将是简单直接的。和往常使用Netty一样,你可以完全使用该协议,而无需关心它内部的实现细节,我们将通过创建一个基于WbeSocket的实时聊天应用程序来演示。

2、WebSocket示例应用程序

为了让示例应用程序展示它的实时功能,我们将通过使用WebSocket协议来实现一个基于浏览器的聊天应用程序,就像你可能在FaceBook的文本消息功能中见到过的那样。我们将通过使用多个用户之间可以同时进行相互通信,从而更进一步。

下图说明应用逻辑:

——客户端发送一个消息

——该消息将被广播到所有其他链接的客户端 这正如你可能会预期的一个聊天室应当的工作方式:所有的人都可以和其他的人聊天。在示例中,我们将只实现服务器端,而客户端则是通过Web页面访问该聊天室的浏览器。正如同你将在接下来的几页中所看到的,WebSocket简化了编写这样的服务器的过程。

3、添加WebSocket支持

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

我们的应用程序将采用下面的约定:如果被请求的URL以/ws结尾,那么我们将会把该协议升级为WebSocket;否则,服务器将使用基本的HTTP/S。在连接已经升级完成之后,所有数据都将会使用WebSocket进行传输。下图说明了该服务器逻辑,一如在Netty中一样,它由一组ChannelHandler实现。 

4、处理HTTP请求

首先,我们将实现该处理HTTP请求的组件。这个组件将提供用于访问聊天室并显示由连接的客户端发送的消息的网页。如下代码给出了这个HttpRequestHandler对应的代码,其扩展了SimpleChannelInboundHandler以处理FullHttpRequest消息。需要注意是,channelRead0()方法的实现是如何转发任何目标URI为/ws的请求的。

//扩展SimpleChannelInboundHandler以处理FullHttpReuqest消息
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 channelRead0(ChannelHandlerContext ctx,
FullHttpRequest request) throws Exception {
//如果请求了WebSocket协议升级,则增加引用技术,并将它传递给下一个ChannelInboundHandler
if (wsUri.equalsIgnoreCase(request.getUri())){
ctx.fireChannelRead(request.retain());
} else {
//处理100Continue请求以符合HTTP1.1规范
if (HttpHeaders.is100ContinueExpected(request)){
send100Continue(ctx);
}
//读取“index.html”
RandomAccessFile file = new RandomAccessFile(INDEX,"r");
HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(),HttpResponseStatus.OK);
response.headers().set(HttpHeaders.Names.CONTENT_TYPE,"text/html;charset=UTF-8");
boolean keepAlive = HttpHeaders.isKeepAlive(request);
//如果请求了keep-alive,则添加所需要的HTTP头信息
if (keepAlive){
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH,file.length());
response.headers().set(HttpHeaders.Names.CONNECTION,HttpHeaders.Values.KEEP_ALIVE);
}
//将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);
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();
}
}

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

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

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

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

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

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

5、处理WebSocket帧

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

BinaryWebSocketFrame——包含了二进制数据

TextWebSocketFrame——包含了文本数据

ContinuationWebSocketFrame——包含属于上一个BinaryWebSocketFrame或TextWebSocketFrame的文本数据或者二进制数据

CloseWebSocketFrame——表示一个CLOSE请求,包含一个关闭的状态码和关闭的原因

PingWebSocketFrame——请求传输一个PongWebSocketFrame

PongWebSocketFrame——作为一个对于PingWebSocketFrame的响应被发送

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

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

public class TextWebSocketFrameHandler extends 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){
//如果该事件表示握手成功,则从该ChannelPipeline中移除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 channelRead0(ChannelHandlerContext channelHandlerContext,
TextWebSocketFrame msg) throws Exception {
//增加消息的引用计数,并将它写到ChannelGroup中所有已经连接的客户端
group.writeAndFlush(msg.retain());
}
}

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

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

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

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

6、初始化ChannelPipeline

以下代码展示了生成的ChatServerInitializer。

public class ChatServerInitializer extends ChannelInitializer<Channel>{
private final ChannelGroup group; public ChatServerInitializer(ChannelGroup group) {
this.group = group;
} @Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.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则将会被移除。

WebSocket协议升级之前的ChannelPipeline的状态如下图,这代表了刚刚被ChatServerInitializer初始化之后的ChannelPipeline。 当WebSocket协议升级完成之后,WebSocketServerProtocolHandler将会把HttpRequestDecoder替换为WebSocketFrameDecoder,把HttpResponseEncoder替换为WebSocketFrameEncoder。为了性能最大化,它将移除任何不再被WebSocket连接所需要的ChannelHandler。这也包括上图所示的HttpObjectAggregator和HttpRequestHandler。

下图展示了这些操作完成之后的ChannelPipeline。需要注意的是,Netty目前支持4个版本的WebSocket协议,他们每个都具有自己的实现类。Netty将会根据客户端(这里指浏览器)所支持的版本,自动地选择正确版本的WebSocketFrameDecoder和WebSocketFrameEncoder。 

7、引导

这幅拼图最后的一部分是引导该服务器,并安装ChatSererInitializer的代码。这将有ChatServer类处理,如下代码所示。

public class ChatServer {
//创建DefaultChannelGroup,其将保存所有已经连接的WebSocket Channel
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;
} //创建ChatServerInitializer
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) throws Exception{
if (args.length != 1){
System.out.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(){
@Override
public void run() {
endpoint.destroy();
}
});
future.channel().closeFuture().syncUninterruptibly();
}
}

8、如何进行加密

在真实世界的场景中,你将很快就会被要求向该服务器添加加密。使用Netty,这不过是将一个SslHandler添加到ChannelPipeline中,并配置它的问题。以下代码展示了如何通过扩展我们的ChatServerInitializer来创建一个SecureChatServerInitializer以完成需求。

//扩展ChatServerInitializer以添加加密
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 channel) throws Exception {
//调用父类的initChannel()方法
super.initChannel(channel);
SSLEngine engine = context.newEngine(channel.alloc());
engine.setUseClientMode(false);
//将SslHandler添加到ChannelPipeline中
channel.pipeline().addFirst(new SslHandler(engine));
}
}

最后一步是调整ChatServer以使用SecureChatServerInitializer,以便在ChannelPipeline中安装SslHandler。

public class SecureChatServer extends ChatServer{
private final SslContext context; public SecureChatServer(SslContext context) {
this.context = context;
} @Override
protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
//返回之前创建的SecureChatServerInitializer以启用加密
return new SecureChatServerInitializer(group,context);
} public static void main(String[] args) throws Exception{
if (args.length != 1){
System.out.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加密需要做的全部。

 
 

Netty实战十二之WebSocket的更多相关文章

  1. Netty实战十四之案例研究(一)

    1.Droplr——构建移动服务 Bruno de Carvalho,首席架构师 在Droplr,我们在我的基础设施的核心部分.从我们的API服务器到辅助服务的各个部分都使用了Netty. 这是一个关 ...

  2. Netty实战十之编解码器框架

    编码和解码,或者数据从一种特定协议的格式到另一种格式的转换.这些任务将由通常称为编解码器的组件来处理.Netty提供了多种组件,简化了为了支持广泛的协议而创建自定义的编解码器的过程.例如,如果你正在构 ...

  3. Java并发编程原理与实战十二:深入理解volatile原理与使用

    volatile:称之为轻量级锁,被volatile修饰的变量,在线程之间是可见的. 可见:一个线程修改了这个变量的值,在另一个线程中能够读取到这个修改后的值. synchronized除了线程之间互 ...

  4. SpringBoot实战(十二)之集成kisso

    关于kisso介绍,大家可以参考官方文档或者是我的博客:https://www.cnblogs.com/youcong/p/9794735.html 一.导入maven依赖 <project x ...

  5. Node.js实战(十二)之Stream

    Stream 是一个抽象接口,Node 中有很多对象实现了这个接口.例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出). Node.js,Str ...

  6. kubernetes实战(十二):k8s使用helm持久化部署redmine集成openLDAP

    1.基本概念 此次安装的有Jenkins.Gitlab.Redmine,我公司目前使用的是独立于k8s集群之外单独部署的Jenkins等服务,此文章会介绍三种服务基于k8s的部署方式,之后集成之前部署 ...

  7. Hyperledger Fabric 实战(十二): Fabric 源码本地调试

    借助开发网络调试 fabric 源码本地调试 准备工作 IDE Goland Go 1.9.7 fabric-samples 模块 chaincode-docker-devmode fabric 源码 ...

  8. Spring Cloud Alibaba 实战(十二) - Nacos配置管理

    本章主要内容是:使用Nacos管理配置以及实现配置管理的原因,配置如何管理以及动态刷新和最佳实现总结,最后是Nacos配置刷新原理解读 该技术类似于Spring Cloud Config 1 配置管理 ...

  9. Python核心技术与实战——十二|Python的比较与拷贝

    我们在前面已经接触到了很多Python对象比较的例子,例如这样的 a = b = a == b 或者是将一个对象进行拷贝 l1 = [,,,,] l2 = l1 l3 = list(l1) 那么现在试 ...

随机推荐

  1. PMP:11.项目采购管理

    项目采购管理包括从项目团队外部采购或获取所需产品.服务或成果的各个过程.  项目采购管理包括编制和管理协议所需的管理和控制过程,例如,合同.订购单.协议备忘录 (MOA),或服务水平协议 (SLA). ...

  2. (转载)Javascript 中的非空判断 undefined,null, NaN的区别

    原文地址:https://blog.csdn.net/oscar999/article/details/9353713 在介绍这三个之间的差别之前, 先来看一下JS  的数据类型. 在 Java ,C ...

  3. Go语言数组

    目录 声明数组 数组在内存的结构 初始化数组 使用数组 函数间传递数组 数组使用注意事项 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类 ...

  4. jupyter notebook的安装与基本操作

    0.前言 最近正在重温Python基础知识,为了方便练习敲代码,于是选择安装jupyter notebook作为代码编辑器. Project Jupyter exists to develop ope ...

  5. 【Spark调优】Broadcast广播变量

    [业务场景] 在Spark的统计开发过程中,肯定会遇到类似小维表join大业务表的场景,或者需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时应该使用Spark的广 ...

  6. Scala - 快速学习05 - 数据结构

    1- 数组(Array) 数组一般包括定长数组和变长数组. 可以不指明数组类型,Scala会自动根据提供的初始化数据来推断出数组的类型. 在Scala中,对数组元素的应用,是使用圆括号,而不是方括号. ...

  7. Scala - 快速学习04 - 求值策略

    表达式求值策略(Evaluation Strategy) Scala中所有的运算都是基于表达式的. Call By Value - 对函数实参求值,且仅求值一次:函数调用之前对所有表达式进行求值 Ca ...

  8. ImportError: No module named '_tkinter', please install the python3-tk package

    ImportError: No module named '_tkinter', please install the python3-tk package 先更新包,命令:sudo apt-get ...

  9. JAVA基础-输入输出流

    一,File类:文件的创建和删除 1.File(String pathname):pathname是指路径名称.用法 File file = new File("d:/1.txt " ...

  10. 【ABP框架系列学习】介绍篇(1)

      0.引言 该系列博文主要在[官方文档]及[tkbSimplest]ABP框架理论研究系列博文的基础上进行总结的,或许大家会质问,别人都已经翻译过了,这不是多此一举吗?原因如下: 1.[tkbSim ...