WebSocket广播式和点对点的通信【原创】

本篇文章主要介绍websocket的两种通信,广播式和点对点的通信。

一、广播式通讯

类似广播一样,只要发出,订阅的人便可以接收到

前端发出消息,通过SockJS连接

代码示例
1、pom.xml引入jar
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-websocket</artifactId>
 </dependency>
2、WebSocketConfig.java 配置WebSocket
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
​
/**
 * @description: 配置WebSocket
 *          注释@EnableWebSocketMessageBroker开始使用STOMP协议来传输基于代理(message broker)的消息
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-12 16:39
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
​
 /**
 *  注册STOMP协议的节点(endpoint),并映射的对应的URL。
 *  注册一个STOMP的endpoint,并指定使用SickJS协议
 * @param registry
 */
 @Override
 public void registerStompEndpoints(StompEndpointRegistry registry) {
 registry.addEndpoint("/endpointSSH").withSockJS();
 registry.addEndpoint("/endpointChat").withSockJS();
 }
​
 /**
 *  配置消息代理(Message Broker)
 *  广播式应配置一个/topic 消息代理
 *  点对点配置 /queue
 * @param registry
 */
 @Override
 public void configureMessageBroker(MessageBrokerRegistry registry) {
 registry.enableSimpleBroker("/queue","/topic");
 }
​
}
3、两个发送和接收消息的实体 ElijahMessage.java ElijahResponse.java
import lombok.AllArgsConstructor;
import lombok.Getter;
​
/**
 * @description: 用于服务器想向浏览器发生消息
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-12 17:10
 */
@AllArgsConstructor
@Getter
public class ElijahResponse {
 private String responseMessage;
}
import lombok.Getter;
​
/**
 * @description: 用于接收服务器发送的消息
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-12 17:09
 */
@Getter
public class ElijahMessage {
 private String name;
}
4、WsController.java WebSocket 控制器
import com.ch7.domain.ElijahMessage;
import com.ch7.domain.ElijahResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
​
import java.security.Principal;
​
/**
 * @description: WebSocket 控制器
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-12 17:15
 */
@Controller
@Slf4j
public class WsController {
​
 /**
 * 通过SimpMessagingTemplate 向浏览器发生消息
 */
 @Autowired
 private SimpMessagingTemplate messagingTemplate;
​
 /**
 *  当浏览器向服务端发生请求时,通过@MessageMapping映射/welcome这个地址
 *  注解@MessageMapping使用方法与@RequestMapping相似
 * @param message
 * @return
 * @throws Exception
 */
 @MessageMapping("welcome")
 @SendTo("/topic/getResponse")
 public ElijahResponse say(ElijahMessage message) throws Exception {
 Thread.sleep(3000);
 return new ElijahResponse("Welcome, " + message.getName() + "!");
 }
​
 /**
 *  点对点聊天
 *
 * @param principal 包含当前用户的信息
 * @param msg
 */
 @MessageMapping("/chat")
 public void handleChar(Principal principal, String msg) {
​
 // 判断发生给谁
 if (principal.getName().equals("ssh")) {
 // 发生消息给用户  接收消息的用户、浏览器订阅地址和消息内容
 messagingTemplate.convertAndSendToUser("elijah",
 "/queue/notifications",
 principal.getName() + "-send: " + msg);
 } else {
 messagingTemplate.convertAndSendToUser("ssh",
 "/queue/notifications",
 principal.getName() + "-send: " + msg);
 }
 }
​
}
5、ws.html 发送消息和接收消息的页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <title>spring-boot-WebSocket-广播式</title>
 <link rel="stylesheet" type="text/css" value="">
</head>
<body onload="disconnect()">
<noscript>
 <h2 style="color: #ff0000">貌似你的浏览器不支持websocket</h2>
</noscript>
​
​
<div>
 <h3>WebSocket</h3>
</div>
​
<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>
​
​
​
​
​
<!--<script src="/static/js/sockjs.min.js" charset="utf-8"></script>-->
<!--<script src="/static/js/stomp.min.js" charset="utf-8"></script>-->
​
<script th:src="@{/static/js/jquery.min.js}"></script>
<script th:src="@{/static/js/sockjs.min.js}"></script>
<script th:src="@{/static/js/stomp.min.js}"></script>
<script type="application/javascript">
​
 var stompClient = null;
