一直以来,网络在很大程度上都是围绕着HTTP的请求/响应模式而构建的。客户端加载一个网页,然后直到用户点击下一页之前,什么都不会发生。在2005年左右,Ajax开始让网络变得更加动态了。但所有的HTTP通信仍然是由客户端控制的,这就需要用户进行互动或定期轮询,以便从服务器加载新数据。

长期以来存在着各种技术让服务器得知有新数据可用时,立即将数据发送到客户端。这些技术种类繁多,例如“推送”或Comet。最常用的一种黑客手段是对服务器发起链接创建假象,被称为长轮询。利用长轮询,客户端可以打开指向服务器的HTTP连接,而服务器会一直保持连接打开,直到发送响应。服务器只要实际拥有新数据,就会发送响应(其他技术包括Flash、XHR multipart请求和所谓的HTML Files)。长轮询和其他技术都非常好用,在Gmail聊天等应用中会经常使用它们。

但是,这些解决方案都存在一个共同的问题:由于HTTP协议的开销,导致它们不适用于低延迟应用。

为了解决这些问题,WebSocket将网络套接字引入到了客户端和服务端,浏览器和服务器之间可以通过套接字建立持久的连接,双方随时都可以互发数据给对方,而不是之前由客户端控制的一请求一应答模式。

HTTP协议的弊端

将HTTP协议的主要弊端总结如下。

(1)HTTP协议为半双工协议。半双工协议指数据可以在客户端和服务端两个方向上传输,但是不能同时传输。它意味着在同一时刻,只有一个方向上的数据传送;

(2)HTTP消息冗长而繁琐。HTTP消息包含消息头、消息体、换行符等,通常情况下采用文本方式传输,相比于其他的二进制通信协议,冗长而繁琐;

(3)针对服务器推送的黑客攻击。例如长时间轮询。

现在,很多网站为了实现消息推送,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端浏览器。这种传统的模式具有很明显的缺点,即浏览器需要不断地向服务器发出请求,然而HTTP request的header是非常冗长的,里面包含的可用数据比例可能非常低,这会占用很多的带宽和服务器资源。

比较新的一种轮询技术是Comet,使用了Ajax。这种技术虽然可达到双向通信,但依然需要发出请求,而且在Comet中,普遍采用了长连接,这也会大量消耗服务器带宽和资源。

为了解决HTTP协议效率低下的问题,HTML5定义了WebSocket协议,能更好地节省服务器资源和带宽并达到实时通信。

WebSocket入门

WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通信的网络技术,WebSocket通信协议于2011年被IETF定为标准RFC6455,WebSocket API被W3C定为标准。

在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了。WebSocket基于TCP双向全双工进行消息传递,在同一时刻,既可以发送消息,也可以接收消息,相比于HTTP的半双工协议,性能得到很大提升。

下面总结一下WebSocket的特点。

  1. 单一的TCP连接,采用全双工模式通信;
  2. 对代理、防火墙和路由器透明;
  3. 无头部信息、Cookie和身份验证;
  4. 无安全开销;
  5. 通过“ping/pong”帧保持链路激活;
  6. 服务器可以主动传递消息给客户端,不再需要客户端轮询。

WebSocket连接建立

建立WebSocket连接时,需要通过客户端或者浏览器发出握手请求,请求消息示例如图:

服务端返回给客户端的应答消息如图:

为了建立一个WebSocket连接,客户端浏览器首先要向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中附加头信息“Upgrade: WebSocket”表明这是一个申请协议升级的HTTP请求。服务器端解析这些附加的头信息,然后生成应答信息返回给客户端,客户端和服务器端的WebSocket连接就建立起来了,双方可以通过这个连接通道自由地传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动关闭连接。

请求消息中的“Sec-WebSocket-Key”是随机的,服务器端会用这些数据来构造出一个SHA-1的信息摘要,把“Sec-WebSocket-Key”加上一个魔幻字符串“258EAFA5-E914- 47DA-95CA-C5AB0DC85B11”。使用SHA-1加密,然后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。

