1.websocket说明
websocket是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
允许服务端主动向客户端推送数据。
在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
http不支持持久性连接。
2.maven依赖
主要依赖starter-websocket、starter-security。
因为可以对客户端(用户)发送消息,所以需要security进行用户登录。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--websocket依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.spring配置
主要是页面配置,websocket不需要在此处配置。
server.port=8180
#定义视图解析器的规则
#文件前缀
spring.mvc.view.prefix=classpath:/templates/
#文件后缀
spring.mvc.view.suffix=.html
4.服务端点配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
// 创建服务器端点
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
这样就可以用@ServerEndpoint来定义服务端站点了。
5.创建服务端站点
@ServerEndpoint定义websocket服务端站点。
响应事件:
- @OnOpen:客户端连接成功响应事件
- @OnMessage:接收客户端消息响应事件
- @OnClose:客户端关闭连接响应事件
- @OnError:发送错误响应事件
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Service;
@Service
@ServerEndpoint(value = "/ws") // 定义websocket服务端站点
public class WebSocketService {
// 连接数
private static int onlineCount = 0;
private static CopyOnWriteArraySet<WebSocketService> webSocketServiceSet = new CopyOnWriteArraySet<WebSocketService>();
// 连接会话
private Session session;
// 客户端连接成功响应事件
@OnOpen
public void onOpen(Session session) {
this.session = session;
onlineCount++;
webSocketServiceSet.add(this);
sendSysMessage("ws站点新增一个连接,当前连接数:" + onlineCount);
}
// 接收客户端消息响应事件
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("ws站点接收消息:" + message);
// sendMessage(message);
sendGroupMessage(message);
}
// 客户端关闭连接响应事件
@OnClose
public void onClose() {
onlineCount--;
webSocketServiceSet.remove(this);
System.out.println("ws站点关闭一个连接,当前连接数:" + onlineCount);
}
// 发送错误响应事件
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
// 发送消息到客户端
private void sendMessage(String message) {
doSend(message, session.getUserPrincipal().getName());
}
// 发送系统消息到客户端
private void sendSysMessage(String message) {
doSend(message, "系统消息");
}
// 群发消息到客户端
private void sendGroupMessage(String message) {
for (WebSocketService wss : webSocketServiceSet) {
wss.doSend(message, session.getUserPrincipal().getName());
}
}
// 发送
private void doSend(String message, String user) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
Date d = new Date();
String dateNowStr = sdf.format(d);
String msg = user + "(" + dateNowStr + "):" + message;
try {
this.session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
6.客户端连接服务端站点
通过js代码连接,websocket.js:
/** websocket共通js */
var websocket = null;
var serverEndpoint = "ws";
// 判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
// 创建WebSocket对象,连接服务器端点【浏览器访问要同下面的一样】
// websocket = new WebSocket("ws://192.168.30.42:8180/"+serverEndpoint);
websocket = new WebSocket("ws://"+window.location.host+"/"+serverEndpoint);
} else {
alert('本浏览器不支持websocket')
}
// 连接发生错误的回调方法
websocket.onerror = function() {
appendMessage("error");
};
// 连接成功建立的回调方法
websocket.onopen = function(event) {
appendMessage("成功连接站点:"+serverEndpoint);
}
// 接收到消息的回调方法
websocket.onmessage = function(event) {
appendMessage(event.data);
}
// 连接关闭的回调方法
websocket.onclose = function() {
appendMessage("关闭连接站点:"+serverEndpoint);
}
// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,
// 防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function() {
websocket.close();
}
// 将消息显示在网页上
function appendMessage(message) {
// 调用页面的全局函数
showMsg(message);
}
// 关闭连接
function closeWebSocket() {
websocket.close();
}
// 发送消息
function sendMessage(message) {
websocket.send(message);
}
//重连
function reconnect() {
if(websocket.OPEN !== websocket.readyState){
websocket = new WebSocket("ws://"+window.location.host+"/"+serverEndpoint);
}
}
webSocket的readyState属性用来定义连接状态,该属性的值有下面几种:
0 :对应常量websocket.CONNECTING (numeric value 0),
正在建立连接连接,还没有完成。The connection has not yet been established.
1 :对应常量websocket.OPEN (numeric value 1),
连接成功建立,可以进行通信。The WebSocket connection is established and communication is possible.
2 :对应常量websocket.CLOSING (numeric value 2)
连接正在进行关闭握手,即将关闭。The connection is going through the closing handshake.
3 : 对应常量websocket.CLOSED (numeric value 3)
连接已经关闭或者根本没有建立。The connection has been closed or could not be opened.
7.测试代码
为了配合测试,写了测试页面代码:
ws1.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>websocket</title>
<script th:src="@{/js/jquery/jquery-3.4.1.js}"></script>
<script th:src="@{/js/websocket/ws1.js}"></script>
<script th:src="@{/js/websocket/websocket.js}"></script>
</head>
<body>
<textarea id="message" cols="40" rows="4"></textarea></br>
<button id="send">发送</button>
<button id="close">关闭</button>
<button id="reconnect">重连</button>
<div id="context"></div>
</body>
</html>
ws1.js:
$(function() {
$("#send").click(function() {
var message = $("#message").val();
sendMessage(message);
$("#message").val("");
});
$("#close").click(function() {
closeWebSocket();
});
$("#reconnect").click(function() {
reconnect();
});
});
//想要js之间互相调用的function,那么函数就必须是全局的
function showMsg(message) {
var context = $("#context").html() + "<br/>" + message;
$("#context").html(context);
}
@Controller
@RequestMapping("/websocket")
public class WebSocketController {
@RequestMapping("/ws1")
public String ws1() {
return "websocket/ws1";
}
}
为了配合测试,开发了简易security登录代码:
/** security配置:用户认证、权限认证 */
@Configuration
public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
// 用户认证配置,使用user-detail机制
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 认证服务注册userDetailsService
// spring5的security必须使用密码编码器,否则抛出异常
auth.userDetailsService(userDetailsService).passwordEncoder(EncryptUtil.getEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
http.csrf().disable();// 禁用csrf,否则无法登录。如果不禁用,则要在form中提交防csrf的参数。
}
}
/** 用户详情实现类 */
@Service
public class CustomUserDetailsService implements UserDetailsService {
/**
* 构建用户详情(用户名、密码、角色)
* @param username 登录用户名
*/
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 角色集合
List<GrantedAuthority> authList = new ArrayList<>();
// 数据库密码:根据用户名查询,此处省略该步骤
String dbPassword = "1";
// 明文密码需要加密
UserDetails userDetails = new User(username, EncryptUtil.encrypt(dbPassword), authList);
return userDetails;
}
}
/** 密码工具类 */
public class EncryptUtil {
private static String SITE_WIDE_SECRET = "uvwxyz";
private static PasswordEncoder encoder;
public static String encrypt(String rawPassword) {
if (null == encoder) {
setEncoder();
}
return encoder.encode(rawPassword);
}
public static void setEncoder() {
encoder = new Pbkdf2PasswordEncoder(SITE_WIDE_SECRET);
}
public static PasswordEncoder getEncoder() {
if (null == encoder) {
setEncoder();
}
return encoder;
}
}
8.测试-聊天室
访问http://localhost:8180/websocket/ws1,用任意用户名+密码为1登录,分别用张三和李四登录:
发送消息:
因为使用了“群发”功能,所以加入改端点的用户都能收到消息,类似于一个聊天室。
github:https://github.com/zhangyangfei/SpringBootLearn.git中的spring-websocket工程。