HTTP协议和HTTPS协议通信过程通常是客户端通过浏览器发出一个请求,服务器接受请求后进行处理并返回结果给客户端,客户端处理结果。
这种机制对于信息变化不是特别频繁的应用可以良好支撑,但对于实时要求高、海量并发的应用来说显得捉襟见肘
WebSocket出现前我们实现推送技术,用的都是轮询,在特定的时间间隔,浏览器自动发出请求,将服务器的消息主动的拉回来,这种情况下,我们需要不断的向服务器发送请求,并且HTTP 请求 的header非常长,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽和服务器资源,并且服务器不能主动向客户端推送数据。在这种情况下需要一种高效节能的双向通信机制来保证数据的实时传输,于是基于HTML5规范的WebSocket应运而生。
HTTP实现实时推送用到的轮询,轮询分两种:长轮询和短轮询(传统轮询)
短轮询:浏览器定时向服务器发送请求,服务器收到请求不管是否有数据到达都直接响应 请求,隔特定时间,浏览器又会发送相同的请求到服务器, 获取数据响应,如图:
缺点:数据交互的实时性较低,服务端到浏览器端的数据反馈效率低
长轮询:浏览器发起请求到服务器,服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这一过程在页面打开期间一直持续不断。如图:
缺点:服务器没有数据到达时,http连接会停留一段时间,造成服务器资源浪费,数据交互的实时性也很低
无论是长轮询还是短轮询,浏览器都要先发起对服务器的连接,才能接收数据,并且实时交互性很低。
WebSocket是HTML5下一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。
HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。
使用websocket有两种方式:1是使用sockjs,2是使用h5的标准。使用Html5标准自然更方便简单.
WebSocket与http协议是基于TCP的–可靠的协议–应用层
HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(PHP等)来处理。简单地说,我们有一个非常快速的 接线员(Nginx) ,他负责把问题转交给相应的 客服(Handler) 。
具体关系如下:
在客户端,没有必要为 WebSockets 使用 JavaScript 库。
实现 WebSockets 的 Web 浏览器将通过 WebSockets 对象公开所有必需的客户端功能(主要指支持 Html5 的浏览器)。
以下 API 用于创建 WebSocket 对象。
var Socket = new WebSocket(url, [protocol] );
以上代码中的第一个参数 url, 指定连接的 URL。第二个参数 protocol 是可选的,指定了可接受的子协议。
以下是 WebSocket 对象的属性。假定我们使用了以上代码创建了 Socket 对象:
Socket.readyState 只读属性 readyState 表示连接状态,可以是以下值:0 - 表示连接尚未建立。1 - 表示连接已建立,可以进行通信。2 - 表示连接正在进行关闭。3 - 表示连接已经关闭或者连接不能打开。
Socket.bufferedAmount 只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。
WebSocket 事件
以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发
WebSocket 方法
以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:
Socket.send() 使用连接发送数据
Socket.close() 关闭连接
// 初始化一个 WebSocket 对象
var ws = new WebSocket(‘ws://localhost:9998/echo’);
// 建立 web socket 连接成功触发事件
ws.onopen = function() {
// 使用 send() 方法发送数据
ws.send(‘发送数据’);
alert(‘数据发送中…’);
};
// 接收服务端数据时触发事件
ws.onmessage = function(evt) {
var received_msg = evt.data;
alert(‘数据已接收…’);
};
// 断开 web socket 连接成功触发事件
ws.onclose = function() {
alert(‘连接已关闭…’);
};
WebSocket 在服务端的实现非常丰富。
Node.js、Java、C++、Python 等多种语言都有自己的解决方案。
Java 的 web 一般都依托于 servlet 容器。
servlet 容器有
Tomcat、Jetty、Resin。
其中 Tomcat7、Jetty7 及以上版本均开始支持 WebSocket(推荐较新的版本,因为随着版本的更迭,对 WebSocket 的支持可能有变更)。
Spring 框架对 WebSocket 也提供了支持。
Spring 对于 WebSocket 的支持基于下面的 jar 包:
org.springframework
spring-websocket
${spring.version}
// 打开连接触发事件
@OnOpen
public void onOpen(Session session, EndpointConfig config, @PathParam(“id”) String id) {
…
}
// 关闭连接触发事件
@OnClose
public void onClose(Session session, CloseReason closeReason) {
…
}
// 传输消息错误触发事件
@OnError
public void onError(Throwable error) {
…
}
ServerEndpointConfig.Configurator
编写完处理器,你需要扩展 ServerEndpointConfig.Configurator 类完成配置:
public class WebSocketServerConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
}
}
Nginx 从 1.3 版开始正式支持 WebSocket 代理。
Nginx配置,开启 WebSocket 代理功能。
参考配置:
server {
#this section is specific to the WebSockets proxying
location /socket.io {
proxy_pass http://app_server_wsgiapp/socket.io;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection “upgrade”;
proxy_read_timeout 600;
}
}
WebSocket复用了HTTP的握手通道:客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。
首先,客户端发起协议升级请求。
采用的是标准的HTTP报文格式,且只支持GET方法。
重点请求首部意义:
• Connection: Upgrade:表示要升级协议
• Upgrade: websocket:表示要升级到websocket协议。
• Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
• Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
在握手阶段,可以通过相关请求首部进行 安全限制、权限校验等。
服务端返回内容:
HTTP/1.1 101 Switching Protocols//状态代码101表示协议切换
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
备注:每个header都以\r\n结尾,并且最后一行加上一个额外的空行\r\n。
服务端回应的HTTP状态码只能在握手阶段使用。过了握手阶段后,就只能采用特定的错误码。
完成协议升级,后续的数据交互都按照新的协议来。
Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。
计算公式为:
toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
验证下前面的返回结果:
const crypto = require(‘crypto’);
const magic = ‘258EAFA5-E914-47DA-95CA-C5AB0DC85B11’;
const secWebSocketKey = ‘w4v7O6xFTi36lq3RNcgctw==’;
let secWebSocketAccept = crypto.createHash(‘sha1’)
.update(secWebSocketKey + magic)
.digest(‘base64’);
console.log(secWebSocketAccept);
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=
一旦WebSocket客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。
WebSocket根据opcode来区分操作的类型。
WebSocket的每条消息可能被切分成多个数据帧。
当WebSocket的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧。
FIN=1表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。
FIN=0,则接收方还需要继续监听接收其余的数据帧。
opcode在数据交换的场景下,表示的是数据的类型。
0x01表示文本
0x02表示二进制
0x00表示延续帧(continuation frame),完整消息对应的数据帧还没接收完。
客户端向服务端两次发送消息,服务端收到消息后回应客户端,
客户端往服务端发送的消息。
第一条消息
FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。
第二条消息
WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。
| Payload Data continued … |
掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:
首先,假设:
• original-octet-i:为原始数据的第i字节。
• transformed-octet-i:为转换后的数据的第i字节。
• j:为i mod 4的结果。
• masking-key-octet-j:为mask key第j字节。
算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。
有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。
• 发送方->接收方:ping
• 接收方->发送方:pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。
Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基础的防护,减少恶意连接、意外连接。
作用:
WebSocket协议中,数据掩码的作用是增强协议的安全性。
为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。
摘自2010年关于安全的一段讲话。其中提到了代理服务器在协议实现上的缺陷可能导致的安全问题。
“We show, empirically, that the current version of the WebSocket consent mechanism is vulnerable to proxy cache poisoning attacks. Even though the WebSocket handshake is based on HTTP, which should be understood by most network intermediaries, the handshake uses the esoteric “Upgrade” mechanism of HTTP [5]. In our experiment, we find that many proxies do not implement the Upgrade mechanism properly, which causes the handshake to succeed even though subsequent traffic over the socket will be misinterpreted by the proxy.”
[TALKING] Huang, L-S., Chen, E., Barth, A., Rescorla, E., and C.
Jackson, “Talking to Yourself for Fun and Profit”, 2010,
下参与者:
• 攻击者、攻击者自己控制的服务器(简称“邪恶服务器”)、攻击者伪造的资源(简称“邪恶资源”)
• 受害者、受害者想要访问的资源(简称“正义资源”)
• 受害者实际想要访问的服务器(简称“正义服务器”)
• 中间代理服务器
攻击步骤一:
对数据载荷进行掩码处理。
限制了浏览器对数据载荷进行掩码处理,但是坏人完全可以实现自己的WebSocket客户端、服务端,不按规则来,攻击可以照常进行。
但是对浏览器加上这个限制后,可以大大增加攻击的难度,以及攻击的影响范围。如果没有这个限制,只需要在网上放个钓鱼网站骗人去访问,一下子就可以在短时间内展开大范围的攻击。
使用springboot的websocket功能首先引入springboot组件。
org.springframework.boot
spring-boot-starter-websocket
1.3.5.RELEASE
springboot的高级组件会自动引用基础的组件,像spring-boot-starter-websocket就引入了spring-boot-starter-web和spring-boot-starter,所以不要重复引入。
首先要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。
注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@ServerEndpoint(value = “/websocket”)
@Component
public class MyWebSocket {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
/**
/**
//群发消息
for (MyWebSocket item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
/**
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
MyWebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
MyWebSocket.onlineCount–;
}
}
使用springboot的唯一区别是要@Component声明下,而使用独立容器是由容器自己管理websocket的,但在springboot中连容器都是spring管理的。
虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
要创建WebSocket,先实例一个WebSocket对象并传入要连接的URL:
var socket = new WebSocket(‘http://localhost:8000’);
执行上面语句后,浏览器会马上尝试创建连接,与XHR类似,WebSocket也有一个表示当前状态的readyState属性。不过,这个属性的值与XHR不相同, socket.readyState值如下:
• 0:正在建立连接, WebSocket.OPENING
• 1:已经建立连接, WebSocket.OPEN
• 2:正在关闭连接, WebSocket.CLOSING
• 3:已经关闭连接, WebSocket.CLOSE
WebSocket没有readystatechange事件,不过,有其他事件对应着不同的状态,readyState的值永远从0开始。
示例如下:
var socket = new WebSocket(‘ws://localhost:8000’);
//正在建立连接
console.log("[readyState]-" + socket.readyState); //0
//连接建立成功回调
socket.onopen = function() {
console.log(‘Connection established.’)
console.log("[readyState]-" + socket.readyState); //1
//发送消息
// socket.send(‘hello world’);
};
//连接失败回调
socket.onerror = function() {
console.log("[readyState]-" + socket.readyState);//3
console.log(‘Connection error.’)
};
//连接关闭回调
socket.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
console.log("[readyState]-" + socket.readyState);//3
console.log(‘Connection closed.’)
console.log(code, reason, wasClean)
};
要关闭WebSocket连接,可以在任何时候调用close方法。
socket.close();
调用了close()之后,readyState的值立即变为2(正在关闭),关闭连接后就会变成3。
WebSocket连接建立之后,可以通过连接发送和接收数据。
使用send()方法像服务器发送数据,如下:
var socket = new WebSocket(‘ws://localhost:8000’);
socket.send(‘hello world’);
当服务器向客户端发来消息时,WebSocket对象会触发message事件。这个message事件与其他传递消息的协议类似,也是把返回的数据保存在event.data属性中。
socket.onmessage = function(event) {
var data = event.data;
//处理数据
};
WebSocket对象还有其他三个事件,在连接生命周期的不同阶段触发。
• open:成功建立连接时触发。
• error:发生错误时触发,连接断开。
• close: 连接关闭时触发。
var socket = new WebSocket(‘ws://localhost:8000’);
socket.onopen = function() {
console.log(‘Connection established.’)
};
socket.onerror = function() {
console.log(‘Connection error.’)
};
socket.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
console.log(‘Connection closed.’)
};
这三个事件中,只有close事件的event对象有额外信息,这个事件的事件对象有三个额外的属性:wasClean、code和reason。
其中wasClean是一个布尔值,表示连接是否已经明确的关闭;
code是服务器返回的数值状态码;
reason是一个字符串,包含服务器发回的信息。