WebSocket生命周期

握手成功之后,服务端和客户端就可以通过“messages”的方式进行通信了,一个消息由一个或者多个帧组成,WebSocket的消息并不一定对应一个特定网络层的帧,它可以被分割成多个帧或者被合并。

帧都有自己对应的类型,属于同一个消息的多个帧具有相同类型的数据。从广义上讲,数据类型可以是文本数据(UTF-8[RFC3629]文字)、二进制数据和控制帧(协议级信令,如信号)。

WebSocket连接生命周期示意图如图:

WebSocket连接关闭

为关闭WebSocket连接,客户端和服务端需要通过一个安全的方法关闭底层TCP连接以及TLS会话。如果合适,丢弃任何可能已经接收的字节;必要时(比如受到攻击),可以通过任何可用的手段关闭连接。

底层的TCP连接,在正常情况下,应该首先由服务器关闭。在异常情况下(例如在一个合理的时间周期后没有接收到服务器的TCP Close),客户端可以发起TCP Close。因此,当服务器被指示关闭WebSocket连接时,它应该立即发起一个TCP Close操作;客户端应该等待服务器的TCP Close。

WebSocket的握手关闭消息带有一个状态码和一个可选的关闭原因,它必须按照协议要求发送一个Close控制帧,当对端接收到关闭控制帧指令时,需要主动关闭WebSocket连接。

Netty WebSocket协议开发

Netty基于HTTP协议栈开发了WebSocket协议栈,利用Netty的WebSocket协议栈可以非常方便地开发出WebSocket客户端和服务端。

场景设计

WebSocket服务端的功能如下:支持WebSocket的浏览器通过WebSocket协议发送请求消息给服务端,服务端对请求消息进行判断,如果是合法的WebSocket请求,则获取请求消息体(文本),并在后面追加字符串“欢迎使用Netty WebSocket服务,现在时刻:系统时间”。

客户端HTML通过内嵌的JS脚本创建WebSocket连接,如果握手成功,在文本框中打印“打开WebSocket服务正常,浏览器支持WebSocket!”。

服务端代码示例:

首先对WebSocket服务端的功能进行简单地讲解。WebSocket服务端接收到请求消息之后,先对消息的类型进行判断,如果不是WebSocket握手请求消息,则返回 HTTP 400 BAD REQUEST 响应给客户端。

服务端对握手请求消息进行处理,构造握手响应返回,双方的Socket连接正式建立,服务端返回的握手应答消息:

