(一)WebSocket简介
WebSocket是一种通讯协议,通过单个TCP连接提供完全多工通讯管道。大白话就是:WebSocket协议相对于Http协议来说,它能在一段时间内一直保持连接,而Http协议是三次握手后就断开连接。WebSocket经常用于聊天室这种实时通讯场景。
WebSocket协议地址相对于Http协议地址来说,Schema部分变了:Http地址:http://host:port/… ,而WebSocke地址:ws://host:port/…
(二)WebSocke API
相关术语:
- 端点(Endpoint)
- 连接(Connection)
- 对点(Peer)
- 会话(Session)
- 客户端端点、服务器端点
1、端点生命周期
(1)打开连接
Endpoint#onOpen(Session,EndpointConfig) ------编程
@OnOpen ------注解
(2)关闭连接
Endpoint#onClose(Session,CloseReason)
@OnClose
(3)错误
Endpoint#onError(Session,Throwable)
@OnError
(4)发送消息
@OnMessage(PS:无对应编程方法)
2、会话(Sessions)
(1)API:javax.websocket.Session
(2)接收消息:javax.websocket.MessageHandler
(3)发送消息:javax.websocket.RemoteEndpoint.Basic
3、配置(Configuration)
(1)服务端配置(javax.websocket.ServerEndpointConfig)
- URI 映射
- 子协议协商
- 扩展点修改
- Origin检测
- 握手修改
- 自定义端点创建
(2)客户端配置(javax.websocket.ClientEndpointConfig) - 子协议
- 扩展点
- 客户端配置修改
3、部署(Deployment)(PS:这里只是介绍原理,并不需要我们手动去部署)
(1)应用部署到WEB容器
- WEB-INF/classes
- WEB-INF/lib
(2)应用部署到独立WebSocket服务器 - javax.websocket.server.ServerApplicationConfig
(3)编程方式(Springboot中的方式) - javax.websocket.server.ServerContainer
(三)Springboot整合WebSocket及其聊天室案例
1、添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、自定义端点
/**
* 聊天室服务端
* 标注为端点:@ServerEndpoint,其中"/chat-room/{username}"为访问路径
*/
@ServerEndpoint("/chat-room/{username}")
public class ChatRoomServerEndpoint {
/**
* 存储所有存活的用户
* 注意1:高并发问题
* 注意2:livingSessions必须是全局变量(保证全局就他一个变量,否则每次调用都会被刷新)
* 注意3:很难保证,用户在退出聊天室时,正确调用了WebSocket.close(),也就是调用后端的onClose()方法,这样
* 就可能会导致Session无法被有效清除,livingSessions会越来越大,服务器压力也会越来越大。
* 所以,我们需要周期性的去检查用户是否还处于活跃状态,不活跃的,移除该用户的session
*/
private static Map<String , Session> livingSessions = new ConcurrentHashMap<>();
/**
* 前端一旦启用WebSocket,机会调用@OnOpen注解标注的方法
* @param username 路径参数
* @param session 会话,每个访问对象都会有一个单独的会话
*/
@OnOpen
public void openSession(@PathParam("username") String username, Session session){
livingSessions.put(session.getId(), session);
sendTextAll("欢迎用户【" + username +"】来到聊天室!");
}
/**
* 服务端发送消息给前端时调用
* @param username 路径参数
* @param session 会话,每个访问对象都会有一个单独的会话
* @param message 待发送的消息
*/
@OnMessage
public void onMessage(@PathParam("username") String username, Session session, String message){
sendTextAll("用户【" + username + "】:" + message);
}
/**
* 客户端关闭WebSocket连接时,调用标注@OnClose的方法
* @param username 路径参数
* @param session 会话,每个访问对象都会有一个单独的会话
*/
@OnClose
public void onClose(@PathParam("username") String username, Session session){
//将当前用户移除
livingSessions.remove(session.getId());
//给所有存活的用户发送消息
sendTextAll("用户【" + username +"】离开聊天室!");
}
/**
* 向指定Session(用户)发送message
*/
private void sendText(Session session, String message){
//发送消息对象
RemoteEndpoint.Basic basicRemote = session.getBasicRemote();
try {
//发送消息
basicRemote.sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 遍历所有存活的用户,并发送消息(PS:就是广播消息)
*/
private void sendTextAll(String message){
//lambda表达式
livingSessions.forEach((sessionId, session) -> sendText(session, message));
}
}
3、改造启动接口,激活WebSocket,并注册WebSocket相关类和端点
/**
* 激活WebSocket:@EnableWebSocket
*/
@SpringBootApplication
@EnableWebSocket
public class WebsocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebsocketApplication.class, args);
}
/**
* 注册 ServerEndpointExporter Bean对象(因为Springboot没有自动注册,所以得手动注册)
*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
/**
* 注册 端点对象
*/
@Bean
public ChatRoomServerEndpoint chatRoomServerEndpoint(){
return new ChatRoomServerEndpoint();
}
}
4、前端页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>聊天室</title>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/sockjs/1.0.0/sockjs.min.js"></script>
<script>
$(document).ready(function () {
var urlPrefix = "ws://localhost:8080/chat-room/";
var ws = null;
//加入聊天室
$('#bt_join').click(function () {
var username = $('#username').val();
//这一步,会调用服务端的@OnOpen注解标注的方法
ws = new WebSocket(urlPrefix + username);
ws.onmessage = function (event) {
//接收服务端返回给前端的消息
$('#text_chat_content').append(event.data + "\n");
};
ws.onclose = function () {
/**
* 问题:为什么这里不是接收服务端的提示信息,而是当前用户自己定义消息?
* 原因:当用户离开聊天室时,在遍历存活用户时,当前用户已经不在存活用户集合中,
* 所以当前用户的提示信息不能由服务端发送,而得由自己去定义!
*/
$('#text_chat_content').append("用户【" + username +"】离开聊天室!" + "\n");
};
});
//发送消息
$('#bt_send').click(function () {
if (ws){
ws.send($('#in_msg').val());
}
});
//离开聊天室
$('#bt_left').click(function () {
if (ws){
ws.close();
}
});
})
</script>
</head>
<body>
聊天消息内容:<br/>
<textarea id="text_chat_content" readonly="readonly" cols="60" rows="20"></textarea>
<br/>
用户:<input id="username" type="text"/>
<button id="bt_join">加入聊天室</button>
<button id="bt_left">离开聊天室</button>
<br/>
输入框:<input id="in_msg" type="text"/>
<button id="bt_send">发送消息</button>
</body>
</html>