在HTTP协议中,所有的请求都是由客户端发起的,由服务端进行响应,服务端无法向客户端推送消息,但是在一些需要即时通信的应用中,又不可避免地需要服务端像客户端推送消息,传统的解决方案主要由如下几种:
轮询,就是客户端在固定的时间间隔下不停地向服务端发送请求,查看服务端是否由最新的数据,若服务端有最新的数据,则返回给客户端,若服务端没有,则返回一个空的JSON或者XML文档
。轮询对开发人员而言实现方便,但是弊端也是明显:客户端需要每次都要新建HTTP请求,服务端需要处理大量的无效请求,在高并发场景下会严重拖慢服务端的运行效率,同时服务端的资源被极大的浪费了。因此这种方式并不可取。在长轮询中,服务端不是每次都会立即响应客户端的请求,只有在服务端有最新数据的时候才会立即响应客户端的请求,否则服务端会持有这个请求而不返回,直到有服务端有最新数据时才返回
。这种方式可以在一定程度上节省网络资源和服务器资源,但是也存在一些问题,例如:
- 如果浏览器在服务器响应之前有新数据要发送,就只能创建一个新的并发请求,或者先尝试断掉当前请求,再创建新的请求。
- TCP和HTTP规范中都有连接超时一说,所以所谓的长轮询并不能一直持续,服务端和客户端的连接需要定期的连接和关闭再连接,这又增大开发人员的工作量,当然也有一些技术能够延长每次连接时间,但毕竟是非主流解决方案。
可以使用Applet和Flase来模拟比全双工通信,通过创建一个只有1个像素点大小的透明的Applet或者Flash,然后将之内嵌再网页中,再从Applet或者Flash的代码中创建一个Socket连接进行双向通信。这种连接方式消除了HTTP协议中的诸多限制,当服务器又消息发送到客户端的时候,开发者可以在Applet或者Flash中调用JavaScript函数将数据显示在页面上,当浏览器有数据要发送给服务器时也一样,通过Applet或者Flash来传递,这种方式真正地实现了全双工通信
,不过也有问题,说明如下:
- 浏览器必须能够运行Java或者Flash
2.无论是Applet还是Flash都存在安全问题
3.随着HTML5标准被各浏览器厂商广泛支持,Flash已经下架。
WebSocket
是一种在单个TCP连接上进行全双工通信的协议
,已被W3C定为标准。使用WebSocket可以使得客户端和服务器之间的数据交换变得更加简单,它允许服务器主动向客户端推送数据
。在WebSocket协议中,浏览器和服务器只需要完成一次握手,两者之间就可以直接创建持久性的连接,并进行双向数据传输
。
WebSocket
使用HTTP/1.1
的协议升级特性,一个WebSocket请求首先使用非正常的HTTP请求
以特定的模式访问一个URL,这个URL有两种模式,分别是ws和wss
,对应HTTP协议中的HTTP和HTTPS
,在请求头中有一个Connection:Upgrade
字段,表示客户端想要对应协议进行升级,另外还有一个Upgrade:websocket
字段,表示客户端想要将请求协议升级为WebSocket协议,这两个字段共同告诉服务器将连接升级为WebSocket这样一种全双工协议,如果服务端同意协议升级,那么在握手完成之后,文本消息或者其他二进制消息就可以同时在两个方向上进行发送,而不需要关闭和重建连接。此时的客户端和服务端关系是对等的,它们可以互相向对方主动发送消息。和传统的解决方案相比,WebSocket主要有如下特点:
WebSocket使用时需要先创建连接
,这使得WebSocket成为一种有状态的协议,在之后的通信过程中可以省略部分状态信息(例如身份认证等)。WebSocket连接在端口80(ws)或者443(wss)上创建
,与HTTP使用的端口相同,这样,基本上所有的防火墙都不会阻止WebSocket连接。WebSocket使用HTTP协议进行握手
,因此它可以自然而然地集成到网络浏览器和HTTP服务器中,而不需要额外的成本。心跳消息(ping 和 pong)
将被反复的发送,进而保持WebSocket连接一直处于活跃状态。Stomp
websocket使用socket实现双工异步通信能力
。但是如果直接使用websocket协议开发程序比较繁琐,我们可以使用它的子协议Stomp
SockJS
sockjs
是websocket协议的实现,增加了对浏览器不支持websocket的时候的兼容支持
SockJS的支持的传输的协议有3类: WebSocket, HTTP Streaming, and HTTP Long Polling。默认使用websocket,如果浏览器不支持websocket,则使用后两种的方式。
SockJS使用”Get /info”从服务端获取基本信息。然后客户端会决定使用哪种传输方式。如果浏览器使用websocket,则使用websocket。如果不能,则使用Http Streaming,如果还不行,则最后使用 HTTP Long Polling
STOMP作用:
提供消息体的格式,允许STOMP客户端(Endpoints)与任意STOMP消息代理(message broker)
进行交互,实现客户端之间进行异步消息传送
。
SpringBoot对WebSocket提供了非常友好的支持,可以方便开发者项目中快速集成WebSocket功能,实现单聊或者群聊。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.cjw</groupId>
<artifactId>websocket01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>websocket01</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
**spring-boot-starter-websocket
**依赖是WebSocket相关依赖,其他的都是前端库,使用jar包的形式对这些前端库进行统一管理,使用webjar添加到项目中的前端库
,在``SpringBoot项目中已经默认添加了静态资源过滤,因此可以直接使用。
STOMP支持
,STOMP是一个简单的可互操作的协议
,通常被用于通过中间服务器在客户端之间进行异步消息传递
。STOMP
即Simple (or Streaming) Text Orientated Messaging Protocol
,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议可以建立在WebSocket之上,也可以建立在其他应用层协议之上。public interface WebSocketMessageBrokerConfigurer {
// 添加这个Endpoint,这样在网页中就可以通过websocket连接上服务,也就是我们配置websocket的服务地址,并且可以指定是否使用socketjs
default void registerStompEndpoints(StompEndpointRegistry registry) {
}
// 配置发送与接收的消息参数,可以指定消息字节大小,缓存大小,发送超时时间
default void configureWebSocketTransport(WebSocketTransportRegistration registry) {
}
// 设置输入消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间
default void configureClientInboundChannel(ChannelRegistration registration) {
}
// 设置输出消息通道的线程数,默认线程为1,可以自己自定义线程数,最大线程数,线程存活时间
default void configureClientOutboundChannel(ChannelRegistration registration) {
}
// 自定义控制器方法的参数类型,有兴趣可以百度google HandlerMethodArgumentResolver这个的用法
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
}
// 自定义控制器方法返回值类型,有兴趣可以百度google HandlerMethodReturnValueHandler这个的用法
default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
}
// 添加自定义的消息转换器,spring 提供多种默认的消息转换器,返回false,
//不会添加消息转换器,返回true,会添加默认的消息转换器,
//当然也可以把自己写的消息转换器添加到转换链中
default boolean configureMessageConverters(List<MessageConverter> messageConverters) {
return true;
}
// 配置消息代理,哪种路径的消息会进行代理处理
default void configureMessageBroker(MessageBrokerRegistry registry) {
}
}
服务端和浏览器的版本要求
WebSocket 服务端在各个主流应用服务器厂商中已基本获得符合 JEE JSR356 标准规范 API 的支持。当前支持websocket的版本:Tomcat 7.0.47+, Jetty 9.1+, GlassFish 4.1+, WebLogic 12.1.3+, and Undertow 1.0+ (and WildFly 8.0+).
浏览器的支持版本:
查看所有支持websocket浏览器的连接:
/**
* 自定义类WebSocketConfig继承WebSocketMessageBrokerConfigurer
* 进行WebSocket配置
* 通过@EnableWebSocketMessageBroker注解开启WebSocket消息代理
*/
@Configuration
//注解开启使用STOMP协议来传输基于代理(message broker)的消息
// 这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
//配置消息代理(Message Broker)
public void configureMessageBroker(MessageBrokerRegistry registry) {
/*
*设置消息代理前缀,即如果消息的前缀是“/topic”,就会将消息转发给消息代理(broker),
*再由消息代理将消息广播给当前连接的客户端。(topic路径交给broker处理)
* */
//点对点应配置一个/user消息代理,广播式应配置一个/topic消息代理
registry.enableSimpleBroker("/topic");
/*表示配置一个或者多个前缀,通过这些前缀过滤出需要被注解方法处理的消息
* 例如,前缀为“/app”的destination可以通过@MessageMapping注解的方法处理
* 而其他destination(例如“/topic” “/queue”)将被直接交给broker处理
* (app路径交给@MessageMapping注解的方法处理)
* */
//点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
registry.setApplicationDestinationPrefixes("/app");
}
//注册STOMP协议的节点(endpoint),并映射指定的url
public void registerStompEndpoints(StompEndpointRegistry registry) {
/*表示定义一个前缀为"/chat"的endPoint,并开启sockjs支持
* sockjs可以解决浏览器对WebSocket的兼容性问题,客户端将通过
* 这里配置的URL建立WebSocket连接
* ()
* */
//注册一个STOMP的endpoint,并指定使用SockJS协议
registry.addEndpoint("/chat").withSockJS();
}
}
@Controller
public class GreetingController {
/*
* @MessageMapping("/hello")注解的方法将用来接收
* ”/app/hello“路径发送来的消息,在注解方法中对消息进行处理后,
* 再将消息转发到@SendTo定义的路径上,而@SendTo路径
* 是一个前缀为“topic”的路径,因此该消息将被交给消息代理broker
* 再由broker进行广播
* */
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Message greeting(Message message) {
return message;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>群聊</title>
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/app.js"></script>
</head>
<body>
<div>
<label for="name">请输入用户名:</label>
<input id="name" placeholder="用户名">
</div>
<div>
<button id="connect" type="button">连接</button>
<button id="disconnect" type="button" disabled>断开连接</button>
</div>
<div id="chat" style="display: none;"></div>
<div>
<label for="name">请输入聊天内容:</label>
<input id="content" placeholder="聊天内容">
</div>
<div id="greetings">
</div>
<button id="send" type="button">发送</button>
<div id="conversation" stype="display:none">聊天进行中。。。。</div>
</body>
</html>
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
$("#chat").show();
} else {
$("#conversation").hide();
$("#chat").hide();
}
$("#greetings").html("");
}
function connect() {
console.log("connect-------------->start");
if (!$("#name").val()) {
return ;
}
var socket = new SockJS('/chat');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
stompClient.subscribe('/topic/greetings', function (greeting) {
console.log("message========" + greeting);
showGreeting(JSON.parse(greeting.body));
});
});
console.log("connect-------------->end");
}
function disconnect() {
console.log("disconnect-------------------> start");
if (stompClient !== null) {
stompClient.disconnect();
}
setConnected(false);
console.log("disconnect-----------------> end");
}
function sendName () {
console.log("sendName--------------------->start");
stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val(),
'content':$("#content").val()}));
console.log("sendName--------------------->end");
}
function showGreeting(message) {
$("#greetings").append(
"" + message.name + ":" + message.content + ""
);
}
$(function () {
$("#content").click(function () {connect();});
$("#disconnect").click(function () {disconnect()});
$("#send").click(function () {sendName();});
});
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("123");
System.out.println("password = " + password);
return new BCryptPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll();
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$SUfihxUrF1gn4XoZuZygAuMb9VzT.n7n6Rh1kA1SlP5TXe4Kvxb8S")
.roles("admin")
.and()
.withUser("sang")
.password("$2a$10$SUfihxUrF1gn4XoZuZygAuMb9VzT.n7n6Rh1kA1SlP5TXe4Kvxb8S")
.roles("user");
}
}
/**
* 自定义类WebSocketConfig继承WebSocketMessageBrokerConfigurer
* 进行WebSocket配置
* 通过@EnableWebSocketMessageBroker注解开启WebSocket消息代理
*/
@Configuration
//注解开启使用STOMP协议来传输基于代理(message broker)的消息
// 这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
//配置消息代理(Message Broker)
public void configureMessageBroker(MessageBrokerRegistry registry) {
/*
*设置消息代理前缀,即如果消息的前缀是“/topic”,就会将消息转发给消息代理(broker),
*再由消息代理将消息广播给当前连接的客户端。(topic路径交给broker处理)
* */
//点对点应配置一个/user消息代理,广播式应配置一个/topic消息代理
/*为了方便群发消息和点对点发送消息添加了queue*/
registry.enableSimpleBroker("/topic1", "/queue");
/*表示配置一个或者多个前缀,通过这些前缀过滤出需要被注解方法处理的消息
* 例如,前缀为“/app”的destination可以通过@MessageMapping注解的方法处理
* 而其他destination(例如“/topic” “/queue”)将被直接交给broker处理
* (app路径交给@MessageMapping注解的方法处理)
* */
//点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
registry.setApplicationDestinationPrefixes("/app");
}
//注册STOMP协议的节点(endpoint),并映射指定的url
public void registerStompEndpoints(StompEndpointRegistry registry) {
/*表示定义一个前缀为"/chat"的endPoint,并开启sockjs支持
* sockjs可以解决浏览器对WebSocket的兼容性问题,客户端将通过
* 这里配置的URL建立WebSocket连接
* ()
* */
//注册一个STOMP的endpoint,并指定使用SockJS协议
registry.addEndpoint("/chat").withSockJS();
}
}
@Controller
public class GreetingController {
/*
* SimpMessagingTemplate进行消息的发送,在SpringBoot中,已经配置好了
* 直接注入进来即可,使用SimpMessageTemplate,
* 可以在任意地方发送消息到broker,也可以发送消息给某一个用户
* (点对点发送)
* */
@Autowired
SimpMessagingTemplate simpMessageingTemplate;
/*
* @MessageMapping("/hello")注解的方法将用来接收
* ”/app/hello“路径发送来的消息,在注解方法中对消息进行处理后,
* 再将消息转发到@SendTo定义的路径上,而@SendTo路径
* 是一个前缀为“topic”的路径,因此该消息将被交给消息代理broker
* 再由broker进行广播
* */
@MessageMapping("/hello")
@SendTo("/topic1/greetings")
public Message greeting(Message message) {
return message;
}
/*
* @MessageMapping("/chat")表示来自/app/chat的路径消息将被chat处理
* Principal获取登录信息
* Chat 客户端发送来的消息
*
* */
@MessageMapping("/chat")
public void chat (Principal principal, Chat chat) {
/*获取当前用户信息*/
String from = principal.getName();
/*将消息存储进来*/
chat.setFrom(from);
/*将消息发送出去*/
/*
*
* public void convertAndSendToUser(String user, String destination, Object payload, @Nullable Map headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException {
Assert.notNull(user, "User must not be null");
Assert.isTrue(!user.contains("%2F"), "Invalid sequence \"%2F\" in user name: " + user);
user = StringUtils.replace(user, "/", "%2F");
destination = destination.startsWith("/") ? destination : "/" + destination;
super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
}
* destinationPrefix:private String destinationPrefix = "/user/";
*
* */
simpMessageingTemplate.convertAndSendToUser(chat.getTo(),
"/queue/chat", chat);
}
public static class Chat {
private String to;
private String from;
private String content;
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单聊</title>
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/chat.js"></script>
</head>
<body>
<div id="chat">
<div id="chatsContent">
</div>
<div>
请输入聊天内容:
<input id="content" placeholder="聊天内容">目标用户:
<input id="to" placeholder="目标用户">
<button id="send" type="button">发送</button>
</div>
</div>
</body>
</html>
var stompClient = null;
function connect() {
console.log("connetct--------------------->");
var socket = new SockJS('/chat');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame){
stompClient.subscribe('/user/queue/chat', function (chat) {
console.log("chat---------------->");
console.log(chat);
showGreeting(JSON.parse(chat.body))
});
})
}
function sendMsg() {
stompClient.send('/app/chat', {},
JSON.stringify({'content':$("#content").val(),
'to':$('#to').val()}));
}
function showGreeting(message) {
console.log(message);
$("#chatsContent").append("" + message.from + ":" + message.content + "");
}
$(function() {
connect();
$("#send").click(function () {sendMsg();});
})
经过SpringBoot自动化配置之后WebSocket使用起来还是非常方便的。通过@MessageMapping注解配置消息接口,通过@SendTo或者SimpMessagingTemplate进行消息转发,通过简单的配置,就能实现点对点、点对面的消息发送。一般在及时通信、通告发布等功能都会用到WebSocket。