1:WebSocket与 Http
WebSocket协议是2011年成为国际标准的、基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。
HTTP1.1 允许只建立一次连接,多次资源请求复用该连接,也就是所谓的 keep-alive,但是 Websocket 其实是一个新协议,跟 HTTP 协议基本没有关系,
只是为了兼容现有浏览器,所以在握手阶段使用了 HTTP1.1。
首先我们来看个典型的 WebSocket 握手
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
Upgrade: websocket
Connection: Upgrade
这个就是 WebSocket 的核心了,告诉 Apache 、 Nginx 等服务器,发起的请求要用 WebSocket 协议。
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
首先, Sec-WebSocket-Key 是一个 Base64 encode 的值,这个是浏览器随机生成的,告诉服务器:我要验证你是不是真的是 WebSocket 助理。
然后, Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
最后, Sec-WebSocket-Version 是告诉服务器所使用的 WebSocket Draft (协议版本)。
然后服务器会返回下列东西,表示已经接受到请求, 成功建立 WebSocket 啦!
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
这里开始就是 HTTP 最后负责的区域了,告诉客户,我已经成功切换协议啦~
Upgrade: websocket
Connection: Upgrade
依然是固定的,告诉客户端即将升级的是 WebSocket 协议,而不是 mozillasocket,lurnarsocket 或者 shitsocket。
然后, Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key 。 服务器:好啦好啦,知道啦,给你看我的 ID CARD 来证明行了吧。
后面的, Sec-WebSocket-Protocol 则是表示最终使用的协议。
至此,HTTP 已经完成它所有工作了,接下来就是完全按照 WebSocket 协议进行了。
2.spring (lower)
SockJS 是 WebSocket 技术的一种模拟。为了应对许多浏览器不支持WebSocket协议的问题,设计了备选SockJs。
开启并使用SockJS后,它会优先选用Websocket协议作为传输协议,如果浏览器不支持Websocket协议,则会在其他方案中,
选择一个较好的协议进行通讯。
服务端使用:
registry.addEndpoint("/endpointChat").withSockJS();
客户端:
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{jquery-1.10.2.min.js}"></script>
var ws = new SockJS('/marco');
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
服务端:
public class MarcoHandler extends AbstractWebSocketHandler {
private static final Logger logger = LoggerFactory.getLogger(MarcoHandler.class);
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
super.handleMessage(session, message);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
Thread.sleep(2000);
session.sendMessage(new TextMessage("Polo!"));
}
}
配置 WebSocketConfigurer:
@Configuration
@EnableWebSocket
public class WebSocketConfigMarco implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
webSocketHandlerRegistry.addHandler(marcoHandler(), "/marco").withSockJS();
}
@Bean
public MarcoHandler marcoHandler() {
return new MarcoHandler();
}
}
3. spring (STOMP)
STOMP,面向消息的简单文本协议。它是在WebSocket 之上提供一个基于帧的线路格式层,用来定义消息的语义。
websocket定义了两种传输信息类型: 文本信息和二进制信息。类型虽然被确定,但是他们的传输体是没有规定的。
所以,需要用一种简单的文本传输类型来规定传输内容,它可以作为通讯中的文本传输协议,即交互中的高级协议来定义交互信息。
STOMP本身可以支持流类型的网络传输协议: websocket协议和tcp协议。
Stomp还提供了一个stomp.js,用于浏览器客户端使用STOMP消息协议传输的js库。
STOMP的优点如下:
(1)不需要自建一套自定义的消息格式
(2)现有stomp.js客户端(浏览器中使用)可以直接使用
(3)能路由信息到指定消息地点
(4)可以直接使用成熟的STOMP代理进行广播 如:RabbitMQ, ActiveMQ
3.4 客户端:
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery-1.10.2.min.js}" type="text/javascript"></script><!-- 2 -->
var stompClient = null;
function connect() {
var socket = new SockJS('/endpointWisely'); //1
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/getResponse', function(respnose){ //2
showResponse(JSON.parse(respnose.body).responseMessage);
});
});
}
function showResponse(message) {
var response = $("#response");
response.html(message);
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
}
function sendName() {
var name = $('#name').val();
stompClient.send("/welcome", {}, JSON.stringify({ 'name': name }));//3
}
endpoint : /endpointWisely //1
url : /welcome //2
subscribe: '/topic/getResponse' //3
3.1 启用 STOMP
@Configuration
@EnableWebSocketMessageBroker//1
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {}
@Configuration
@EnableWebSocketMessageBroker //1
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpointWisely").withSockJS(); //2
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic"); //3
}
}
3.2 自定义匹配规则
//1 启动 STOMP 消息代理
//2 注册 STOMP 的endpoint ,并指定使用 SocketJS协议
//3 配置 topic 代理
3.3 处理消息
@MessageMapping注解和@RequestMapping注解功能类似,只不过@RequestMapping表明此方法是Stomp客户端向服务端send消息的目标地址。
@Controller
public class WsController {
@MessageMapping("/welcome")
@SendTo("/topic/getResponse")
public WiselyResponse say(WiselyMessage message) throws Exception {
Thread.sleep(3000);
return new WiselyResponse("Welcome, " + message.getName() + "!");
}
}
4. 配置 Security 点对点
4.1 Security
4.1.1 添加Security jar 包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
4.1.2 添加Security 配置
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/chat")
.permitAll()
.and()
.logout()
.permitAll();
}
//4
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("wyf").password("wyf").roles("USER")
.and()
.withUser("wisely").password("wisely").roles("USER");
}
//5忽略静态资源的拦截
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/static/**");
}
}
4.2 WebSocket
4.2.1 添加WebSocket 配置
WebSocketConfig: registerStompEndpoints
registry.addEndpoint("/endpointChat").withSockJS();
WebSocketConfig:configureMessageBroker
registry.enableSimpleBroker("/queue","/topic");
4.2.2 添加控制器 WsController
@Autowired
private SimpMessagingTemplate messagingTemplate;//1
@MessageMapping("/chat")
public void handleChat(Principal principal, String msg) { //2
if (principal.getName().equals("wyf")) {//3
messagingTemplate.convertAndSendToUser("wisely",
"/queue/notifications", principal.getName() + "-send:"
+ msg);
} else {
messagingTemplate.convertAndSendToUser("wyf",
"/queue/notifications", principal.getName() + "-send:"
+ msg);
}
}
通过SimpMessagingTemplate 向浏览器发送信息 //1
在Spring MVC中,可以直接在参数中获取principal,principal中包含当前用户的信息//2
如果发送人是wyf,则发送给wisely;否则则相反//3
通过 convertAndSendToUser 向用户发送信息,第一个参数是接收消息用户,第二个是浏览器订阅地址,第三个是消息本身
4.3 客户端
4.3.1 添加登录页面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<meta charset="UTF-8" />
<head>
<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>
4.3.2 添加聊天页面
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <meta charset="UTF-8" /> <head> <title>Home</title>
<script th:src="@{jquery-1.10.2.min.js}"></script>
<script th:src="@{sockjs.min.js}"></script> <script th:src="@{stomp.min.js}"></script> </head> <body> <p>聊天室</p> <form id="wiselyForm"> <textarea rows="4" cols="60" name="text"></textarea> <input type="submit"/> </form> <script th:inline="javascript"> $('#wiselyForm').submit(function(e){ e.preventDefault(); var text = $('#wiselyForm').find('textarea[name="text"]').val(); sendSpittle(text); }); var sock = new SockJS("/endpointChat"); //1 var stomp = Stomp.over(sock); stomp.connect('guest', 'guest', function(frame) { stomp.subscribe("/user/queue/notifications", handleNotification);//2 }); function handleNotification(message) { $('#output').append("<b>Received: " + message.body + "</b><br/>") } function sendSpittle(text) { stomp.send("/chat", {}, text);//3 } $('#stop').click(function() {sock.close()}); </script> <div id="output"></div> </body> </html>
注1:连接 endpoint 名称 “/endpointChat”
2: 订阅 /user/queue/notifications 发送的消息,这里与在控制器 messagingTemplate,convertAndSendToUser 中定义的订阅地址保持一致。这里的/user 是必须的,使用了/user 才会发送消息到指定的用户。
3:url ,/chat
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
}
@Override // WebSecurityConfig
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.passwordEncoder(new MyPasswordEncoder())
.withUser("wyf").password("wyf").roles("USER")
.and()
.withUser("wisely").password("wisely").roles("USER");
}
参考 : springboot 实战
spring 实战
https://www.cnblogs.com/nnngu/p/9347635.html