【Netty】WebSocket
一、前言
前面学习了codec和ChannelHandler之间的关系,接着学习WebSocket。
二、WebSocket
2.1. WebSocket介绍
WebSocket协议允许客户端和服务器随时传输消息,要求他们异步处理接收的消息,而几乎所有的浏览器都支持WebSocket协议,Netty支持WebSocket协议的所有实现,可以在应用中直接使用。
2.2. WebSocket应用示例
下面示例展示了如何使用WebSocket协议实现基于浏览器的实时聊天应用,示例逻辑图如下图所示。
处理逻辑如下
· 客户端发送消息。
· 消息转发至其他所有客户端。
本示例中只实现服务端部分,客户端网页为index.html。
2.3 添加WebSocket支持
升级握手机制可用于从标准HTTP或HTTPS协议切换到WebSocket,使用WebSocket的应用程序以HTTP/S开头,当请求指定URL时将会启动该协议。本应用有如下惯例:如果URL请求以/ ws结尾,我们将使用升级的WebSocket协议,否则,将使用HTTP/S协议,连接升级后,所有数据将使用WebSocket传输。下图展示服务端的逻辑。
1. 处理HTTP请求
首先我们实现处理HTTP请求的组件,该组件将为访问聊天室的页面提供服务,并显示连接的客户端发送的消息。下面是HttpRequestHandler代码,其继承SimpleChannelInboundHandler。
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 (wsUri.equalsIgnoreCase(request.getUri())) {
ctx.fireChannelRead(request.retain());
} else {
if (HttpHeaders.is100ContinueExpected(request)) {
send100Continue(ctx);
}
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) {
response.headers().set(
HttpHeaders.Names.CONTENT_LENGTH, file.length());
response.headers().set( HttpHeaders.Names.CONNECTION,
HttpHeaders.Values.KEEP_ALIVE);
}
ctx.write(response);
if (ctx.pipeline().get(SslHandler.class) == null) {
ctx.write(new DefaultFileRegion(
file.getChannel(), 0, file.length()));
} else {
ctx.write(new ChunkedNioFile(file.getChannel()));
}
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请求,对于WebSocket而言,数据使用帧进行传输,完整的数据包含多帧。
2. 处理WebSocket帧
WebSocket定义了六种帧,如下图所示。
对于聊天应用而言,其包含如下帧:CloseWebSocketFrame、PingWebSocketFrame、PongWebSocketFrame、TextWebSocketFrame。
下面代码展示了用于处理TextWebSocketFrames的ChannelHandler。
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) {
ctx.pipeline().remove(HttpRequestHandler.class);
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 {
group.writeAndFlush(msg.retain());
}
}
3. 初始化ChannelPipeline
为在ChannelPipeline中添加ChannelHandler,需要继承ChannelInitializer并且实现initChannel方法,下面是ChatServerInitializer的代码。
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));
}
}
对于使用HTTP协议(升级前)和WebSocket协议(升级后)的管道中的处理器分别如下图所示。
4. Bootstrapping
ChatServer类用于启动服务器并且安装ChatServerInitializer,其代码如下。
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 {
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() {
@Override
public void run() {
endpoint.destroy();
}
});
future.channel().closeFuture().syncUninterruptibly();
}
}
上述代码就完成了服务端的所有代码,接着进行测试。
2.4 加密应用
上述代码中可正常进行通信,但是并未加密,首先需要添加SecureChatServerInitializer,其代码如下。
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 {
super.initChannel(ch);
SSLEngine engine = context.newEngine(ch.alloc());
ch.pipeline().addFirst(new SslHandler(engine));
}
}
然后添加SecureChatServerInitializer,代码如下。
public class SecureChatServer extends ChatServer {
private final SslContext context;
public SecureChatServer(SslContext context) {
this.context = context;
}
@Override
protected ChannelInitializer<Channel> createInitializer(
ChannelGroup group) {
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();
}
}
2.5 测试应用
在编译的classes文件夹中加入index.html(客户端),其中index.html的源码如下
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Chat</title>
</head>
<body>
<script type="text/javascript">
var socket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:8080/ws");
socket.onmessage = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + '\n' + event.data
};
socket.onopen = function(event) {
var ta = document.getElementById('responseText');
ta.value = "connected!";
};
socket.onclose = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + "connection is shutdown";
};
} else {
alert("your broswer do not support WebSocket!");
} function send(message) {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
socket.send(message);
} else {
alert("connection is not start.");
}
}
</script>
<form onsubmit="return false;">
<h3>WebSocket ChatRoom:</h3>
<textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
<br>
<input type="text" name="message" style="width: 300px" value="">
<input type="button" value="Send Message" onclick="send(this.form.message.value)">
<input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="Clear message">
</form>
<br>
<br>
</body>
</html>
然后启动ChatServer(非加密方式),最后在浏览器中访问localhost:8080(可打开多个窗口,多个客户端),其运行效果如下。
可以看到两个客户端之间可以正常进行通信,互相发送消息。
三、总结
本篇博文通过一个示例讲解了WebSocket协议的具体使用,可完成不同客户端之间的通信,也谢谢各位园友的观看~
【Netty】WebSocket的更多相关文章
- 【Netty】源码分析目录
前言 为方便系统的学习Netty,特整理文章目录如下. [Netty]第一个Netty应用 [Netty]Netty核心组件介绍 [Netty]Netty传输 [Netty]Netty之ByteBuf ...
- 【Netty】(7)---搭建websocket服务器
[Netty](7)---搭建websocket服务器 说明:本篇博客是基于学习某网有关视频教学. 目的:创建一个websocket服务器,获取客户端传来的数据,同时向客户端发送数据 一.服务端 1. ...
- 【Netty】(8)---理解ChannelPipeline
ChannelPipeline ChannelPipeline不是单独存在,它肯定会和Channel.ChannelHandler.ChannelHandlerContext关联在一起,所以有关概念这 ...
- 【Netty】(6) ---源码ServerBootstrap
[Netty]6 ---源码ServerBootstrap 之前写了两篇与Bootstrap相关的文章,一篇是ServerBootstrap的父类,一篇是客户端Bootstrap类,博客地址: [Ne ...
- 【Netty】(5)源码 Bootstrap
[Netty]5 源码 Bootstrap 上一篇讲了AbstractBootstrap,为这篇做了个铺垫. 一.概述 Bootstrap 是 Netty 提供的一个便利的工厂类, 我们可以通过它来完 ...
- 【Netty】netty学习之nio了解
[一]五种IO模型: (1)阻塞IO(2)非阻塞IO(任务提交,工作线程处理,委托线程等待工作线程处理结果的同时,也可以做其他的事情)(3)IO复用模型.(委托线程接收多个任务,将任务提交给工作线程. ...
- 【转】WebSocket 是什么原理?为什么可以实现持久连接?
WebSocket是HTML5出的东西 也就是说HTTP协议没有变化 但HTTP是不支持持久连接的(长连接,循环连接的不算)或者说WebSocket干脆就不是基于HTTP来执行的.但是...说不通啊. ...
- 【Netty】netty学习之nio网络编程的模型
[一]NIO服务器编程结构 [二]Netty3.x服务端线程模型
- 【Netty】Netty入门之WebSocket小例子
服务端: 引入Netty依赖 <!-- netty --> <dependency> <groupId>io.netty</groupId> <a ...
随机推荐
- 学习MVC之租房网站(四)-实现Service层并进行单元测试
在上一篇<学习MVC之租房网站(三)-编写Eneity类并创建数据库>中,记录了编写Eneity类并采用CodeFirst的方式创建数据库的过程,接下来就到了Service层的实现了,并且 ...
- NOIP2015游记——一次开心又失望的旅行
啊,一年一度的NOIP终于是结束了 以前的大神都有写自己的感受 然而我居然给忘了!!!! 吓得我赶紧来写一份游记 Day.-INF--出发前一个星期 机智的我选择了停课 就是为了OIER这伟大而又光荣 ...
- T-SQL编程语句
书接上回 一起学习下SQL中的表连接 一般情况下咱们会使用鼠标去进行表连接操作,那针对于像我比较懒的并且眼盲的,不喜欢来回切换,咱们就用到了点T-SQL表连接语句 一般情况咱们从两个表中查出来相似的内 ...
- OpenCV 玩九宫格数独(二):knn 数字识别
欢迎大家关注腾讯云技术社区-博客园官方主页,我们将持续在博客园为大家推荐技术精品文章哦~ 作者:刘潇龙 前言 首先需要说明,这里所说的数字识别不是手写数字识别! 但凡对机器学习有所了解的人,相信看到数 ...
- windows的bat脚本
一个小小的设置固定ip和关闭防火墙的脚本: @echo //-=-=-=-=-=-=-=-=-=-=-=-=-=-=@echo // [固定设置]@echo // 设置IP,子网掩码,网关@echo ...
- mina.net 梳理
LZ最近离职,闲着也是闲着,打算梳理下 公司做的是电商,CTO打算把2.0系统用java 语言开发,LZ目前不打算做java,所以 选择离职.离职前,在公司负责的最后一个项目 供应链系统. 系统分为 ...
- oracle表信息
获取表: select table_name from user_tables; //当前用户的表 select table_name from all_tables; //所有用户的表 select ...
- 设计模式的征途—5.原型(Prototype)模式
相信大多数的人都看过<西游记>,对孙悟空拔毛变出小猴子的故事情节应该都很熟悉.孙悟空可以用猴毛根据自己的形象复制出很多跟自己一模一样的小猴兵出来,其实在设计模式中也有一个类似的模式,我们可 ...
- springmvc 方法参数自定义的解析
1.实现HandlerMethodArgumentResolver接口: 2.在配置文件中添加配置<mvc:argument-resolvers> <bean class=&qu ...
- Python之函数知识
Python函数分类 a,内置函数 b,自定义函数 c,导入函数 一个函数就相当于一个功能块,比如获取数据库,更新数据库,函数其实就是代码的分块,调用函数来执行代码块 一块就代表一个功能 内置函数有以 ...