WebSocket 概述
WebSocket 协议本质上是一个基于 TCP 的协议。为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
To establish a WebSocket connection, the client sends a WebSocket handshake request, and the server sends a WebSocket handshake response。建立一个websocket连接,客户端发送握手请求,服务器返回握手响应.
WebSocket协议是RFC-6455规范定义的一个Web领域的重要的功能:全双工,即客户端和服务器之间的双向通信。它是一个令人兴奋的功能,业界在此领域上已经探索很久,使用的技术包括Java Applet、XMLHttpRequest、Adobe Flash、ActiveXObject、各种Comet技术、服务器端的发送事件等。需要理解一点,在使用WebSocket协议前,需要先使用HTTP协议用于构建最初的握手。这依赖于一个机制——建立HTTP,请求协议升级(或叫协议转换)。当服务器同意后,它会响应HTTP状态码101,表示同意切换协议。假设通过TCP套接字成功握手,HTTP协议升级请求通过,那么客户端和服务器端都可以彼此互发消息。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:
- WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样;
- WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。
非 WebSocket 模式传统 HTTP 客户端与服务器的交互如下图所示:
图 1. 传统 HTTP 请求响应客户端服务器交互图

使用 WebSocket 模式客户端与服务器的交互如下图:
图 2.WebSocket 请求响应客户端服务器交互图

