netty中的websocket
使用WebSocket 协议来实现一个基于浏览器的聊天室应用程序,图12-1 说明了该应用程序的逻辑:
(1)客户端发送一个消息;
(2)该消息将被广播到所有其他连接的客户端。

WebSocket
在从标准的HTTP或者HTTPS协议切换到WebSocket时,将会使用一种称为升级握手①的机制。因此,使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确切时刻特定于应用程序;它可能会发生在启动时,也可能会发生在请求了某个特定的URL之后。
有关WebSocket更多的信息见《websocket之一:websocket简介》
回到示例,我们的应用程序将采用下面的约定:如果被请求的URL 以/ws 结尾,那么我们将会把该协议升级为WebSocket;否则,服务器将使用基本的HTTP/S。在连接已经升级完成之后,所有数据都将会使用WebSocket 进行传输。图12-2 说明了该服务器逻辑,一如在Netty 中一样,它由一组ChannelHandler 实现。我们将会在下一节中,解释用于处理HTTP 以及WebSocket 协议的技术时,描述它们。

1、处理HTTP 请求
首先,我们将实现该处理HTTP 请求的组件。这个组件将提供用于访问聊天室并显示由连接的客户端发送的消息的网页。代码清单12-1 给出了这个HttpRequestHandler 对应的代码,其扩展了SimpleChannelInboundHandler 以处理FullHttpRequest 消息。messageReceived()方法的实现是如何转发任何目标URI 为/ws 的请求的。
主要任务有:
1、如果是ws协议,则不处理直接转发给下一个handler;
2、如果是http协议,
- 先校验是否符合http1.1规范;
- 输出html页面,用于聊天室的客户端;(见《Netty实现简单HTTP服务器》)
- 输出完index.html页面后,根据keep-alive决定是否要关闭连接;
package com.dxz.nettydemo.websocket; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import java.io.File;
import java.io.RandomAccessFile;
import java.net.URISyntaxException;
import java.net.URL; import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultFileRegion;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile; //扩展SimpleChannelInboundHandler 以处理FullHttpRequest 消息
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 {
//如果请求了WebSocket协议升级,则增加引用计数(调用retain()方法),并将它传递给下一个ChannelInboundHandler
//或者说是不处理WebSocket相关的请求,将其转发给下一个handler
if (wsUri.equalsIgnoreCase(request.getUri())) {
//之所以需要调用retain()方法,是因为调用channelRead()
//方法完成之后,它将调用FullHttpRequest 对象上的release()方法以释放它的资源
ctx.fireChannelRead(request.retain());
} else {
//处理100 Continue请求以符合HTTP1.1 规范
if (HttpHeaders.is100ContinueExpected(request)) {
send100Continue(ctx);
}
RandomAccessFile file = new RandomAccessFile(INDEX, "r");
/*FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
//设置消息头类型
response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
//构造响应消息体
StringBuilder buf = new StringBuilder();*/
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);
//如果没有请求keep-alive,在写操作完成后关闭Channel
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
if(file != null) {
try {
file.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
} 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();
}
}
2、处理WebSocket帧
帧类型见《websocket之一:websocket简介》中的帧类型介绍。
由IETF 发布的WebSocket RFC,定义了6 种帧,Netty 为它们每种都提供了一个POJO 实现。
WebSocketFrame 中定义的对应6种帧的类型
| 帧 类 型 | 描 述 |
| BinaryWebSocketFrame | 包含了二进制数据 |
| TextWebSocketFrame | 包含了文本数据 |
| ContinuationWebSocketFrame | 包含属于上一个BinaryWebSocketFrame或TextWebSocketFrame 的文本数据或者二进制数据 |
| CloseWebSocketFrame | 表示一个CLOSE 请求,包含一个关闭的状态码和关闭的原因 |
| PingWebSocketFrame | 请求传输一个PongWebSocketFrame |
| PongWebSocketFrame | 作为一个对于PingWebSocketFrame 的响应被发送 |
我们的聊天应用程序将使用下面几种帧类型:
CloseWebSocketFrame;
PingWebSocketFrame;
PongWebSocketFrame;
TextWebSocketFrame。
TextWebSocketFrame 是我们唯一真正需要处理的帧类型。为了符合WebSocket RFC,Netty 提供了WebSocketServerProtocolHandler 来处理其他类型的帧。
package com.dxz.nettydemo.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; 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 {
//如果是HANDSHAKE_COMPLETE事件表示握手成功,则从该Channelipeline中移除HttpRequestHandler,
//因为将不会接收到任何HTTP 消息了
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
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 arg0, TextWebSocketFrame msg) throws Exception {
//增加消息的引用计数,并将写到ChannelGroup 中所有已经连接的客户端
System.out.println("服务端收到:"+msg.text());
group.writeAndFlush(new TextWebSocketFrame("server ack:" + msg.text())); }
}
3、初始化ChannelPipeline
为了将ChannelHandler 安装到ChannelPipeline 中,你扩展了ChannelInitializer,并实现了initChannel()方法。
package com.dxz.nettydemo.websocket; import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler; 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 {
//将所有需要的ChannelHandler 添加到ChannelPipeline 中
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));
}
}
基于WebSocket 聊天服务器的ChannelHandler
| ChannelHandler | 职 责 |
| HttpServerCodec | 将字节解码为HttpRequest、HttpContent 和LastHttpContent。并将HttpRequest、HttpContent 和LastHttpContent 编码为字节 |
| ChunkedWriteHandler | 写入一个文件的内容 |
| HttpObjectAggregator | 将一个HttpMessage 和跟随它的多个HttpContent 聚合为单个FullHttpRequest 或者FullHttpResponse(取决于它是被用来处理请求还是响应)。安装了这个之后, ChannelPipeline 中的下一个ChannelHandler 将只会收到完整的HTTP 请求或响应 |
| HttpRequestHandler | 处理FullHttpRequest(那些不发送到/ws URI 的请求) |
| WebSocketServerProtocolHandler | 按照WebSocket 规范的要求,处理WebSocket 升级握手、PingWebSocketFrame 、PongWebSocketFrame 和CloseWebSocketFrame |
| TextWebSocketFrameHandler | 处理TextWebSocketFrame 和握手完成事件 |
Netty 的WebSocketServerProtocolHandler 处理了所有委托管理的WebSocket 帧类型以及升级握手本身。如果握手成功,那么所需的ChannelHandler 将会被添加到ChannelPipeline中,而那些不再需要的ChannelHandler 则将会被移除。
WebSocket 协议升级之前的ChannelPipeline 的状态如图12-3 所示。这代表了刚刚被ChatServerInitializer 初始化之后的ChannelPipeline。

