一、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() { @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中来寻找答案:
1 import io.netty.buffer.ByteBuf; 2 import io.netty.buffer.Unpooled; 3 import io.netty.channel.ChannelFuture; 4 import io.netty.channel.ChannelFutureListener; 5 import io.netty.channel.ChannelHandlerContext; 6 import io.netty.channel.SimpleChannelInboundHandler; 7 import io.netty.handler.codec.http.DefaultFullHttpResponse; 8 import io.netty.handler.codec.http.FullHttpRequest; 9 import io.netty.handler.codec.http.FullHttpResponse; 10 import io.netty.handler.codec.http.HttpUtil; 11 import io.netty.handler.codec.http.websocketx.*; 12 import io.netty.util.CharsetUtil; 13 14 import java.util.logging.Level; 15 import java.util.logging.Logger; 16 17 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; 18 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; 19 20 /** 21 * @author lilinfeng 22 * @version 1.0 23 * @date 2014年2月14日 24 */ 25 public class WebSocketServerHandler extends SimpleChannelInboundHandler
(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>
演示效果大致如下: