netty(4)高级篇-Websocket协议开发
一、HTTP协议的弊端
将HTTP协议的主要弊端总结如下:
- (1) 半双工协议:可以在客户端和服务端2个方向上传输,但是不能同时传输。同一时刻,只能在一个方向上传输。
- (2) HTTP消息冗长:相比于其他二进制协议,有点繁琐。
- (3) 针对服务器推送的黑客攻击,例如长时间轮询。
现在很多网站的消息推送都是使用轮询,即客户端每隔1S或者其他时间给服务器发送请求,然后服务器返回最新的数据给客户端。HTTP协议中的Header非常冗长,因此会占用很多的带宽和服务器资源。
比较新的技术是Comet,使用了AJAX。虽然可以双向通信,但是依然需要发送请求,而且在Comet中,普遍采用了长连接,也会大量消耗服务器的带宽和资源。
为了解决这个问题,HTML5定义的WebSocket协议。
二、WebSocket协议介绍
在WebSocket API中,浏览器和服务器只需要一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了。
WebSocket基于TCP双向全双工协议,即在同一时刻,即可以发送消息,也可以接收消息,相比于HTTP协议,是一个性能上的提升。
特点:
- 单一的TCP连接,全双工;
- 对代理、防火墙和路由器透明;
- 无头部信息、Cookie和身份验证;
- 无安全开销;
- 通过"ping/pong"帧保持链路激活;
- 服务器可以主动传递消息给客户端,不再需要客户端轮询;
拥有以上特点的WebSocket就是为了取代轮询和Comet技术,使得客户端浏览器具备像C/S架构下桌面系统一样的实时能力。
浏览器通过js建立一个WebSocket的请求,连接建立后,客户端和服务器端可以通过TCP直接交换数据。
因为WebSocket本质上是一个TCP连接,稳定,所以在Comet和轮询比拥有性能优势,如图所示:
三、WebSocket连接
3.1 连接建立
client端发送握手请求,请求消息如图所示:
- 这个请求和普通的HTTP请求不同,包含了一些附加头信息,其中附加头信息"Upgrade: Websocket"表明这是一个申请协议升级的HTTP请求。
- 服务器尝试解析这个信息,然后返回应答信息给客户端,因此客户端和服务器端的WebSocket连接就建立起来了,双方可以通过这个连接通道自由的传递信息。
- 这个连接会持续到某一方主动断开连接。
服务端的应答请求如图所示:
client消息中的"Sec-WebSocket-Key"是随机的,服务器端会用这些数据来构造一个"SHA-1"的信息摘要,把"Sec-WebSocket-Key"加上一个魔幻字符串。使用"SHA-1"加密,然后进行BASE64编码,将结果作为"Sec-Webscoket-Accept"头的值。
3.2 生命周期
- 握手成功,连接建立后,以"Messages"的方式通信。
- 一个消息由一个或者多个"帧"组成。
- 帧都有自己的类型,同一消息的多个帧类型相同。
- 广义上,类型可以是文本、二进制、控制帧如信号。
3.3 连接关闭
- 安全方法是关闭底层TCP连接以及TLS会话。
- 底层的TCP连接,正常情况下,应该由服务器先关闭。
- 异常时(比如合理的时间内没有接收到服务器的TCP Close),可以由客户端发起TCP Close。因此,在client发起TCP Close时,服务器应该立即发起一个TCP Close操作;客户端则等待服务器的TCP Close;
- 关闭消息带有一个状态码和可选的关闭原因,它必须按照协议要求发送一个Close控制帧。
四、协议开发
官方demo: http://netty.io/4.1/xref/io/netty/example/http/websocketx/server/package-summary.html
功能介绍:
服务器端开发:
- import io.netty.bootstrap.ServerBootstrap;
- import io.netty.channel.Channel;
- import io.netty.channel.ChannelInitializer;
- import io.netty.channel.ChannelPipeline;
- import io.netty.channel.EventLoopGroup;
- import io.netty.channel.nio.NioEventLoopGroup;
- import io.netty.channel.socket.SocketChannel;
- import io.netty.channel.socket.nio.NioServerSocketChannel;
- import io.netty.handler.codec.http.HttpObjectAggregator;
- import io.netty.handler.codec.http.HttpServerCodec;
- import io.netty.handler.stream.ChunkedWriteHandler;
- /**
- * @author lilinfeng
- * @version 1.0
- * @date 2014年2月14日
- */
- public class WebSocketServer {
- public void run(int port) throws Exception {
- EventLoopGroup bossGroup = new NioEventLoopGroup();
- EventLoopGroup workerGroup = new NioEventLoopGroup();
- try {
- ServerBootstrap b = new ServerBootstrap();
- b.group(bossGroup, workerGroup)
- .channel(NioServerSocketChannel.class)
- .childHandler(new ChannelInitializer<SocketChannel>() {
- @Override
- protected void initChannel(SocketChannel ch)
- throws Exception {
- ChannelPipeline pipeline = ch.pipeline();
- pipeline.addLast("http-codec",
- new HttpServerCodec());
- pipeline.addLast("aggregator",
- new HttpObjectAggregator(65536));
- ch.pipeline().addLast("http-chunked",
- new ChunkedWriteHandler());
- pipeline.addLast("handler",
- new WebSocketServerHandler());
- }
- });
- Channel ch = b.bind(port).sync().channel();
- System.out.println("Web socket server started at port " + port
- + '.');
- System.out
- .println("Open your browser and navigate to http://localhost:"
- + port + '/');
- ch.closeFuture().sync();
- } finally {
- bossGroup.shutdownGracefully();
- workerGroup.shutdownGracefully();
- }
- }
- public static void main(String[] args) throws Exception {
- int port = 8080;
- if (args.length > 0) {
- try {
- port = Integer.parseInt(args[0]);
- } catch (NumberFormatException e) {
- e.printStackTrace();
- }
- }
- new WebSocketServer().run(port);
- }
- }
HttpServerCodec:将请求和应答消息解码为HTTP消息
HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息
ChunkedWriteHandler:向客户端发送HTML5文件
看上去和HTTP协议的非常类似,下面从Handler中来寻找答案:
- import io.netty.buffer.ByteBuf;
- import io.netty.buffer.Unpooled;
- import io.netty.channel.ChannelFuture;
- import io.netty.channel.ChannelFutureListener;
- import io.netty.channel.ChannelHandlerContext;
- import io.netty.channel.SimpleChannelInboundHandler;
- import io.netty.handler.codec.http.DefaultFullHttpResponse;
- import io.netty.handler.codec.http.FullHttpRequest;
- import io.netty.handler.codec.http.FullHttpResponse;
- import io.netty.handler.codec.http.HttpUtil;
- import io.netty.handler.codec.http.websocketx.*;
- import io.netty.util.CharsetUtil;
- import java.util.logging.Level;
- import java.util.logging.Logger;
- import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
- import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
- /**
- * @author lilinfeng
- * @version 1.0
- * @date 2014年2月14日
- */
- public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
- private static final Logger logger = Logger
- .getLogger(WebSocketServerHandler.class.getName());
- private WebSocketServerHandshaker handshaker;
- @Override
- public void channelRead0(ChannelHandlerContext ctx, Object msg)
- throws Exception {
- // 传统的HTTP接入
- if (msg instanceof FullHttpRequest) {
- handleHttpRequest(ctx, (FullHttpRequest) msg);
- }
- // WebSocket接入
- else if (msg instanceof WebSocketFrame) {
- handleWebSocketFrame(ctx, (WebSocketFrame) msg);
- }
- }
- @Override
- public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
- ctx.flush();
- }
- private void handleHttpRequest(ChannelHandlerContext ctx,
- FullHttpRequest req) throws Exception {
- // 如果HTTP解码失败,返回HHTP异常
- if (!req.decoderResult().isSuccess()
- || (!"websocket".equals(req.headers().get("Upgrade")))) {
- sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1,
- BAD_REQUEST));
- return;
- }
- // 构造握手响应返回,本机测试
- WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
- "ws://localhost:8080/websocket", null, false);
- handshaker = wsFactory.newHandshaker(req);
- if (handshaker == null) {
- WebSocketServerHandshakerFactory
- .sendUnsupportedVersionResponse(ctx.channel());
- } else {
- handshaker.handshake(ctx.channel(), req);
- }
- }
- private void handleWebSocketFrame(ChannelHandlerContext ctx,
- WebSocketFrame frame) {
- // 判断是否是关闭链路的指令
- if (frame instanceof CloseWebSocketFrame) {
- handshaker.close(ctx.channel(),
- (CloseWebSocketFrame) frame.retain());
- return;
- }
- // 判断是否是Ping消息
- if (frame instanceof PingWebSocketFrame) {
- ctx.channel().write(
- new PongWebSocketFrame(frame.content().retain()));
- return;
- }
- // 本例程仅支持文本消息,不支持二进制消息
- if (!(frame instanceof TextWebSocketFrame)) {
- throw new UnsupportedOperationException(String.format(
- "%s frame types not supported", frame.getClass().getName()));
- }
- // 返回应答消息
- String request = ((TextWebSocketFrame) frame).text();
- if (logger.isLoggable(Level.FINE)) {
- logger.fine(String.format("%s received %s", ctx.channel(), request));
- }
- ctx.channel().write(
- new TextWebSocketFrame(request
- + " , 欢迎使用Netty WebSocket服务,现在时刻:"
- + new java.util.Date().toString()));
- }
- private static void sendHttpResponse(ChannelHandlerContext ctx,
- FullHttpRequest req, FullHttpResponse res) {
- // 返回应答给客户端
- if (res.getStatus().code() != 200) {
- ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(),
- CharsetUtil.UTF_8);
- res.content().writeBytes(buf);
- buf.release();
- HttpUtil.setContentLength(res, res.content().readableBytes());
- }
- // 如果是非Keep-Alive,关闭连接
- ChannelFuture f = ctx.channel().writeAndFlush(res);
- if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
- f.addListener(ChannelFutureListener.CLOSE);
- }
- }
- @Override
- public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
- throws Exception {
- cause.printStackTrace();
- ctx.close();
- }
- }
(1) 第一次握手由HTTP协议承载,所以是一个HTTP消息,根据消息头中是否包含"Upgrade"字段来判断是否是websocket。
(2) 通过校验后,构造WebSocketServerHandshaker,通过它构造握手响应信息返回给客户端,同时将WebSocket相关的编码和解码类动态添加到ChannelPipeline中。
下面分析链路建立之后的操作:
(1) 客户端通过文本框提交请求给服务端,Handler收到之后已经解码之后的WebSocketFrame消息。
(2) 如果是关闭按链路的指令就关闭链路
(3) 如果是维持链路的ping消息就返回Pong消息。
(4) 否则就返回应答消息
五、客户端以及测试
html5中的JS代码:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- Netty WebSocket 时间服务器
- </head>
- <br>
- <body>
- <br>
- <script type="text/javascript">
- var socket;
- if (!window.WebSocket) {
- window.WebSocket = window.MozWebSocket;
- }
- if (window.WebSocket) {
- socket = new WebSocket("ws://localhost:8080/websocket");
- socket.onmessage = function (event) {
- var ta = document.getElementById('responseText');
- ta.value = "";
- ta.value = event.data
- };
- socket.onopen = function (event) {
- var ta = document.getElementById('responseText');
- ta.value = "打开WebSocket服务正常,浏览器支持WebSocket!";
- };
- socket.onclose = function (event) {
- var ta = document.getElementById('responseText');
- ta.value = "";
- ta.value = "WebSocket 关闭!";
- };
- }
- else {
- alert("抱歉,您的浏览器不支持WebSocket协议!");
- }
- function send(message) {
- if (!window.WebSocket) {
- return;
- }
- if (socket.readyState == WebSocket.OPEN) {
- socket.send(message);
- }
- else {
- alert("WebSocket连接没有建立成功!");
- }
- }
- </script>
- <form onsubmit="return false;">
- <input type="text" name="message" value="Netty最佳实践"/>
- <br><br>
- <input type="button" value="发送WebSocket请求消息" onclick="send(this.form.message.value)"/>
- <hr color="blue"/>
- <h3>服务端返回的应答消息</h3>
- <textarea id="responseText" style="width:500px;height:300px;"></textarea>
- </form>
- </body>
- </html>
演示效果大致如下:
这里只是对WebSocket协议最基本的演示,WebSocket支持多种协议,文本,二进制,控制帧。
netty(4)高级篇-Websocket协议开发的更多相关文章
- WebSocket协议开发
一直以来,网络在很大程度上都是围绕着HTTP的请求/响应模式而构建的.客户端加载一个网页,然后直到用户点击下一页之前,什么都不会发生.在2005年左右,Ajax开始让网络变得更加动态了.但所有的HTT ...
- netty websocket协议开发
websocket的好处我们就不用多说了,就是用于解决长连接.服务推送等需要的一种技术. 以下我们来看一个例子: package com.ming.netty.http.websocket; impo ...
- netty(5)高级篇-私有协议栈
来源:<Netty权威指南> 作者:李林峰 一.私有协议介绍 由于现代软件的复杂性,一个大型软件系统往往会被人为地拆分称为多个模块,另外随着移动互联网的兴起,网站的规模越来越大,业务功能 ...
- 真正实现Netty私有协议开发
首先<Netty权威指南>私有协议开发那一章的样例代码是编译不通过的(但是这丝毫不影响本书的价值)处理方案可以参考:http://www.itnose.net/detail/6112870 ...
- Netty 框架学习 —— 添加 WebSocket 支持
WebSocket 简介 WebSocket 协议是完全重新设计的协议,旨在为 Web 上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息 Netty 对于 ...
- netty高级篇(3)-HTTP协议开发
一.HTTP协议简介 应用层协议http,发展至今已经是http2.0了,拥有以下特点: (1) CS模式的协议 (2) 简单 - 只需要服务URL,携带必要的请求参数或者消息体 (3) 灵活 - 任 ...
- iOS开发网络篇—HTTP协议
iOS开发网络篇—HTTP协议 说明:apache tomcat服务器必须占用8080端口 一.URL 1.基本介绍 URL的全称是Uniform Resource Locator(统一资源定位符) ...
- iOS开发——高级技术精选&底层开发之越狱开发第一篇
底层开发之越狱开发第一篇 做越狱开发也有一些时间了,有很多东西想总结一下,希望给他人一些借鉴,也是自己对过去开发经历的一些总结.个人不推荐使用盗版,这里主要以技术介绍为主. 这个系列里面主要介绍怎样进 ...
- 02.iOS开发网络篇—HTTP协议
iOS开发网络篇—HTTP协议 说明:apache tomcat服务器必须占用8080端口 一.URL 1.基本介绍 URL的全称是Uniform Resource Locator(统一资源定位符) ...
随机推荐
- 正则表达式(c#)
只能输入数字:"^[0-9]*$". 只能输入n位的数字:"^\d{n}$". 只能输入至少n位的数字:"^\d{n,}$". 只能输入 ...
- ckeditor:复制内容到ckeditor时,只保留文本,忽略其样式解决方法
打开ckeditor 包下的config.js,在 CKEDITOR.editorConfig= function(config){...}添加一句配置: config.forcePasteAsPla ...
- ASP.NET Web API框架揭秘:路由系统的几个核心类型
ASP.NET Web API框架揭秘:路由系统的几个核心类型 虽然ASP.NET Web API框架采用与ASP.NET MVC框架类似的管道式设计,但是ASP.NET Web API管道的核心部分 ...
- SQL Server监控清单
SQL Server监控清单 一. 服务器1. 状态监控(1) 服务器是否可访问?(2) 相应的数据库服务是否启用?(3) 操作系统事件日志中的错误或告警(4) 磁盘可用空间 服务器状态监控,不管使用 ...
- Linux 学习 step by step (2)
Linux 学习 step by step (2) Linux,想要我说爱你真的不容易了,尽管,你是ubutun,尽管,你有蛮界面.但是,操作你,还是没有操作windows那么的如鱼得水了.为了更 ...
- Bottle GET method. Request
python bottle framework #!/usr/bin/python # -*- coding utf-8 -*- from bottle import route, run, debu ...
- 使用EasyMock对Servlet进行简单的测试
这是一个WebProject,但不需配置web.xml,因为EasyMock用不上它 首先是用到的实体类User.java package com.jadyer.model; public class ...
- hdu 4515 年月份模拟题
小Q系列故事——世界上最遥远的距离 Time Limit: 500/200 MS (Java/Others) Memory Limit: 65535/32768 K (Java/Others) ...
- Android开发之Ubuntu上Eclipse不显示手机设备
一.准备工作 A.开启Android设备,用USB数据线连接到Ubuntu系统. B.启用设备的USB调试模试 C.启动Eclipse,在Devices栏会现一个有很多???号的不明设备 ...
- Android监听WebView滑动到底部
MainActivity如下: package cn.testwebview; import android.app.Activity; import android.graphics.Bitmap; ...