websocket概述
- 原理简介
WebSocket protocol 是HTML5一种新的协议。它与HTTP协议一样是基于应用层的协议。它实现了浏览器与服务器全双工通信(full-duplex)。但它实际上是基于TCP协议,它与HTTP协议的关系
WebSocket 的握手被http 服务器当做 Upgrade request http包处理,服务器响应过后协议升级为TCP连接,连接建立后
client 发送websocket 握手请求.目前
在JavaEE7中也实现了WebSocket协议的支持。
websocket api在浏览器端的广泛实现似乎只是一个时间问题了, 值得注意的是服务器端没有标准的api, 各个实现都有自己的一套api, 并且jcp也没有类似的提案, 所以使用websocket开发服务器端有一定的风险.可能会被锁定在某个平台上或者将来被迫升级.
- 使用场景
现很多网站为了实现即时通讯,所用的技术都是轮询(polling)。这样浏览器需要不断的向服务器发出请求,然而HTTP request 的header是非常长的,里面包含的有用数据可能只是一个很小的值,这样会占用很多的带宽。WebSocket 协议中,在以下场景中会给我们带来很大便利:
- 实时通讯
- 服务器主动向所有客户端进行消息推送
- 以及越来越火的基于H5手机APP的聊天室以及消息推送
- 与传统socket的区别
Socket 其实并不是一个协议。它工作在 OSI 模型会话层(第5层),是为了方便大家直接使用更底层协议(一般是 TCP 或 UDP )而存在的一个抽象层。而且而且它大都在java或者C++这类的编程语言中实现,而在浏览器的基于javascript之类的脚本解释型语言中没有实现。而websocket协议大多数浏览器都已经支持,可以让我们在浏览器中像socket通信一样的去使用TCP直接通信。
- 支持websocket协议的服务器
在服务器端,也出现了一些实现websocket协议的项目:
jetty 7.0.1 包含了一个初步的实现
resin 包含有websocket 实现
pywebsocket, apache http server 扩展
apache tomcat 7.0.27 版本
Nginx 1.3.13 版本
jWebSocket java实现版
nodejs 利用谷歌V8的浏览器引擎也支持websocket
- 支持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的框架
- 基于 C 的 libwebsocket.org
- 基于 Node.js 的 Socket.io 它包含nodeJS 服务器端的实现和浏览器端的实现。
- 基于 Python 的 ws4py
- 基于 C++ 的 WebSocket++
- Apache 对 WebSocket 的支持: Apache Module mod_proxy_wstunnel
- Nginx 对 WebSockets 的支持: NGINX as a WebSockets Proxy 、 NGINX Announces Support for WebSocket Protocol 、WebSocket proxying
- lighttpd 对 WebSocket 的支持:mod_websocket
- 基于java的实现 服务端tomcat7和浏览器端 socketJS
- 基于 C 的 libwebsocket.org
- 基于 Node.js 的 Socket.io 它包含nodeJS 服务器端的实现和浏览器端的实现。
- 基于 Python 的 ws4py
- 基于 C++ 的 WebSocket++
- Apache 对 WebSocket 的支持: Apache Module mod_proxy_wstunnel
- Nginx 对 WebSockets 的支持: NGINX as a WebSockets Proxy 、 NGINX Announces Support for WebSocket Protocol 、WebSocket proxying
- lighttpd 对 WebSocket 的支持:mod_websocket
- 基于java的实现 服务端tomcat7和浏览器端 socketJS
- websocket实例
- websocket数据包协议详解
WebSocket协议主要分为两部分,第一部分是连接许可验证和验证后的数据交互.连接许可验证比较简单,由Client发送一个类似于HTTP的请求,服务端获取请求后根据请求的KEY生成对应的值并返回.
连接请求内容:
1
2
3
4
5
6
7
8
GET / HTTP/1.1
Connection:Upgrade
Host:127.0.0.1:8088
Origin:
null
Sec-WebSocket-Extensions:x-webkit-deflate-frame
Sec-WebSocket-Key:puVOuWb7rel6z2AVZBKnfw==
Sec-WebSocket-Version:13
Upgrade:websocket
服务端接收请求后主要是成针对Sec-WebSocket-Key生成对就Sec-WebSocket-Accept 的key,生成Sec-WebSocket-Accept 值比较简单就是Sha1(Sec-WebSocket-Key+258EAFA5-E914-47DA-95CA-C5AB0DC85B11)即可,C#代码如下:
1
2
3
4
5
SHA1 sha1 =
new
SHA1CryptoServiceProvider();
byte
[] bytes_sha1_in = Encoding.UTF8.GetBytes(request.SecWebSocketKey+
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
);
byte
[] bytes_sha1_out = sha1.ComputeHash(bytes_sha1_in);
string
str_sha1_out = Convert.ToBase64String(bytes_sha1_out);
response.SecWebSocketAccept = str_sha1_out;
服务端返回内容:
1
2
3
4
5
6
7
8
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Server:beetle websocket server
Upgrade:WebSocket
Date:Mon, 26 Nov 2012 23:42:44 GMT
Access-Control-Allow-Credentials:
true
Access-Control-Allow-Headers:content-type
Sec-WebSocket-Accept:FCKgUr8c7OsDsLFeJTWrJw6WO8Q=
经过服务器的返回处理后连接握手成功,后面就可以进行TCP通讯.WebSocket在握手后发送数据并象下层TCP协议那样由用户自定义,还是需要遵循对应的应用协议规范...这也是在文章之说没有直接基于Socket tcp方便的原因.
数据交互协议:
这图有点难看懂...里面包括几种情况有掩码,数据长度小于126,小于UINT16和小于UINT64等几种情况.后面会慢慢详细说明.整个协议头大概分三部分组成,第一部分是描述消息结束情况和类型,第二部分是描述是否存在掩码长度,第三部分是扩展长度描述和掩码值.
从图中可以看到WebSocket协议数据主要通过头两个字节来描述数据包的情况
第一个字节
最高位用于描述消息是否结束,如果为1则该消息为消息尾部,如果为零则还有后续数据包;后面3位是用于扩展定义的,如果没有扩展约定的情况则必须为0.可以通过以下c#代码方式得到相应值
1
mDataPackage.IsEof = (data[start] >> 7) > 0;
最低4位用于描述消息类型,消息类型暂定有15种,其中有几种是预留设置.c#代码可以这样得到消息类型:
1
2
int
type = data[start] & 0xF;
mDataPackage.Type = (PackageType)type;
第二个字节
消息的第二个字节主要用一描述掩码和消息长度,最高位用0或1来描述是否有掩码处理,可以通过以下c#代码方式得到相应值
1
bool
hasMask = (data[start] >>7) > 0;
剩下的后面7位用来描述消息长度,由于7位最多只能描述127所以这个值会代表三种情况,一种是消息内容少于126存储消息长度,如果消息长度少于UINT16的情况此值为126,当消息长度大于UINT16的情况下此值为127;这两种情况的消息长度存储到紧随后面的byte[],分别是UINT16(2位byte)和UINT64(4位byte).可以通过以下c#代码方式得到相应值
1
2
3
4
5
6
7
8
9
10
11
12
mPackageLength = (
uint
)(data[start] & 0x7F);
start++;
if
(mPackageLength == 126)
{
mPackageLength = BitConverter.ToUInt16(data, start);
start = start + 2;
}
else
if
(mPackageLength == 127)
{
mPackageLength = BitConverter.ToUInt64(data, start);
start = start + 8;
}
如果存在掩码的情况下获取4位掩码值:
1
2
3
4
5
6
7
8
if
(hasMask)
{
mDataPackage.Masking_key =
new
byte
[4];
Buffer.BlockCopy(data, start, mDataPackage.Masking_key, 0, 4);
start = start + 4;
count = count - 4;
}
获取消息体
当得到消息体长度后就可以获取对应长度的byte[],有些消息类型是没有长度的如%x8 denotes a connection close.对于Text类型的消息对应的byte[]是相应字符的UTF8编码.获取消息体还有一个需要注意的地方就是掩码,如果存在掩码的情况下接收的byte[]要做如下转换处理:
1
2
3
4
5
6
if
(mDataPackage.Masking_key !=
null
)
{
int
length = mDataPackage.Data.Count;
for
(
var
i = 0; i < length; i++)
mDataPackage.Data.Array[i] = (
byte
)(mDataPackage.Data.Array[i] ^ mDataPackage.Masking_key[i % 4]);
}
- javaEE7、Websockets实例
WebSocket协议主要分为两部分,第一部分是连接许可验证和验证后的数据交互.连接许可验证比较简单,由Client发送一个类似于HTTP的请求,服务端获取请求后根据请求的KEY生成对应的值并返回.
连接请求内容:
1
2
3
4
5
6
7
8
|
GET / HTTP/1.1
Connection:Upgrade
Host:127.0.0.1:8088
Origin:
null
Sec-WebSocket-Extensions:x-webkit-deflate-frame
Sec-WebSocket-Key:puVOuWb7rel6z2AVZBKnfw==
Sec-WebSocket-Version:13
Upgrade:websocket
|
服务端接收请求后主要是成针对Sec-WebSocket-Key生成对就Sec-WebSocket-Accept 的key,生成Sec-WebSocket-Accept 值比较简单就是Sha1(Sec-WebSocket-Key+258EAFA5-E914-47DA-95CA-C5AB0DC85B11)即可,C#代码如下:
1
2
3
4
5
|
SHA1 sha1 =
new
SHA1CryptoServiceProvider();
byte
[] bytes_sha1_in = Encoding.UTF8.GetBytes(request.SecWebSocketKey+
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
);
byte
[] bytes_sha1_out = sha1.ComputeHash(bytes_sha1_in);
string
str_sha1_out = Convert.ToBase64String(bytes_sha1_out);
response.SecWebSocketAccept = str_sha1_out;
|
服务端返回内容:
1
2
3
4
5
6
7
8
|
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Server:beetle websocket server
Upgrade:WebSocket
Date:Mon, 26 Nov 2012 23:42:44 GMT
Access-Control-Allow-Credentials:
true
Access-Control-Allow-Headers:content-type
Sec-WebSocket-Accept:FCKgUr8c7OsDsLFeJTWrJw6WO8Q=
|
经过服务器的返回处理后连接握手成功,后面就可以进行TCP通讯.WebSocket在握手后发送数据并象下层TCP协议那样由用户自定义,还是需要遵循对应的应用协议规范...这也是在文章之说没有直接基于Socket tcp方便的原因.
数据交互协议:
这图有点难看懂...里面包括几种情况有掩码,数据长度小于126,小于UINT16和小于UINT64等几种情况.后面会慢慢详细说明.整个协议头大概分三部分组成,第一部分是描述消息结束情况和类型,第二部分是描述是否存在掩码长度,第三部分是扩展长度描述和掩码值.
从图中可以看到WebSocket协议数据主要通过头两个字节来描述数据包的情况
第一个字节
最高位用于描述消息是否结束,如果为1则该消息为消息尾部,如果为零则还有后续数据包;后面3位是用于扩展定义的,如果没有扩展约定的情况则必须为0.可以通过以下c#代码方式得到相应值
1
|
mDataPackage.IsEof = (data[start] >> 7) > 0;
|
最低4位用于描述消息类型,消息类型暂定有15种,其中有几种是预留设置.c#代码可以这样得到消息类型:
1
2
|
int
type = data[start] & 0xF;
mDataPackage.Type = (PackageType)type;
|
第二个字节
消息的第二个字节主要用一描述掩码和消息长度,最高位用0或1来描述是否有掩码处理,可以通过以下c#代码方式得到相应值
1
|
bool
hasMask = (data[start] >>7) > 0;
|
剩下的后面7位用来描述消息长度,由于7位最多只能描述127所以这个值会代表三种情况,一种是消息内容少于126存储消息长度,如果消息长度少于UINT16的情况此值为126,当消息长度大于UINT16的情况下此值为127;这两种情况的消息长度存储到紧随后面的byte[],分别是UINT16(2位byte)和UINT64(4位byte).可以通过以下c#代码方式得到相应值
1
2
3
4
5
6
7
8
9
10
11
12
|
mPackageLength = (
uint
)(data[start] & 0x7F);
start++;
if
(mPackageLength == 126)
{
mPackageLength = BitConverter.ToUInt16(data, start);
start = start + 2;
}
else
if
(mPackageLength == 127)
{
mPackageLength = BitConverter.ToUInt64(data, start);
start = start + 8;
}
|
如果存在掩码的情况下获取4位掩码值:
1
2
3
4
5
6
7
8
|
if
(hasMask)
{
mDataPackage.Masking_key =
new
byte
[4];
Buffer.BlockCopy(data, start, mDataPackage.Masking_key, 0, 4);
start = start + 4;
count = count - 4;
}
|
获取消息体
当得到消息体长度后就可以获取对应长度的byte[],有些消息类型是没有长度的如%x8 denotes a connection close.对于Text类型的消息对应的byte[]是相应字符的UTF8编码.获取消息体还有一个需要注意的地方就是掩码,如果存在掩码的情况下接收的byte[]要做如下转换处理:
1
2
3
4
5
6
|
if
(mDataPackage.Masking_key !=
null
)
{
int
length = mDataPackage.Data.Count;
for
(
var
i = 0; i < length; i++)
mDataPackage.Data.Array[i] = (
byte
)(mDataPackage.Data.Array[i] ^ mDataPackage.Masking_key[i % 4]);
}
|
1. 介绍
| |
2. WebSocket服务器端
|
5. WebSockets 握手
6. 下载源码
- Spring+Websocket实现消息的推送
Websocet服务端实现
WebSocketConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Configuration
@EnableWebMvc
@EnableWebSocket
public
class
WebSocketConfig
extends
WebMvcConfigurerAdapter
implements
WebSocketConfigurer {
@Override
public
void
registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(systemWebSocketHandler(),
"/webSocketServer"
).addInterceptors(
new
WebSocketHandshakeInterceptor());
registry.addHandler(systemWebSocketHandler(),
"/sockjs/webSocketServer"
).addInterceptors(
new
WebSocketHandshakeInterceptor())
.withSockJS();
}
@Bean
public
WebSocketHandler systemWebSocketHandler(){
return
new
SystemWebSocketHandler();
}
}
|
1
|
<
context:component-scan
base-package
=
"com.ldl.origami.websocket"
/>
|
@Configuration
@EnableWebMvc
@EnableWebSocket
这三个大致意思是使这个类支持以@Bean的方式加载bean,并且支持springmvc和websocket,不是很准确大致这样,试了一下@EnableWebMvc不加也没什么影响,@Configuration本来就支持springmvc的自动扫描
1
|
registry.addHandler(systemWebSocketHandler(),
"/webSocketServer"
).addInterceptors(
new
WebSocketHandshakeInterceptor())
|
1
2
3
|
registry.addHandler(systemWebSocketHandler(),
"/sockjs/webSocketServer"
).addInterceptors(
new
WebSocketHandshakeInterceptor())
.withSockJS();
}
|
首先SystemWebSocketHandler.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
public
class
SystemWebSocketHandler
implements
WebSocketHandler {
private
static
final
Logger logger;
private
static
final
ArrayList<WebSocketSession> users;
static
{
users =
new
ArrayList<>();
logger = LoggerFactory.getLogger(SystemWebSocketHandler.
class
);
}
@Autowired
private
WebSocketService webSocketService;
@Override
public
void
afterConnectionEstablished(WebSocketSession session)
throws
Exception {
logger.debug(
"connect to the websocket success......"
);
users.add(session);
String userName = (String) session.getAttributes().get(Constants.WEBSOCKET_USERNAME);
if
(userName!=
null
){
//查询未读消息
int
count = webSocketService.getUnReadNews((String) session.getAttributes().get(Constants.WEBSOCKET_USERNAME));
session.sendMessage(
new
TextMessage(count +
""
));
}
}
@Override
public
void
handleMessage(WebSocketSession session, WebSocketMessage<?> message)
throws
Exception {
//sendMessageToUsers();
}
@Override
public
void
handleTransportError(WebSocketSession session, Throwable exception)
throws
Exception {
if
(session.isOpen()){
session.close();
}
logger.debug(
"websocket connection closed......"
);
users.remove(session);
}
@Override
public
void
afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)
throws
Exception {
logger.debug(
"websocket connection closed......"
);
users.remove(session);
}
@Override
public
boolean
supportsPartialMessages() {
return
false
;
}
/**
* 给所有在线用户发送消息
*
* @param message
*/
public
void
sendMessageToUsers(TextMessage message) {
for
(WebSocketSession user : users) {
try
{
if
(user.isOpen()) {
user.sendMessage(message);
}
}
catch
(IOException e) {
e.printStackTrace();
}
}
}
/**
* 给某个用户发送消息
*
* @param userName
* @param message
*/
public
void
sendMessageToUser(String userName, TextMessage message) {
for
(WebSocketSession user : users) {
if
(user.getAttributes().get(Constants.WEBSOCKET_USERNAME).equals(userName)) {
try
{
if
(user.isOpen()) {
user.sendMessage(message);
}
}
catch
(IOException e) {
e.printStackTrace();
}
break
;
}
}
}
}
|
然后WebSocketHandshakeInterceptor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
class
WebSocketHandshakeInterceptor
implements
HandshakeInterceptor {
private
static
Logger logger = LoggerFactory.getLogger(HandshakeInterceptor.
class
);
@Override
public
boolean
beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object
> attributes)
throws
Exception {
if
(request
instanceof
ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(
false
);
if
(session !=
null
) {
//使用userName区分WebSocketHandler,以便定向发送消息
String userName = (String) session.getAttribute(Constants.SESSION_USERNAME);
attributes.put(Constants.WEBSOCKET_USERNAME,userName);
}
}
return
true
;
}
@Override
public
void
afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
}
|
这个的主要作用是取得当前请求中的用户名,并且保存到当前的WebSocketHandler中,以便确定WebSocketHandler所对应的用户,具体可参考HttpSessionHandshakeInterceptor
用户登录建立websocket连接
index.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<script type=
"text/javascript"
src=
"http://localhost:8080/Origami/websocket/sockjs-0.3.min.js"
></script>
<script>
var
websocket;
if
(
'WebSocket'
in
window) {
websocket =
new
WebSocket(
"ws://localhost:8080/Origami/webSocketServer"
);
}
else
if
(
'MozWebSocket'
in
window) {
websocket =
new
MozWebSocket(
"ws://localhost:8080/Origami/webSocketServer"
);
}
else
{
websocket =
new
SockJS(
"http://localhost:8080/Origami/sockjs/webSocketServer"
);
}
websocket.onopen =
function
(evnt) {
};
websocket.onmessage =
function
(evnt) {
$(
"#msgcount"
).html(
"(<font color='red'>"
+evnt.data+
"</font>)"
)
};
websocket.onerror =
function
(evnt) {
};
websocket.onclose =
function
(evnt) {
}
</script>
|
使用sockjs时要注意
1、这两个的写法
1
|
<script type=
"text/javascript"
src=
"http://localhost:8080/Origami/websocket/sockjs-0.3.min.js"
></script>
|
1
|
websocket =
new
SockJS(
"http://localhost:8080/Origami/sockjs/webSocketServer"
);
|
1
2
3
|
<
web-app
version
=
"3.0"
xmlns
=
"http://java.sun.com/xml/ns/javaee"
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation
=
"http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_1.xsd"
>
|
1
|
version
|
1
|
web-app_3_1.xsd
|
然后在这个servlet中加入
1
|
<
async-supported
>true</
async-supported
>
|
1
2
3
4
5
6
7
8
9
10
|
<
servlet
>
<
servlet-name
>appServlet</
servlet-name
>
<
servlet-class
>org.springframework.web.servlet.DispatcherServlet</
servlet-class
>
<
init-param
>
<
param-name
>contextConfigLocation</
param-name
>
<
param-value
>classpath*:servlet-context.xml</
param-value
>
</
init-param
>
<
load-on-startup
>1</
load-on-startup
>
<
async-supported
>true</
async-supported
>
</
servlet
>
|
然后所有的filter中也加入
1
|
<
async-supported
>true</
async-supported
>
|
3、添加相关依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<
dependency
>
<
groupId
>com.fasterxml.jackson.core</
groupId
>
<
artifactId
>jackson-annotations</
artifactId
>
<
version
>2.3.0</
version
>
</
dependency
>
<
dependency
>
<
groupId
>com.fasterxml.jackson.core</
groupId
>
<
artifactId
>jackson-core</
artifactId
>
<
version
>2.3.1</
version
>
</
dependency
>
<
dependency
>
<
groupId
>com.fasterxml.jackson.core</
groupId
>
<
artifactId
>jackson-databind</
artifactId
>
<
version
>2.3.3</
version
>
</
dependency
>
|
返回用户未读的消息
当连接建立后,会进入SystemWebSocketHandler的afterConnectionEstablished方法,代码看上边,取出WebSocketHandshakeInterceptor中保存的用户名
查询信息后使用session.sendMessage(new TextMessage(count + ""));返回给用户,从哪来回哪去
服务端推送消息给用户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Controller
public
class
AdminController {
static
Logger logger = LoggerFactory.getLogger(AdminController.
class
);
@Autowired
(required =
false
)
private
AdminService adminService;
@Bean
public
SystemWebSocketHandler systemWebSocketHandler() {
return
new
SystemWebSocketHandler();
}
@RequestMapping
(
"/auditing"
)
@ResponseBody
public
String auditing(HttpServletRequest request){
//无关代码都省略了
int
unReadNewsCount = adminService.getUnReadNews(username);
systemWebSocketHandler().sendMessageToUser(username,
new
TextMessage(unReadNewsCount +
""
));
return
result;
}
}
|
在这里可以使用sendMessageToUser给某个用户推送信息,也可以使用sendMessageToUsers给所有用户推送信息