​
 function setConnected(connected) {
 console.log('Connected status: ' + connected);
 document.getElementById("connect").disabled = connected;
 document.getElementById("disconnect").disabled = !disconnect;
 document.getElementById("conversationDiv").style.visibility= (connected ? 'visible' : 'hidden');
​
 // $("#conversationDiv").style.visibility = (connected ? 'visible' : 'hidden');
 $('response').html();
 }
​
 /**
 * 打开连接
 */
 function connect() {
 // 1、连接SockJS的endpoint
 var socket = new SockJS('/endpointSSH');
 // 2、使用STOMP 子协议的WebSocket客户端
 stompClient = Stomp.over(socket);
 // 3、连接websockst服务端
 stompClient.connect({}, function (frame) {
 setConnected(true);
 console.log('Connected: ' + frame);
 // 4、 通过stomp.subscribe订阅/topic/getResponse目标(destination)发生的消息,后端在@SendTo定义
 stompClient.subscribe('/topic/getResponse', function (respose) {
 showResponse(JSON.parse(respose.body).responseMessage);
 });
 });
 }
​
 /**
 * 关闭连接
 */
 function disconnect() {
 if (stompClient != null) {
 stompClient.disconnect();
 }
 setConnected(false);
 console.log("Disconnected");
 }
​
 function sendName() {
 var name = $('#name').val();
 // 5、通过 stompClient.send 向/welcome 目标发送消息 服务端在@MessageMapping中定义的
 stompClient.send("/welcome", {}, JSON.stringify({'name': name}));
 }
​
 function showResponse(message) {
 var response = $("#response");
 response.html(message);
 }
​
</script>
</body>
</html>
​
展示结果:
websocket-广播式.png

在之前学习过socket连接对象也可通过WebSocket(不通过SockJS)连接

var socket = new WebSocket(url);

https://www.jianshu.com/p/bd0667b270ca

目前的是通过sockjs来

1、连接SockJS的endpoint

2、使用STOMP 子协议的WebSocket客户端

3、连接websockst服务端

4、 通过stomp.subscribe订阅/topic/getResponse目标(destination)发生的消息,后端在@SendTo定义

5、通过 stompClient.send 向/welcome 目标发送消息 服务端在@MessageMapping中定义的

STOMP帧由命令,一个或多个头信息、一个空行及负载(文本或字节)所组成;

其中可用的COMMAND 包括:

CONNECT、SEND、SUBSCRIBE、UNSUBSCRIBE、BEGIN、COMMIT、ABORT、ACK、NACK、DISCONNECT;

数据执行流程

CONNECT accept-version:1.1,1.0 heart-beat:10000,10000

连接成功的返回为:

<<< CONNECTED version:1.1 heart-beat:0,0

订阅目标(destination)/topic/getResponse:

SUBSCRIBE id:sub-0 destination:/topic/getResponse

向目标(destination)/welcome 发生消息的格式为:

SEND destination:/welcome content-length:17

{"name":"elijah"}

从目标(destination)/topic/getResponse接收的格式为:

<<< MESSAGE destination:/topic/getResponse content-type:application/json;charset=UTF-8 subscription:sub-0 message-id:5nd0pfjf-73 content-length:38

{"responseMessage":"Welcome, elijah!"}

二、点对点式通信:

点对点多用于聊天室,一对一的通信,这里是基础的登录(springsecurity)到聊天室然后进行两天

代码示例

相关登录配置
1、pom.xml 引入jar
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
2、WebSecurityConfig.java 鉴权配置
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
​
/**
 * @description: 登录时的鉴权配置
 *
 * @author: Shenshuaihu
 * @version: 1.0
 * @data: 2019-07-15 18:31
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
​
 @Override
 protected void configure(HttpSecurity http) throws Exception {
 http
 .authorizeRequests()
 // /和login不拦截
 .antMatchers("/", "login").permitAll()
 .anyRequest().authenticated()
 .and()
 .formLogin()
 // 页面访问路径
 .loginPage("/login")
 // 登录成功转向/char
 .defaultSuccessUrl("/chat")
 .permitAll()
 .and()
 .logout()
 .permitAll();
​
 }
​
 /**
 * 内存中分配两个用户
 * @param auth
 * @throws Exception
 */
 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 auth
 .inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
 .withUser("elijah")
 .password(new BCryptPasswordEncoder().encode("elijah"))
 .roles("USER")
 .and()
 .withUser("ssh")
 .password(new BCryptPasswordEncoder().encode("ssh"))
 .roles("USER");
 }
