WebSocket简介
WebSocket协议是基于TCP的一种网络协议。它实现了浏览器与服务器全双工通信——允许服务器主动发送信息给客户端。
WebSocket是通过一个socket来实现双工异步通信的。直接使用WebSocket或者SockJS协议显得特别繁琐。使用它的子协议STOMP,它是一个更高级别的协议,STMOP协议使用一个基于帧格式来定义消息,与HTTP的Request和Response类似。
SpringBoot对使用WebSocket提供了支持,配置源码在org.springframework.boot.autoconfigure.websocket包下。
案例中使用到的maven依赖:
<!--springBoot其它依赖省略-->
<!-- webSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
广播模式
广播式即服务端有消息时,会将信息发送给所有连接了当前endpoint的浏览器。
1、 写消息类
/**
* 浏览器向服务端发送的消息
*/
public class AricMessage {
private String name;
public String getName() {
return name;
}
}
/**
* 服务端向浏览器发送的消息
*/
public class AricResponse {
private String responseMessage;
public AricResponse(String responseMessage) {
this.responseMessage = responseMessage;
}
public String getResponseMessage() {
return responseMessage;
}
}
2、配置websocket
/**
* 配置WebSocket
*/
@Configuration
//注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {//注册STOMP协议的节点(endpoint),并映射指定的url
//注册一个STOMP的endpoint,并指定使用SockJS协议
registry.addEndpoint("/endpointAric").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {//配置消息代理(Message Broker)
//广播式应配置一个/topic消息代理
registry.enableSimpleBroker("/topic");
}
}
3、写Controller
/**
* webSocket控制器
*/
@Controller
public class WebSocketController {
@MessageMapping("/welcome") //当浏览器向服务端发送请求时,通过@MessageMapping映射/welcome这个地址,类似于@ResponseMapping
@SendTo("/topic/getResponse")//当服务器有消息时,会对订阅了@SendTo中的路径的浏览器发送消息
public AricResponse say(AricMessage message) {
try {
//睡眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new AricResponse("welcome," + message.getName() + "!");
}
}
4、在src\main\resource\templates目录下编写webSocket.html,使用thymeleaf模板引擎
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>SpringBoot实现广播式WebSocket</title>
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery-1.11.3.min.js}"></script>
</head>
<body>
<noscript><h2 style="color:#ff0000">抱歉,您的浏览器不支持改功能!</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">连接</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
</div>
<div id="conversationDiv">
<label>请输入您的姓名</label><input type="text" id="name" />
<button id="sendName" onclick="sendName();">发送</button>
<p id="response"></p>
</div>
</div>
</body>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected){
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
$("#response").html();
}
function connect(){
var socket = new SockJS('/endpointAric'); //连接SockJS的endpoint名称为"endpointWisely"
stompClient = Stomp.over(socket);//使用STMOP子协议的WebSocket客户端
stompClient.connect({},function(frame){//连接WebSocket服务端
setConnected(true);
console.log('Connected:' + frame);
//通过stompClient.subscribe订阅/topic/getResponse 目标(destination)发送的消息,这个是在控制器的@SentTo中定义的
stompClient.subscribe('/topic/getResponse',function(response){
showResponse(JSON.parse(response.body).responseMessage);
});
});
}
function disconnect(){
if(stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName(){
var name = $("#name").val();
//通过stompClient.send向/welcome 目标(destination)发送消息,这个是在控制器的@MessageMapping中定义的
stompClient.send("/welcome",{},JSON.stringify({'name':name}));
}
function showResponse(message){
var response = $("#response");
response.html(message);
}
</script>
</html>
5、配置viewController,为webSocket.html提供路径映射。
/**
* 配置viewController,为页面提供路径映射
*/
@Controller
public class WebMvcConfig extends WebMvcConfigurerAdapter{
/**
* 配置viewController,提供映射路径
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/webSocket").setViewName("/webSocket");
}
}
6、打开多个浏览器,访问地址,其中一个发送消息,其它浏览器能接收消息。
点对点式
点对点方式解决消息由谁发送,由谁接收的问题。
使用到Spring Security,简单配置。
/** * Spring Security配置 */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "login").permitAll() //设置Spring Security对/和"/login"路径不拦截 .anyRequest().authenticated() .and().formLogin().loginPage("/login")//设置Spring Security的登陆页面访问路径为login .defaultSuccessUrl("/chat") //登陆成功后转向/chat路径 .permitAll().and().logout() .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //配置两个用户,角色是user auth.inMemoryAuthentication() .withUser("james").password("james").roles("user") .and() .withUser("curry").password("curry").roles("user"); } @Override public void configure(WebSecurity web) throws Exception { // 设置Spring Secutiry不拦截/resources/static/目录下的静态资源 web.ignoring().antMatchers("/resources/static/**"); } }
配置WebSocket
/** * 配置WebSocket */ @Configuration //注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样 @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{ @Override public void registerStompEndpoints(StompEndpointRegistry registry) {//注册STOMP协议的节点(endpoint),并映射指定的url //注册一个STOMP的endpoint,并指定使用SockJS协议 registry.addEndpoint("/endpointAric").withSockJS(); //注册名为"endpointChat"的endpoint registry.addEndpoint("/endpointChat").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) {//配置消息代理(Message Broker) //广播式应配置一个/topic消息代理 // registry.enableSimpleBroker("/topic"); //点对点式应配置/queue和/topic消息代理 registry.enableSimpleBroker("/queue","/topic"); } }
编写Controller
/** * webSocket控制器 */ @Controller public class WebSocketController { //通过simpMessagingTemplate向浏览器发送消息 @Autowired private SimpMessagingTemplate simpMessagingTemplate; @MessageMapping("/chat") //在springmvc中,可以直接在参数中获得principal,pinciple中包含当前用户信息 public void handleChat(Principal principal,String msg){ if ("james".equals(principal.getName())) {//硬编码,对用户姓名进行判断 //向用户发送消息,第一个参数:接收消息的用户,第二个参数:浏览器订阅地址,第三个参数:消息 simpMessagingTemplate.convertAndSendToUser("curry", "/queue/notifications", principal.getName() + "-send: " + msg); } else { simpMessagingTemplate.convertAndSendToUser("james", "/queue/notifications", principal.getName() + "-send: " + msg); } } @MessageMapping("/welcome") //当浏览器向服务端发送请求时,通过@MessageMapping映射/welcome这个地址,类似于@ResponseMapping @SendTo("/topic/getResponse")//当服务器有消息时,会对订阅了@SendTo中的路径的浏览器发送消息 public AricResponse say(AricMessage message) { try { //睡眠3秒 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return new AricResponse("welcome," + message.getName() + "!"); } }
在src\main\resource\templates目录下编写login.html和chat.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>首页登陆</title> </head> <body> <div th:if="${param.error}"> 用户名或密码错误! </div> <div th:if="${param.logout}"> 已退出! </div> <form th:action="@{/login}" method="post"> <div><label>用户名: <input type="text" name="username" /> </label></div> <div><label>密码: <input type="password" name="password" /> </label></div> <div><input type="submit" value="登陆" /></div> </form> </body> </html>
聊天页面
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>聊天室</title> <script th:src="@{sockjs.min.js}"></script> <script th:src="@{stomp.min.js}"></script> <script th:src="@{jquery-1.11.3.min.js}"></script> </head> <body> <p>聊天室</p> <form id="aricForm"> <textarea rows="4" cols="60" name="text"></textarea> <input type="submit" /> </form> <div id="output"></div> </body> <script type="text/javascript"> $("#aricForm").submit(function(e){ e.preventDefault(); var text = $("#aricForm").find('textarea[name="text"]').val(); sendSpittle(text); }); var sock = new SockJS("/endpointChat");//连接endpoint名为"endpointChat"的endpoint var stomp = Stomp.over(sock); stomp.connect('guest','guest',function(frame){ //订阅/user/queue/notifications发送的消息,这里与在simpMessagingTemplate.convertAndSendToUser定义地址一致,/user是必须的 stomp.subscribe("/user/queue/notifications",handleNotification); }); function handleNotification(message){ $("#output").append("<b>Received: " + message.body + "</b><br/>"); } function sendSpittle(text){ stomp.send("/chat",{},text); } $("#stop").click(function(){ sock.close(); }); </script> </html>
配置viewController
/** * 配置viewController,为页面提供路径映射 */ @Controller public class WebMvcConfig extends WebMvcConfigurerAdapter{ /** * 配置viewController,提供映射路径 */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/webSocket").setViewName("/webSocket"); registry.addViewController("/login").setViewName("/login"); registry.addViewController("/chat").setViewName("/chat"); } }
- 打开浏览器,实现点对点聊天