WebSocket——为Web应用带来桌面应用般的灵活性【转载+整理】

原文地址java

本文内容

  • WebSocket 简介
  • 浏览器端的 JavaScript 实现
  • Java 端的 WebSocket 实现
  • 对 Web 应用的从新思考
  • 使用WebSocket时所需注意的要点
  • WebSocket与RESTful的比较
  • 文件上传的示例
  • 实现 Websocket 的浏览器
  • 实现 Websocket 协议服务器端项目
  • 参考资料

即时通信,在 Web 早先开发就有,那时最多见的实现手段是轮询(polling)。轮询是在某个时间间隔(如1秒),由浏览器向服务器发出 HTTP request,而后服务器返回最新的数据给客户端的浏览器。这种方式最明显的缺点,是浏览器须要不断的向服务器发出请求,即使都没有消息了,并且,HTTP request 的 header 很长,实际有用数据可能只是一个很小的值,无形中占用带宽。以后,出现了 Comet,但这种技术必定程度上只是模拟全双工通讯,效率较低,并须要服务器有较好的支持。git

(数据通讯中,数据在线路上的传送方式能够分为单工通讯、半双工通讯和全双工通讯三种,具体不解释,好比,遥控器是单工的,对讲机是半双工的,电话是全双工的。)github

WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通讯(full-duplex),能更好的节省服务器资源和带宽并达到实时通信。浏览器经过 Http 仅能实现单向通讯,轮询也好,comet 也罢,都不是很理想;flash 中的 socket 和 xmlsocket 能够实现真正的双向通讯。经过 flex ajax bridge,能够在 Javascript 中使用这两项功能。若是 Websocket 一旦在浏览器中获得实现,将取代上面两项技术,获得普遍的使用。web

Websocket Demo

早期的Web技术都是基于HTTP协议而发展起来的,而HTTP只是一个简单的基于请求 —— 响应操做的协议,全部的请求都是由客户端发起的。这套框架本来足以知足用户的需求,但在现在开发者所设计的web应用中,由客户端发起通讯这种方式有着很大的制约。虽然人们提出了各类临时方案,但它们都是基于HTTP协议的,只是应用了轮询或长轮询技术(例如Comet)。Comet可以让负责处理请求的线程获得释放,以防止服务器资源耗尽。因为轮询这种机制并不可靠,所以在2007年时,有人提出了一种名为WebSocket的全双工(full- duplex)类型的通讯方式。这项提议用了整整4年的时间才成为一个标准。可是,尽管它已成为一种标准,但它的使用率却至关有限。本文将为读者解释妨碍 WebSocket应用的两大缘由,而且提出了一个设计框架,开发者可使用这套框架快速地发挥WebSocket的潜能,而且极大地丰富应用的体验。 ajax

致使 WebSocket 使用率低下的第一个缘由在于应用服务器与浏览器对其支持不足。但随着新一代应用服务器与浏览器的出现,这种情况获得了很大的改善。而第二个缘由比起前一点来讲其影响更大,亦即要充分利用WebSocket的所有潜能,必须对Web应用进行颠覆性的从新设计。而这个从新设计过程须要将基础的请求 —— 响应这一结构转变为更复杂的双向消息传递结构。应用程序的从新设计每每是一个开销很大的过程,而软件供应商很难从这一过程当中看到任何显著的利益。 apache

咱们首先将对WebSocket作个简单的介绍,随后展现一种使用WebSocket从新构建应用程序的方法,最后经过一个简单的示例表现这一方法的各类要点。 编程

WebSocket 简介


