英文名称: | WebSocket |
开发组织: | IETF |
标准编号: | RFC6455、RFC7936 |
所属层次: | 应用层 |
WebSocket
协议是一种在单个TCP连接上进行全双工通信的协议。 全双工就是指客户端和服务端可以同时进行双向通信,强调同时、双向通信、互不干扰。
WebSocket
协议改变了HTTP协议的由客户端发送请求,服务端进行响应的模型。
WebSocket
协议是目前唯一真正实现全双工通信的协议,与长连接和轮询技术相比,WebSocket
的优越性不言自明,长连接的连接资源(线程资源)随着连接数量的增多,必会耗尽,客户端轮询会给服 务器造成很大的压力,而WebSocket
是在物理层非网络层建立一条客户端至服务器的长连接,以此来保证服务器向客 户端的即时推送,既不耗费线程资源,又不会不断向服务器轮询请求。
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
对于请求握手需要注意如下:
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
进行同样的编码,然后对比,如果相同则可以进行后续处理。101
进行响应。握手成功后,就与HTTP协议没有关系了。直接在TCP
上以二进制数据进行双向通信。
如果是通过HTTP协议升级而来的,那么一般是ws://
开头。
如果是通过HTTPS协议升级而来的,那么一般是wss://
开头。
帧是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
组合在一起表示数据。通常都给他们起一个名字,方便人们进行指代。
帧以FIN
开始,用来指明这一帧是不是最后一个分片(消息很大的情况下,要把大消息分成很多帧进行传输),
FIN
占1bit
,由于只占一个bit
,它要么是1
,要么是0
,1
就表示该帧是最后一帧,0
就表示不是最后一帧。如果这个消息没有进行分片传输,那么这唯一的一帧就是最后一帧。
RSV1
、RSV2
、RSV3
三个各占1bit
。
RSV1
、RSV2
、RSV3
是用来进行扩展该协议的,如果没有扩展,正常情况下都为0
。
MASK
占1bit
,由于只占一个bit
,它要么是1
,要么是0
,1
就表示该帧经过了掩码变换,0
就表示该帧没有经过了掩码变换。
协议规定:客户端发给服务端的帧,必须进行掩码变换;而服务端发给客户端的帧则必须不进行掩码变换。
Masking-key
就是掩码。
如果MASK
被设置为1
,Masking-key
占4bytes
,否则该块不存在。
如果掩码存在,那么所有数据(Payload Data
)都需要与掩码做一次异或运算,所有数据依此与4bytes
的掩码进行疑惑运算。 如果不存在掩码,那么后面的数据就可以直接使用。
Payload len
表示数据(Payload Data
)的长度,单位:字节(byte
)。
Payload len
占7bit
,这7bit
可以表示128
个无符号整数。 也就是Payload len
的取值范围是0 ~ 127
。显然,最大如果只能表示127
个字节,这样的数据也太小了吧。 所以,规定0 ~ 125
才表示数据的长度,如果是126
,紧挨着Payload len
的2
个字节才表示数据大小; 如果是127
,紧挨着Payload len
的8
个字节才表示数据大小。
Payload Data
才是真正的数据。数据又分为扩展的数据和应用数据。
opcode
占4bit
。这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
,而且不能进行分片传输。
浏览器中的WebSocket API
由W3C进行标准化。参考文档:https://www.w3.org/TR/websockets
创建实例。
URL
是服务器地址。subProtocol
是可选的,一般不填。
示例:
var webSocket = new WebSocket('ws://localhost:8080');
连接成功的回掉函数。
关闭连接的回掉函数。
event
对象包含两个属性:
event.code
表示错误码。它是无符号整数。
event.reason
表示关闭原因。它是字符串类型,这个字符串的长度不超过125byte
。
出现了错误的回掉。
正常接收到服务端发来的数据的回掉。
event
对象包含1个属性:
event.data
表示表示的就是数据(Paydata
)。
关闭连接。
code
参数表示状态码,无符号整数。此值只允许传入1000
,或者范围在[3000 ~ 3999]
之间的值,传入其他值会发生异常。
如果省略了code
参数,就使用缺省值1000
。
reason
参数是字符串,它不能超过123
个字节。
这么设计的原因是:协议规定,发送控制帧的时候,数据(Payload
)不能超过125
个字节。 因为这里把数据(Payload
)拆分成了code
和reason
两部分,code
占了2
个字节,reason
就只能占剩下的123
个字节了。
示例:
webSocket.close();
webSocket.close(1000, "normal close");
发送数据给服务器。
因为数据既可以是字符串,也可以是二进制的。所以这里的data
类型就比较多了。
如果发送的是字符串,一定要注意,其编码必须是UTF-8
。
示例:
webSocket.send("I Love you");
浏览器中的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);
};
};
到目前为止,浏览器中的WebSocket API
已经受到绝大多数现代浏览器的支持。部分老版本的浏览器不支持。 对于不支持WebSocket API
的,下面有几个替代方案供你使用:
为了兼容性考虑,最好不要直接使用WebSocket API
,提倡使用第三方开发的库, 因为这些第三方库会在不支持WebSocket API
的浏览器上选择其他实现的好的替代方案,省去了我们处理兼容性问题。 另外,WebSocket API
并没有主动发送ping
帧,而大部分第三方库都处理了,我们就不用管了。
常用的第三方库有:
他们的使用方法与WebSocket API
完全一样,只是他们的实现更完备。
JSR-356是Java EE7
中对WebSocket
协议的支持。
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
放到Tomcat的webapp
目录下, 然后重启Tomcat。
现在就可以以WebSocket
协议进行访问了。URL
是:ws://websocket/xx
Spring4.0
以后加入了对WebSocket
协议的支持。
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
协议与服务器进行通信的。
Android客户端可以使用的支持WebSocket
协议的库有两个:
iOS中支持WebSocket
协议的库有两个: