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依赖

javax.websocket

javax.websocket-api

1.1

provided

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依赖。

前端链接与交互代码:

websocket Demo---- user000

这个案例的基本实现是客户端向服务端发送任意消息,服务端能接受到该消息,然后返回一条固定信息。

也可以验证发送的请求为websocket:

参考文章有以下文章,感谢作者大大!

万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践

新手快速入门:WebSocket简明教程

websocket之三:Tomcat的WebSocket实现

java WebSocket开发入门WebSocket

推荐阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。