WebSocket 是在 TCP/IP 协议之上建立的一种帧协议,客户端经过向服务器发送一种特殊的 HTTP 请求来启动 WebSocket。在最初的握手过程以后,客户端与服务器就可以自由地以异步方式互相进行帧的传送了。帧分为两种类型:即控制帧与数据帧。最小的控制帧仅有2比特的大小,客户端最小的数据帧为6比特,服务端最小的数据帧为2比特。数据帧既能够是文本型,也能够是二进制的。文本帧都通过了UTF-8的编码。帧能够实现分块,所以一个大数据集能够分解为多个帧。WebSocket不会为帧附加任何标识信息,所以不一样类型的信息对应的帧不可混用。只有控制帧可以在处理一个大消息时的一系列中间帧中出现。在这些基础的帧之上,还能够定义更复杂的协议。比方说,一个帧可以带有校验和或是它的序列号等相关信息。 api

WebSocket API


WebSocket 并不限定于仅在某个特定的编程语言、系统或是操做系统中使用。多数主流的编程语言以及许多浏览器都已开始支持WebSocket 的编程。虽然在不一样的平台与编程语言中存在着大量的标准,但本文仅关注JavaScript HTML5以及Java(J2EE)对WebSocket的支持。在浏览器这方面有两种实现标准,其最新版本分别为Hixie-76和HyBi-17(不久以后发展为IETF RFC 6455)。HyBi的实现相对更高级,而且获得了目前全部主流浏览器的支持。而在服务端方面,基于Java的实现则是目前最为流行的。早些时候在 Java上曾经出现过几种WebSocket的实现,它们以后已发展为JSR 356这种实现。JSR表明Java规范请求,对规范请求的说明有帮于让以后的各类实现保持一致性,而且易于使用。JSR也让开发者没必要依赖于某个特定的实现。JSR 356与servlet规范是相互分离的,但它也容许开发者访问某些servlet对象。JSR 356的内容涵盖了WebSocket链接的客户端与服务端, 咱们稍后的讨论将集中于配合浏览器端的JavaScript所实现的服务端。JSR 356目前属于J2EE 7的一部分,全部流行的开源Java应用服务器都支持它,包括Tomcat、Jetty、Glassfish以及TJWS等等。除此以外,在Java环境中还存在着大约20种各自独立的WebSocket服务端解决方案,其中有些方案也支持JSR 356。因为WebSocket是J2EE 7的一部分,于是在由Oracle与IBM所推出的商业应用服务器上一样也获得支持。 数组

正如我以前所说,WebSocket是一种消息传递协议。它的API提供了各类在通讯双方进行消息传递与接收的方法。这里并不存在经典的订阅者与发布者的关系。消息只有两种类型,即文本型与二进制型。不过,在这些类型的消息处理函数中能够对消息进行逻辑上的分离。在Java中可以以某种方式处理被分解为多个块的部分消息,而JavaScript还没有支持这种程度的控制能力。如同以前所说,WebSocket是一种很是泛用的协议,它能够在握手时指定所需的逻辑子协议。当不一样的系统可以验证所连到的系统支持这种逻辑子协议及扩展时,使用WebSocket进行系统集成就变得容易不少。 WebSocket帧格式容许在它的基础上使用可协商的扩展,这与意味着通常来讲帧可能会提供更多的信息,而且可能会引入不一样的帧类型。 浏览器

浏览器端的 JavaScript 实现


因为WebSocket协议的握手过程是由客户端发起的,所以须要经过包含了WebSocket接口的JavaScript代码对全部WebSocket操做进行封装。

该接口已经实现了标准化1,并经过接口定义语言(IDL)进行定义,如如下代码所示:

[Constructor(in DOMString url, in optional DOMString protocols)]
[Constructor(in DOMString url, in optional DOMString[] protocols)]
interface WebSocket {
  readonly attribute DOMString url;
  // ready state
  const unsigned short CONNECTING = 0;
  const unsigned short OPEN = 1;
  const unsigned short CLOSING = 2;
  const unsigned short CLOSED = 3;
  readonly attribute unsigned short readyState;
  readonly attribute unsigned long bufferedAmount;
 
  // networking
           attribute Function onopen;
           attribute Function onmessage;
           attribute Function onerror;
           attribute Function onclose;
  readonly attribute DOMString protocol;
  void send(in DOMString data);
  void close();
};
WebSocket implements EventTarget;