上图对比可以看出,相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。
我们再通过客户端和服务端交互的报文看一下 WebSocket 通讯与传统 HTTP 的不同:
在客户端,new WebSocket 实例化一个新的 WebSocket 客户端对象,连接类似 ws://yourdomain:port/path 的服务端 WebSocket URL,WebSocket 客户端对象会自动解析并识别为 WebSocket 请求,从而连接服务端端口,执行双方握手过程,客户端发送数据格式类似:
客户端发送的数据如下:
1
2
3
4
5
6
7
8
|
GET /mychat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13
Origin: http://example.com
|
服务器返回的数据如下:
1
2
3
4
5
|
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
|
The handshake resembles HTTP, but actually isn’t. It allows the server to interpret part of the handshake request as HTTP, then switch to WebSocket.
Note that each line ends with an EOL (end of line) sequence, \n or \r\n. There must be a blank line at the end.
这个握手很像HTTP,但是实际上却不是,它允许服务器以HTTP的方式解释一部分handshake的请求,然后切换为websocket
注意,每一行结尾都要以EOL 空白符结束,\n 或者\r\n。
这里是非常重要的地方!网上资料很少,我就是在这里遇到了问题,开始建立连接之后,永远都不返回句柄,我以为只需要客户端像server发送数据,而server不用返回确认数据,看到这里我才知道我错了!
客户端发送 base64加密的 Sec-WebSocket-Key,服务器接收到这个key之后,将258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼在key之后,生成HSmrc0sMlYUkAGmm5OPpG2HaGWk=258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后将得到的结果进行sha-1哈希,最后base64加密,作为Sec-WebSocket-Accept的值 发送给客户端。
如果accept这个值错误的话会抛出 Error during WebSocket handshake: Sec-WebSocket-Accept mismatch
最后关闭连接。。一定要注意哦。
这样,握手才算完成!连接确认之后,客户端及服务器端就可以相互发数据了。注意哦,它是全双工的哦,意思就是不会阻塞。
如图
概念性的问题都清楚了,来看看WEBSOCKET为我们提供了哪些API
1、构造函数 WebSocket(char *host);
创建一个websocket对象,接受一个参数以ws://靠头,就像发起一个HTTP请求一样用http://开头
var ws=new WebSocket(“ws://10.32.21.27:9880”);
2、ws.onopen
连接成功后触发
3、ws.onerror
连接出错触发
4、ws.onmessage
server端有数据返回时触发
5、ws.onclose
关闭连接时触发
还有一个最重要的
6、ws.send
用来发送消息到server
7、ws.close
关闭连接
完整的client端的代码如下
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
|
<!DOCTYPE HTML>
<
html
>
<
head
>
<
meta
http-equiv
=
"content-type"
content
=
"text/html"
/>
<
meta
name
=
"author"
content
=
"blog.anchen8.net"
/>
<
title
>Untitled 2</
title
>
<
script
>
var socket;
function connect(){
try{
socket=new WebSocket('ws://10.32.21.27:9880');
}catch(e){
alert('error');
return;
}
socket.onopen = sOpen;
socket.onerror=sError;
socket.onmessage=sMessage;
socket.onclose=sClose
}
function sOpen(){
alert('connect success!')
}
function sError(){
alert('connect error')
}
function sMessage(msg){
alert('server says:'+msg)
}
function sClose(){
alert('connect close')
}
function send(){
socket.send('hello ,i am siren!')
}
function close(){
socket.close();
}
</
script
>
</
head
>
<
body
>
<
input
id
=
"msg"
type
=
"text"
>
<
button
id
=
"connect"
onclick
=
"connect();"
>Connect</
button
>
<
button
id
=
"send"
onclick
=
"send();"
>Send</
button
>
<
button
id
=
"close"
onclick
=
"close();"
>Close</
button
>
</
body
>
</
html
>
|
后端代码
/**
* Socket建立连接(握手)和断开
*
* @Date 2015年6月11日 下午2:23:09
*/
public class HandShake implements HandshakeInterceptor {
//建立握手
public boolean beforeHandshake(ServerHttpRequest request,ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
System.out.println("Websocket:用户[ID:"+ ((ServletServerHttpRequest) request).getServletRequest()
.getSession(false).getAttribute("uid") + "]已经建立连接");
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
//获得了httpSession并获取uid
HttpSession session = servletRequest.getServletRequest().getSession(false);
// 通过session里的uid标记用户
Long uid = (Long)session.getAttribute("uid");
if (uid != null) {
attributes.put("uid", uid);
} else {
return false;
}
}
return true;
}
//可重写断开连接的函数
public void afterHandshake(ServerHttpRequest request,ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
}
<pre name="code" class="java">/**
* WebScoket配置处理器
*
* @Date 2015年6月11日 下午1:15:09
*/
@Component
@EnableWebSocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
@Resource
MyWebSocketHandler handler;
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//用来注册websocket server实现类,第二个参数是访问websocket的地址
registry.addHandler(handler, "/ws").addInterceptors(new HandShake());
//这个是使用Sockjs的注册方法。用户登录后建立websocket连接,默认选择websocket连接,如果浏览器不支持,则使用sockjs进行模拟连接
registry.addHandler(handler, "/ws/sockjs").addInterceptors(new HandShake()).withSockJS();
}
}
/**
* Socket处理器
*
* @Date 2015年6月11日 下午1:19:50
*/
@Component
public class MyWebSocketHandler implements WebSocketHandler {
//存储uid和websocketsession的哈希表
public static final Map<Long, WebSocketSession> userSocketSessionMap;
@Autowired
private MessageService messageService;
static {
userSocketSessionMap = new HashMap<Long, WebSocketSession>();
}
/**
* 建立连接后
*/
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Long uid = (Long) session.getAttributes().get("uid");
if (userSocketSessionMap.get(uid) == null) {
userSocketSessionMap.put(uid, session);
}
}
/**
* 消息处理,在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理
*/
public void handleMessage(WebSocketSession session,WebSocketMessage<?> message) throws Exception {
if (message.getPayloadLength() == 0)
return;
Message msg = new Gson().fromJson(message.getPayload().toString(),Message.class);
msg.setCreateTime(new Date());
sendMessageToUser(Math.abs(msg.getToId()),msg);
}
/**
* 消息传输错误处理
*/
public void handleTransportError(WebSocketSession session,Throwable exception) throws Exception {
if (session.isOpen()) {
session.close();
}
Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator();
// 移除Socket会话
while (it.hasNext()) {
Entry<Long, WebSocketSession> entry = it.next();
//如果哈希表中的websocketsession的uid等于目标用户id相等
if (entry.getValue().getId().equals(session.getId())) {
userSocketSessionMap.remove(entry.getKey());
System.out.println("Socket会话已经移除:用户ID" + entry.getKey());
break;
}
}
}
/**
* 关闭连接后
*/
public void afterConnectionClosed(WebSocketSession session,CloseStatus closeStatus) throws Exception {
System.out.println("Websocket:" + session.getId() + "已经关闭");
Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator();
// 移除Socket会话
while (it.hasNext()) {
Entry<Long, WebSocketSession> entry = it.next();
//如果用户id相等
if (entry.getValue().getId().equals(session.getId())) {
userSocketSessionMap.remove(entry.getKey());
System.out.println("Socket会话已经移除:用户ID" + entry.getKey());
break;
}
}
}
public boolean supportsPartialMessages() {
return false;
}
/**
* 给所有在线用户发送消息
*
* @param message
* @throws IOException
*/
public void broadcast(Message msg) throws IOException {
msg.setStatus(1);//广播默认状态是已接收
messageService.addMessage(msg);
//为推送的消息加一个头
Map<String, Object> param=new HashMap<String, Object>();
param.put("messgae",msg);
final TextMessage message=new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(param));
Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator();
// 多线程群发
while (it.hasNext()) {
final Entry<Long, WebSocketSession> entry = it.next();
if (entry.getValue().isOpen()) {
// entry.getValue().sendMessage(message);
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 userName
* @param message
* @throws IOException
*/
public void sendMessageToUser(Long uid,Message msg)throws IOException {
int id=msg.getId();
if(id==0){ //如果id为0表示新推送的消息
id=messageService.addMessage(msg); //将消息加入数据库
msg.setId(id); //将该id返回给msg
}
//为推送的消息加一个头
Map<String, Object> param=new HashMap<String, Object>();
param.put("message",msg);
final TextMessage message=new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(param));
WebSocketSession session = userSocketSessionMap.get(uid);
//如果选择的用户恰好在线,则推送消息给该用户,并修改消息状态为已接受(默认状态是未接收)
if (session != null && session.isOpen()) {
//使用WebSocketSession的sendMesaageAPI向指定连接发送信息
session.sendMessage(message);
// msg.setStatus(1);
}
}
<pre name="code" class="java">//记得用户上线时创建Session
request.getSession().setAttribute("uid",id);
@Resource
private MyWebSocketHandler handler;
//广播
handler.broadcast(msg);
//给某人发指定消息
handler.sendMessageToUser(Math.abs(toId),msg);
//别忘了配置Sping4的环境
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/ws/*</url-pattern>
</servlet-mapping>