一、什么是WebSocket
首先需要明白webSocket的概念,下边是维基百科的解释
WebSocket是一种通信协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
首先,要明白WebSocket是一种通信协议,区别于HTTP协议,HTTP协议只能实现客户端请求,服务端响应的这种单项通信。
而WebSocket可以实现客户端与服务端的双向通讯,说白了,最大也是最明显的区别就是可以做到服务端主动将消息推送给客户端。
其余的特点有:
- 握手阶段采用 HTTP 协议。
- 数据格式轻量,性能开销小。客户端与服务端进行数据交换时,服务端到客户端的数据包头只有2到10字节,客户端到服务端需要加上另外4字节的掩码。HTTP每次都需要携带完整头部。
- 更好的二进制支持,可以发送文本,和二进制数据
- 没有同源限制,客户端可以与任意服务器通信
- 协议标识符是ws(如果加密,则是wss),请求的地址就是后端支持websocket的API。
二、几种与服务端实时通信的方法
我们都知道,不使用WebSocket与服务器实时交互,一般有两种方法。AJAX轮询和Long Polling长轮询。
1、AJAX轮询
AJAX轮询也就是定时发送请求,也就是普通的客户端与服务端通信过程,只不过是无限循环发送,这样,可以保证服务端一旦有最新消息,就可以被客户端获取。
ajax轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。
场景再现:
客户端:啦啦啦,有没有新信息(Request)
服务端:没有(Response)
客户端:啦啦啦,有没有新信息(Request)
服务端:没有。。(Response)
客户端:啦啦啦,有没有新信息(Request)
服务端:你好烦啊,没有啊。。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:好啦好啦,有啦给你。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:。。。。。没。。。。没。。。没有(Response) —- loop
2、Long Polling长轮询
Long Polling长轮询是客户端和浏览器保持一个长连接,等服务端有消息返回,断开。
然后再重新连接,也是个循环的过程,无穷尽也。。。
long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。
场景再现:
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)
服务端:额。。 等待到有消息的时候。。来 给你(Response)
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop
总结
从上面可以看出其实这两种方式,都是在不断地建立HTTP连接,然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性。
何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。
从上面很容易看出来,不管怎么样,上面这两种都是非常消耗资源的。
ajax轮询 需要服务器有很快的处理速度和资源(速度)。long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)
3、Websocket
Websocket解决了HTTP的这几个难题。首先,被动性,当服务器完成协议升级后,服务端就可以主动推送信息给客户端啦。所以上面的情景可以做如下修改。
场景再现
客户端:啦啦啦,我要建立Websocket协议,需要的服务:chat,Websocket协议版本:17(HTTP Request)
服务端:ok,确认,已升级为Websocket协议(HTTP Protocols Switched)
客户端:麻烦你有信息的时候推送给我噢。。
服务端:ok,有的时候会告诉你的。
服务端:balabalabalabala
服务端:balabalabalabala
服务端:哈哈哈哈哈啊哈哈哈哈
服务端:笑死我了哈哈哈哈哈哈哈
需要经过一次HTTP请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你 )
只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。
同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的。。),没有信息的时候就交给接线员(Nginx),不需要占用本身速度就慢的客服(Handler)了
三、实现消息暂存
pringboot整合websocket
1.maven引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.创建配置文件,开启websocket
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
// 开启WebSocket支持
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.前端实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>websocket</title>
<script src="/js/jquery-3.0.0.js"></script>
</head>
<body>
<h3>对谁发送消息:</h3>
<span>userId:</span><input type="text" id="userId"><br>
<span>消息:</span>
<input type="text" id="message">
<button id="bt">发送消息</button>
<br>
<span>使用websocket的发送信息的方法向服务器发送消息:</span>
<input type="text" id="websocketMessage">
<button id="websocketBtn">websocket发送消息</button>
<script>
var webSocket = null;
var bt = document.getElementById('bt');
if ('WebSocket' in window) {
//连接时带上userId
webSocket = new WebSocket('ws://' + location.hostname + ':' + location.port + '/webSocket/2');
// webSocket = new WebSocket('ws://localhost:8080/webSocket/2');
} else {
alert('浏览器不支持websocket');
}
webSocket.onopen = function (event) {
console.log('建立连接');
}
webSocket.onclose = function (event) {
console.log('连接关闭');
}
webSocket.onmessage = function (event) {
alert('收到服务端发来的消息:' + event.data);
}
webSocket.onerror = function () {
alert('websocket通信发生错误');
}
webSocket.onbeforeunload = function () {
webSocket.close();
}
bt.onclick = function () {
var userId = document.getElementById('userId').value;
var message = document.getElementById('message').value;
var userIdAndMessage = {'userId': userId, 'message': message};
$.post("/sendMessage", userIdAndMessage)
}
$('#websocketBtn').click(function () {
webSocket.send($('#websocketMessage').val());
})
</script>
</body>
</html>
4.后端实现
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@Component
//用户连接的时候带上用户id
@ServerEndpoint("/webSocket/{userId}")
public class WebSocket {
private Session session;
private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>();
private static ConcurrentHashMap<Integer, Session> webSocketSession = new ConcurrentHashMap<Integer, Session>();
//用户连接,将userId和websocket.Session存入ConcurrentHashMap中,
这样做为实现之后对指定人进行消息推送
@OnOpen
public void onOpen(Session session, @PathParam("userId") int userId) {
this.session = session;
webSocketSet.add(this);
webSocketSession.put(userId, session);
System.out.println("新连接者的用户号:" + userId);
System.out.println("当前在线用户总数:" + webSocketSet.size());
}
//连接关闭,关闭时要将ConcurrentHashMap中的断线用户信息清除掉
@OnClose
public void onClose() {
webSocketSet.remove(this);
int offLineUserId = 0;
for (int userId : webSocketSession.keySet()) {
if (webSocketSession.get(userId) == this) {
offLineUserId = userId;
webSocketSession.remove(userId);
break;
}
}
System.out.println("用户“" + offLineUserId + "”离线");
System.out.println("在线人数:" + webSocketSession.size());
System.out.println(webSocketSession);
}
//接收客户端发来的websocket消息
@OnMessage
public void onMessage(String message) {
System.out.println("收到客户端发来的消息:" + message);
}
public static void toUserSendMessage(int userId, String message) {
try {
webSocketSession.get(userId).getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
//发出广播消息
public void sendBroadcastMessage(String message) {
for (WebSocket webSocket : webSocketSet) {
System.out.println("广播消息,message:" + message);
try {
webSocket.session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
(2)接收浏览器的请求,给指定人推送消息
import com.example.websocket.service.WebSocket;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpServletResponse;
@Controller
public class SendMessageController {
@PostMapping("/sendMessage")
public void sendMessage(int userId, String message, HttpServletResponse response) {
WebSocket.toUserSendMessage(userId, message);
}
}