WebSocket的构建函数包含两个参数:

  • WebSocket的URL
  • 必要的子协议的数组或单个元素,这一参数是可选的

WebSocket的URL都是以“ws”为前两个字符,它表明所使用的是WebSocket协议,而其他部分与HTTP协议中的URL相同,包括主机、端口、路径以及查询字符串。若是须要使用安全链接,能够在协议名称上加一个额外的“s”字符。

能够指定的消息处理函数共有四种:onopen、onmessage、onclose和onerror。在传递消息时须要调用send方法,而在关闭链接时则须要调用close方法。因为不存在相似于connect这样的方法,所以客户端必须监听onopen消息,以确认链接已创建,随后才可以进行send操做。另外一种选择是对WebSocket对象的readyState属性进行轮询,但这种方式并不推荐使用。显然,在onmessage处理函数中老是可以调用send操做的。send操做由客户端异步执行,这也意味着JavaScript在将消息传递给接收者的过程当中无须等待其结果,而是直接返回。文本消息或二进制消息在接收时不存在任何差异,所以在onmessage处理函数中必须对事件的data参数进行检查。WebSocket提供了一些属性,可用于获取状态、判断二进制消息的格式等目的。而其它浏览器厂商的特定实现中还能够包含更多的属性,所以请记得仔细阅读浏览器的文档,以了解详细的信息。

Java 端的 WebSocket 实现


Java中的JSR 356定义了常见的(客户端)与服务端的Java WebSocket通讯API。在Java的实现中会指定终结点与服务端终结点对象,这与JavaScript中的WebSocket实现颇为相似。能够经过注解的方式将某个Java类指定为一个终结点对象,而经过OnOpen、OnMessage、OnError和 OnClose等注解信息指定事件处理函数。在每种类型的处理函数中,均可以将重要的Session对象做为一个传入参数。Session对象让开发者可以访问发送消息的功能,而且可以保持与WebSocket链接相关的状态特性。消息的发送可使用同步或异步机制,而且在两种类型的发送机制中均可以指定超时时间。经过指定相应的解码器,二进制与文本数据都可以自动转换为任意的Java对象,而编码器则容许WebSocket发送任意类型的Java对象。对于某个特定的WebSocket URL路径,消息处理函数只能对应文本消息类型或二进制消息类型的其中一种。Java中未提供消息链的功能,但也能够经过编程的方式对其进行组织。 Java端的API很容易上手,它提供了一种可自定义的配置对象,可以影响最初的握手过程,决定所支持的子协议、版本,而且提供访问重要的servlet 对象API的功能。终结点不只可以经过注解的方式进行部署,也可以经过编程的方式所生成。

对 Web 应用的从新思考


WebSocket对于如下类型的应用程序的开发是一种很是天然的选择:

  1. 须要玩家之间实时协做的游戏
  2. 实时监控系统
  3. 须要用户进行协做的系统,例如聊天、共享文档的编辑等等。

其实,WebSocket在传统的Web应用中也可以展示其优点。大多数Web应用都是基于请求 - 响应这一范式进行设计的。虽然AJAX可以实现异步操做,但在继续处理下一步操做之间,仍然必须等待响应返回。而因为WebSocket链接只需创建一次,从而避免了为每次数据交换重建链接的过程,而且在后续的通讯中也无需发送多余的HTTP头信息。这种优点在SSL类型的链接上体现得尤其明显,由于最初的链接握手是一个开销很大的操做。浏览器端的WebSocket发送操做是彻底异步的,而Java的服务端代码在发送消息后无需进行等待。因为发送消息的这种自由度,在应用中或许须要对某些操做进行手动记录,以保持应用状态的一致性。在使用WebSocket时也可以模拟请求 - 响应这一范式,但如此一来,WebSocket做为一种真正的异步双向消息传递系统的优点也被大大消减了。因为以上所描述的这些特性,所以应当鼓励开发者在某些场景中对应用程序的设计方式进行从新思考。