连接建立成功后,到被关闭之前,双方都可以主动向对方发送消息,这点跟HTTP的一请求一应答模式存在很大的差别。相比于HTTP,它的网络利用率更高,可以通过全双工的方式进行消息发送和接收。

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.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler; 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(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//首先添加HttpServerCodec,将请求和应答消息编码或者解码为HTTP消息;
pipeline.addLast("http-codec", new HttpServerCodec());
//增加HttpObjectAggregator,它的目的是将HTTP消息的多个部分组合成一条完整的HTTP消息;
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
//添加ChunkedWriteHandler,来向客户端发送HTML5文件,
//它主要用于支持浏览器和服务端进行WebSocket通信;
ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
//增加WebSocket服务端handler。
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);
}
} 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.websocketx.*;
import io.netty.util.CharsetUtil; import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpHeaders.*;
import static io.netty.handler.codec.http.HttpVersion.*; public class WebSocketServerHandler extends SimpleChannelInboundHandler { private WebSocketServerHandshaker handshaker; @Override
public void messageReceived(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 传统的HTTP接入
//第一次握手请求消息由HTTP协议承载,所以它是一个HTTP消息,执行handleHttpRequest方法来处理WebSocket握手请求。
if (msg instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) msg);
}
// WebSocket接入
// 客户端通过文本框提交请求消息给服务端,WebSocketServerHandler接收到的是已经解码后的WebSocketFrame消息。
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解码失败,返回HTTP异常
// 首先对握手请求消息进行判断,如果消息头中没有包含Upgrade字段或者它的值不是websocket,则返回HTTP 400响应。
if (!req.getDecoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1,BAD_REQUEST));
return;
}
// 构造握手响应返回,本机测试
// 握手请求简单校验通过之后,开始构造握手工厂,
// 创建握手处理类WebSocketServerHandshaker,
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
              "ws://localhost:8080/websocket", null, false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
} else {
// 通过它构造握手响应消息返回给客户端,
// 同时将WebSocket相关的编码和解码类动态添加到ChannelPipeline中,用于WebSocket消息的编解码,
// 添加WebSocketEncoder和WebSocketDecoder之后,服务端就可以自动对WebSocket消息进行编解码了
handshaker.handshake(ctx.channel(), req);
}
}
private void handleWebSocketFrame(ChannelHandlerContext ctx,WebSocketFrame frame) {
// 判断是否是关闭链路的指令
// 首先需要对控制帧进行判断,如果是关闭链路的控制消息
// 就调用WebSocketServerHandshaker的close方法关闭WebSocket连接;
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(),(CloseWebSocketFrame) frame.retain());
return;
}
// 判断是否是Ping消息
// 如果是维持链路的Ping消息,则构造Pong消息返回。
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
return;
}
// 本例程仅支持文本消息,不支持二进制消息
// WebSocket通信双方使用的都是文本消息,所以对请求消息的类型进行判断,不是文本的抛出异常。
if (!(frame instanceof TextWebSocketFrame)) {
throw new UnsupportedOperationException(
String.format("%s frame types not supported", frame.getClass().getName()));
} // 返回应答消息
// 从TextWebSocketFrame中获取请求消息字符串,
String request = ((TextWebSocketFrame) frame).text();
// 对它处理后通过构造新的TextWebSocketFrame消息返回给客户端,
// 由于握手应答时动态增加了TextWebSocketFrame的编码类,所以,可以直接发送TextWebSocketFrame对象。
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();
setContentLength(res, res.content().readableBytes());
} // 如果是非Keep-Alive,关闭连接
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!isKeepAlive(req) || res.getStatus().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}

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服务正常,浏览器支持";
};
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>
<input type="text" name="message" value="Netty WebSocket"/>
<br><br>
<input type="button" value="发送消息" onclick="send(this.form.message.value)">
<hr color="blue"/>
<h3>服务器返回的应答消息</h3>
<textarea id="responseText" style="width: 500px;height: 300px;"></textarea>
</form>
</body>
</html>

