WebSocket简介
很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
短轮询与长轮询的代码区别:
为了更好的节约资源,并且能够更实时地进行通讯。HTML5 定义了 WebSocket 协议,WebSocket是一种在单个TCP连接上进行全双工通信的协议。具有更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
Websocket 使用 ws 或 wss 的统一资源标志符(URI),其中 wss 表示使用了 TLS 的 Websocket。(相当于http和https的区别)。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
如图XHR Polling(短轮询) 与 WebSocket 之间的区别:
WebSocket 优点:
1)较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小;
2)更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
3)保持连接状态:与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息;
4)更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容;
5)可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。
由于 WebSocket 拥有上述的优点,所以它被广泛地应用在即时通讯/IM、实时音视频、在线教育和游戏等领域
WebSocket API
Websocket的兼容性:
由上图可知:目前主流的 Web 浏览器都支持 WebSocket
在浏览器中要使用 WebSocket 提供的能力,我们就必须先创建 WebSocket 对象,该对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
接下来我们将从以下四个方面来介绍 WebSocket API:
1)WebSocket 构造函数;
2)WebSocket 对象的属性;
3)WebSocket 的方法;
4)WebSocket 事件。
Websocket构造函数
const myWebSocket = new WebSocket(url [, protocols]);
相关参数说明如下:
1)url:表示连接的 URL,这是 WebSocket 服务器将响应的 URL;
2)protocols(可选):一个协议字符串或者一个包含协议字符串的数组。
WebSocket属性
每个属性的具体含义如下:
1)binaryType:使用二进制的数据类型连接;
2)bufferedAmount(只读):未发送至服务器的字节数;
3)extensions(只读):服务器选择的扩展;
4)onclose:用于指定连接关闭后的回调函数;
5)onerror:用于指定连接失败后的回调函数;
6)onmessage:用于指定当从服务器接受到信息时的回调函数;
7)onopen:用于指定连接成功后的回调函数;
8)protocol(只读):用于返回服务器端选中的子协议的名字;
9)readyState(只读):返回当前 WebSocket 的连接状态,共有 4 种状态:
– CONNECTING — 正在连接中,对应的值为 0;
– OPEN — 已经连接并且可以通讯,对应的值为 1;
– CLOSING — 连接正在关闭,对应的值为 2;
– CLOSED — 连接已关闭或者没有连接成功,对应的值为 3
10)url(只读):返回值为当构造函数创建 WebSocket 实例对象时 URL 的绝对路径。
WebSocket方法
WebSocket 主要方法有两个:
close([code[, reason]])
:该方法用于关闭 WebSocket 连接,如果连接已经关闭,则此方法不执行任何操作;send(data)
:该方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的数据的大小来增加 bufferedAmount 的值 。若数据无法传输(比如数据需要缓存而缓冲区已满)时,套接字会自行关闭。
WebSocket事件
使用 addEventListener() 或将一个事件监听器赋值给 WebSocket 对象的 oneventname 属性,来监听下面的事件。
以下是几个事件:
- close:当一个 WebSocket 连接被关闭时触发,也可以通过 onclose 属性来设置;
- error:当一个 WebSocket 连接因错误而关闭时触发,也可以通过 onerror 属性来设置;
- message:当通过 WebSocket 收到数据时触发,也可以通过 onmessage 属性来设置;
- open:当一个 WebSocket 连接成功时触发,也可以通过 onopen 属性来设置。
WebSocket实现
- WebSocket实例对象:
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
//创建一个websocket实例对象
var ws = new WebSocket('ws://localhost:8080');
//webSocket.readyState返回实例对象的当前状态
CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
//例子
switch (ws.readyState) {
case WebSocket.CONNECTING:
// do something
break;
case WebSocket.OPEN:
// do something
break;
case WebSocket.CLOSING:
// do something
break;
case WebSocket.CLOSED:
// do something
break;
default:
// this never happens
break;
}
//webSocket.onopen连接成功的回调函数
ws.onopen = function () {
ws.send('Hello Server!');
}
//如果要指定多个回调函数,可以使用addEventListener方法:
ws.addEventListener('open', function (event) {
ws.send('Hello Server!');
});
//webSocket.onclose链接关闭的回调函数
ws.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
};
ws.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
});
//webSocket.onmessage用于指定收到服务器数据后的回调函数
ws.onmessage = function(event) {
var data = event.data;
// 处理数据
};
ws.addEventListener("message", function(event) {
var data = event.data;
// 处理数据
});
//服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)
ws.onmessage = function(event){
if(typeof event.data === String) {
console.log("Received data string");
}
if(event.data instanceof ArrayBuffer){
var buffer = event.data;
console.log("Received arraybuffer");
}
}
//除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型
// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
console.log(e.data.size);
};
// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
console.log(e.data.byteLength);
};
//webSocket.send(),实例对象的send()方法用于向服务器发送数据。
//发送文本
ws.send('your message');
//发送Blob
var file = document
.querySelector('input[type="file"]')
.files[0];
ws.send(file);
//发送 ArrayBuffer 对象的例子
// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var ii = 0; ii < img.data.length; ii++) {
binary[ii] = img.data[ii];
}
ws.send(binary.buffer);
//实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。
var data = new ArrayBuffer(10000000);
socket.send(data);
if (socket.bufferedAmount === 0) {
// 发送完毕
} else {
// 发送还没结束
}
//webSocket.onerror,实例对象的onerror属性,用于指定报错时的回调函数
socket.onerror = function(event) {
// handle error event
};
socket.addEventListener("error", function(event) {
// handle error event
});
WebSocket案例
websocket和servlet非常类似,都是处理前端请求的容器,其本身不能单独存在需要借web服务器。servlet需要借助tomcat服务器,websocket容器也需要借助服务器。
伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。
JavaEE 7中出了JSR-356:Java API for WebSocket规范。不少Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat从7.0.27开始支持 WebSocket,从7.0.47开始支持JSR-356,因此websocket容器需要部署在Tomcat7.0.47以上的版本才能运行。
Java实现websocket容器:
- 创建Maven项目:
- 导入websocket依赖
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
- websocket容器
package com.example;
import java.io.IOException;
import java.util.logging.Logger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
/**
* @Class: Test
* @Description: 简单websocket demo
*/
@ServerEndpoint(value="/websocketTest/{userId}")
public class WsTest {
private Logger logger = Logger.getLogger("WebSocket");
private static String userId;
//连接时执行
@OnOpen
public void onOpen(@PathParam("userId") String userId,Session session) throws IOException{
this.userId = userId;
logger.info("有新的链接!");
System.out.println("新连接:"+userId);
}
//关闭时执行
@OnClose
public void onClose(){
logger.info("有链接关闭!");
System.out.println("连接:"+this.userId);
}
//收到消息时执行
@OnMessage
public void onMessage(String message, Session session) throws IOException {
System.out.println("收到用户"+this.userId+"的消息"+message);
session.getBasicRemote().sendText("收到 "+this.userId+" 的消息 "); //回复用户
}
//连接错误时执行
@OnError
public void onError(Session session, Throwable error){
System.out.println("用户id为:"+this.userId+"的连接发送错误");
error.printStackTrace();
}
}
Endpoint的生命周期
Tomcat自7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356 ),而在7.0.5版本之前(7.0.2版本之后)则采用自定义API,即WebSocketServlet。根据JSR356的规定,Java WebSocket应用由一系列的WebSocket Endpoint组成。Endpoint是一个Java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口,就像Servlet之于HTTP请求一样(不同之处在于Endpoint每个链接一个实例)。
我们可以通过两种方式定义Endpoint
,第一种是编程式,即继承类javax.websocket.Endpoint并实现其方法。第二种是注解式,即定义一个POJO对象,为其添加Endpoint
相关的注解。
Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。Endpoint接口明确定义了与其生命周期相关的方法,规范实现者确保在生命周期的各个阶段调用实例的相关方法。
Endpoint的生命周期方法如下:
- onOpen:当开启一个新的会话时调用。这是客户端与服务器握手成功后调用的方法。等同于注解@OnOpen。
- onClose:当会话关闭时调用。等同于注解@OnClose。
- onError:当链接过程中异常时调用。等同于注解@OnError。
- 当客户端链接到一个Endpoint时,服务器端会为其创建一个唯一的会话
(javax.websocket.Session)
。会话在WebSocket握手之后创建,并在链接关闭时结束。当生命周期中触发各个事件时,都会将当前会话传给Endpoint。- 通过为Session添加
MessageHandler
消息处理器来接收消息。当采用注解方式定义Endpoint时,我们还可以通过@OnMessage指定接收消息的方法。发送消息则由RemoteEndpoint
完成,其实例由Session维护,根据使用情况,我们可以通过Session.getBasicRemote
获取同步消息发送的实例或者通过Session.getAsyncRemote
获取异步消息发送的实例。- WebSocket通过
javax.websocket.WebSocketContainer
接口维护应用中定义的所有Endpoint。它在每个Web应用中只有一个实例,类似于传统Web应用中的ServletContext。
WebSocket加载与处理
通过websocket案例可以得到一个websocket容器,那么tomcat如何加载这个容器的呢?
Tomcat提供了一个javax.servlet.ServletContainerInitializer
的实现类org.apache.tomcat.websocket.server.WsSci
。因此Tomcat的WebSocket加载是通过SCI机制完成的。WsSci可以处理的类型有三种:添加了注解@ServerEndpoint的类、Endpoint的子类以及ServerApplicationConfig
的实现类。
Web应用启动时,通过WsSci.onStartup方法完成WebSocket的初始化:
- 构造
WebSocketContainer
实例,Tomcat提供的实现类为WsServerContainer。在WsServerContainer构造方法中,Tomcat除了初始化配置外,还会为ServletContext添加一个过滤器org.apache.tomcat.websocket.server.WsFilter
,它用于判断当前请求是否为WebSocket请求,以便完成握手。 - 对于扫描到的
Endpoint
子类和添加了注解@ServerEndpoint
的类,如果当前应用存在ServerApplicationConfig实现,则通过ServerApplicationConfig获取Endpoint子类的配置(ServerEndpointConfig实例,包含了请求路径等信息)和符合条件的注解类,将结果注册到WebSocketContainer上,用于处理WebSocket请求。 - 通过
ServerApplicationConfig
接口我们以编程的方式确定只有符合一定规则的Endpoint可以注册到WebSocketContainer,而非所有。规范通过这种方式为我们提供了一种定制化机制。 - 如果当前应用没有定义ServerApplicationConfig的实现类,那么WsSci默认只将所有扫描到的注解式Endpoint注册到WebSocketContainer。因此,如果采用可编程方式定义Endpoint,那么必须添加ServerApplicationConfig实现。
如上图所示,当服务器接收到来自客户端的请求时,首先WsFilter会判断该请求是否是一个WebSocket Upgrade请求(即包含Upgrade: websocket头信息)。如果是,则根据请求路径查找对应的Endpoint处理类,并进行协议Upgrade。
在协议Upgrade过程中,除了检测WebSocket扩展、添加相关的转换外,最主要的是添加WebSocket相关的响应头信息、构造Endpoint实例、构造HTTP Upgrade处理类WsHttpUpgradeHandler
。
将WsHttpUpgradeHandler传递给具体的Tomcat协议处理器(ProtocolHandler)进行Upgrade。接收到Upgrade的动作后,Tomcat的协议处理器(HTTP协议)不再使用原有的Processor处理请求,而是替换为专门的Upgrade Processor。
根据I/O的不同,Tomcat提供的Upgrade Processor实现如下:
- org.apache.coyote.http11.upgrade.BioProcessor;
- org.apache.coyote.http11.upgrade.NioProcessor;
- org.apache.coyote.http11.upgrade.Nio2Processor;
- org.apache.coyote.http11.upgrade.AprProcessor;
替换成功后,WsHttpUpgradeHandler会对Upgrade Processor进行初始化(按以下顺序):
- 创建WebSocket会话。
- 为Upgrade Processor的输出流添加写监听器。WebSocket向客户端推送消息具体由org.apache.tomcat.websocket.server.WsRemoteEndpointImplServer完成。
- 构造WebSocket会话,执行当前Endpoint的onOpen方法。
- 为Upgrade Processor的输入流添加读监听器,完成消息读取。WebSocket读取客户端消息具体由org.apache.tomcat.websocket.server.WsFrameServer完成。
通过这种方式,Tomcat实现了WebSocket请求处理与具体I/O方式的解耦。
websocket整个的处理案例如下:
websocket后台容器:
package com.example;
import java.io.IOException;
import java.util.logging.Logger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
/**
* @Class: Test
* @Description: 简单websocket demo
*/
//必须要添加该注解定义实现类
@ServerEndpoint(value="/websocketTest/{userId}")
public class WsTest {
private Logger logger = Logger.getLogger("WebSocket");
private static String userId;
//连接时执行
@OnOpen
public void onOpen(@PathParam("userId") String userId,Session session) throws IOException{
this.userId = userId;
logger.info("有新的链接!");
System.out.println("新连接:"+userId);
}
//关闭时执行
@OnClose
public void onClose(){
logger.info("有链接关闭!");
System.out.println("连接:"+this.userId);
}
//收到消息时执行
@OnMessage
public void onMessage(String message, Session session) throws IOException {
System.out.println("收到用户"+this.userId+"的消息"+message);
session.getBasicRemote().sendText("收到 "+this.userId+" 的消息 "); //回复用户
}
//连接错误时执行
@OnError
public void onError(Session session, Throwable error){
System.out.println("用户id为:"+this.userId+"的连接发送错误");
error.printStackTrace();
}
}
注意后台不要忘了映入maven依赖。
前端链接与交互代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
websocket Demo---- user000 <br />
<input id="text" type="text" />
<button onclick="send()"> Send </button>
<button onclick="closeWebSocket()"> Close </button>
<div id="message"> </div>
<script type="text/javascript">
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
websocket = new WebSocket("ws://localhost:8080/demo/websocketTest/user00");
console.log("link success")
}else{
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(event){
setMessageInnerHTML("open");
}
console.log("-----")
//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//关闭连接
function closeWebSocket(){
websocket.close();
}
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</body>
</html>
这个案例的基本实现是客户端向服务端发送任意消息,服务端能接受到该消息,然后返回一条固定信息。
也可以验证发送的请求为websocket:
参考文章有以下文章,感谢作者大大!
万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/156233.html