假设某一个应用程序包含了复杂的用户界面,其中某些区域的功能须要经过服务端的大量计算才可以生成对应的内容。传统的基于AJAX的实现方式能够选择一种延迟调用的机制,经过某个内容请求调用以生成这一区域的内容。而在使用WebSocket的场合下,服务端能够在浏览器作好准备的状况下直接发送内容,而无需对某个AJAX请求进行响应。AJAX请求这一方式的缺陷在于,因为浏览器所发送的请求是串行的,所以服务端的处理过程没法针对请求的顺序进行相应的优化。而WebSocket为服务端提供了一个自行决定最佳的内容生成方式的机会,于是可以提高Web应用的总体响应性。

要用效地利用WebSocket的功能,还须要仔细考虑几个额外的要点。因为在WebSocket中随时可能出现网络链接的丢失,使数据没法正确地传递,所以对于一些相当重要的数据须要进行一些额外的手动记录操做。通常来讲,所收到的每条消息都必须提供足够的信息,以指示如何对其进行处理。但没有有效的手段可以了解信息的请求者是谁,是来自客户端的请求,仍是说服务端想要更新某些内容。在具体使用WebSocket的过程当中,可能须要对 Web应用的设计进行更深刻的从新思考。此外,JavaScript代码的功能能够迁移至服务端,打个比方,用户的输入能够当即发送给服务端进行处理,经过这种方式可以实现一些复杂的数据校验操做,而这些校验功能或许是JavaScript所没法处理的。用户的输入还可以即时地保存在后台系统中,所以浏览器就无需将最终的数据传递给服务器进行额外的数据校验,由于数据在保存在后台期间已经通过了校验。若是要使某个应用从富Web客户端转为一种轻量级的客户端,就能够考虑以这种方式增长服务端代码的职责。

使用WebSocket时所需注意的要点


在Web应用开发时使用WebSocket也会面对一些特别的挑战,WebSocket的Session与HTTP的Session之间并没有任何关联,虽然也可将其用做相似的目标。在Session中能够附加某些通用的数据,所以全部的消息处理过程均可以依赖于Session中所维护的某些状态和数据。WebSocket的Session也能够根据空闲(不活跃)时间间隔的配置产生超时状况,正如HTTP Session同样。不过有些系统会自动地持续发送Ping这一控制消息,以防止出现超时。JSR 356建议将HTTP Session与WebSocket Session的超时进行同步。一旦HTTP Session超时,在其范围内所建立的全部WebSocket链接也都必须关闭。但有些Web应用的设计不会产生任何HTTP Session,而有些应用的Session超时不依赖于HTTP Session,而是由JavaScript所管理的,所以这种机制并不可以进行可靠的推广。

另外一种须要注意的要点在于,某些浏览器会维护一个链接池,以重用链接的方式访问相同的网站,所以这种流程能够被串行化。而若是浏览器为 WebSocket链接也建立一个链接池,那么它会受到严重的制约。由于若是没有某种机制保持WebSocket链接的关闭,这个链接就永远处于活跃状态,而其它任何建立新链接的尝试都会产生死锁。所以,最佳实践的推荐作法是只使用一个WebSocket链接。

浏览器没法对经过WebSocket进行传递的数据进行缓存,所以经过WebSocket传递能够在浏览器中缓存的资源
(例如图片、CSS等)并不是一种有效的途径。

WebSocket与RESTful的比较


在网络上对于RESTful与WebSocket之间的讨论从未停歇2。不过,这些比较中的大部分都不是在一个层面上的比较,比如关公战秦琼。REST是指表述性状态转换,多数状况下它须要依赖底层的HTTP协议实现,也就是说REST是一个基于请求 - 响应的协议。REST这种风格没有通过标准化,所以任何一种经过HTTP进行通讯的方式在某些范围内均可以称为REST。REST一般会将新增、读取、更新和删除操做(CRUD)与对应的HTTP方法PUT、GET、DELETE之间创建映射关系。而WebSocket所处理的是消息,所以对于单一的 RPC来讲不存在一个肯定的范围。REST的通讯数据格式一般仅限JSON格式以及请求参数,而一个WebSocket消息体能够表现为任何类型,包括纯粹的二进制数据3