​
 /**
 * 静态资源不拦截
 * @param web
 * @throws Exception
 */
 @Override
 public void configure(WebSecurity web) throws Exception {
 web.ignoring().antMatchers("/resource/static/**");
 }
}
3、login.html 登录页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <title>spring-boot-login</title>
 <link rel="stylesheet" type="text/css" value="">
</head>
<body>
​
<div>
 <h3>登录</h3>
</div>
​
 <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>
聊天代码
1、相关配置

WebSocketConfig.java

WsController.java

方法在上面广播式代码里

两个页面也需要配置

2、char.html 聊天窗口
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <title>spring-boot-WebSocket-点对点式-home</title>
 <link rel="stylesheet" type="text/css" value="">
</head>
<body>
​

<p>聊天室</p>
​
<form id="elijahForm">
 <textarea rows="4" cols="60" name="text"></textarea>
 <input type="submit">
</form>
​
​
​
​
<script th:src="@{/static/js/jquery.min.js}"></script>
<script th:src="@{/static/js/sockjs.min.js}"></script>
<script th:src="@{/static/js/stomp.min.js}"></script>
<script type="application/javascript">
​
​
 $('#elijahForm').submit(function (e) {
 e.preventDefault();
 var text = $('#elijahForm').find('textarea[name="text"]').val();
 sendSpittle(text);
 });
​
 // 1、连接SockJS的endpoint
 var socket = new SockJS('/endpointChat');
 // 2、使用STOMP 子协议的WebSocket客户端
 stomp = Stomp.over(socket);
 // 3、连接websockst服务端 //默认的和STOMP端点连接
 stomp.connect("guest", "guest", function (frame) {
 // 4、 通过stomp.subscribe订阅/topic/getResponse目标(destination)发生的消息,后端在@SendTo定义
 stomp.subscribe("/user/queue/notifications", function (message) {
 debugger;
 var content = message.body;
 var obj = JSON.parse(content);
 console.log("admin用户特定的消息1:" + obj.message)
 console.log("收到一条新消息:" + JSON.parse(respose.message).responseMessage)
 $('#output').append("<b>Received: " + message.body + "</b><br/>")
 });
 });
​
​
 function sendSpittle(text) {
 stomp.send("/chat", {}, text);
 }
​
 $('#stop').click(function () {
 socket.close();
 })
</script>
​
<div id="output"></div>
​
</body>
</html>
​
展示结果:
websocket-点对点式.png

三、其他说明:

1、基本概念:

STOMP:

STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议

如何理解 STOMP 与 WebSocket 的关系: 1) HTTP协议解决了 web 浏览器发起请求以及 web 服务器响应请求的细节,假设 HTTP 协议 并不存在,只能使用 TCP 套接字来 编写 web 应用,你可能认为这是一件疯狂的事情;

  1. 直接使用 WebSocket(SockJS) 就很类似于 使用 TCP 套接字来编写 web 应用,因为没有高层协议,就需要我们定义应用间所发送消息的语义,还需要确保连接的两端都能遵循这些语义;

  2. 同 HTTP 在 TCP 套接字上添加请求-响应模型层一样,STOMP 在 WebSocket 之上提供了一个基于帧的线路格式层,用来定义消息语义;

2、所遇到的坑:

使用springsecurity时在内容中设置密码没有处理会报错

There is no PasswordEncoder mapped for the id "null"

是高版本的security所导致

.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
 .withUser("elijah")
 .password(new BCryptPasswordEncoder().encode("elijah"))
 .roles("USER")

参考文档:

https://blog.csdn.net/jqsad/article/details/77745379

https://www.jianshu.com/p/bd0667b270ca

参考书籍汪云飞 SpringBoot实战

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容