当WebSocket 协议升级完成之后,WebSocketServerProtocolHandler 将会把HttpRequestDecoder 替换为WebSocketFrameDecoder,把HttpResponseEncoder 替换为
WebSocketFrameEncoder。为了性能最大化,它将移除任何不再被WebSocket 连接所需要的ChannelHandler。这也包括了图12-3 所示的HttpObjectAggregator 和HttpRequest
Handler。
图12-4 展示了这些操作完成之后的ChannelPipeline。需要注意的是,Netty目前支持4个版本的WebSocket协议,它们每个都具有自己的实现类。Netty将会根据客户端(这里指浏览
器)所支持的版本①,自动地选择正确版本的WebSocketFrameDecoder和WebSocketFrameEncoder。

4、引导
package com.dxz.nettydemo.websocket; import java.net.InetSocketAddress; import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.ImmediateEventExecutor; 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) throws Exception {
String portStr= "9999";
int port = Integer.parseInt(portStr);
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();
}
}
5、测试
启动WebSocket服务端后,浏览器访问http://localhost:9999/

点击连接和发送后,服务端收到并打印客户端发送的消息

netty中的websocket的更多相关文章
- 一款基于Netty开发的WebSocket服务器
代码地址如下:http://www.demodashi.com/demo/13577.html 一款基于Netty开发的WebSocket服务器 这是一款基于Netty框架开发的服务端,通信协议为We ...
- Netty 中的心跳机制
在TCP长连接或者WebSocket长连接中一般我们都会使用心跳机制–即发送特殊的数据包来通告对方自己的业务还没有办完,不要关闭链接. 网络的传输是不可靠的,当我们发起一个链接请求的过程之中会发生什么 ...
- Netty(五)序列化protobuf在netty中的使用
protobuf是google序列化的工具,主要是把数据序列化成二进制的数据来传输用的.它主要优点如下: 1.性能好,效率高: 2.跨语言(java自带的序列化,不能跨语言) protobuf参考文档 ...
- 【C#】MVC项目中搭建WebSocket服务器
前言 因为项目需要,前端页面中需要不断向后台请求获取一个及一个以上的状态值.最初的方案是为每个状态值请求都建立一个定时器循环定时发起Ajax请求,结果显而 易见.在HTTP1.1协议中,同一客户端浏览 ...
- 带你认识HTML5中的WebSocket
这篇文章主要介绍了带你认识HTML5中的WebSocket,本文讲解了HTML5 中的 WebSocket API 是个什么东东.HTML5 中的 WebSocket API 的用法.带Socket. ...
- 【转】Netty那点事(二)Netty中的buffer
[原文]https://github.com/code4craft/netty-learning/blob/master/posts/ch2-buffer.md 上一篇文章我们概要介绍了Netty的原 ...
- Spring 4.0 中的 WebSocket 架构
两年前,客户端与服务器端的全双工双向通信作为一个很重要的功能被纳入到WebSocket RFC 6455协议中.在HTML5中,WebSocket已经成为一个流行词,大家对这个功能赋予很多构想,很多时 ...
- Netty那点事: 概述, Netty中的buffer, Channel与Pipeline
Netty那点事(一)概述 Netty和Mina是Java世界非常知名的通讯框架.它们都出自同一个作者,Mina诞生略早,属于Apache基金会,而Netty开始在Jboss名下,后来出来自立门户ne ...
- Netty中的EventLoop和线程模型
一.前言 在学习了ChannelHandler和ChannelPipeline的有关细节后,接着学习Netty的EventLoop和线程模型. 二.EventLoop和线程模型 2.1. 线程模型 线 ...
随机推荐
- response.setHeader()用法
response.setHeader()下载中文文件名乱码问题 收藏 1. HTTP消息头 (1)通用信息头 即能用于请求消息中,也能用于响应信息中,但与被传输的实体内容没有关系的信息头,如Data ...
- jmeter-请求参数化
新建个scv文件,将我们需要传递的数据写进去(建议用notepad等编辑器,直接用excel转csv格式有可能会出现不能识别参数) 有多个参数用,分开 另存为 2.jmeter 新建请求,选择函数对话 ...
- Java多线程 - Callable和Future
已知的创建多线程的方法有继承Tread类和实现Runnable方法.此外Java还提供了Callable接口,Callable接口也提供了一个call()方法来做为线程执行体.但是call()方法与r ...
- tensorflow笔记:模型的保存与训练过程可视化
tensorflow笔记系列: (一) tensorflow笔记:流程,概念和简单代码注释 (二) tensorflow笔记:多层CNN代码分析 (三) tensorflow笔记:多层LSTM代码分析 ...
- 入门教程:.NET开源OpenID Connect 和OAuth解决方案IdentityServer v3 介绍 (一)
现代的应用程序看起来像这样: 典型的交互操作包括: 浏览器与 web 应用程序进行通信 Web 应用程序与 web Api (有时是在他们自己的有时代表用户) 通信 基于浏览器的应用程序与 web A ...
- spring3: Bean的作用域
3.4 Bean的作用域 什么是作用域呢?即“scope”,在面向对象程序设计中一般指对象或变量之间的可见范围.而在Spring容器中是指其创建的Bean对象相对于其他Bean对象的请求可见范围. ...
- za2
程序集?生成后 一个exe,一个dll. 也可以是一个项目. vs 快速生成字段的代码段快捷键,快速生成构造函数,生成普通方法结构的快捷键 ************************* ...
- 51nod 1043 数位dp
http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1043 1043 幸运号码 基准时间限制:1 秒 空间限制:131072 ...
- 最新Mysql5.7安装教程
可以从MSQL官网下载MySQL服务器安装软件包,我下载为版本“mysql-installer-community-5.7.3.0-m13.msi”不多说 方法/步骤 1 双击进入安装,如下图: ...
- lzugis——Arcgis Server for JavaScript API之自定义InfoWindow
各位看到这个标题不要嫌烦,因为本人最近一直在研究相关的问题,所以相关文章也只能是这些,同时希望看过我的文章的朋友,我的文章能够给你帮助. 在前面的两篇相关的文章里面,实现InfoWindow是通过di ...