WebSocket
1.1、WebSocket协议简介
英文名称:WebSocket
开发组织:IETF
标准编号:RFC6455RFC7936
所属层次:应用层

WebSocket协议是一种在单个TCP连接上进行全双工通信的协议。 全双工就是指客户端和服务端可以同时进行双向通信,强调同时、双向通信、互不干扰。

WebSocket协议改变了HTTP协议的由客户端发送请求,服务端进行响应的模型。

WebSocket协议是目前唯一真正实现全双工通信的协议,与长连接和轮询技术相比,WebSocket的优越性不言自明,长连接的连接资源(线程资源)随着连接数量的增多,必会耗尽,客户端轮询会给服 务器造成很大的压力,而WebSocket是在物理层非网络层建立一条客户端至服务器的长连接,以此来保证服务器向客 户端的即时推送,既不耗费线程资源,又不会不断向服务器轮询请求。

1.1.1、请求握手

WebSocket协议的握手使用的是HTTP协议。

下面是一个请求握手的示例:

GET /xx HTTP/1.1
Pragma: no-cache
Cache-Control: no-cache
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: WBQn4/IgSnD3KjrxvvJpbg==
Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: upafRZxTkaMPUBSr9VvuDXRambA=
Sec-WebSocket-Version: 13

对于请求握手需要注意如下:

  • HTTP协议必须是HTTP1.1或者更高版本。
  • 必须以GET方式进行请求。
  • 必须包含请求头Connection: Upgrade,告诉服务器,要进行协议切换。
  • 必须包含请求头Upgrade: websocket表明要切换到WebSocket协议。
  • 必须包含请求头Sec-WebSocket-Version告诉服务器客户端使用的WebSocket协议版本号, 目前是固定的数字13, 也就是RFC6455定义的。
  • 必须包含请求头Sec-WebSocket-Key,其取值的生成算法如下:
    Sec-WebSocket-Key = BASE64(UUID(random()))
  • 必须包含响应头Sec-WebSocket-Accept,其取值的生成算法如下:
    Sec-WebSocket-Accept = BASE64(SHA1((Sec-WebSocket-Key) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
    客户端得到Sec-WebSocket-Accept的值后,对请求头Sec-WebSocket-Key进行同样的编码,然后对比,如果相同则可以进行后续处理。
  • 如果协议切换成功,服务端以HTTP状态码101进行响应。

握手成功后,就与HTTP协议没有关系了。直接在TCP上以二进制数据进行双向通信。

1.1.2、URL中的协议

如果是通过HTTP协议升级而来的,那么一般是ws://开头。

如果是通过HTTPS协议升级而来的,那么一般是wss://开头。

1.1.3、帧

帧是WebSocket协议进行一次数据传输的单位,官方提供了一个结构图,如下:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

帧是二进制的,也就是以bit为单位定义数据。这些bit有的以1个, 有的以多个连续的bit组合在一起表示数据。通常都给他们起一个名字,方便人们进行指代。

1.1.3.1、FIN

帧以FIN开始,用来指明这一帧是不是最后一个分片(消息很大的情况下,要把大消息分成很多帧进行传输),

FIN1bit,由于只占一个bit,它要么是1,要么是01就表示该帧是最后一帧,0就表示不是最后一帧。如果这个消息没有进行分片传输,那么这唯一的一帧就是最后一帧。

1.1.3.2、RSV1、RSV2、RSV3

RSV1RSV2RSV3三个各占1bit

RSV1RSV2RSV3是用来进行扩展该协议的,如果没有扩展,正常情况下都为0

1.1.3.3、MASK

MASK1bit,由于只占一个bit,它要么是1,要么是01就表示该帧经过了掩码变换,0就表示该帧没有经过了掩码变换。

协议规定:客户端发给服务端的帧,必须进行掩码变换;而服务端发给客户端的帧则必须不进行掩码变换。

1.1.3.4、Masking-key

Masking-key就是掩码。

如果MASK被设置为1Masking-key4bytes,否则该块不存在。

如果掩码存在,那么所有数据(Payload Data)都需要与掩码做一次异或运算,所有数据依此与4bytes的掩码进行疑惑运算。 如果不存在掩码,那么后面的数据就可以直接使用。

1.1.3.5、Payload len

Payload len表示数据(Payload Data)的长度,单位:字节(byte)。

Payload len7bit,这7bit可以表示128个无符号整数。 也就是Payload len的取值范围是0 ~ 127。显然,最大如果只能表示127个字节,这样的数据也太小了吧。 所以,规定0 ~ 125才表示数据的长度,如果是126,紧挨着Payload len2个字节才表示数据大小; 如果是127,紧挨着Payload len8个字节才表示数据大小。

1.1.3.6、Payload Data

Payload Data才是真正的数据。数据又分为扩展的数据和应用数据。

1.1.3.7、opcode

opcode4bit。这4bit可以表示16个数字。 下面是用十六进制表示的每个值的作用:

取值作用
x0
x1表明这一帧的数据是用UTF-8编码的文本。
x2表明这一帧的数据是二进制的数据。
x3 ~ x7预留的
x8关闭连接
x9用作ping
A用作pong
A ~ F预留的

从上面可以看得出:x8 ~ F之间的值用于控制;而x1 ~ x7之间的值只是表明数据的额外信息的。 据此,把帧分为了控制帧和非控制帧,非控制帧也成为数据帧。控制帧具体分为:close帧、ping帧、pong帧。

对于控制帧,Payload Data部分也是有的,此时,Payload Data用来表示一些额外信息,比如关闭原因等等, 只是,Payload Data不能超过125bytes,而且不能进行分片传输。

1.2、浏览器中的WebSocket API

浏览器中的WebSocket APIW3C进行标准化。参考文档:https://www.w3.org/TR/websockets

1.2.2、var webSocket = new WebSocket(String URL [, String subProtocol])

创建实例。

URL是服务器地址。subProtocol是可选的,一般不填。

示例:

var webSocket = new WebSocket('ws://localhost:8080');
1.2.3、webSocket.onopen = function(event){}

连接成功的回掉函数。

1.2.4、webSocket.onclose = function(event){}

关闭连接的回掉函数。

event对象包含两个属性:

event.code表示错误码。它是无符号整数。

event.reason表示关闭原因。它是字符串类型,这个字符串的长度不超过125byte

1.2.5、webSocket.onerror = function(event){}

出现了错误的回掉。

1.2.6、webSocket.onmessage = function(event){}

正常接收到服务端发来的数据的回掉。

event对象包含1个属性:

event.data表示表示的就是数据(Paydata)。

1.2.7、webSocket.close([int code] [, String reason])

关闭连接。

code参数表示状态码,无符号整数。此值只允许传入1000,或者范围在[3000 ~ 3999]之间的值,传入其他值会发生异常。

如果省略了code参数,就使用缺省值1000

reason参数是字符串,它不能超过123个字节。

这么设计的原因是:协议规定,发送控制帧的时候,数据(Payload)不能超过125个字节。 因为这里把数据(Payload)拆分成了codereason两部分,code占了2个字节,reason就只能占剩下的123个字节了。

示例:

webSocket.close();
webSocket.close(1000, "normal close");
1.2.8、webSocket.send(String | ArrayBuffer | Blob data)

发送数据给服务器。

因为数据既可以是字符串,也可以是二进制的。所以这里的data类型就比较多了。

如果发送的是字符串,一定要注意,其编码必须是UTF-8

示例:

webSocket.send("I Love you");
1.2.9、总示例

浏览器中的WebSocket API的语法非常简单,简单到难以置信。下面是其使用示例:

var webSocket = new WebSocket('ws://localhost:8080');
webSocket.onopen = function(event) {
    socket.send('I am the client and I\'m listening!');

    socket.onmessage = function(event) {
        console.log('onmessage', event);
        if (event.data == "xx") {
            socket.close();
        }
    };

    socket.onerror = function(event) {
        console.log('onerror()', event);
    };

    socket.onclose = function(event) {
        console.log('onclose()', event);
    };
};
1.2.10、浏览器的支持度

到目前为止,浏览器中的WebSocket API已经受到绝大多数现代浏览器的支持。部分老版本的浏览器不支持。 对于不支持WebSocket API的,下面有几个替代方案供你使用:

  • Flash技术 —— Flash可以提供一个简单的替换。 使用Flash最明显的缺点是并非所有客户端都安装了Flash,而且某些客户端,如iPhone/iPad,不支持Flash。
  • AJAX Long-Polling技术 —— 用AJAX的long-polling来模拟WebSocket在业界已经有一段时间了。它是一个可行的技术,但它不能优化发送的信息。也就是说,它是一个解决方案,但不是最佳的技术方案。

参考

为了兼容性考虑,最好不要直接使用WebSocket API,提倡使用第三方开发的库, 因为这些第三方库会在不支持WebSocket API的浏览器上选择其他实现的好的替代方案,省去了我们处理兼容性问题。 另外,WebSocket API并没有主动发送ping帧,而大部分第三方库都处理了,我们就不用管了。

常用的第三方库有:

  • Socket.IO
  • SockJS

他们的使用方法与WebSocket API完全一样,只是他们的实现更完备。

1.3、SockJS

参考

1.4、JSR-356

JSR-356Java EE7中对WebSocket协议的支持。

Tomcat7.0.27版本开始支持JSR-356参考

Jetty9.1版本开始支持JSR-356参考

1.4.1、使用JSR-356创建服务端示例

1、通过mvn archetype:generate命令生成项目骨架。在选择模板的时候,过滤webapp-javaee字样, 会得到org.codehaus.mojo.archetypes:webapp-javaee7 (Archetype for a web application using Java EE 7.), 输入2即可,其他按照提示输入或者选择即可。

生成的项目是使用JavaEE7实现的。工程结构如下:

WebSocket-JSR-356-Server
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── fpliu
        │           └── newton
        └── webapp
            └── index.html

2、创建一个消息实体类:

package com.fpliu.newton.entity;

public final class Message {

    private String username;
    private String message;

    public Message() {
    }

    public Message(String username, String message) {
        this.username = username;
        this.message = message;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

3、创建把对象转换为字符串的类(字符串用JSON格式):

package com.fpliu.newton.entity;

import com.google.gson.Gson;

import javax.websocket.EncodeException;
import javax.websocket.EndpointConfig;

public class MessageEncoder implements javax.websocket.Encoder.Text {

    @Override
    public String encode(Message message) throws EncodeException {
        return new Gson().toJson(message);
    }

    @Override
    public void init(EndpointConfig endpointConfig) {

    }

    @Override
    public void destroy() {

    }
}

4、创建把字符串转换为对象的类(字符串用JSON格式):

package com.fpliu.newton.entity;

import com.google.gson.Gson;

import javax.websocket.DecodeException;
import javax.websocket.EndpointConfig;

public class MessageDecoder implements javax.websocket.Decoder.Text {

    @Override
    public Message decode(String s) throws DecodeException {
        return new Gson().fromJson(s, Message.class);
    }

    @Override
    public boolean willDecode(String s) {
        return true;
    }

    @Override
    public void init(EndpointConfig endpointConfig) {

    }

    @Override
    public void destroy() {

    }
}

5、创建一个Endpoint类:

package com.fpliu.newton;

import com.fpliu.newton.entity.Message;
import com.fpliu.newton.entity.MessageDecoder;
import com.fpliu.newton.entity.MessageEncoder;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@ServerEndpoint(value = "/websocket/xx", encoders = MessageEncoder.class, decoders = MessageDecoder.class)
public class XXEndpoint extends Endpoint {

    private static final Set sessions = Collections.synchronizedSet(new HashSet());

    @Override
    public void onOpen(Session session, EndpointConfig endpointConfig) {
        sessions.add(session);
    }

    @Override
    public void onClose(Session session, CloseReason closeReason) {
        sessions.remove(session);
    }

    @Override
    public void onError(Session session, Throwable throwable) {
        super.onError(session, throwable);
    }

    @OnMessage
    public void onMessage(Message message) throws IOException, EncodeException {
        for(Session session: sessions) {
            session.getBasicRemote().sendObject(message);
        }
    }
}

6、打包:

mvn clean package

7、把target/WebSocket-JSR-356.war放到Tomcatwebapp目录下, 然后重启Tomcat

现在就可以以WebSocket协议进行访问了。URL是:ws://websocket/xx

1.5、Spring对WebSocket协议的支持

Spring4.0以后加入了对WebSocket协议的支持。

1.5.1、使用SpringMVC实现

参考

1.5.2、使用Spring Boot实现

1、通过mvn archetype:generate命令生成项目骨架。在选择模板的时候,过滤websocket字样, 会得到org.springframework.boot:spring-boot-sample-websocket-archetype (Spring Boot WebSocket Sample), 输入1即可,其他按照提示输入或者选择即可。

生成的项目是使用Spring Boot实现的。工程结构如下:

WebSocket-Spring-Boot-Maven
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── samples
    │   │       └── websocket
    │   │           ├── client
    │   │           │   ├── GreetingService.java
    │   │           │   ├── SimpleClientWebSocketHandler.java
    │   │           │   └── SimpleGreetingService.java
    │   │           ├── config
    │   │           │   └── SampleWebSocketsApplication.java
    │   │           ├── echo
    │   │           │   ├── DefaultEchoService.java
    │   │           │   ├── EchoService.java
    │   │           │   └── EchoWebSocketHandler.java
    │   │           └── snake
    │   │               ├── Direction.java
    │   │               ├── Location.java
    │   │               ├── Snake.java
    │   │               ├── SnakeTimer.java
    │   │               ├── SnakeUtils.java
    │   │               └── SnakeWebSocketHandler.java
    │   └── resources
    │       └── static
    │           ├── echo.html
    │           ├── index.html
    │           └── snake.html
    └── test
        └── java
            └── samples
                └── websocket
                    ├── echo
                    │   ├── CustomContainerWebSocketsApplicationTests.java
                    │   └── SampleWebSocketsApplicationTests.java
                    └── snake
                        └── SnakeTimerTests.java

2、构建:

mvn clean package

3、运行服务:

java -jar target/WebSocket-Spring-1.0-SNAPSHOT.jar

4、在浏览器中打开网址:http://localhost:8080进行查看。 里面有两个示例。就是通过WebSocket协议与服务器进行通信的。

1.6、Jetty9 WebSocket API

参考

1.7、OKHttp对WebSocket协议的支持
1.8、Java-WebSocket
1.9、Android客户端实现WebSocket协议

Android客户端可以使用的支持WebSocket协议的库有两个:

1.10、iOS客户端实现WebSocket协议

iOS中支持WebSocket协议的库有两个:

  • Starscream
  • SocketRocket