WebSocket协议开发的更多相关文章

  1. netty(4)高级篇-Websocket协议开发

    一.HTTP协议的弊端 将HTTP协议的主要弊端总结如下: (1) 半双工协议:可以在客户端和服务端2个方向上传输,但是不能同时传输.同一时刻,只能在一个方向上传输. (2) HTTP消息冗长:相比于 ...

  2. netty websocket协议开发

    websocket的好处我们就不用多说了,就是用于解决长连接.服务推送等需要的一种技术. 以下我们来看一个例子: package com.ming.netty.http.websocket; impo ...

  3. dotnet core 开发无缝兼容Http和Websocket协议的接口服务

    在应用接口开发中往往要针对不同协义开发相应的代理服务,但对于Websocket和http这两种协议来说就有些不同,从实现上来看Websocket可以说是Http的升级子协议, 两者在协议处理上基本一致 ...

  4. workerman-chat(PHP开发的基于Websocket协议的聊天室框架)(thinkphp也是支持socket聊天的)

    workerman-chat(PHP开发的基于Websocket协议的聊天室框架)(thinkphp也是支持socket聊天的) 一.总结 1.下面链接里面还有一个来聊的php聊天室源码可以学习 2. ...

  5. WebSocket协议

    websocket 简介 (2013-04-09 15:39:28) 转载▼   分类: websocket 一 WebSocket是html5新增加的一种通信协议,目前流行的浏览器都支持这个协议,例 ...

  6. Jmeter对基于websocket协议的压力测试

      WebSocket protocol 是HTML5一种新的协议.它实现了浏览器与服务器全双工通信(full-duplex).   浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就 ...

  7. 性能测试培训:WebSocket协议的接口性能之Jmeter

    性能测试培训:WebSocket协议的接口性能之Jmeter poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.poptest测试开 ...

  8. 开源的C#实现WebSocket协议客户端和服务器websocket-sharp组件解析

    很久没有写博客了(至少自己感觉很长时间没有写了),没办法啊,楼主也是需要生活的人啊,这段一直都在找工作什么的.(整天催我代码的人,还望多多谅解啊,我会坚持写我们的项目的,还是需要相信我的,毕竟这是一个 ...

  9. 刨根问底HTTP和WebSocket协议

    HTML5的新成员:WebSocket 上篇介绍了HTTP1.1协议的基本内容,这篇文章将继续分析WebSocket协议,然后对这两个进行简单的比较. WebSocket WebSocket协议还很年 ...

随机推荐

  1. nyoj305_表达式求值

    表达式求值 时间限制:3000 ms  |  内存限制:65535 KB 难度:3   描述 Dr.Kong设计的机器人卡多掌握了加减法运算以后,最近又学会了一些简单的函数求值,比如,它知道函数min ...

  2. codeforces 490C. Hacking Cypher 解题报告

    题目链接:http://codeforces.com/problemset/problem/490/C 题目意思:给出一个可能有10^6 位长的字符串且没有前导0的整数,问能否一分为二,使得前面的一部 ...

  3. mvc EF

    一:数据库不存在时重新创建数据库 复制内容到剪贴板程序代码 Database.SetInitializer<testContext>(new CreateDatabaseIfNotExis ...

  4. UML类图五种关系与代码的对应关系

    转: UML类图中的五种关系的耦合强弱比较:依赖<关联<聚合<组合<继承 一.依赖关系: (一)说明 虚线+箭头 可描述为:Uses a 依赖是类的五种关系中耦合最小的一种关系 ...

  5. 20145206邹京儒《Java程序设计》第3周学习总结

    20145206 <Java程序设计>第3周学习总结 教材学习内容总结 第四章 4.1 定义类 class Clothes{ String color; char size; } publ ...

  6. LoadRunner函数

    一.基础函数简介 在VU左边导航栏中,有三个LoadRunner框架函数,分别是vuser_init().Action().vuser_end().这三个函数存在于任何Vuser类型的脚本中. vus ...

  7. MVC基础知识 – 2.新语法

    1.自动属性 Auto-Implemented Properties 2.隐式类型 var 3.参数默认值 和 命名参数 4.对象初始化器 与 集合初始化器 { } 5.匿名类 & 匿名方法 ...

  8. Delphi操作XML简介

    参考:http://www.delphifans.com/InfoView/Article_850.html Delphi 7支持对XML文档的操作,可以通过 TXMLDocument类来实现对XML ...

  9. 一个通过网络转换Ico到Png图片的小小程序(Ico2Png)

    做软件界面需要用到ico文件, 结果皮肤库不支持ico格式的图标, 所以就想到了把ico转换成png. 网上ico转png的软件貌似并不多, 反倒是png转ico很大一片~~~~~~~~~ 要转换ic ...

  10. linux 下解压rar文件

    今天遇到要在linux 服务器上解压几个rar 文件的问题,rar似乎是win 专属的压缩格式,所以linux 原生工具链中并没有支持rar 解压的工具.经过一系列搜索确定了一个脚linuxrar 的 ...