固然,WebSocket也可以用于与REST类似的目的,但在大多数状况下,这种作法有些刻意为之了。正如上文所述,在使用WebSocket过程当中须要应用一些不一样的设计原则。下表描述了这二者之间的主要区别4

WebSocket

REST

已实现标准化

获得普遍支持

异步消息传递

(同步)请求/响应

基于帧

基于HTTP方法(get,put,delete,post)

子协议

可发现的操做

二进制与文本

目前大都为 JSON 数据

并行双向更新

目前大都为 CRUD 操做(Create、Retrieve、Update 和 Delete)

文件上传的示例


如下示例展示了如何经过使用WebSocket将一个文件上传至服务器,首先最好定义一个服务端的终结点。

@ServerEndpoint("/upload/{file}")
public class UploadServer {

其中要定义两个消息处理函数,一个用于接收上传文件的二进制数据,而另外一个则用于命令接口。因为在WebSocket中容许分离文本与二进制消息,所以在定义两个处理函数时无需进行额外的操做。用于接收命令的OnMessage处理函数定义以下:

@OnMessage
public void processCmd(CMD cmd, Session ses) {

CMD类的定义以下

static class CMD {
        public int cmd;
        public String data;
    }

为了将文本消息转换为CMD对象,须要指定一个解码器,其定义以下:

public static class CmdDecoder implements Decoder.Text<CMD> {

将文本信息编码为JSON格式并非一种强制性的要求,只是在这个示例中须要用到JSON。大文件的上传是分多个块进行的,以减小内存的开销。在浏览器中没法利用WebSocket的部分帧,所以须要用到完整的帧来模拟块传送。因为浏览器以异步的方式发送全部的消息,所以没法得知服务端是否已经接收到了一个完整的文件。命令接口的做用是完成如下工做:

  1. 通知服务器上传即将开始,而且为上传文件设定一个名称
  2. 通知服务器已上传了一个完整的文件
  3. 向客户端发送确认,表示文件已成功地保存了

一样的CMD对象能够进行重用,以知足各类需求。传入的命令是按照如下方式进行处理的:

@OnMessage
    public void processCmd(CMD cmd, Session ses) {
        switch (cmd.cmd) {
        case 1: // start
            fileName = cmd.data;
            break;
        case 2: // finish
            close(ses);
            cmd.cmd = 3;
            ses.getAsyncRemote().sendObject(cmd);
            break;
        }
    }

这种实现方式假设浏览器端会将全部发送消息的活动进行串行化,即全部消息的到达顺序与发送顺序是一致的。可是若是某个客户端使用了某些并行方式进行发送,那么就须要一种更为复杂的实现方式,让每一个所发送的消息都带有一个ID。另外一种方案是为每一个收到的文件块都发送一次确认消息,只是这样一来WebSocket的优点也就丧失殆尽了。因为CMD对象的目标是将消息发送至客户端,所以必须提供一个编码器:

public static class CmdEncoder implements Encoder.Text<CMD> {

在ServerEndpoint的注解中必须指定解码器与编码器信息,以下所示:

@ServerEndpoint(value = "/upload/{file}", decoders = UploadServer.CmdDecoder.class, encoders=UploadServer.CmdEncoder.class)
public class UploadServer {

二进制消息的处理函数定义以下:

@OnMessage
public void savePart(byte[] part, Session ses) {
    if (uploadFile == null) {
        if (fileName != null)
            try {
                uploadFile = new RandomAccessFile(fileName, "rw");
            } catch (FileNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                return;
            }
    }
    if (uploadFile != null)
    try {
        uploadFile.write(part);
        System.err.printf("Stored part of %db%n", part.length);
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

此外还能够为OnClose事件加入一个处理函数,万一出现链接异常关闭的状况,它将负责删除不完整的文件。

客户端的实现利用了HTML5中的工做线程(Worker)功能,不幸的是,Firefox没有采用在Worker中实现文件对象克隆的方式,所以这个示例只能在IE或Chrome中进行测试。若是该解决方案对于浏览器的可移植性有很高的要求,那么能够用一个不使用Worker的 JavaScript代码段来代替这个基于Worker的解决方案。但因为未使用独立的线程(即Worker),所以这种方案的性能会有所降低。 Worker的代码以下所示:

var files = [];
var endPoint = "ws" + (self.location.protocol == "https:" ? "s" : "") + "://"
        + self.location.hostname
        + (self.location.port ? ":" + self.location.port : "")
        + "/echoserver/upload/*";
var socket;
var ready;
function upload(blobOrFile) {
    if (ready)
        socket.send(blobOrFile);
}
 
function openSocket() {
    socket = new WebSocket(endPoint);
 
    socket.onmessage = function(event) {
        self.postMessage(JSON.parse(event.data));
    };
 
    socket.onclose = function(event) {
        ready = false;
    };
 
    socket.onopen = function() {
        ready = true;
        process();
    };
}
 
function process() {
    while (files.length > 0) {
        var blob = files.shift();
        socket.send(JSON.stringify({
            "cmd" : 1,
            "data" : blob.name
        }));
        const
        BYTES_PER_CHUNK = 1024 * 1024 * 2;
        // 1MB chunk sizes.
        const
        SIZE = blob.size;
 
        var start = 0;
        var end = BYTES_PER_CHUNK;
 
        while (start < SIZE) {
 
            if ('mozSlice' in blob) {
                var chunk = blob.mozSlice(start, end);
            } else if ('slice' in blob) {
                    var chunk = blob.slice(start, end);
            } else {
                var chunk = blob.webkitSlice(start, end);
            }
 
            upload(chunk);
 
            start = end;
            end = start + BYTES_PER_CHUNK;
        }
        socket.send(JSON.stringify({
            "cmd" : 2,
            "data" : blob.name
        }));
        //self.postMessage(blob.name + " Uploaded Succesfully");
    }
}
 
self.onmessage = function(e) {
    for (var j = 0; j < e.data.files.length; j++)
        files.push(e.data.files[j]);
 
    //self.postMessage("Job size: "+files.length);
 
    if (ready) {
        process();
    } else
        openSocket();
}

很方便的一点在于,与Worker进行交互的JavaScript代码也可以利用消息传递机制。当用户在浏览器中选择文件进行上传时,这一操做的信息就会传递给Worker。后者会以批量的方式处理第一个准备上传的文件,它将文件分红多个片断,即多个块,而后经过WebSocket将这些块依次上传。最后发送一个cmd = 2的命令消息。而命令消息的处理函数会将消息从新发送给主JavaScript代码,通知所上传的文件已经完成了。若是客户端选择上传许多大文件,那么这段代码会对浏览器端带来至关大的压力。为此须要对代码进行从新调整,让它在收到上一个文件上传成功的消息后才继续上传下一个文件。这部份内容的修改就留给各位读者做为一个练习吧。在附录1中能够找到本示例的完整源代码。

实现 Websocket 的浏览器


Chrome Supported in version 4+
Firefox Supported in version 4+
Internet Explorer Supported in version 10+
Opera Supported in version 10+
Safari Supported in version 5+

实现 Websocket 协议服务器端项目


jetty 7.0.1 包含了一个初步的实现
resin  
pywebsocket apache http server 扩展
apache tomcat 7.0.27 版本
Nginx 1.3.13 版本
jWebSocket java实现版

参考资料


 

Websocket Demo

相关文章
相关标签/搜索