WebSocket
一、什么是WebSocket?
- WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)
- 它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的
- WebSocket是一个持久化的协议
WebSocket允许我们创建“实时”应用程序,与传统API协议相比,该应用程序速度更快且开销更少。
二、websocket的原理
- websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信
- 在websocket出现之前,web交互一般是基于http协议的短连接或者长连接
- websocket是一种全新的协议,不属于http无状态协议,协议名为"ws"
三、WebSocket是如何工作的
按照传统的定义,WebSocket是一种双工协议,主要用于客户端-服务器通信通道。它本质上是双向的,这意味着通信在客户端与服务器之间来回发生。
使用 WebSocket 开发的连接只要任何参与方中断连接就会持续存在。一旦一方断开连接,另一方将无法进行通信,因为连接会在其前面自动断开。
WebSocket需要HTTP的支持来发起连接。说到它的实用性,当涉及到数据的无缝流和各种不同步流量时,它是现代 Web 应用程序开发的支柱。
四、为什么需要WebSocket以及何时应该避免使用
WebSocket 是一种重要的客户端-服务器通信工具,人们需要充分了解其实用性并避免使用其最大潜力的场景。
在以下情况下使用 WebSocket:
1.开发实时网络应用程序
WebSocket 最常见的用途是实时应用程序开发,其中它有助于在客户端连续显示数据。当后端服务器不断发回这些数据时,WebSocket 允许在已经打开的连接中不间断地推送或传输这些数据。WebSocket 的使用使此类数据传输变得快速并充分利用了应用程序的性能。
此类 WebSocket 实用程序的一个现实示例是比特币交易网站。在这里,WebSocket 协助部署的后端服务器向客户端发送数据处理。
2.创建聊天应用程序
聊天应用程序开发人员在一次性交换和发布/广播消息等操作中向 WebSocket 寻求帮助。由于使用相同的 WebSocket 连接来发送/接收消息,因此通信变得简单快捷。
3.正在开发游戏应用程序
在游戏应用程序开发过程中,服务器必须不间断地接收数据,而不要求 UI 刷新。WebSocket 可以在不影响游戏应用程序 UI 的情况下实现这一目标。
既然已经清楚了应该在哪里使用 WebSocket,请不要忘记了解应该避免使用 WebSocket 的情况,让自己远离大量的操作麻烦。
当需要获取旧数据或仅需要一次性处理数据时,不应该使用 WebSocket。在这些情况下,使用 HTTP 协议是明智的选择。
五、WebSocekt与HTTP的关系
- 相同点
- 都是基于tcp的,都是可靠性传输协议
- 都是应用层协议
- 不同点
- WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息
- HTTP是单向的
- WebSocket是需要浏览器和服务器握手进行建立连接的
- 而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接
- 两者之间的联系
- WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。
总结:
- 首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;
- 然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;
- 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。
由于 HTTP 和 WebSocket 都用于应用程序通信,因此人们经常感到困惑,并且很难从这两者中选择一个。看一下下面提到的文本,可以更清楚地了解 HTTP 和 WebSocket。
如前所述,WebSocket 是一种框架式双向协议。相反,HTTP 是一个在 TCP 协议之上运行的单向协议。
由于WebSocket协议能够支持连续的数据传输,因此主要用于实时应用程序开发。HTTP 是无状态的,用于开发RESTful和 SOAP 应用程序。Soap仍然可以使用HTTP来实现,但是REST被广泛传播和使用。
在 WebSocket 中,通信发生在两端,这使其成为更快的协议。在 HTTP 中,连接是在一端建立的,这使得它比 WebSocket 有点慢。
WebSocket使用统一的TCP连接,需要一方终止连接。在发生这种情况之前,连接将保持活动状态。HTTP 需要为单独的请求构建不同的连接。请求完成后,连接会自动断开。
六、使用WebSocket
6.1 Java原生实现
6.1.1 使用注解
6.1.1.1 @ServerEndpoint
表示当前类是一个websocket的服务器,values属性指定一个连接服务器的url地址
@ServerEndpoint("/connect")
public class WebSocketServer {
}
6.1.1.2 @OnOpen
打开连接的方法,只有在客户端连接服务端的时候这个方法会调用一次
// session表示一个websocket客服端的连接会话每一个客户端连接就会创建一个Session会话
// 注意:它不是HtppSession中的会话,而是websocket里的
@OnOpen
public void onOpen(Session session) {
log.info("客服端已连接");
// 将session添加到用户列表中
users.add(session);
}
6.1.1.3 @OnMessage
接受客户端发送的消息方法
@OnMessage
public void onMessage(String message, Session session) throws IOException {
log.info("消息:" + message);
// 向当前客户端发送一个消息
session.getBasicRemote().sendText("Hello client");
}
6.1.1.4 @OnClose
当客户端断开连接后调用此方法
@OnClose
public void onClose(Session session){
log.info("客户端已断开连接");
}
6.1.2 群发信息案列
1)添加WebSocket依赖
<!-- spring websocket在spring4.0版本开始支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.3.23</version>
</dependency>
2)编写消息对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
/**
* 发送人
*/
private String fromUser;
/**
* 发送时间
*/
private String sendTime;
/**
* 发送内容
*/
private String content;
}
3)编写用户登录的controller
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String userName = req.getParameter("username");
// 将用户名保存到HttpSession中
req.getSession().setAttribute("username",userName);
// 重定向到聊天首页
resp.sendRedirect("chat.html");
}
}
4)编写握手连接处理类
由于websocket在交互得第一次请求是基于HTTP协议进行握手的,因此可以在这个类中得到握手请求对象,从而得到HttpSession的信息
public class WebSocketHandshake extends Configurator {
/**
* 重写握手处理方法
* @param sec
* @param request 请求对象
* @param response 响应对象
*/
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
// 获取HttpSession对象
HttpSession httpSession = (HttpSession) request.getHttpSession();
// 获取用户名
String userName = (String) httpSession.getAttribute("username");
// 将用户保存到当前用户连接websocket的Session中
sec.getUserProperties().put("user",userName);
}
}
5)编写WebSocket服务器
@Slf4j
@ServerEndpoint(value = "/connect", configurator = WebSocketHandshake.class)
public class CharServer {
/**
* 用户列表
* key为用户id或者name,
* value则是每一个客户端的Session
*/
private static final Map<String, Session> users = new HashMap<>();
@OnOpen
public void onOpen(Session session) {
// 获取Session中的用户名
String userName = (String) session.getUserProperties().get("user");
// 添加到用户列表中
users.put(userName, session);
}
@OnMessage
public void onMessage(String message, Session session) throws Exception {
// 获取发送人
String fromUser = (String) session.getUserProperties().get("user");
// 创建发送时间
String sendTime = new SimpleDateFormat("hh:mm").format(new Date());
// 封装消息对象并序列化为json
Message msg = new Message(fromUser, sendTime, message);
String jsonMessage = new ObjectMapper().writeValueAsString(msg);
// 群发给所有人
for (String userName : users.keySet()){
Session s = users.get(userName);
s.getBasicRemote().sendText(jsonMessage);
}
}
@OnClose
public void onClose(Session session) {
// 将用户移除在线列表
String userName = (String) session.getUserProperties().get("user");
users.remove(userName);
}
}
6)编写登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>用户登录</h1>
<form name="f1" method="post" action="login">
Name:<input type="text" name="username"/><br>
<input type="submit" value="登录">
</form>
</body>
</html>
7)编写聊天室页面实现信息发送
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery-1.8.2.js"></script>
</head>
<body>
<h1>聊天室</h1>
<div id="msg"></div>
<input type="text" id="message"/><br>
<input type="button" value="发送">
<script>
// 构建websocket实例,连接后台server的请求地址
// websocket在第一次请求时使用http协议连接服务端,告诉服务器
// 接下来要使用websocket进行通信,此时将进行协议升级,会在
// http的请求头中带有upgrade:websocket的头信息
let ws = new WebSocket('ws://localhost:8080/connect');
// 接收服务端的消息
ws.onmessage = function (event){
// 将消息填充到div中
let data = event.data;
// 将json字符串转换为json对象
data = $.parseJSON(data);
$('#msg').append(data.fromUser + ':' + data.sendTime + '<br>');
$('#msg').append(data.content + '<br>');
}
$(function() {
$(':button').on('click', function() {
let msg = $('#message').val();
// 发送消息
ws.send(msg);
$('#message').val('');
})
})
</script>
</body>
</html>
6.2 结合spring实现
6.2.1 使用注解
6.2.1.1 @EnableWebSocket
启动websocket支持,一般放在websocket配置类上。
@Configuration
// 启用websocket支持
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
// ...
}
6.2.2 具体案列
1)添加websocket依赖
2)编写消息对象类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
private String fromUser;
private String sendTime;
private String content;
}
3)编写websocket配置类
@Configuration
// 启用websocket支持
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
/**
* 装配服务端
*
* @return
*/
@Bean
public WebSocketHandler webSocketHandler() {
return new ChatServer();
}
/**
* 装配HttpSession的拦截器,这样就可以在握手阶段
* 获取HttpSession的内容,在使用WebSocketSession时
* 就能直接得到HttpSession的数据
*
* @return
*/
@Bean
public HandshakeInterceptor handshakeInterceptor() {
return new HttpSessionHandshakeInterceptor();
}
/**
* 给服务端注册请求的端点(映射连接地址)
*
* @param registry
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 给ChatServer设置连接的url
registry.addHandler(webSocketHandler(), "/connect")
// 设置握手拦截器
.addInterceptors(handshakeInterceptor());
}
}
4)编写MvcConfig、WebConfig以及主配置AppConfig
可以参考以前编写的配置类
5)编写spring封装的websocket服务端
需要继承一个TextWebSocketHandler接口,表示一个服务端用户处理文本数据的消息。
public class ChatServer extends TextWebSocketHandler {
/**
* 用户列表
* 每一个用户连接时都会创建一个WebSocketSession对象
*/
private static final Map<String, WebSocketSession> users = new HashMap<>();
/**
* 客户端建立连接后执行的方法,等效于onOpen方法
*
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 获取登录的用户信息
String username = (String) session.getAttributes().get("user");
// 保存到用户列表
users.put(username, session);
}
/**
* 接收客户端的消息,等效于onMessage方法
*
* @param session
* @param message
* @throws Exception
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 获取消息载体,也就是客户端发送的文本内容
String msgContent = message.getPayload();
// 获取发送人
String fromUser = (String) session.getAttributes().get("user");
// 发送时间
String sendTime = new SimpleDateFormat("hh:mm").format(new Date());
// 封装消息对象
Message msg = new Message(fromUser, sendTime, msgContent);
// 序列化为json字符串
String json = new ObjectMapper().writeValueAsString(msg);
// 群发给所有人
for (String userName : users.keySet()) {
WebSocketSession s = users.get(userName);
// 发送消息,必须是一个TextMessage对象
s.sendMessage(new TextMessage(json));
}
}
/**
* 连接关闭后执行的方法,等效于onClose方法
*
* @param session
* @param status
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String userName = (String) session.getAttributes().get("user");
// 将用户移出在线列表
users.remove(userName);
}
}
6)编写一个简单的用户登录Controller
@Controller
public class UserController {
@PostMapping("/user/login")
public String login(String username, HttpSession session) {
// 将用户信息保存到会话作用域
session.setAttribute("user", username);
// 重定向到聊天的首页
return "redirect:/static/chat.html";
}
}
7)编写登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>用户登录</h1>
<form name="f1" method="post" action="../user/login">
Name:<input type="text" name="username"/><br>
<input type="submit" value="登录">
</form>
</body>
</html>
8)编写聊天室页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery-1.8.2.js"></script>
</head>
<body>
<h1>聊天室</h1>
<div id="msg"></div>
<input type="text" id="message"/><br>
<input type="button" value="发送">
<script>
// 创建websocket对象
let ws = new WebSocket('ws://localhost:8080/connect');
// 接收服务端的消息
ws.onmessage = function (event){
// 将消息填充到div中
let data = event.data;
// 将json字符串转换为json对象
data = $.parseJSON(data);
$('#msg').append(data.fromUser + ':' + data.sendTime + '<br>');
$('#msg').append(data.content + '<br>');
}
$(function() {
$(':button').on('click', function() {
let msg = $('#message').val();
// 发送消息
ws.send(msg);
$('#message').val('');
})
})
</script>
</body>
</html>
七、STOMP协议
7.1 简介
STOMP允许消息客户端(生产者、消费者)与任意消息代理(Broker)之间进行异步消息传输的简单文本定向消息协议。但STOMP并不是为WebSocket而设计的,它是属于消息队列的一种协议(AMQP、JMS等都属于消息队列协议)。许多消息队列都支持STOMP协议(例如:RabbitMQ、ActiveMQ)。由于它的简单性,因此可以用于定义websocket的消息体格式。我们先建立了webscoket连接, 接下来我只需要在webscoket连接的基础上建立stomp连接,因此STOMP协议格式的消息就会写入到websocket的payload中。
7.2 协议格式
STOMP协议由命令(Command)、头(Header)、消息体(Body)组成,与http协议结构相似。结构如下:
COMMAND
header1:value1
header2:value2
Body^@
其中Command包含SEND, SUBSCRIBE, MESSAGE, CONNECT, CONNECTED
等命令。header则类似于http的content-length, content-type等。Body就是具体的消息内容,可以是二进制或者文本。其中^@代表null结尾。
八、WebSocket的消息代理
8.1 使用注解
8.1.1 @EnableWebSocketMessageBroker
启用websocket消息代理中间件,作用于WebSocket配置类中。
@Configuration
// 启用websocket消息代理中间件
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// ...
}
8.2 具体案列
1)添加spring-websocket的消息代理依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>5.3.23</version>
</dependency>
2)编写消息对象类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
private String content;
private String sendTime;
}
3)编写WebSocketConfig配置类
@Configuration
// 启用websocket消息代理中间件
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注册一个连接消息中间件的端点(路径url)
*
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("broker");
}
/**
* 配置消息代理,主要是设置相关的主题
* 消息代理是服务中心的核心,spring-websocket内置了
* 一个简单的消息代理,但也只是能够满足基本要求,如果
* 需要强大的消息中心的功能,通常都会集成第三方的消息队列
* 例如:RabbitMQ等
*
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 启用spring内置简单的消息代理并设置一个主图(topic)的前缀,用于消息的发送和订阅
// enableSimpleBroker:启动内部的消息代理
// enableStompBrokerRelay:启用第三方的消息代理
registry.enableSimpleBroker("/news", "/video");
// 如果需要集成外部其他的消息代理,使用下的方法
// registry.enableStompBrokerRelay();
}
}
4)编写余下的配置类(MvcConfig、WebConfig、AppConfig等)
可以参考之前的配置类
5)编写发布消息的controller
注意:需要注入一个SimpMessagingTemplate类(消息处理模版),用于发布消息
@RestController
@RequiredArgsConstructor
public class PublishController {
private final SimpMessagingTemplate template;
@PostMapping("/publish/{topic}/{sub}")
public void publish(String message, @PathVariable("topic") String topic,@PathVariable("sub") String sub) {
String sendTime = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
Message msg = new Message(message, sendTime);
// 将消息发布到消息代理指定的主题中
template.convertAndSend("/" + topic + "/" + sub, msg);
}
}
6)编写页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery-1.8.2.js"></script>
</head>
<body>
<h1>后台管理</h1>
<form id="f1">
<select id="topic">
<option>--请选择--</option>
<option value="sport">体育</option>
<option value="recreation">娱乐</option>
</select>
<input type="text" name="message" id="message"/>
<input type="button" value="发布"/><br>
</form>
<script>
$(function(){
$(':button').on('click',function() {
let param = $('#f1').serialize();
let subTopic = $('#topic').val();
$.ajax({
url: '/publish/news/' + subTopic,
type: 'post',
data: param,
success: function(data){
$('#message').val('');
}
})
})
})
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery-1.8.2.js"></script>
<script src="js/stomp.min.js"></script>
</head>
<body>
<h1>体育新闻页</h1>
<div id="msg"></div>
<script>
$(function() {
// 创建websocket实例
let ws = new WebSocket('ws://localhost:8080/broker');
// 将websocket包装成stomp客户端
let stompClient = Stomp.over(ws);
// 连接服务器并订阅消息
// {}:放入请求头 - 键值对
stompClient.connect({},function() {
// 执行订阅
stompClient.subscribe('/news/sport', function(data) {
// 接收发布的通知内容
// 取出STOMP中的body部分并解析为json对象
let msg = $.parseJSON(data.body);
$('#msg').append(msg.sendTime + '<br>')
$('#msg').append(msg.content + '<br>')
});
});
})
</script>
</body>
</html>