SpringBoot学习9.2-websocket

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工程。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值