Websocket是html5提出的一个协议规范,websocket约定了一个通信的规范,通过一个握手的机制,客户端(浏览器)和服务器(webserver)之间能建立一个类似tcp的连接,从而方便c-s之间的通信。在websocket出现之前,web交互一般是基于http协议的短连接或者长连接。WebSocket是为解决客户端与服务端实时通信而产生的技术。websocket协议本质上是一个基于tcp的协议,是先通过HTTP/HTTPS协议发起一条特殊的http请求进行握手后创建一个用于交换数据的TCP连接,此后服务端与客户端通过此TCP连接进行实时通信。
web server实现推送技术或者即时通讯,用的都是轮询(polling),在特点的时间间隔(比如1秒钟)由浏览器自动发出请求,将服务器的消息主动的拉回来,在这种情况下,我们需要不断的向服务器发送请求,然而HTTP request 的header是非常长的,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽和服务器资源。
而最比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求(reuqest)。
WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可以随时向服务器发送数据。 此外,服务器与客户端之间交换的标头信息很小。
从服务器角度来说,websocket有以下好处:
1、节省每次请求的header
http的header一般有几十字节
2、Server Push
服务器可以主动传送数据给客户端
如图:只需要请求一次握手成功过后就升级为tcp协议实现长连接双工通讯
反之:http协议轮训
看到这里相信已经对这轮训和长连接有了一个深刻的认识了吧!
与http协议不同的请求/响应模式不同,Websocket在建立连接之前有一个Handshake(Opening Handshake)过程,在关闭连接前也有一个Handshake(Closing Handshake)过程,建立连接之后,双方即可双向通信。
在websocket协议发展过程中前前后后就出现了多个版本的握手协议,这里分情况说明一下:
基于flash的握手协议
使用场景是IE的多数版本,因为IE的多数版本不都不支持WebSocket协议,以及FF、CHROME等浏览器的低版本,还没有原生的支持WebSocket。此处,server唯一要做的,就是准备一个WebSocket-Location域给client,没有加密,可靠性很差。
客户端请求:
GET /ls HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: www.qixing318.com
Origin: http://www.cn-lilu168.com
服务器返回:
HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://www.cn-lilu168.com
WebSocket-Location: ws://www.cn-lilu168.com/ls
. 基于md5加密方式的握手协议
客户端请求:
GET /demo HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Key2:
Upgrade: WebSocket
Sec-WebSocket-Key1:
Origin: http://www.cn-lilu168.com
[8-byte security key]
服务端返回:
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://www.cn-lilu168.com
WebSocket-Location: ws://example.com/demo
[16-byte hash response]
其中 Sec-WebSocket-Key1,Sec-WebSocket-Key2 和 [8-byte security key] 这几个头信息是web server用来生成应答信息的来源,依据 draft-hixie-thewebsocketprotocol-76 草案的定义。
web server基于以下的算法来产生正确的应答信息:
GET /ls HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: www.qixing318.com
Sec-WebSocket-Origin: http://www.cn-lilu168.com
Sec-WebSocket-Key: 2SCVXUeP9cTjV+0mWB8J6A==
Sec-WebSocket-Version: 13
服务器返回:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: mLDKNeBNWz6T9SxU+o0Fy/HgeSw=
其中 server就是把客户端上报的key拼上一段GUID( “258EAFA5-E914-47DA-95CA-C5AB0DC85B11″),拿这个字符串做SHA-1 hash计算,然后再把得到的结果通过base64加密,最后再返回给客户端。
————-Date Froming——
Websocket协议通过序列化的数据帧传输数据。数据封包协议中定义了opcode、payload length、Payload data等字段。其中要求:
客户端向服务器传输的数据帧必须进行掩码处理:服务器若接收到未经过掩码处理的数据帧,则必须主动关闭连接。
服务器向客户端传输的数据帧一定不能进行掩码处理。客户端若接收到经过掩码处理的数据帧,则必须主动关闭连接。
针对上情况,发现错误的一方可向对方发送close帧(状态码是1002,表示协议错误),以关闭连接。
具体数据帧格式如下图所示:
websocket_frame
FIN
标识是否为此消息的最后一个数据包,占 1 bit
RSV1, RSV2, RSV3: 用于扩展协议,一般为0,各占1bit
Opcode
数据包类型(frame type),占4bits
0x0:标识一个中间数据包
0x1:标识一个text类型数据包
0x2:标识一个binary类型数据包
0x3-7:保留
0x8:标识一个断开连接类型数据包
0x9:标识一个ping类型数据包
0xA:表示一个pong类型数据包
0xB-F:保留
MASK:占1bits
用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。
Payload length
Payload data的长度,占7bits,7+16bits,7+64bits:
如果其值在0-125,则是payload的真实长度。
如果值是126,则后面2个字节形成的16bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
如果值是127,则后面8个字节形成的64bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
这里的长度表示遵循一个原则,用最少的字节表示长度(尽量减少不必要的传输)。举例说,payload真实长度是124,在0-125之间,必须用前7位表示;不允许长度1是126或127,然后长度2是124,这样违反原则。
Payload data
应用层数据
server解析client端的数据
接收到客户端数据后的解析规则如下:
1byte
1bit: frame-fin,x0表示该message后续还有frame;x1表示是message的最后一个frame
3bit: 分别是frame-rsv1、frame-rsv2和frame-rsv3,通常都是x0
4bit: frame-opcode,x0表示是延续frame;x1表示文本frame;x2表示二进制frame;x3-7保留给非控制frame;x8表示关 闭连接;x9表示ping;xA表示pong;xB-F保留给控制frame
2byte
1bit: Mask,1表示该frame包含掩码;0表示无掩码
7bit、7bit+2byte、7bit+8byte: 7bit取整数值,若在0-125之间,则是负载数据长度;若是126表示,后两个byte取无符号16位整数值,是负载长度;127表示后8个 byte,取64位无符号整数值,是负载长度
3-6byte: 这里假定负载长度在0-125之间,并且Mask为1,则这4个byte是掩码
7-end byte: 长度是上面取出的负载长度,包括扩展数据和应用数据两部分,通常没有扩展数据;若Mask为1,则此数据需要解码,解码规则为- 1-4byte掩码循环和数据byte做异或操作。
websocket 在任何时候都会处于下面4种状态中的其中一种:
客户端
在支持WebSocket的浏览器中,在创建socket之后。可以通过onopen,onmessage,onclose即onerror四个事件实现对socket进行响应
一个简单是示例:
var ws = new WebSocket("ws://localhost:8080");
ws.onopen = function()
{
console.log("open");
ws.send("hello");
};
ws.onmessage = function(evt) {
console.log(evt.data); };
ws.onclose = function(evt) {
console.log("WebSocketClosed!"); };
ws.onerror = function(evt) {
console.log("WebSocketError!"); };
首先申请一个WebSocket对象,参数是需要连接的服务器端的地址,同http协议使用http://开头一样,WebSocket协议的URL使用ws://开头,另外安全的WebSocket协议使用wss://开头。
client先发起握手请求:
GET /echobot HTTP/1.1
Host: 192.168.14.215:9000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://192.168.14.215
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
Sec-WebSocket-Key: mh3xLXeRuIWNPwq7ATG9jA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
服务端响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: SIEylb7zRYJAEgiqJXaOW3V+ZWQ=
开发环境:
maven+ssm + tomcat8 + jdk7
博主是使用的maven项目
org.springframework
spring-websocket
${spring.version}
org.springframework
spring-messaging
${spring.version}
com.fasterxml.jackson.core
jackson-core
${jackson-version}
com.fasterxml.jackson.core
jackson-databind
${jackson-version}
com.fasterxml.jackson.core
jackson-annotations
${jackson-version}
/**
* WebScoket配置处理器
* 1.继承了WebMvcConfigurerAdapter(如写Spring MVC的时候,要添加一个新页面访问总是要新增一个Controller或者在已有的一个Controller中新增一个方法,
* 然后再跳转到设置的页面上去考虑到大部分应用场景中View和后台都会有数据交互,这样的处理也无可厚非,
* 不过我们肯定也有只是想通过一个URL Mapping然后不经过Controller处理直接跳转到页面上的需求!
* Spring为我们提供了一个办法!那就是 WebMvcConfigurerAdapter
* 2.实现WebSocketConfigurer接口,
* 重写registerWebSocketHandlers方法,这是一个核心实现方法,
* 配置websocket入口,允许访问的域、
* 注册Handler、SockJs支持和拦截器。
*
* @author Administrator
*/
@CrossOrigin(origins = "*", maxAge = 3600)
@Component
@EnableWebSocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
@Resource
MyWebSocketHandler handler;
/**
* 连接处理器registerWebSocketHandlers
*/
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry){
System.out.println("WebSocketConfig");
//1.registry.addHandler注册和路由的功能,当客户端发起websocket连接,把/path交给对应的handler处理,而不实现具体的业务逻辑,可以理解为收集和任务分发中心。
//2.addInterceptors,顾名思义就是为handler添加拦截器,可以在调用handler前后加入我们自己的逻辑代码。
//用来注册websocket server实现类,第二个参数是访问websocket的地址
registry.addHandler(handler, "/ws").addInterceptors(new HandShake());
//这个是使用Sockjs的注册方法。用户登录后建立websocket连接,默认选择websocket连接,如果浏览器不支持,则使用sockjs进行模拟连接
registry.addHandler(handler, "/ws/sockjs").addInterceptors(new HandShake()).withSockJS();
}
}
/**
* Socket建立连接(握手)和断开
*/
public class HandShake implements HandshakeInterceptor {
//建立握手
@Autowired
WebSocketService webSocketService;
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
String parameter = ((ServletServerHttpRequest) request).getServletRequest().getParameter("uid");
System.out.println("Websocket:用户[ID:" + parameter + "]已经建立连接");
//判断进来的请求是否是ServletServerHttpRequest所满足的格式
if (request instanceof ServletServerHttpRequest) {
//....这里可实现部分拦截逻辑........
}
//拦截
return false;
}
//可重写断开连接的函数
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
System.out.println("after hand");
}
}
package websocket;
/**
* Socket处理器
*/
@Component
@Service
public class MyWebSocketHandler implements WebSocketHandler{
// 存储uid和websocketsession的哈希表
// 用于保存HttpSession与WebSocketSession的映射关系
public static final Map userSocketSessionMap;
static {
userSocketSessionMap = new ConcurrentHashMap();
}
/**
* 建立连接后,把登录用户的id写入WebSocketSession
*/
public void afterConnectionEstablished(WebSocketSession session)
throws Exception {
int uid = Integer.parseInt(session.getAttributes().get("uid").toString());
if (userSocketSessionMap.get(uid) == null){
// 根据id和session存入userSocketSessionMap中
userSocketSessionMap.put(uid, session);
System.out.println(" 进入 websocket");
// 群发broadcast(谁上线)
this.broadcast(new TextMessage(new GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(....当前上线的人和将要群发的信息)));
}
}
/**
* 消息处理,在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理
*/
public void handleMessage(WebSocketSession session,
WebSocketMessage> message) throws Exception {
// 用来将一个Json数据转换为对象Message
Message msg = new Gson().fromJson(message.getPayload().toString(),Message.class);
//.......该地方可实现业务逻辑 心跳什么的
//将消息发送到某一人
sendMessageToUser(msg.getToid,new TextMessage(
new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create()
.toJson(msg)));
}
};
/**
* 消息传输错误处理
*/
// 处理来自底层websocket消息传输的错误。
public void handleTransportError(WebSocketSession session,
Throwable exception) throws Exception {
Iterator> it = userSocketSessionMap.entrySet().iterator();
// 移除当前抛出异常用户的Socket会话
while (it.hasNext()) {
Entry entry = it.next();
// 如果哈希表中的websocketsession的uid等于目标用户id相等
if (entry.getValue().getId().equals(session.getId())) {
userSocketSessionMap.remove(entry.getKey());// 在哈希表中移除会话
System.out.println("Socket会话已经移除:用户ID" + entry.getKey());
// 群发 下线消息
this.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(编辑将要群发的消息,string/Object)));
break;
}
}
}
/**
* 关闭连接后
*/
public void afterConnectionClosed(WebSocketSession session,
CloseStatus closeStatus) throws Exception {
System.out.println("Websocket:" + session.getId() + "已经关闭");
Iterator> it = userSocketSessionMap
.entrySet().iterator();
// 移除当前用户的Socket会话
while (it.hasNext()) {
Entry entry = it.next();
// 如果哈希表中的websocketsession的uid等于目标用户id相等
if (entry.getValue().getId().equals(session.getId())) {
userSocketSessionMap.remove(entry.getKey());// 在哈希表中移除会话
System.out.println("Socket会话已经移除:用户ID" + entry.getKey());
// 群发下线消息
this.broadcast(new TextMessage(new GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss").create()
.toJson(编辑将要群发的消息,string/Object)));
break;
}
}
}
public boolean supportsPartialMessages(){
return false;
}
/**
* 给所有在线用户发送消息
* @param message
* @throws IOException
*/
public void broadcast(final TextMessage message) throws IOException {
Iterator> it = userSocketSessionMap
.entrySet().iterator();
// 多线程群发
while (it.hasNext()) {
final Entry entry = it.next();
// 判断每一个用户是否连接
if (entry.getValue().isOpen()) {
// 利用匿名内部类
// 相当于new Thread(实现Runnable接口)
new Thread(new Runnable() {
public void run() {
try {
if (entry.getValue().isOpen()) {
// 使用WebSocketSession的sendMesaageAPI向指定连接发送信息
entry.getValue().sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
/**
* 给某个用户发送消息
* @param msg
*
* @param userName
* @param message
* @throws IOException
* @throws Exception
*/
public void sendMessageToUser(Integer integer,TextMessage message)throws IOException, Exception {
WebSocketSession session = userSocketSessionMap.get(integer);
// 如果选择的用户恰好在线,则推送消息给该用户
if (session != null && session.isOpen()) {
// 使用WebSocketSession的sendMesaageAPI向指定连接发送信息
session.sendMessage(message);
}
}
}
package="websocket"/>
再配合前端的html代码触发 wsocket
<html>
<head>
<title>Testing websocketstitle>
head>
<body>
<div>
<label>消息:label><input type="text" id="msg" />
div>
<div>
<input type="submit" value="发送" onclick="start()" />
div>
<div id="messages">div>
<script type="text/javascript">
var webSocket =
new WebSocket('ws://localhost/Spring-websocket/ws?uid=22');
webSocket.onerror = function(event) {
onError(event)
};
webSocket.onopen = function(event) {
onOpen(event)
};
webSocket.onmessage = function(event) {
alert(event);
onMessage(event)
};
function onMessage(event) {
alert(event);
document.getElementById('messages').innerHTML
+= '
' + event.data;
}
function onOpen(event) {
document.getElementById('messages').innerHTML
= 'Connection established';
}
function onError(event) {
alert(event.data);
}
function start() {
var info={
from:'',
fromName:'',
to:'2',
text:document.getElementById('msg').value
};
webSocket.send(info);
return true;
}
script>
body>
html>
其中,ws://localhost/Spring-websocket/ws?uid=22是websoscket的服务器地址。