SpringBoot+STOMP 实现聊天室(单聊+多聊)及群发消息详解
上篇系列文章:springboot+websocket构建在线聊天室(群聊+单聊)
最近发现stomp协议来实现长连接,非常简单(springboot封装的比较好)
本系列文章:
1、springboot+websocket构建在线聊天室(群聊+单聊)
2、SpringBoot+STOMP 实现聊天室(单聊+多聊)及群发消息详解
3、websocket stomp+rabbitmq实现消息推送
1、STOMP简介
STOMP是WebSocket的子协议,STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。
STOMP是一个非常简单和容易实现的协议,其设计灵感源自于HTTP的简单性。尽管STOMP协议在服务器端的实现可能有一定的难度,但客户端的实现却很容易。例如,可以使用Telnet登录到任何的STOMP代理,并与STOMP代理进行交互。
STOMP服务端
STOMP服务端被设计为客户端可以向其发送消息的一组目标地址。STOMP协议并没有规定目标地址的格式,它由使用协议的应用自己来定义。
STOMP客户端
对于STOMP协议来说, 客户端会扮演下列两种角色的任意一种:
- 作为生产者,通过SEND帧发送消息到指定的地址
- 作为消费者,通过发送SUBSCRIBE帧到已知地址来进行消息订阅,而当生产者发送消息到这个订阅地址后,订阅该地址的其他消费者会受到通过MESSAGE帧收到该消息
实际上,WebSocket结合STOMP相当于构建了一个消息分发队列,客户端可以在上述两个角色间转换,订阅机制保证了一个客户端消息可以通过服务器广播到多个其他客户端,作为生产者,又可以通过服务器来发送点对点消息。
STOMP帧结构
COMMAND
header1:value1
header2:value2Body^@
^@表示行结束符
一个STOMP帧由三部分组成:命令,Header(头信息),Body(消息体)
- 命令使用UTF-8编码格式,命令有SEND、SUBSCRIBE、MESSAGE、CONNECT、CONNECTED等。
- Header也使用UTF-8编码格式,它类似HTTP的Header,有content-length,content-type等。
- Body可以是二进制也可以是文本。注意Body与Header间通过一个空行(EOL)来分隔。
来看一个实际的帧例子:
SEND
destination:/broker/roomId/1
content-length:57{“type":"ENTER","content":"o7jD64gNifq-wq-C13Q5CRisJx5E"}
- 第1行:表明此帧为SEND帧,是COMMAND字段。
- 第2行:Header字段,消息要发送的目的地址,是相对地址。
- 第3行:Header字段,消息体字符长度。
- 第4行:空行,间隔Header与Body。
- 第5行:消息体,为自定义的JSON结构。
这里就不详细讲解STOMP协议了,大家感兴趣,可以去看官网:STOMP
2、环境介绍
后端:springboot 2.0.8
前端:angular7 (由于本人最近在学习angular,就选择它来做测试页面:p)
(当然html +js 也可以,欢迎参考我的github上的WebSocketTest.html )
3、后端
这里先讲解一下STOMP整个通讯流程:
1、首先客户端与服务器建立 HTTP 握手连接,连接点 EndPoint 通过 WebSocketMessageBroker 设置。(这是下面操作的前提)
2、客户端通过 subscribe 向服务器订阅消息主题(”/topic”或者“/all”),topic在本demo中是聊天室(单聊+多聊)的订阅主题,all是群发的订阅主题
3、客户端可通过 stompClient.send 向服务器发送消息,消息通过路径 /app/chat或者/appAll达到服务端,服务端将其转发到对应的Controller(根据Controller配置的 @MessageMapping(“/chat")或者@MessageMapping(“/chaAll") 信息)
4、服务器一旦有消息发出,将被推送到订阅了相关主题的客户端(Controller中的@SendTo(“/topic”)或者Controller中的@SendTo(“/all”)表示将方法中 return 的信息推送到 /topic 主题)。还有一种方法就是利用 messagingTemplate 发送到指定目标 (messagingTemplate.convertAndSend(destination, response);
大家理解接下的代码,可以参考这个大致流程
3.1、依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.8.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><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.webjars</groupId><artifactId>webjars-locator-core</artifactId></dependency><dependency><groupId>org.webjars</groupId><artifactId>sockjs-client</artifactId><version>1.0.2</version></dependency><dependency><groupId>org.webjars</groupId><artifactId>stomp-websocket</artifactId><version>2.3.3</version></dependency><!--websocket 相关依赖-->
3.2、application.properties
server.port =8080
3.3、WebSocketConfig
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {/** 用户可以订阅来自"/topic"和"/user"的消息,* 在Controller中,可通过@SendTo注解指明发送目标,这样服务器就可以将消息发送到订阅相关消息的客户端** 在本Demo中,使用topic来达到聊天室效果(单聊+多聊),使用all进行群发效果** 客户端只可以订阅这两个前缀的主题*/config.enableSimpleBroker("/topic","/all");/** 客户端发送过来的消息,需要以"/app"为前缀,再经过Broker转发给响应的Controller*/config.setApplicationDestinationPrefixes("/app");}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {/** 路径"/websocket"被注册为STOMP端点,对外暴露,客户端通过该路径接入WebSocket服务*/registry.addEndpoint("/websocket").setAllowedOrigins("*").withSockJS();}}
注意:
@EnableWebSocketMessageBroker ,使用此注解来标识使能WebSocket的broker.即使用broker来处理消息.
3.4、消息类
RequestMessage :
public class RequestMessage {private String sender;//消息发送者private String room;//房间号private String type;//消息类型private String content;//消息内容public RequestMessage() {}public RequestMessage(String sender,String room, String type, String content) {this.sender = sender;this.room = room;this.type = type;this.content = content; }public String getSender() {return sender;}public String getRoom() {return room;}public String getType() {return type;}public String getContent() {return content;}public void setSender(String sender) {this.sender = sender;}public void setReceiver(String room) {this.room = room;}public void setType(String type) {this.type = type;}public void setContent(String content) {this.content = content;}
ResponseMessage :
public class ResponseMessage {private String sender;private String type;private String content;public ResponseMessage() {}public ResponseMessage(String sender, String type, String content) {this.sender = sender;this.type = type;this.content = content;}public String getSender() {return sender;}public String getType() {return type;}public String getContent() {return content;}public void setSender(String sender) {this.sender = sender;}public void setType(String type) {this.type = type;}public void setContent(String content) {this.content = content;}
}
3.5、WebSocketTestController
@RestController
public class WebSocketTestController {@Autowiredprivate SimpMessagingTemplate messagingTemplate;/**聊天室(单聊+多聊)** @CrossOrigin 跨域** @MessageMapping 注解的方法可以使用下列参数:* * 使用@Payload方法参数用于获取消息中的payload(即消息的内容)* * 使用@Header 方法参数用于获取特定的头部* * 使用@Headers方法参数用于获取所有的头部存放到一个map中* * java.security.Principal 方法参数用于获取在websocket握手阶段使用的用户信息* @param requestMessage* @throws Exception*/@CrossOrigin@MessageMapping("/chat")public void messageHandling(RequestMessage requestMessage) throws Exception {String destination = "/topic/" + HtmlUtils.htmlEscape(requestMessage.getRoom());String sender = HtmlUtils.htmlEscape(requestMessage.getSender()); //htmlEscape 转换为HTML转义字符表示String type = HtmlUtils.htmlEscape(requestMessage.getType());String content = HtmlUtils.htmlEscape(requestMessage.getContent());ResponseMessage response = new ResponseMessage(sender, type, content);messagingTemplate.convertAndSend(destination, response);}/*** 群发消息* @param requestMessage* @return* @throws Exception*/@CrossOrigin@MessageMapping("/chatAll")public void messageHandlingAll(RequestMessage requestMessage) throws Exception {String destination = "/all";String sender = HtmlUtils.htmlEscape(requestMessage.getSender()); //htmlEscape 转换为HTML转义字符表示String type = HtmlUtils.htmlEscape(requestMessage.getType());String content = HtmlUtils.htmlEscape(requestMessage.getContent());ResponseMessage response = new ResponseMessage(sender, type, content);messagingTemplate.convertAndSend(destination, response);}}
注意:
1、使用@MessageMapping注解来标识所有发送到“/chat”这个destination的消息,都会被路由到这个方法进行处理
2、使用@SendTo注解来标识这个方法返回的结果,都会被发送到它指定的destination,“/topic”
3、传入的参数RequestMessage requestMessage为客户端发送过来的消息,是自动绑定的。
这样后端就写好了,是不是觉得很简单~
4、前端
新建一个angular7项目
可以参考我之前的文章:
angular7教程(1)——初步了解angular7及项目构建_小牛呼噜噜的博客-CSDN博客_angular7
然后创建新组件websocket
ng generate component websocket
配置路由:
import { NgModule } from '@angular/core';
import { LoginComponent } from './login/login.component';
import { WebsocketComponent } from './websocket/websocket.component';
import { Routes, RouterModule } from '@angular/router';const routes: Routes = [{ path: '', redirectTo: '/login', pathMatch: 'full' },{ path: 'login', component: LoginComponent },{ path: 'websocket', component: WebsocketComponent },
];@NgModule({imports: [RouterModule.forRoot(routes)],exports: [RouterModule]
})
export class AppRoutingModule { }
具体可以参考:
angular7教程(2)——新建页面_小牛呼噜噜的博客-CSDN博客_angular 新建页面
安装stomp相关的依赖:
npm install stompjs --save
npm install sockjs-client --save
websocket.component.ts:
import { Component, OnInit } from '@angular/core';
import * as Stomp from 'stompjs';
import * as SockJS from 'sockjs-client';@Component({selector: 'app-websocket',templateUrl: './websocket.component.html',styleUrls: ['./websocket.component.scss']
})
export class WebsocketComponent implements OnInit {public stompClient;public serverUrl = "http://localhost:8080/websocket";public room;//频道号constructor() { }ngOnInit() {this.connect();}connect() {const ws = new SockJS(this.serverUrl);this.stompClient = Stomp.over(ws);const that = this;this.stompClient.connect({}, function (frame) {that.stompClient.subscribe('/topic/' + that.room, (message) => {if (message.body) {const sender = JSON.parse(message.body)['sender'];const language = JSON.parse(message.body)['language'];const content = JSON.parse(message.body)['content'];const type = JSON.parse(message.body)['type'];}});});}}
运行项目:
ng serve --port 4300
报错:
ERROR in ./node_modules/stompjs/lib/stomp-node.js
Module not found: Error: Can't resolve 'net' in 'E:.....'
解决方案:
npm i net -S
打开浏览器,打开调试模式
发现报错:
browser-crypto.js:3 Uncaught ReferenceError: global is not defined
解决方案:
在polyfills.ts
文件中手动填充它:
// Add global to window, assigning the value of window itself.
(window as any).global = window;
成功
现在前端已经能和后端 建立websocket连接了,那接下来我们来丰富前端页面
我在这里采用了PrimeNG这个UI组件框架还要用到font-awesome图标库,因为PrimeNG用到了font-awesome的css(查了好久,才发现这个问题)
下载依赖:
npm install primeng --save
npm install primeicons --savenpm install font-awesome --save
导入css(修改src目录下单styles.scss):
/* You can add global styles to this file, and also import other style files */@import
"../node_modules/font-awesome/css/font-awesome.min.css", //is used by the style of ngprime
"../node_modules/primeng/resources/primeng.min.css", // this is needed for the structural css
"../node_modules/primeng/resources/themes/omega/theme.css",
"../node_modules/primeicons/primeicons.css";
// "../node_modules/primeflex/primeflex.css";
接着我们修改app.module.ts,来增加我们需要的模块:
import {ButtonModule} from 'primeng/button';imports: [... ,ButtonModule],
新建一个前端显示类:
item.ts:
这个用来定义聊天室的消息
export class Item {type: string;user: String;content: string;constructor(type: string, user: String, content: string) {this.type = type;this.user = user;this.content = content;}}
再建一个atim.ts:
这个用来定义群发的消息
export class Item {type: string;user: String;content: string;constructor(type: string, user: String, content: string) {this.type = type;this.user = user;this.content = content;}}
然后我们改造websocket.html:
<p>websocket works!
</p><div class="ui-g ui-fluid"><div class="ui-g-12 ui-md-4"><div class="ui-inputgroup"><span class="ui-inputgroup-addon"><i class="fa fa-user"></i></span><input type="text" pInputText placeholder="Sender" [(ngModel)]='sender'> {{sender}} </div><br><div class="ui-inputgroup"><span class="ui-inputgroup-addon">$$</span><input type="text" pInputText placeholder="Room" [(ngModel)]='room'> <p-button label="Connect" (click)="connect()"></p-button><p-button label="Disconnect" (click)="disconnect()"></p-button>{{room}}</div><br><br><br><div class="ui-inputgroup"><span class="ui-inputgroup-addon">--</span><input type="text" pInputText placeholder="Type" [(ngModel)]='type'> {{type}}</div><br><div class="ui-inputgroup"><span class="ui-inputgroup-addon"><i class="fa fa-credit-card"></i></span> <input type="text" pInputText placeholder="Message" [(ngModel)]='message'> <p-button label="Send" (click)="sendMessage()"></p-button>{{message}}</div><br><div class="ui-inputgroup"><span class="ui-inputgroup-addon"><i class="fa fa fa-cc-visa"></i></span> <input type="text" pInputText placeholder="MessageAll" [(ngModel)]='messageAll'> <p-button label="群发消息" (click)="sendMessageToAll()"></p-button></div><br>= = = = = =<p>这是来自聊天室的消息:</p><br><div ><li *ngFor="let item of items">{{item.user}} - {{item.content}} </li></div>= = = = = =<p>这是群发的消息:</p><br><div ><li *ngFor="let atem of atems">{{atem.user}} - {{atem.content}} </li></div></div>
</div>
接着改造websocket.component.ts:
import { Component, OnInit } from '@angular/core';
import * as Stomp from 'stompjs';
import * as SockJS from 'sockjs-client';
import { Item } from '../entity/item';
import { Atem } from '../entity/atem';@Component({selector: 'app-websocket',templateUrl: './websocket.component.html',styleUrls: ['./websocket.component.scss']
})
export class WebsocketComponent implements OnInit {public stompClient;public serverUrl = "http://localhost:8080/websocket";public room;//频道号public sender;//发送者public type;//消息的类型public message;//消息内容public messageAll;//群发消息的内容items = [];atems = [];constructor() { }ngOnInit() {// this.connect();}connect() {if(this.sender===undefined) {alert("发送者不能为空")return}if(this.room===undefined) {alert("房间号不能为空")return}const ws = new SockJS(this.serverUrl);this.stompClient = Stomp.over(ws);const that = this;this.stompClient.connect({}, function (frame) {//获取聊天室的消息that.stompClient.subscribe('/topic/' + that.room, (message) => {if (message.body) {const sender = JSON.parse(message.body)['sender'];// const language = JSON.parse(message.body)['language'];const content = JSON.parse(message.body)['content'];const type = JSON.parse(message.body)['type'];const newitem = new Item(type,sender,content);that.items.push(newitem);}else{return}});//获取群发消息that.stompClient.subscribe('/all', (message) =>{if (message.body) {const sender = JSON.parse(message.body)['sender'];// const language = JSON.parse(message.body)['language'];const content = JSON.parse(message.body)['content'];const type = JSON.parse(message.body)['type'];const newatem = new Atem(type,sender,content);that.atems.push(newatem);}else{return}})});}//断开连接的方法disconnect() {if (this.stompClient !== undefined) {this.stompClient.disconnect();}else{alert("当前没有连接websocket")}this.stompClient = undefined;alert("Disconnected");}//发送消息(单聊)sendMessage() {if(this.stompClient===undefined) {alert("websocket还未连接")return};if(this.type===undefined) {alert("消息类型不能为空")return};if(this.message===undefined) {alert("消息内容不能为空")return};this.stompClient.send('/app/chat',{},JSON.stringify({'sender': this.sender,'room': this.room,'type': this.type,'content': this.message }));}//群发消息sendMessageToAll() {if(this.stompClient===undefined) {alert("websocket还未连接")return};if(this.messageAll===undefined) {alert("群发消息内容不能为空")return};this.stompClient.send('/app/chatAll',{},JSON.stringify({'sender': this.sender,'room': "00000",'type': "00000",'content': this.messageAll }));}}
测试+最终效果图:
聊天室功能测试(单聊+多聊):
群发功能测试:
这样我们就完成了STOMP完成聊天室(单聊+多聊)及群发功能的实现。
小伙伴们是不是感觉STOMP协议比原生的websocket协议开发简单快捷多了
上篇文章:springboot+websocket构建在线聊天室(群聊+单聊)
大家可以思考一下这几个简单的问题:websocket如何实现用户登录的安全验证,结合rabbitmq 如何实现消息推送
参考:
STOMP
https://stomp.github.io/stomp-specification-1.2.html
https://juejin.im/post/5b7071ade51d45665816f8c0
Spring+STOMP实现WebSocket广播订阅、权限认证、一对一通讯(附源码)_杰明Jamin的博客-CSDN博客_stomp 登录验证
SpringBoot+STOMP 实现聊天室(单聊+多聊)及群发消息详解相关推荐
- 微信团队分享:微信直播聊天室单房间1500万在线的消息架构演进之路
本文由微信开发团队工程师" kellyliang"原创发表于"微信后台团队"公众号,收录时有修订和改动. 1.引言 随着直播和类直播场景在微信内的增长,这些业务 ...
- 微信直播聊天室单房间1500万在线的消息架构演进之路
1.引言 随着直播和类直播场景在微信内的增长,这些业务对临时消息(在线状态时的实时消息)通道的需求日益增长,直播聊天室组件应运而生.直播聊天室组件是一个基于房间的临时消息信道,主要提供消息收发.在线状 ...
- SpringBoot与webSocket实现在线聊天室——实现私聊+群聊+聊天记录保存
SpringBoot与webSocket实现在线聊天室--实现私聊+群聊+聊天记录保存 引用参考:原文章地址:https://blog.csdn.net/qq_41463655/article/det ...
- 融云「聊天室属性」: 语聊房、直播间有序运行和丝滑体验的绝技
信息技术的发展一日千里,但当技术照进现实,转变为让娱乐.生活更丰富,让工作更方便的实用能力,这个过程并非一日之功,也不是一成不变. 比如,IM 即时通讯中聊天室产品的聊天室属性,表面"其貌不 ...
- Springboot实现Web聊天室
Springboot实现Web聊天室 一.项目创建 二.代码编写 三.测试 四.参考 一.项目创建 新建Spring项目,选择JDK版本: 选择Spring Web: 二.代码编写 导入.jar包: ...
- php 微信 群聊,vbot微信机器人微信聊天消息详解(18):群组变动
<vbot微信机器人微信聊天消息详解(18):群组变动>要点: 本文介绍了vbot微信机器人微信聊天消息详解(18):群组变动,希望对您有用.如果有疑问,可以联系我们. 当微信群新增了成员 ...
- php获取微信聊天图片,vbot微信聊天机器人微信聊天消息详解(4):图片消息
<vbot微信聊天机器人微信聊天消息详解(4):图片消息>要点: 本文介绍了vbot微信聊天机器人微信聊天消息详解(4):图片消息,希望对您有用.如果有疑问,可以联系我们. 图片是资源文件 ...
- 微信发送视频消息php,vbot微信聊天机器人微信聊天消息详解(5):视频消息
<vbot微信聊天机器人微信聊天消息详解(5):视频消息>要点: 本文介绍了vbot微信聊天机器人微信聊天消息详解(5):视频消息,希望对您有用.如果有疑问,可以联系我们. 视频消息是资源 ...
- php点击事件唤起微信聊天,vbot微信机器人微信聊天消息详解(15):点击消息
<vbot微信机器人微信聊天消息详解(15):点击消息>要点: 本文介绍了vbot微信机器人微信聊天消息详解(15):点击消息,希望对您有用.如果有疑问,可以联系我们. 点击是机器人在账号 ...
最新文章
- 人工智能将会如何影响和服务医疗行业?未来十年会有哪些值得期待的应用?
- 查看binlog文件的2种方式
- 【C++】类型转换简述:四种类型转换方式的说明及应用
- TypeError系列之:TypeError: __init__() missing 2 required positional arguments
- python变量以及类型(含笔记)
- Kafka集群部署CentOS 7
- django html中文乱码,如何使用Python/Django执行HTML解码/编码?
- [CSS揭秘]不规则投影
- Python入门——爬取pubmed文献做分析
- 数字电路与逻辑设计 答案(第三版)
- 用C语言打印一个菱形图案!
- 机器人学回炉重造(1):正运动学、标准D-H法与改进D-H法的区别与应用(附ABB机械臂运动学建模matlab代码)
- 计算机网络连接叹号,网络连接不上,教您网络连接不上显示感叹号
- 深入浅出理解数据库s锁和x锁
- (二十四) 单链表的逆置(java)
- 13天Java进阶笔记-day7-异常、线程
- [HNOI2003] 消防局的设立
- No result defined for action com.zhen.user.UserInfoAction and result success
- 【嵌入式算法】空间向量夹角公式及其应用
- python for循环加速_干货总结,24招加速你的Python代码,值得收藏